diff --git a/gns3server/controller/link.py b/gns3server/controller/link.py index 4467a0b4e..e716bfd03 100644 --- a/gns3server/controller/link.py +++ b/gns3server/controller/link.py @@ -114,7 +114,7 @@ class Link: """ return self._markers - async def inherit_marker(self, def_name, marker_def): + async def inherit_marker(self, def_name, marker_def, dump=True): """ Apply a project-level marker definition to this link. @@ -147,6 +147,7 @@ class Link: highlight_duration=marker_def.get("highlight_duration"), enabled=not marker_def.get("paused", False), inherited_from=def_name, + dump=dump, ) def _persist_markers(self): diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index 177a2cc9a..e6a0ffd6a 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -939,9 +939,15 @@ class Project: raise ControllerError(f"Marker definition '{name}' not found") self._marker_definitions[name]["paused"] = True marker_name = f"global-{name}" - for link in list(self._links.values()): - if marker_name in link.markers and link.markers[marker_name].get("inherited_from") == name: - await link.update_marker(marker_name, enabled=False, inherited=True) + affected = [ + link for link in self._links.values() + if marker_name in link.markers and link.markers[marker_name].get("inherited_from") == name + ] + await self._marker_apply_concurrently( + affected, + lambda link: link.update_marker(marker_name, enabled=False, inherited=True, dump=False), + lambda link, e: f"Failed to pause marker {marker_name} on link {link.id}: {e}", + ) self.dump() self.emit_notification("project.updated", self.asdict()) @@ -952,9 +958,15 @@ class Project: raise ControllerError(f"Marker definition '{name}' not found") self._marker_definitions[name]["paused"] = False marker_name = f"global-{name}" - for link in list(self._links.values()): - if marker_name in link.markers and link.markers[marker_name].get("inherited_from") == name: - await link.update_marker(marker_name, enabled=True, inherited=True) + affected = [ + link for link in self._links.values() + if marker_name in link.markers and link.markers[marker_name].get("inherited_from") == name + ] + await self._marker_apply_concurrently( + affected, + lambda link: link.update_marker(marker_name, enabled=True, inherited=True, dump=False), + lambda link, e: f"Failed to resume marker {marker_name} on link {link.id}: {e}", + ) self.dump() self.emit_notification("project.updated", self.asdict()) @@ -1058,7 +1070,7 @@ class Project: # needs a full re-fan-out: drop every copy, then re-apply. await self._marker_apply_concurrently( affected, - lambda link: link.stop_marker(f"global-{name}", inherited=True), + lambda link: link.stop_marker(f"global-{name}", inherited=True, dump=False), lambda link, e: f"Failed to remove inherited marker global-{name} from link {link.id}: {e}", ) await self._apply_def_to_all_links(name) @@ -1068,7 +1080,8 @@ class Project: affected, lambda link: link.update_marker( f"global-{name}", bpf=d["bpf"], tag=d.get("tag"), direction=d.get("direction"), - color=d.get("color"), highlight_duration=d.get("highlight_duration"), inherited=True + color=d.get("color"), highlight_duration=d.get("highlight_duration"), inherited=True, + dump=False ), lambda link, e: f"Failed to sync marker global-{name} on link {link.id}: {e}", ) @@ -1110,9 +1123,12 @@ class Project: """ d = self._marker_definitions[def_name] + # dump=False: per-link topology writes are the dominant cost on large + # projects — the callers (create/update_marker_definition) dump once + # after the fan-out. await self._marker_apply_concurrently( list(self._links.values()), - lambda link: link.inherit_marker(def_name, d), + lambda link: link.inherit_marker(def_name, d, dump=False), lambda link, e: f"Marker definition '{def_name}' could not be applied to link {link.id}: {e}", ) @@ -1129,7 +1145,9 @@ class Project: for def_name, d in self._marker_definitions.items(): try: - await link.inherit_marker(def_name, d) + # dump=False: the caller (link create / project open) dumps once + # after; per-def dumps here would be N full topology writes. + await link.inherit_marker(def_name, d, dump=False) except ControllerError as e: log.warning( "Marker definition '%s' could not be applied to new link %s: %s", diff --git a/gns3server/controller/udp_link.py b/gns3server/controller/udp_link.py index d9ca8f250..b25e1126b 100644 --- a/gns3server/controller/udp_link.py +++ b/gns3server/controller/udp_link.py @@ -353,7 +353,7 @@ class UDPLink(Link): # explicitly deletes a marker via the REST API, and a marker is torn # down automatically only when its link is deleted. - async def start_marker(self, name, bpf, tag=None, direction=None, data_link_type="DLT_EN10MB", capture_node_id=None, color=None, highlight_duration=None, enabled=True, inherited_from=None): + async def start_marker(self, name, bpf, tag=None, direction=None, data_link_type="DLT_EN10MB", capture_node_id=None, color=None, highlight_duration=None, enabled=True, inherited_from=None, dump=True): """ Attach a traffic-insight marker to this link. @@ -412,9 +412,12 @@ class UDPLink(Link): if self._created: await self.update() self._project.emit_notification("link.updated", self.asdict()) - self._project.dump() + # Bulk fan-out passes dump=False: N per-link topology writes on a + # 500-link project are the dominant cost — the caller dumps once after. + if dump: + self._project.dump() - async def stop_marker(self, name, inherited=False): + async def stop_marker(self, name, inherited=False, dump=True): """ Remove a traffic-insight marker from this link. @@ -455,9 +458,10 @@ class UDPLink(Link): except Exception: pass # best-effort: old compute without the route leaves the file self._project.emit_notification("link.updated", self.asdict()) - self._project.dump() + if dump: + self._project.dump() - async def update_marker(self, name, bpf=None, tag=None, enabled=None, direction=_UNSET, color=None, highlight_duration=None, inherited=False): + async def update_marker(self, name, bpf=None, tag=None, enabled=None, direction=_UNSET, color=None, highlight_duration=None, inherited=False, dump=True): """ Update an existing marker's fields and push to uBridge fine-grained — no full NIO reapply, so sibling markers' pcaps stay open. bpf/tag/direction @@ -536,4 +540,5 @@ class UDPLink(Link): # correct in _markers; the next NIO reapply converges uBridge. pass self._project.emit_notification("link.updated", self.asdict()) - self._project.dump() + if dump: + self._project.dump()