From c9a93c0aeb00c91b08a4b60ff0fe363b710f649f Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Mon, 10 Aug 2026 23:54:21 +0800 Subject: [PATCH] perf: skip per-link topology dump during project-open prepare Stage timing showed prepare taking 96s for 1275 links (vs 0.31s for the batch dispatch itself). Root cause: _prepare_link_from_topology called add_link (dump=True default) and update_link_style/update_show_filters_icon (each unconditionally dump the full topology). 1275 links x serialize- and-write-the-whole-topology = the entire 96s. - add_link(..., dump=False): the project is dumped once at the end of open - set link._link_style / _show_filters_icon directly instead of the update_* helpers, which also avoids spurious 'link.updated' notifications before the link is finalised The final self.dump() at the end of project.open already persists everything. --- .gitignore | 1 + gns3server/controller/project.py | 20 +++++++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 28a535038..5d4619d10 100644 --- a/.gitignore +++ b/.gitignore @@ -85,3 +85,4 @@ venv # Tiktoken cache files gns3server/agent/gns3_copilot/cache/tiktoken/ +gns3.log diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index a23c1095f..3b452eea5 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -838,7 +838,7 @@ class Project: batch HTTP call per compute instead of one round-trip per link. """ - link = await self.add_link(link_id=link_data["link_id"]) + link = await self.add_link(link_id=link_data["link_id"], dump=False) if "filters" in link_data: try: await link.update_filters(link_data["filters"]) @@ -865,10 +865,14 @@ class Project: "capture_node_id": marker.get("capture_node_id"), "direction": marker.get("direction"), } + # Set style/icon directly: the update_* helpers unconditionally dump + # the whole topology and emit "link.updated", neither of which is + # appropriate mid-prepare (the link is finalised, notified and the + # project dumped once at the end of open). if "link_style" in link_data: - await link.update_link_style(link_data["link_style"]) + link._link_style = link_data["link_style"] if "show_filters_icon" in link_data: - await link.update_show_filters_icon(link_data["show_filters_icon"]) + link._show_filters_icon = link_data["show_filters_icon"] for node_link in link_data.get("nodes", []): node = self.get_node(node_link["node_id"]) port = node.get_port(node_link["adapter_number"], node_link["port_number"]) @@ -1706,11 +1710,14 @@ class Project: # Create nodes in parallel with limited concurrency # to avoid overwhelming the system with too many simultaneous operations + _stage_t0 = time.time() pool = Pool(concurrency=100) for compute, name, node_id, node_data in nodes_to_create: pool.append(self.add_node, compute, name, node_id, dump=False, **node_data) await pool.join() + log.info("Project open stage timing: nodes=%d created in %.2fs", len(nodes_to_create), time.time() - _stage_t0) # Pre-allocate UDP ports for all links in batch to reduce HTTP round-trips + _stage_t1 = time.time() ports_per_compute = {} for link_data in topology.get("links", []): if "link_id" not in link_data.keys(): @@ -1723,6 +1730,7 @@ class Project: count = ports_per_compute.get(compute.id, 0) if count > 0: await self.preallocate_udp_ports_for_compute(compute, count) + log.info("Project open stage timing: preallocate ports in %.2fs", time.time() - _stage_t1) # Create links via the bulk path: build every link locally (no NIO # HTTP), then dispatch all NIOs to each compute in a single batch # request. This replaces one HTTP round-trip per link (~5000 for a @@ -1740,6 +1748,7 @@ class Project: prepared = await asyncio.gather(*[_prepare_one(d) for d in link_data_list]) valid = [p for p in prepared if p is not None] + log.info("Project open stage timing: prepare %d links in %.2fs", len(link_data_list), time.time() - _stage_t1) # Group the prepared NIO entries by destination compute and send # each compute a single /nios/batch request. @@ -1763,9 +1772,14 @@ class Project: ) if per_compute: + _stage_t2 = time.time() await asyncio.gather( *[_dispatch_batch(c, n) for c, n in per_compute.items()] ) + log.info( + "Project open stage timing: batch dispatch %d NIOs across %d compute(s) in %.2fs", + sum(len(n) for n in per_compute.values()), len(per_compute), time.time() - _stage_t2 + ) # Finalise every link: wire node/port back-references, mark created, # notify clients, and apply project-level marker definitions.