From fa2ad85837a8fc7557e3c6852784a7179c667a1e Mon Sep 17 00:00:00 2001 From: Yu <1305203710@qq.com> Date: Sun, 7 Jun 2026 19:15:10 +0800 Subject: [PATCH 1/2] Add embedded DCI simulation backend --- configs/dci_runtime_topology.example.json | 109 ++++++++++++++++++++++ configs/tianjun.example.toml | 8 ++ restart_tianjun.bat | 7 +- src/tianjun/cli.py | 54 ++++++++++- src/tianjun/node_agent/clients.py | 6 ++ src/tianjun/simulation/__init__.py | 4 +- src/tianjun/simulation/backend.py | 77 ++++++++++++++- start_tianjun.bat | 7 +- 8 files changed, 256 insertions(+), 16 deletions(-) create mode 100644 configs/dci_runtime_topology.example.json diff --git a/configs/dci_runtime_topology.example.json b/configs/dci_runtime_topology.example.json new file mode 100644 index 0000000..506e31a --- /dev/null +++ b/configs/dci_runtime_topology.example.json @@ -0,0 +1,109 @@ +{ + "cluster": { + "name": "dci-runtime-topology", + "description": "Long-running DCI simulation topology for embedded or standalone Tianjun demos." + }, + "time_scale": 0.08, + "physical_topology": { + "topology_id": "huawei_public_case_reference_dci_runtime_v1", + "topology_nodes": ["USER", "PE1", "PE2", "PE3", "Border1", "Border2", "Border3", "IPCORE", "DC1", "DC2", "DC3"], + "topology_edges": [ + {"source": "USER", "target": "PE1", "propagation_delay_ms": 1.8, "bandwidth_mbps": 2000}, + {"source": "USER", "target": "PE2", "propagation_delay_ms": 2.2, "bandwidth_mbps": 1800}, + {"source": "USER", "target": "PE3", "propagation_delay_ms": 2.0, "bandwidth_mbps": 1900}, + {"source": "PE1", "target": "Border1", "propagation_delay_ms": 1.4, "bandwidth_mbps": 4000}, + {"source": "PE2", "target": "Border2", "propagation_delay_ms": 1.6, "bandwidth_mbps": 3800}, + {"source": "PE3", "target": "Border3", "propagation_delay_ms": 1.5, "bandwidth_mbps": 3900}, + {"source": "Border1", "target": "IPCORE", "propagation_delay_ms": 6.2, "bandwidth_mbps": 10000}, + {"source": "Border2", "target": "IPCORE", "propagation_delay_ms": 6.5, "bandwidth_mbps": 10000}, + {"source": "Border3", "target": "IPCORE", "propagation_delay_ms": 6.1, "bandwidth_mbps": 10000}, + {"source": "Border1", "target": "DC1", "propagation_delay_ms": 1.2, "bandwidth_mbps": 8000}, + {"source": "Border2", "target": "DC2", "propagation_delay_ms": 1.3, "bandwidth_mbps": 7600}, + {"source": "Border3", "target": "DC3", "propagation_delay_ms": 1.2, "bandwidth_mbps": 7800} + ], + "compute_attachments": { + "dci-dc1-beijing-vm-0": "DC1", + "dci-dc1-beijing-vm-1": "DC1", + "dci-dc1-beijing-vm-2": "DC1", + "dci-dc1-beijing-vm-3": "DC1", + "dci-dc1-hangzhou-vm-0": "DC1", + "dci-dc1-hangzhou-vm-1": "DC1", + "dci-dc1-hangzhou-vm-2": "DC1", + "dci-dc1-hangzhou-vm-3": "DC1", + "dci-dc2-chengdu-vm-0": "DC2", + "dci-dc2-chengdu-vm-1": "DC2", + "dci-dc2-chengdu-vm-2": "DC2", + "dci-dc2-chengdu-vm-3": "DC2", + "dci-dc2-chongqing-vm-0": "DC2", + "dci-dc2-chongqing-vm-1": "DC2", + "dci-dc2-chongqing-vm-2": "DC2", + "dci-dc2-chongqing-vm-3": "DC2", + "dci-dc3-guangzhou-vm-0": "DC3", + "dci-dc3-guangzhou-vm-1": "DC3", + "dci-dc3-guangzhou-vm-2": "DC3", + "dci-dc3-guangzhou-vm-3": "DC3", + "dci-dc3-shenzhen-vm-0": "DC3", + "dci-dc3-shenzhen-vm-1": "DC3", + "dci-dc3-shenzhen-vm-2": "DC3", + "dci-dc3-shenzhen-vm-3": "DC3" + }, + "provenance": { + "source": "Tianjun embedded runtime demo", + "purpose": "persistent local topology that can consume Hermes pending tasks through leases" + } + }, + "links": [ + {"source_region": "east", "target_region": "dc1", "latency_ms": {"p50": 2.4, "jitter": 0.5}, "bandwidth_mbps": {"p50": 3600, "jitter": 160}, "packet_loss": 0.0005, "path_reliability": 0.998}, + {"source_region": "east", "target_region": "dc2", "latency_ms": {"p50": 16.3, "jitter": 2.1}, "bandwidth_mbps": {"p50": 2400, "jitter": 260}, "packet_loss": 0.002, "path_reliability": 0.994}, + {"source_region": "east", "target_region": "dc3", "latency_ms": {"p50": 16.1, "jitter": 1.9}, "bandwidth_mbps": {"p50": 2600, "jitter": 240}, "packet_loss": 0.0018, "path_reliability": 0.995}, + {"source_region": "west", "target_region": "dc1", "latency_ms": {"p50": 16.3, "jitter": 2.2}, "bandwidth_mbps": {"p50": 2300, "jitter": 280}, "packet_loss": 0.0022, "path_reliability": 0.993}, + {"source_region": "west", "target_region": "dc2", "latency_ms": {"p50": 2.6, "jitter": 0.6}, "bandwidth_mbps": {"p50": 3500, "jitter": 170}, "packet_loss": 0.0006, "path_reliability": 0.998}, + {"source_region": "west", "target_region": "dc3", "latency_ms": {"p50": 17.0, "jitter": 2.4}, "bandwidth_mbps": {"p50": 2200, "jitter": 300}, "packet_loss": 0.0024, "path_reliability": 0.992}, + {"source_region": "south", "target_region": "dc1", "latency_ms": {"p50": 16.1, "jitter": 1.9}, "bandwidth_mbps": {"p50": 2600, "jitter": 240}, "packet_loss": 0.0018, "path_reliability": 0.995}, + {"source_region": "south", "target_region": "dc2", "latency_ms": {"p50": 17.0, "jitter": 2.4}, "bandwidth_mbps": {"p50": 2200, "jitter": 300}, "packet_loss": 0.0024, "path_reliability": 0.992}, + {"source_region": "south", "target_region": "dc3", "latency_ms": {"p50": 2.5, "jitter": 0.5}, "bandwidth_mbps": {"p50": 3550, "jitter": 165}, "packet_loss": 0.0005, "path_reliability": 0.998} + ], + "workload_profiles": { + "batch": { + "execution_time": {"base_seconds": 2, "per_cpu_second": 0.45, "per_memory_gb_second": 0.03, "per_input_gb_second": 0.2, "minimum_seconds": 2, "maximum_seconds": 36}, + "capacity": {"qps_per_cpu": 10, "qps_per_gpu": 0}, + "pricing": {"on_demand_per_hour": 8, "spot_per_hour": 4} + }, + "batch_cpu": { + "execution_time": {"base_seconds": 2, "per_cpu_second": 0.45, "per_memory_gb_second": 0.03, "per_input_gb_second": 0.2, "minimum_seconds": 2, "maximum_seconds": 36}, + "capacity": {"qps_per_cpu": 10, "qps_per_gpu": 0}, + "pricing": {"on_demand_per_hour": 8, "spot_per_hour": 4} + }, + "inference": { + "execution_time": {"base_seconds": 3, "per_cpu_second": 0.3, "per_gpu_second": 0.8, "per_memory_gb_second": 0.02, "minimum_seconds": 3, "maximum_seconds": 45}, + "capacity": {"qps_per_gpu": 120, "qps_per_cpu": 12, "max_batch_size": 8}, + "pricing": {"on_demand_per_hour": 18, "spot_per_hour": 9} + } + }, + "nodes": [ + {"node_id": "dci-dc1-beijing-vm-0", "region": "dc1", "location": "beijing", "service_region": "east", "labels": ["dci", "cloudsim", "cloudsimplus", "cpu", "latency-sensitive"], "capacity": {"cpu": 8, "memory": 32, "gpu": 0, "storage": 120}, "cost_per_tick": 0.9, "base_reliability": 0.985, "health_score": 0.94, "performance_factors": {"batch": 1.12, "batch_cpu": 1.12, "inference": 0.95}}, + {"node_id": "dci-dc1-beijing-vm-1", "region": "dc1", "location": "beijing", "service_region": "east", "labels": ["dci", "cloudsim", "cloudsimplus", "cpu", "latency-sensitive"], "capacity": {"cpu": 8, "memory": 32, "gpu": 0, "storage": 120}, "cost_per_tick": 0.92, "base_reliability": 0.984, "health_score": 0.935, "performance_factors": {"batch": 1.08, "batch_cpu": 1.08, "inference": 0.95}}, + {"node_id": "dci-dc1-beijing-vm-2", "region": "dc1", "location": "beijing", "service_region": "east", "labels": ["dci", "cloudsim", "cloudsimplus", "cpu"], "capacity": {"cpu": 12, "memory": 48, "gpu": 0, "storage": 160}, "cost_per_tick": 1.05, "base_reliability": 0.982, "health_score": 0.91, "performance_factors": {"batch": 1.18, "batch_cpu": 1.18, "inference": 1.0}}, + {"node_id": "dci-dc1-beijing-vm-3", "region": "dc1", "location": "beijing", "service_region": "east", "labels": ["dci", "cloudsim", "cloudsimplus", "cpu"], "capacity": {"cpu": 12, "memory": 48, "gpu": 0, "storage": 160}, "cost_per_tick": 1.06, "base_reliability": 0.981, "health_score": 0.91, "performance_factors": {"batch": 1.15, "batch_cpu": 1.15, "inference": 1.0}}, + {"node_id": "dci-dc1-hangzhou-vm-0", "region": "dc1", "location": "hangzhou", "service_region": "east", "labels": ["dci", "cloudsim", "cloudsimplus", "cpu", "low-latency"], "capacity": {"cpu": 16, "memory": 64, "gpu": 0, "storage": 200}, "cost_per_tick": 1.12, "base_reliability": 0.983, "health_score": 0.891, "performance_factors": {"batch": 1.24, "batch_cpu": 1.24, "inference": 1.05}}, + {"node_id": "dci-dc1-hangzhou-vm-1", "region": "dc1", "location": "hangzhou", "service_region": "east", "labels": ["dci", "cloudsim", "cloudsimplus", "cpu", "low-latency"], "capacity": {"cpu": 16, "memory": 64, "gpu": 0, "storage": 200}, "cost_per_tick": 1.1, "base_reliability": 0.984, "health_score": 0.899, "performance_factors": {"batch": 1.28, "batch_cpu": 1.28, "inference": 1.05}}, + {"node_id": "dci-dc1-hangzhou-vm-2", "region": "dc1", "location": "hangzhou", "service_region": "east", "labels": ["dci", "cloudsim", "cloudsimplus", "cpu", "low-latency"], "capacity": {"cpu": 12, "memory": 48, "gpu": 0, "storage": 180}, "cost_per_tick": 1.0, "base_reliability": 0.982, "health_score": 0.889, "performance_factors": {"batch": 1.2, "batch_cpu": 1.2, "inference": 1.02}}, + {"node_id": "dci-dc1-hangzhou-vm-3", "region": "dc1", "location": "hangzhou", "service_region": "east", "labels": ["dci", "cloudsim", "cloudsimplus", "cpu", "low-latency"], "capacity": {"cpu": 12, "memory": 48, "gpu": 0, "storage": 180}, "cost_per_tick": 1.01, "base_reliability": 0.981, "health_score": 0.877, "performance_factors": {"batch": 1.16, "batch_cpu": 1.16, "inference": 1.0}}, + {"node_id": "dci-dc2-chengdu-vm-0", "region": "dc2", "location": "chengdu", "service_region": "west", "labels": ["dci", "cloudsim", "cloudsimplus", "cpu"], "capacity": {"cpu": 12, "memory": 48, "gpu": 0, "storage": 180}, "cost_per_tick": 0.96, "base_reliability": 0.982, "health_score": 0.902, "performance_factors": {"batch": 1.1, "batch_cpu": 1.1, "inference": 0.98}}, + {"node_id": "dci-dc2-chengdu-vm-1", "region": "dc2", "location": "chengdu", "service_region": "west", "labels": ["dci", "cloudsim", "cloudsimplus", "cpu"], "capacity": {"cpu": 12, "memory": 48, "gpu": 0, "storage": 180}, "cost_per_tick": 0.95, "base_reliability": 0.983, "health_score": 0.922, "performance_factors": {"batch": 1.12, "batch_cpu": 1.12, "inference": 0.98}}, + {"node_id": "dci-dc2-chengdu-vm-2", "region": "dc2", "location": "chengdu", "service_region": "west", "labels": ["dci", "cloudsim", "cloudsimplus", "cpu"], "capacity": {"cpu": 16, "memory": 64, "gpu": 0, "storage": 220}, "cost_per_tick": 1.08, "base_reliability": 0.984, "health_score": 0.926, "performance_factors": {"batch": 1.2, "batch_cpu": 1.2, "inference": 1.0}}, + {"node_id": "dci-dc2-chengdu-vm-3", "region": "dc2", "location": "chengdu", "service_region": "west", "labels": ["dci", "cloudsim", "cloudsimplus", "cpu"], "capacity": {"cpu": 16, "memory": 64, "gpu": 0, "storage": 220}, "cost_per_tick": 1.09, "base_reliability": 0.985, "health_score": 0.944, "performance_factors": {"batch": 1.22, "batch_cpu": 1.22, "inference": 1.0}}, + {"node_id": "dci-dc2-chongqing-vm-0", "region": "dc2", "location": "chongqing", "service_region": "west", "labels": ["dci", "cloudsim", "cloudsimplus", "cpu", "reliable"], "capacity": {"cpu": 16, "memory": 64, "gpu": 0, "storage": 220}, "cost_per_tick": 1.07, "base_reliability": 0.986, "health_score": 0.953, "performance_factors": {"batch": 1.2, "batch_cpu": 1.2, "inference": 1.0}}, + {"node_id": "dci-dc2-chongqing-vm-1", "region": "dc2", "location": "chongqing", "service_region": "west", "labels": ["dci", "cloudsim", "cloudsimplus", "cpu", "reliable"], "capacity": {"cpu": 16, "memory": 64, "gpu": 0, "storage": 220}, "cost_per_tick": 1.07, "base_reliability": 0.987, "health_score": 0.959, "performance_factors": {"batch": 1.22, "batch_cpu": 1.22, "inference": 1.0}}, + {"node_id": "dci-dc2-chongqing-vm-2", "region": "dc2", "location": "chongqing", "service_region": "west", "labels": ["dci", "cloudsim", "cloudsimplus", "cpu", "reliable"], "capacity": {"cpu": 12, "memory": 48, "gpu": 0, "storage": 180}, "cost_per_tick": 0.98, "base_reliability": 0.986, "health_score": 0.957, "performance_factors": {"batch": 1.14, "batch_cpu": 1.14, "inference": 0.98}}, + {"node_id": "dci-dc2-chongqing-vm-3", "region": "dc2", "location": "chongqing", "service_region": "west", "labels": ["dci", "cloudsim", "cloudsimplus", "cpu", "reliable"], "capacity": {"cpu": 12, "memory": 48, "gpu": 0, "storage": 180}, "cost_per_tick": 0.99, "base_reliability": 0.987, "health_score": 0.96, "performance_factors": {"batch": 1.16, "batch_cpu": 1.16, "inference": 0.98}}, + {"node_id": "dci-dc3-guangzhou-vm-0", "region": "dc3", "location": "guangzhou", "service_region": "south", "labels": ["dci", "cloudsim", "cloudsimplus", "cpu"], "capacity": {"cpu": 16, "memory": 64, "gpu": 0, "storage": 220}, "cost_per_tick": 1.05, "base_reliability": 0.985, "health_score": 0.95, "performance_factors": {"batch": 1.18, "batch_cpu": 1.18, "inference": 1.0}}, + {"node_id": "dci-dc3-guangzhou-vm-1", "region": "dc3", "location": "guangzhou", "service_region": "south", "labels": ["dci", "cloudsim", "cloudsimplus", "cpu"], "capacity": {"cpu": 16, "memory": 64, "gpu": 0, "storage": 220}, "cost_per_tick": 1.05, "base_reliability": 0.984, "health_score": 0.947, "performance_factors": {"batch": 1.16, "batch_cpu": 1.16, "inference": 1.0}}, + {"node_id": "dci-dc3-guangzhou-vm-2", "region": "dc3", "location": "guangzhou", "service_region": "south", "labels": ["dci", "cloudsim", "cloudsimplus", "cpu"], "capacity": {"cpu": 12, "memory": 48, "gpu": 0, "storage": 180}, "cost_per_tick": 0.97, "base_reliability": 0.982, "health_score": 0.93, "performance_factors": {"batch": 1.1, "batch_cpu": 1.1, "inference": 0.98}}, + {"node_id": "dci-dc3-guangzhou-vm-3", "region": "dc3", "location": "guangzhou", "service_region": "south", "labels": ["dci", "cloudsim", "cloudsimplus", "cpu"], "capacity": {"cpu": 12, "memory": 48, "gpu": 0, "storage": 180}, "cost_per_tick": 0.98, "base_reliability": 0.981, "health_score": 0.917, "performance_factors": {"batch": 1.08, "batch_cpu": 1.08, "inference": 0.98}}, + {"node_id": "dci-dc3-shenzhen-vm-0", "region": "dc3", "location": "shenzhen", "service_region": "south", "labels": ["dci", "cloudsim", "cloudsimplus", "cpu", "low-latency"], "capacity": {"cpu": 16, "memory": 64, "gpu": 0, "storage": 220}, "cost_per_tick": 1.08, "base_reliability": 0.983, "health_score": 0.905, "performance_factors": {"batch": 1.2, "batch_cpu": 1.2, "inference": 1.02}}, + {"node_id": "dci-dc3-shenzhen-vm-1", "region": "dc3", "location": "shenzhen", "service_region": "south", "labels": ["dci", "cloudsim", "cloudsimplus", "cpu", "low-latency"], "capacity": {"cpu": 16, "memory": 64, "gpu": 0, "storage": 220}, "cost_per_tick": 1.08, "base_reliability": 0.982, "health_score": 0.895, "performance_factors": {"batch": 1.18, "batch_cpu": 1.18, "inference": 1.02}}, + {"node_id": "dci-dc3-shenzhen-vm-2", "region": "dc3", "location": "shenzhen", "service_region": "south", "labels": ["dci", "cloudsim", "cloudsimplus", "cpu", "low-latency"], "capacity": {"cpu": 12, "memory": 48, "gpu": 0, "storage": 180}, "cost_per_tick": 1.0, "base_reliability": 0.981, "health_score": 0.888, "performance_factors": {"batch": 1.1, "batch_cpu": 1.1, "inference": 1.0}}, + {"node_id": "dci-dc3-shenzhen-vm-3", "region": "dc3", "location": "shenzhen", "service_region": "south", "labels": ["dci", "cloudsim", "cloudsimplus", "cpu", "low-latency"], "capacity": {"cpu": 12, "memory": 48, "gpu": 0, "storage": 180}, "cost_per_tick": 1.01, "base_reliability": 0.981, "health_score": 0.886, "performance_factors": {"batch": 1.08, "batch_cpu": 1.08, "inference": 1.0}} + ] +} diff --git a/configs/tianjun.example.toml b/configs/tianjun.example.toml index 0fb467b..a5a3563 100644 --- a/configs/tianjun.example.toml +++ b/configs/tianjun.example.toml @@ -51,6 +51,14 @@ server = "http://127.0.0.1:8024" max_cycles = 30 poll_interval_seconds = 1.0 +[simulation] +server = "http://127.0.0.1:8024" +inventory = "${TIANJUN_HOME}/configs/dci_runtime_topology.example.json" +auto_start = false +poll_interval_seconds = 1.0 +time_scale = 0.08 +verbose = false + [real_agent] server = "http://127.0.0.1:8024" node_config = "${TIANJUN_HOME}/examples/real_node_agent.example.json" diff --git a/restart_tianjun.bat b/restart_tianjun.bat index c178a19..7a6be9d 100644 --- a/restart_tianjun.bat +++ b/restart_tianjun.bat @@ -37,8 +37,8 @@ if errorlevel 1 ( echo Starting Tianjun Engine control plane... echo Dashboard: %URL% -echo Starting control plane... -start "Tianjun Control Plane" cmd /k python -B main.py serve --config "%CONFIG%" --default-execution-mode simulation --host %HOST% --port %PORT% +echo Starting control plane with embedded DCI simulation backend... +start "Tianjun Control Plane" cmd /k python -B main.py serve --config "%CONFIG%" --default-execution-mode simulation --host %HOST% --port %PORT% --auto-sim-backend echo Waiting for control plane health check... powershell -NoProfile -ExecutionPolicy Bypass -Command ^ @@ -52,6 +52,5 @@ if errorlevel 1 ( start "" "%URL%" echo Tianjun Engine control plane is running in a separate window. -echo No simulated nodes are started automatically. -echo Start CloudSim Plus, sim-backend, or a node agent manually when you want nodes to appear. +echo Embedded DCI simulated nodes are running and will pull pending tasks automatically. pause diff --git a/src/tianjun/cli.py b/src/tianjun/cli.py index 94b972e..0ee140b 100644 --- a/src/tianjun/cli.py +++ b/src/tianjun/cli.py @@ -20,7 +20,7 @@ from tianjun.scenarios import load_scenario_payload, node_from_dict, task_from_dict from tianjun.domain import ExecutionMode from tianjun.inventory import load_inventory_config -from tianjun.simulation import run_simulation_backend +from tianjun.simulation import BackgroundSimulationBackend, run_simulation_backend def add_llm_options(parser: argparse.ArgumentParser) -> None: @@ -145,6 +145,11 @@ def build_parser() -> argparse.ArgumentParser: serve.add_argument("--scenario", type=Path, help="Optional demo scenario to preload. Omitted by default for a clean control plane.") serve.add_argument("--demo", action="store_true", help="Preload examples/runtime_scenario.json demo nodes and tasks.") serve.add_argument("--inventory", type=Path, help="Config-driven simulated inventory JSON/TOML/YAML to register at startup.") + serve.add_argument("--auto-sim-backend", action="store_true", help="Start an embedded long-running simulation backend with --inventory.") + serve.add_argument("--sim-node-id", action="append", help="Limit embedded simulation backend to one node id; repeat to include multiple nodes.") + serve.add_argument("--sim-poll-interval", type=float, help="Embedded simulation backend poll interval in seconds.") + serve.add_argument("--sim-time-scale", type=float, help="Embedded simulation acceleration factor. Smaller is faster.") + serve.add_argument("--sim-verbose", action="store_true", help="Print concise embedded simulation backend logs.") serve.add_argument("--default-execution-mode", choices=[item.value for item in ExecutionMode], help="Default execution mode for chat/policy-generated tasks when no explicit execution payload is provided.") serve.add_argument("--state-db", type=Path) serve.add_argument("--heartbeat-timeout-seconds", type=float) @@ -305,11 +310,17 @@ def main() -> None: default_execution_mode = first_present(args.default_execution_mode, app_config.get("server.default_execution_mode"), app_config.get("simulation.default_execution_mode")) if default_execution_mode: control_plane.policy_generator.default_execution_mode = ExecutionMode(str(default_execution_mode)) + auto_sim_backend = bool( + args.auto_sim_backend + or config_bool(app_config.get("simulation.auto_start"), default=False) + or config_bool(app_config.get("server.auto_sim_backend"), default=False) + ) + if auto_sim_backend and inventory is None: + inventory = config_path("configs/dci_runtime_topology.example.json") if inventory: - # Validate the inventory path/configuration, but do not register its - # nodes here. The dashboard should discover resources only after the - # CloudSimPlus/simulation backend reports them. - load_inventory_config(inventory) + inventory_config = load_inventory_config(inventory) + else: + inventory_config = None if scenario and not control_plane.tasks: payload = load_scenario_payload(scenario) for node_data in payload.get("nodes", []): @@ -318,13 +329,46 @@ def main() -> None: control_plane.submit_task(task_from_dict(task_data)) chat_runtime = ChatRuntime.with_llm_settings(control_plane, resolved_llm_settings(args, app_config)) server = build_http_server(control_plane, host, port, chat_runtime=chat_runtime) + background_sim: BackgroundSimulationBackend | None = None + if auto_sim_backend: + if inventory_config is None: + raise ValueError("embedded simulation backend requires --inventory or simulation.inventory.") + configured_node_ids = app_config.get("simulation.node_ids") + if isinstance(configured_node_ids, str): + configured_node_ids = [item.strip() for item in configured_node_ids.split(",") if item.strip()] + + def sim_log(message: str) -> None: + if args.sim_verbose or config_bool(app_config.get("simulation.verbose"), default=False): + print(f"[sim] {message}", flush=True) + + background_sim = BackgroundSimulationBackend( + config=inventory_config, + control_plane=control_plane, + node_ids=args.sim_node_id or configured_node_ids, + poll_interval_seconds=float(first_present( + args.sim_poll_interval, + app_config.get("simulation.poll_interval_seconds"), + default=1.0, + )), + time_scale=float(first_present( + args.sim_time_scale, + app_config.get("simulation.time_scale"), + default=0.08, + )), + log=sim_log, + ) + background_sim.start() print(f"Control plane listening on http://{host}:{port}") print(f"Dashboard available at http://{host}:{port}/dashboard") + if background_sim is not None: + print(f"Embedded simulation backend active with {len(background_sim.node_ids)} nodes.") try: server.serve_forever() except KeyboardInterrupt: pass finally: + if background_sim is not None: + background_sim.stop() server.server_close() if state_store is not None: state_store.close() diff --git a/src/tianjun/node_agent/clients.py b/src/tianjun/node_agent/clients.py index c9c2472..df99d3e 100644 --- a/src/tianjun/node_agent/clients.py +++ b/src/tianjun/node_agent/clients.py @@ -17,6 +17,9 @@ def __init__(self, control_plane: CentralControlPlane) -> None: def register_node(self, node: Node) -> dict[str, Any]: return self.control_plane.register_node(node_from_dict(node.to_dict())) + def register_topology(self, payload: dict[str, Any]) -> dict[str, Any]: + return self.control_plane.register_topology(payload) + def submit_task(self, task: Task) -> dict[str, Any]: return self.control_plane.submit_task(task_from_dict(task.to_dict())) @@ -72,6 +75,9 @@ def __init__(self, base_url: str) -> None: def register_node(self, node: Node) -> dict[str, Any]: return self._post_json("/nodes/register", node.to_dict()) + def register_topology(self, payload: dict[str, Any]) -> dict[str, Any]: + return self._post_json("/topology/register", payload) + def submit_task(self, task: Task) -> dict[str, Any]: return self._post_json("/tasks", task.to_dict()) diff --git a/src/tianjun/simulation/__init__.py b/src/tianjun/simulation/__init__.py index 8b13d16..fc4ef9a 100644 --- a/src/tianjun/simulation/__init__.py +++ b/src/tianjun/simulation/__init__.py @@ -1,3 +1,3 @@ -from .backend import run_simulation_backend +from .backend import BackgroundSimulationBackend, run_simulation_backend -__all__ = ["run_simulation_backend"] +__all__ = ["BackgroundSimulationBackend", "run_simulation_backend"] diff --git a/src/tianjun/simulation/backend.py b/src/tianjun/simulation/backend.py index 24c68c9..35973fe 100644 --- a/src/tianjun/simulation/backend.py +++ b/src/tianjun/simulation/backend.py @@ -1,14 +1,16 @@ from __future__ import annotations import time +from threading import Event, Thread from dataclasses import dataclass, field from pathlib import Path from typing import Any, Callable +from ..application.control_plane import CentralControlPlane from ..domain import Node, Task from ..execution.executors import ExecutionResult, ExecutorRegistry from ..inventory import load_inventory_config, nodes_from_inventory -from ..node_agent.clients import HttpControlPlaneClient +from ..node_agent.clients import DirectControlPlaneClient, HttpControlPlaneClient from ..scenarios import task_from_dict LogFn = Callable[[str], None] @@ -246,6 +248,78 @@ def _stages_from_result(self, result: ExecutionResult) -> list[dict[str, Any]]: return normalized +def _register_topology_if_present(client: Any, config: dict[str, Any], log: LogFn) -> None: + topology = config.get("physical_topology") or config.get("topology") + if not isinstance(topology, dict): + return + client.register_topology(topology) + log(f"topology online: {topology.get('topology_id', 'physical_topology')}") + + +class BackgroundSimulationBackend: + """Embedded simulation backend for one-process demos. + + The control plane remains authoritative. This background runtime only acts + like a group of simulated node agents: register topology/nodes, heartbeat, + pull leases and report task progress/results. + """ + + def __init__( + self, + *, + config: dict[str, Any], + control_plane: CentralControlPlane, + node_ids: list[str] | None = None, + poll_interval_seconds: float = 1.0, + time_scale: float | None = None, + log: LogFn | None = None, + ) -> None: + wanted = set(node_ids or []) + nodes = [node for node in nodes_from_inventory(config) if not wanted or node.node_id in wanted] + if not nodes: + raise ValueError("background simulation backend has no nodes to register; check config or node filters.") + self.config = config + self.client = DirectControlPlaneClient(control_plane) + self.runtime = SimulatedNodeRuntime( + nodes=nodes, + client=self.client, + executors=ExecutorRegistry(simulation_config=config), + poll_interval_seconds=poll_interval_seconds, + time_scale=float(time_scale if time_scale is not None else config.get("time_scale", 0.08)), + log=log or (lambda message: None), + ) + self.poll_interval_seconds = poll_interval_seconds + self.log = log or (lambda message: None) + self._stop = Event() + self._thread: Thread | None = None + + @property + def node_ids(self) -> list[str]: + return [node.node_id for node in self.runtime.nodes] + + def start(self) -> None: + if self._thread is not None and self._thread.is_alive(): + return + _register_topology_if_present(self.client, self.config, self.log) + self.runtime.register_nodes() + self._thread = Thread(target=self._run, name="tianjun-background-sim", daemon=True) + self._thread.start() + self.log(f"background simulation backend running with {len(self.node_ids)} nodes") + + def stop(self, timeout: float = 5.0) -> None: + self._stop.set() + if self._thread is not None: + self._thread.join(timeout=timeout) + + def _run(self) -> None: + try: + while not self._stop.is_set(): + self.runtime.tick() + self._stop.wait(max(0.0, self.poll_interval_seconds)) + finally: + self.runtime.mark_nodes_offline() + + def run_simulation_backend( *, config_path: str | Path, @@ -272,6 +346,7 @@ def log(message: str) -> None: print(f"[sim] {message}", flush=True) client = HttpControlPlaneClient(server) + _register_topology_if_present(client, config, log) runtime = SimulatedNodeRuntime( nodes=nodes, client=client, diff --git a/start_tianjun.bat b/start_tianjun.bat index 87c975b..9e59429 100644 --- a/start_tianjun.bat +++ b/start_tianjun.bat @@ -35,8 +35,8 @@ if errorlevel 1 ( exit /b 1 ) -echo Starting control plane... -start "Tianjun Control Plane" cmd /k python -B main.py serve --config "%CONFIG%" --default-execution-mode simulation --host %HOST% --port %PORT% +echo Starting control plane with embedded DCI simulation backend... +start "Tianjun Control Plane" cmd /k python -B main.py serve --config "%CONFIG%" --default-execution-mode simulation --host %HOST% --port %PORT% --auto-sim-backend echo Waiting for control plane health check... powershell -NoProfile -ExecutionPolicy Bypass -Command ^ @@ -50,5 +50,4 @@ if errorlevel 1 ( start "" "%URL%" echo Tianjun Engine control plane is running in a separate window. -echo No simulated nodes are started automatically. -echo Start CloudSim Plus, sim-backend, or a node agent manually when you want nodes to appear. +echo Embedded DCI simulated nodes are running and will pull pending tasks automatically. From e516a51844498a768e86c574747976a04e50aac5 Mon Sep 17 00:00:00 2001 From: Yu <1305203710@qq.com> Date: Sun, 7 Jun 2026 19:24:26 +0800 Subject: [PATCH 2/2] Make topology route follow live task state --- .../dashboard/static/css/pages/topology.css | 39 ++ .../dashboard/static/js/topology.js | 338 ++++++++++++++++-- 2 files changed, 344 insertions(+), 33 deletions(-) diff --git a/src/tianjun/interfaces/dashboard/static/css/pages/topology.css b/src/tianjun/interfaces/dashboard/static/css/pages/topology.css index 75c580b..9a11dfa 100644 --- a/src/tianjun/interfaces/dashboard/static/css/pages/topology.css +++ b/src/tianjun/interfaces/dashboard/static/css/pages/topology.css @@ -18,6 +18,35 @@ padding-bottom: 2px; } +.path-metrics { + grid-template-columns: repeat(6, minmax(0, 1fr)); + gap: 10px; + margin-top: 12px; +} + +.path-metric-card { + min-width: 0; + padding: 10px 12px; + border: 0.5px solid rgba(14, 165, 233, .2); + border-radius: 8px; + background: rgba(240, 249, 255, .76); +} + +.path-metric-card span { + display: block; + margin-bottom: 5px; + color: var(--color-gray-400); + font-size: 12px; + font-weight: 800; +} + +.path-metric-card b { + display: block; + color: #075985; + font-size: 14px; + overflow-wrap: anywhere; +} + .network-topology-shell { width: 100%; min-width: 820px; @@ -1158,6 +1187,10 @@ grid-template-columns: minmax(0, 1fr); } + .path-metrics { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + .topology-layout > aside { min-height: auto; } @@ -1175,3 +1208,9 @@ overflow-x: auto; } } + +@media (max-width: 720px) { + .path-metrics { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} diff --git a/src/tianjun/interfaces/dashboard/static/js/topology.js b/src/tianjun/interfaces/dashboard/static/js/topology.js index 439f632..94ecde7 100644 --- a/src/tianjun/interfaces/dashboard/static/js/topology.js +++ b/src/tianjun/interfaces/dashboard/static/js/topology.js @@ -4,16 +4,53 @@ let activeTopologyKey = "global"; let selectedDetail = null; let resizeHandler = null; let latestTopologyReport = null; +let livePathContext = null; const schedulerStatus = { task: "inference-task-027", source: "User-Access", - target: "DC2 / 深圳资源区", + target: "DC2 / 成都资源区", strategy: "延迟优先 + 负载均衡", link: "正常", gnn: "0.91", }; +const dcZoneModel = { + dc1: { + dcName: "DC1", + region: "东部", + border: "Border1", + pe: "PE1", + gateway: "DC1-GW", + zones: { + beijing: { side: "left", leaf: "leaf-a", cluster: "cluster-a", label: "北京资源区", clusterName: "北京计算集群" }, + hangzhou: { side: "right", leaf: "leaf-c", cluster: "cluster-b", label: "杭州资源区", clusterName: "杭州计算集群" }, + }, + }, + dc2: { + dcName: "DC2", + region: "西部", + border: "Border2", + pe: "PE2", + gateway: "DC2-GW", + zones: { + chengdu: { side: "left", leaf: "leaf-a", cluster: "cluster-a", label: "成都资源区", clusterName: "成都计算集群" }, + chongqing: { side: "right", leaf: "leaf-c", cluster: "cluster-b", label: "重庆资源区", clusterName: "重庆计算集群" }, + }, + }, + dc3: { + dcName: "DC3", + region: "华南", + border: "Border3", + pe: "PE3", + gateway: "DC3-GW", + zones: { + guangzhou: { side: "left", leaf: "leaf-a", cluster: "cluster-a", label: "广州资源区", clusterName: "广州计算集群" }, + shenzhen: { side: "right", leaf: "leaf-c", cluster: "cluster-b", label: "深圳资源区", clusterName: "深圳计算集群" }, + }, + }, +}; + const globalTopology = { key: "global", id: "global", @@ -31,15 +68,15 @@ const globalTopology = { nodes: [ node("user-access", "User-Access", "用户业务接入点", "access", "access", 50, 10, "业务请求入口与调度流量接入", "3ms · 10Gbps", "正常"), node("border1", "Border1", "DC1 侧出口路由", "border", "border", 23, 27, "东部数据中心边界出口", "40Gbps · 0.5~0.7ms", "正常"), - node("border2", "Border2", "DC2 侧入口路由", "border", "border", 72, 27, "华南数据中心边界入口", "40Gbps · 0.5~0.7ms", "正常"), + node("border2", "Border2", "DC2 侧入口路由", "border", "border", 72, 27, "西部数据中心边界入口", "40Gbps · 0.5~0.7ms", "正常"), node("pe1", "PE1", "DC1 侧 MPLS VPN 边缘", "pe", "pe", 23, 44, "Provider Edge 边缘路由器", "40Gbps", "正常"), node("pe3", "PE3(枢纽)", "DC2/DC3 侧 VPN + 分叉", "pe", "pe", 72, 44, "跨域 VPN 枢纽与分支调度入口", "20Gbps", "正常"), node("p-core-a", "P-Core-A", "MPLS 标签交换 · 6ms", "core", "core", 40, 61, "Provider Core 骨干路由器", "10Gbps · 6ms", "正常"), node("p-core-b", "P-Core-B", "MPLS 标签交换 · 6ms", "core", "core", 58, 61, "Provider Core 骨干路由器", "10Gbps · 6ms", "拥塞"), node("dc1", "DC1(东部)", "北京 · 杭州 · 8 节点", "dc", "dc", 23, 82, "东部资源池,可下钻查看内部调度资源", "40Gbps · 0.5~0.7ms", "正常", "dc1", { region: "东部", zones: 2, vmTotal: 8, avgCpu: 42, tasks: 14, scheduleState: "可调度" }), - node("dc2", "DC2(华南)", "广州 · 深圳 · 8 节点", "dc", "dc", 72, 82, "华南资源池,可下钻查看内部调度资源", "40Gbps", "正常", "dc2", { region: "华南", zones: 2, vmTotal: 8, avgCpu: 58, tasks: 18, scheduleState: "调度中" }), - node("border3", "Border3", "DC3 分支边界 · 1.0ms", "border", "border", 87, 61, "西部数据中心分支入口路由", "1.0ms", "正常"), - node("dc3", "DC3(西部)", "成都 · 重庆 · 8 节点", "dc", "dc", 87, 82, "西部资源池,可下钻查看内部调度资源", "分支链路 · Border3 1.0ms", "正常", "dc3", { region: "西部", zones: 2, vmTotal: 8, avgCpu: 47, tasks: 13, scheduleState: "可调度" }), + node("dc2", "DC2(西部)", "成都 · 重庆 · 8 节点", "dc", "dc", 72, 82, "西部资源池,可下钻查看内部调度资源", "40Gbps", "正常", "dc2", { region: "西部", zones: 2, vmTotal: 8, avgCpu: 58, tasks: 18, scheduleState: "调度中" }), + node("border3", "Border3", "DC3 分支边界 · 1.0ms", "border", "border", 87, 61, "华南数据中心分支入口路由", "1.0ms", "正常"), + node("dc3", "DC3(华南)", "广州 · 深圳 · 8 节点", "dc", "dc", 87, 82, "华南资源池,可下钻查看内部调度资源", "分支链路 · Border3 1.0ms", "正常", "dc3", { region: "华南", zones: 2, vmTotal: 8, avgCpu: 47, tasks: 13, scheduleState: "可调度" }), ], links: [ link("user-access", "pe3", "access", "3ms · 10Gbps", "接入链路", { showLabel: true, labelAnchor: "mid" }), @@ -51,7 +88,7 @@ const globalTopology = { link("pe3", "border2", "main", "20Gbps · 1.2ms", "PE 到边界主链路", { showLabel: false }), link("border2", "dc2", "main", "40Gbps · 0.5~0.7ms", "DC 出入口链路", { showLabel: true, labelAnchor: "right" }), link("pe3", "border3", "branch", "Border3 · 1.0ms", "DC3 分支链路", { showLabel: false }), - link("border3", "dc3", "branch", "分支链路", "西部数据中心分支链路", { showLabel: false }), + link("border3", "dc3", "branch", "分支链路", "华南数据中心分支链路", { showLabel: false }), ], footer: [ "点击任意数据中心可查看内部节点", @@ -86,36 +123,36 @@ const dcTopologies = { key: "dc2", dcName: "DC2", title: "DC2 数据中心内部拓扑", - subtitle: "华南数据中心 Spine-Leaf Fabric 与服务支撑拓扑", - region: "华南", + subtitle: "西部数据中心 Spine-Leaf Fabric 与服务支撑拓扑", + region: "西部", border: "Border2", - pe: "PE3", + pe: "PE2", gateway: "DC2-GW", zones: [ - zone("guangzhou", "广州资源区", ["Leaf-GZ-1", "Leaf-GZ-2"], "广州计算集群", "高负载", "congested", 76, 68, 13), - zone("shenzhen", "深圳资源区", ["Leaf-SZ-1", "Leaf-SZ-2"], "深圳计算集群", "正在调度", "scheduling", 39, 48, 5), + zone("chengdu", "成都资源区", ["Leaf-CD-1", "Leaf-CD-2"], "成都计算集群", "正在调度", "scheduling", 58, 51, 9), + zone("chongqing", "重庆资源区", ["Leaf-CQ-1", "Leaf-CQ-2"], "重庆计算集群", "可调度", "ok", 34, 45, 4), ], - path: "User-Access → DC2 → 深圳计算集群 / VM-02", - internalPath: "DC2-GW → Spine-A → Fabric Bus → Leaf-SZ-1 → 深圳计算集群 → VM-02", - routeLeaf: "leaf-c", - routeCluster: "cluster-b", + path: "User-Access → DC2 → 成都计算集群 / VM-02", + internalPath: "DC2-GW → Spine-A → Fabric Bus → Leaf-CD-1 → 成都计算集群 → VM-02", + routeLeaf: "leaf-a", + routeCluster: "cluster-a", routeVm: "VM-02", }), dc3: makeDcTopology({ key: "dc3", dcName: "DC3", title: "DC3 数据中心内部拓扑", - subtitle: "西部数据中心 Spine-Leaf Fabric 与服务支撑拓扑", - region: "西部", + subtitle: "华南数据中心 Spine-Leaf Fabric 与服务支撑拓扑", + region: "华南", border: "Border3", pe: "PE3", gateway: "DC3-GW", zones: [ - zone("chengdu", "成都资源区", ["Leaf-CD-1", "Leaf-CD-2"], "成都计算集群", "正在调度", "scheduling", 58, 51, 9), - zone("chongqing", "重庆资源区", ["Leaf-CQ-1", "Leaf-CQ-2"], "重庆计算集群", "可调度", "ok", 34, 45, 4), + zone("guangzhou", "广州资源区", ["Leaf-GZ-1", "Leaf-GZ-2"], "广州计算集群", "高负载", "congested", 76, 68, 13), + zone("shenzhen", "深圳资源区", ["Leaf-SZ-1", "Leaf-SZ-2"], "深圳计算集群", "正在调度", "scheduling", 39, 48, 5), ], - path: "User-Access → DC3 → 成都计算集群 / VM-02", - internalPath: "DC3-GW → Spine-A → Fabric Bus → Leaf-CD-1 → 成都计算集群 → VM-02", + path: "User-Access → DC3 → 广州计算集群 / VM-02", + internalPath: "DC3-GW → Spine-A → Fabric Bus → Leaf-GZ-1 → 广州计算集群 → VM-02", routeLeaf: "leaf-a", routeCluster: "cluster-a", routeVm: "VM-02", @@ -160,7 +197,7 @@ function normalizeGlobalTopology() { link("border2", "pe2", "main", "40Gbps · 0.5~0.7ms", "出口 B 到 PE2", { showLabel: false }), link("pe2", "dc2", "main", "40Gbps · 0.5~0.7ms", "PE2 到 DC2 资源池", { showLabel: true, labelAnchor: "right" }), link("border3", "pe3", "main", "40Gbps · 0.5~0.7ms", "分支出口到 PE3", { showLabel: false }), - link("pe3", "dc3", "main", "40Gbps", "PE3 到 DC3 西部资源池", { showLabel: false }), + link("pe3", "dc3", "main", "40Gbps", "PE3 到 DC3 华南资源池", { showLabel: false }), link("pe1", "pe2", "main", "VPN 互联", "PE1 与 PE2 跨域 VPN 互联", { showLabel: true, labelAnchor: "below" }), link("pe2", "pe3", "main", "VPN 分支互联", "PE2 与 PE3 分支 VPN 互联", { showLabel: true, labelAnchor: "below" }), ]; @@ -259,11 +296,225 @@ function makeDcTopology(config) { }; } +function updateLiveTopology(report) { + livePathContext = buildLivePathContext(report); + if (!livePathContext) return; + + schedulerStatus.task = livePathContext.taskId; + schedulerStatus.source = livePathContext.source; + schedulerStatus.target = livePathContext.targetLabel; + schedulerStatus.strategy = livePathContext.strategy; + schedulerStatus.link = livePathContext.linkStatus; + schedulerStatus.gnn = livePathContext.gnn; + + const targetRoute = globalRouteForTargetDc(livePathContext.dcKey); + globalTopology.currentRoute = targetRoute.nodes; + globalTopology.currentPathText = livePathContext.globalPathText; + globalTopology.footer = [ + `实时来源:${livePathContext.sourceKind} / tick ${livePathContext.tick ?? "--"}`, + `目标节点:${livePathContext.nodeId}`, + `链路画像:${livePathContext.latencyText} / 风险 ${livePathContext.riskText}`, + ]; + + const dcTopology = dcTopologies[livePathContext.dcKey]; + if (!dcTopology) return; + dcTopology.routeLeaf = livePathContext.leafId; + dcTopology.routeCluster = livePathContext.clusterId; + dcTopology.routeVm = livePathContext.vmName; + dcTopology.vmId = livePathContext.vmId; + dcTopology.currentPath = livePathContext.globalPathText.replace("实时调度路径:", ""); + dcTopology.internalPath = `${dcTopology.nodes.find((item) => item.id === "gw")?.name ?? dcTopology.dcName} → Spine-A → Fabric Bus → ${livePathContext.leafLabel} → ${livePathContext.clusterName} → ${livePathContext.vmName}`; + dcTopology.currentRoute = ["gw", "spine-a", "fabric-bus", livePathContext.leafId, livePathContext.clusterId, livePathContext.vmId]; + dcTopology.footer = [ + `当前路径:${dcTopology.internalPath}`, + `实时任务:${livePathContext.taskId} / 阶段 ${livePathContext.stage}`, + `路径指标:${livePathContext.latencyText} / ${livePathContext.bandwidthText} / 稳定性 ${livePathContext.gnn}`, + ]; + dcTopology.zones = dcTopology.zones.map((zoneInfo) => { + const matching = livePathContext.zoneLabel === zoneInfo.name; + return { + ...zoneInfo, + scheduleState: matching ? livePathContext.zoneState : zoneInfo.scheduleState, + status: matching ? livePathContext.zoneStatus : zoneInfo.status, + tasks: livePathContext.zoneTaskCounts.get(zoneInfo.id) ?? zoneInfo.tasks, + cpu: livePathContext.zoneCpu.get(zoneInfo.id) ?? zoneInfo.cpu, + memory: livePathContext.zoneMemory.get(zoneInfo.id) ?? zoneInfo.memory, + }; + }); + for (const item of dcTopology.nodes) { + if (item.id === livePathContext.clusterId && item.metrics) { + item.metrics = dcTopology.zones.find((zoneInfo) => zoneInfo.name === livePathContext.zoneLabel) ?? item.metrics; + item.status = livePathContext.zoneState; + item.health = livePathContext.zoneStatus; + } + if (item.id === "scheduler" && item.scheduler) { + item.scheduler.latestPath = livePathContext.globalPathText.replace("实时调度路径:", ""); + item.scheduler.gnnScore = livePathContext.gnn; + item.scheduler.assignedTasks = Number(report?.totals?.running ?? 0) + Number(report?.totals?.pending ?? 0); + item.scheduler.avoidance = livePathContext.linkStatus === "拥塞" ? "正在规避高风险链路" : "当前路径风险可控"; + } + } +} + +function buildLivePathContext(report) { + if (!report) return null; + const nodes = Array.isArray(report.nodes) ? report.nodes : []; + const nodeByNodeId = new Map(nodes.map((nodeItem) => [nodeItem.node_id, nodeItem])); + const active = latestBy([...(report.active_runs ?? [])], "updated_at") ?? latestBy([...(report.active_runs ?? [])], "tick"); + const progressEvents = [...(report.recent_progress_events ?? [])]; + const progressCandidate = active + ? (latestBy(progressEvents.filter((item) => item.task_id === active.task_id), "updated_at") ?? latestBy(progressEvents.filter((item) => item.task_id === active.task_id), "tick")) + : (latestBy(progressEvents, "updated_at") ?? latestBy(progressEvents, "tick")); + const progress = active + ? progressCandidate + : (Number(progressCandidate?.tick ?? -1) >= Number(report.tick ?? 0) - 2 ? progressCandidate : null); + const decision = latestBy([...(report.recent_decisions ?? [])], "tick"); + const record = [...(report.execution_records ?? report.recent_records ?? [])].at(-1); + const pending = [...(report.pending_task_queue ?? [])].at(-1); + const source = active ? "active_run" : progress ? "progress" : decision ? "decision" : record ? "record" : pending ? "pending" : "inventory"; + const payload = active ?? progress ?? decision ?? record ?? pending ?? {}; + const task = payload.task ?? pending ?? {}; + const nodeId = payload.node_id ?? payload.target_node_id ?? task.target_node_id ?? task.last_scheduled_node ?? firstOnlineNodeId(nodes); + const node = nodeByNodeId.get(nodeId) ?? {}; + const parsed = parseDciNode(nodeId, node); + if (!parsed.dcKey) return null; + const zoneModel = dcZoneModel[parsed.dcKey]?.zones?.[parsed.location] ?? firstZoneModel(parsed.dcKey); + const vmNumber = parsed.vmIndex == null ? 2 : parsed.vmIndex + 1; + const vmName = `VM-${String(vmNumber).padStart(2, "0")}`; + const vmId = `${zoneModel.cluster}-vm-${String(vmNumber).padStart(2, "0")}`; + const snapshot = payload.network_snapshot ?? decision?.network_snapshot ?? {}; + const latency = firstNumber(snapshot.deterministic_latency_ms, snapshot.stable_latency_ms, snapshot.robust_latency_ms, payload.network_delay_ticks, payload.metrics?.network_latency_ms); + const risk = firstNumber(snapshot.uncertainty, payload.network_risk); + const bandwidth = firstNumber(snapshot.guaranteed_bandwidth_mbps, payload.effective_bandwidth_mbps); + const gnn = firstNumber(snapshot.gnn_stability_score, snapshot.feature_fusion_score, snapshot.model_prediction?.gnn_stability_score); + const utilization = payload.metrics?.simulated_utilization ?? {}; + const cpu = percentFrom(utilization.cpu, node.cpu_utilization, node.used_cpu_ratio); + const memory = percentFrom(utilization.memory, node.memory_utilization, node.used_memory_ratio); + const stage = payload.stage ?? (record ? "completed" : pending ? "pending" : "ready"); + const runningTaskIds = new Set((report.active_runs ?? []).map((item) => item.task_id)); + const zoneTaskCounts = zoneAggregate(nodes, report, "tasks"); + const zoneCpu = zoneAggregate(nodes, report, "cpu"); + const zoneMemory = zoneAggregate(nodes, report, "memory"); + if (cpu != null) zoneCpu.set(parsed.location, Math.max(zoneCpu.get(parsed.location) ?? 0, cpu)); + if (memory != null) zoneMemory.set(parsed.location, Math.max(zoneMemory.get(parsed.location) ?? 0, memory)); + if (runningTaskIds.has(payload.task_id)) { + zoneTaskCounts.set(parsed.location, Math.max(1, zoneTaskCounts.get(parsed.location) ?? 0)); + } + const running = source === "active_run" || source === "progress"; + const linkStatus = risk != null && risk > 0.28 ? "拥塞" : running ? "调度中" : "正常"; + const sourceKind = { + active_run: "正在执行", + progress: "最新进度", + decision: "最新决策", + record: "最新执行记录", + pending: "待调度任务", + inventory: "在线拓扑", + }[source]; + const taskId = payload.task_id ?? task.task_id ?? "等待任务"; + const dcName = dcZoneModel[parsed.dcKey]?.dcName ?? parsed.dcKey.toUpperCase(); + const targetLabel = `${dcName} / ${zoneModel.label} / ${vmName}`; + return { + sourceKind, + taskId, + nodeId, + dcKey: parsed.dcKey, + zoneLabel: zoneModel.label, + clusterName: zoneModel.clusterName, + clusterId: zoneModel.cluster, + leafId: zoneModel.leaf, + leafLabel: nodeName(zoneModel.leaf, parsed.location), + vmName, + vmId, + tick: payload.tick ?? report.tick, + stage, + source: task.source_region ?? task.data_region ?? "User-Access", + targetLabel, + strategy: decision?.policy_name ?? task.task_type ?? payload.task_type ?? "实时租约调度", + linkStatus, + gnn: gnn == null ? "--" : `${Math.round(gnn * 100)}%`, + riskText: risk == null ? "--" : `${Math.round(risk * 100)}%`, + latencyText: latency == null ? "--" : `${Number(latency).toFixed(1)}ms`, + bandwidthText: bandwidth == null ? "--" : `${Math.round(Number(bandwidth))}Mbps`, + globalPathText: `实时调度路径:User-Access → ${dcName} → ${zoneModel.clusterName} / ${vmName}`, + zoneState: running ? "正在调度" : "可调度", + zoneStatus: running ? "scheduling" : "ok", + zoneTaskCounts, + zoneCpu, + zoneMemory, + }; +} + +function latestBy(items, key) { + return items.filter(Boolean).sort((a, b) => Number(a?.[key] ?? 0) - Number(b?.[key] ?? 0)).at(-1); +} + +function firstNumber(...values) { + for (const value of values) { + const numeric = Number(value); + if (Number.isFinite(numeric)) return numeric; + } + return null; +} + +function percentFrom(...values) { + const value = firstNumber(...values); + if (value == null) return null; + return Math.round(value <= 1 ? value * 100 : value); +} + +function parseDciNode(nodeId = "", node = {}) { + const match = String(nodeId).match(/^dci-dc(\d+)-([a-z]+)-vm-(\d+)$/i); + const dcKey = match ? `dc${match[1]}` : String(node.region ?? "").match(/^dc\d+$/i)?.[0]?.toLowerCase(); + return { + dcKey, + location: String(match?.[2] ?? node.location ?? "").toLowerCase(), + vmIndex: match ? Number(match[3]) : null, + }; +} + +function firstZoneModel(dcKey) { + const zones = Object.values(dcZoneModel[dcKey]?.zones ?? {}); + return zones[0] ?? { leaf: "leaf-a", cluster: "cluster-a", label: "资源区", clusterName: "计算集群" }; +} + +function firstOnlineNodeId(nodes) { + return nodes.find((node) => node.online !== false)?.node_id ?? ""; +} + +function zoneAggregate(nodes, report, metric) { + const result = new Map(); + for (const node of nodes) { + const parsed = parseDciNode(node.node_id, node); + if (!parsed.location) continue; + if (metric === "tasks") { + const count = Array.isArray(node.running_tasks) ? node.running_tasks.length : 0; + result.set(parsed.location, (result.get(parsed.location) ?? 0) + count); + } else if (metric === "cpu") { + const value = percentFrom(node.cpu_utilization, node.used_cpu_ratio); + if (value != null) result.set(parsed.location, Math.max(result.get(parsed.location) ?? 0, value)); + } else if (metric === "memory") { + const value = percentFrom(node.memory_utilization, node.used_memory_ratio); + if (value != null) result.set(parsed.location, Math.max(result.get(parsed.location) ?? 0, value)); + } + } + for (const run of report?.active_runs ?? []) { + const parsed = parseDciNode(run.node_id, {}); + if (parsed.location) result.set(parsed.location, Math.max(1, result.get(parsed.location) ?? 0)); + } + return result; +} + +function nodeName(leafId, location) { + const suffix = leafId.endsWith("a") || leafId.endsWith("b") ? "1" : "2"; + return `Leaf-${String(location || "zone").toUpperCase()}-${suffix}`; +} + function currentTopology() { return activeTopologyKey === "global" ? globalTopology : dcTopologies[activeTopologyKey] ?? globalTopology; } function currentTargetDcKey() { + if (livePathContext?.dcKey) return livePathContext.dcKey; const match = String(schedulerStatus.target ?? "").match(/\bDC(\d+)\b/i); return match ? `dc${match[1]}` : null; } @@ -276,11 +527,11 @@ function globalRouteForTargetDc(targetDcKey) { }, dc2: { nodes: ["user-access", "border2", "pe2", "dc2"], - pathText: "当前任务调度路径:User-Access → DC2 → 深圳计算集群 / VM-02", + pathText: "当前任务调度路径:User-Access → DC2 → 成都计算集群 / VM-02", }, dc3: { nodes: ["user-access", "border3", "pe3", "dc3"], - pathText: "当前任务调度路径:User-Access → DC3 → 成都计算集群 / VM-02", + pathText: "当前任务调度路径:User-Access → DC3 → 广州计算集群 / VM-02", }, }; return routes[targetDcKey] ?? routes.dc2; @@ -336,7 +587,7 @@ function isRouteLink(topology, item) { return route.some((id, index) => { const next = route[index + 1]; return next && ((item.source === id && item.target === next) || (item.source === next && item.target === id)); - }) || item.type.includes("route"); + }) || (topology.kind === "global" && item.type.includes("route")); } function renderGlobalLinks(topology) { @@ -534,8 +785,9 @@ function renderGatewayBridge(topology) { } function renderSpineRow(topology) { + const target = topology.key === currentTargetDcKey(); return `