From 804cf7b6efecc2b249f4eade7658b142f5986f75 Mon Sep 17 00:00:00 2001 From: Deban Kumar Sahu Date: Fri, 5 Jun 2026 20:17:25 +0530 Subject: [PATCH 1/6] refactored the project_to_nodes() function --- .../common/projection/project_nodes.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/archunitpython/common/projection/project_nodes.py b/src/archunitpython/common/projection/project_nodes.py index cc2ce2a..05d95d4 100644 --- a/src/archunitpython/common/projection/project_nodes.py +++ b/src/archunitpython/common/projection/project_nodes.py @@ -27,23 +27,21 @@ def project_to_nodes( all_labels: set[str] = set() for edge in graph: - if edge.external and not include_externals: - # Still record the source (internal file) - all_labels.add(edge.source) - outgoing[edge.source].append(edge) - continue - all_labels.add(edge.source) - all_labels.add(edge.target) - outgoing[edge.source].append(edge) if edge.source != edge.target: # Don't count self-edges as incoming - incoming[edge.target].append(edge) + outgoing[edge.source].append(edge) + + if (edge.external and include_externals) or not edge.external: + # Record the target (external file) + all_labels.add(edge.target) + if edge.source != edge.target: # Don't count self-edges as incoming + incoming[edge.target].append(edge) return [ ProjectedNode( label=label, incoming=incoming.get(label, []), - outgoing=[e for e in outgoing.get(label, []) if e.source != e.target], + outgoing=outgoing.get(label, []), ) for label in sorted(all_labels) ] From 33d789c36cac3588a8f6724ad447608926f19bea Mon Sep 17 00:00:00 2001 From: Deban Kumar Sahu Date: Fri, 5 Jun 2026 20:29:33 +0530 Subject: [PATCH 2/6] fixed comment --- src/archunitpython/common/projection/project_nodes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/archunitpython/common/projection/project_nodes.py b/src/archunitpython/common/projection/project_nodes.py index 05d95d4..5ef25d9 100644 --- a/src/archunitpython/common/projection/project_nodes.py +++ b/src/archunitpython/common/projection/project_nodes.py @@ -32,7 +32,7 @@ def project_to_nodes( outgoing[edge.source].append(edge) if (edge.external and include_externals) or not edge.external: - # Record the target (external file) + # Record the target all_labels.add(edge.target) if edge.source != edge.target: # Don't count self-edges as incoming incoming[edge.target].append(edge) From e7eae3d30044246fcab3b93c71d2eb459042d39b Mon Sep 17 00:00:00 2001 From: Deban Kumar Sahu Date: Sun, 7 Jun 2026 13:52:43 +0530 Subject: [PATCH 3/6] refactor: reduced the search for `incoming` and `outgoing` node to one loop from 2 loop --- .../common/projection/cycles/cycle_utils.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/archunitpython/common/projection/cycles/cycle_utils.py b/src/archunitpython/common/projection/cycles/cycle_utils.py index 2544f16..125828b 100644 --- a/src/archunitpython/common/projection/cycles/cycle_utils.py +++ b/src/archunitpython/common/projection/cycles/cycle_utils.py @@ -25,11 +25,18 @@ def transform_edge_data(edges: list[NumberEdge]) -> list[NumberNode]: unique_ids = CycleUtils.find_unique_nodes(edges) nodes = [] for node_id in unique_ids: + incoming = [] + outgoing = [] + for edge in edges: + if edge.to_node==node_id: + incoming.append(edge) + if edge.from_node==node_id: + outgoing.append(edge) nodes.append( NumberNode( node=node_id, - incoming=[e for e in edges if e.to_node == node_id], - outgoing=[e for e in edges if e.from_node == node_id], + incoming=incoming, + outgoing=outgoing, ) ) return nodes From a06869871388b87e8661579898af7a2e90b5f27c Mon Sep 17 00:00:00 2001 From: Deban Kumar Sahu Date: Sun, 7 Jun 2026 14:13:33 +0530 Subject: [PATCH 4/6] perf: Improved the `transform_edge_data()` method by changing the underlying algorithm. - Now it first build a node_id map and then iterate through edge directly and append edges to specific node_id's incoming and outgoing list. --- .../common/projection/cycles/cycle_utils.py | 24 ++++++------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/src/archunitpython/common/projection/cycles/cycle_utils.py b/src/archunitpython/common/projection/cycles/cycle_utils.py index 125828b..eb5841a 100644 --- a/src/archunitpython/common/projection/cycles/cycle_utils.py +++ b/src/archunitpython/common/projection/cycles/cycle_utils.py @@ -23,23 +23,13 @@ def get_outgoing_neighbours( def transform_edge_data(edges: list[NumberEdge]) -> list[NumberNode]: """Convert a list of edges into a list of nodes with in/out edges.""" unique_ids = CycleUtils.find_unique_nodes(edges) - nodes = [] - for node_id in unique_ids: - incoming = [] - outgoing = [] - for edge in edges: - if edge.to_node==node_id: - incoming.append(edge) - if edge.from_node==node_id: - outgoing.append(edge) - nodes.append( - NumberNode( - node=node_id, - incoming=incoming, - outgoing=outgoing, - ) - ) - return nodes + node_id_map = {node_id: NumberNode(node = node_id) for node_id in unique_ids} + + for edge in edges: + node_id_map[edge.to_node].incoming.append(edge) + node_id_map[edge.from_node].outgoing.append(edge) + + return list(node_id_map.values()) @staticmethod def find_unique_nodes(edges: list[NumberEdge]) -> list[int]: From 08fc5ffdfa16fe066009c96412a045f89c1e3946 Mon Sep 17 00:00:00 2001 From: Deban Kumar Sahu Date: Sat, 13 Jun 2026 13:21:55 +0530 Subject: [PATCH 5/6] perf: Improved the performance of the Tarjan algorithm. --- .../common/projection/cycles/tarjan_scc.py | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/archunitpython/common/projection/cycles/tarjan_scc.py b/src/archunitpython/common/projection/cycles/tarjan_scc.py index eabea08..827ce38 100644 --- a/src/archunitpython/common/projection/cycles/tarjan_scc.py +++ b/src/archunitpython/common/projection/cycles/tarjan_scc.py @@ -2,6 +2,9 @@ from __future__ import annotations +from collections import defaultdict +from typing import Dict, Set + from archunitpython.common.projection.cycles.model import NumberEdge @@ -12,7 +15,7 @@ def __init__(self, node_id: int) -> None: self.id = node_id self.index = -1 self.lowlink = -1 - self.neighbours: list[int] = [] + self.neighbours: Set[int] = set() class TarjanSCC: @@ -27,8 +30,11 @@ def find_strongly_connected_components(self, edges: list[NumberEdge]) -> list[li self._graph: dict[int, _Vertex] = {} self._index = 0 self._stack: list[_Vertex] = [] + self._on_stack: Set[int] = set() self._sccs: list[list[NumberEdge]] = [] - self._edges = edges + self._outgoing_edge_map: Dict[int,list[NumberEdge]] = defaultdict(list) + for edge in edges: + self._outgoing_edge_map[edge.from_node].append(edge) self._init(edges) @@ -47,8 +53,7 @@ def _init(self, edges: list[NumberEdge]) -> None: self._graph[edge.to_node] = _Vertex(edge.to_node) v = self._graph[edge.from_node] - if edge.to_node not in v.neighbours: - v.neighbours.append(edge.to_node) + v.neighbours.add(edge.to_node) def _visit(self, vertex: _Vertex) -> None: """DFS visit for Tarjan's algorithm.""" @@ -56,27 +61,30 @@ def _visit(self, vertex: _Vertex) -> None: vertex.lowlink = self._index self._index += 1 self._stack.append(vertex) + self._on_stack.add(vertex.id) for neighbour_id in vertex.neighbours: w = self._graph[neighbour_id] if w.index < 0: self._visit(w) vertex.lowlink = min(vertex.lowlink, w.lowlink) - elif w in self._stack: + elif w.id in self._on_stack: vertex.lowlink = min(vertex.lowlink, w.index) if vertex.lowlink == vertex.index: - scc_vertices: list[_Vertex] = [] + scc_ids: Set[int] = set() while True: w = self._stack.pop() - scc_vertices.append(w) + self._on_stack.remove(w.id) + scc_ids.add(w.id) if w.id == vertex.id: break - if scc_vertices: - scc_ids = {v.id for v in scc_vertices} - scc_edges = [ - e for e in self._edges if e.from_node in scc_ids and e.to_node in scc_ids - ] + if scc_ids: + scc_edges: list[NumberEdge] = [] + for id in scc_ids: + for edge in self._outgoing_edge_map[id]: + if edge.to_node in scc_ids: + scc_edges.append(edge) if scc_edges: self._sccs.append(scc_edges) From 49c491bc3620bb452a0a52d533c4573c948a743e Mon Sep 17 00:00:00 2001 From: Deban Kumar Sahu Date: Tue, 30 Jun 2026 16:47:06 +0530 Subject: [PATCH 6/6] perf: Converted the `to_remove` variable to set from list as it's only used to store search space, and set will give better performance. --- src/archunitpython/common/projection/cycles/johnsons_apsp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/archunitpython/common/projection/cycles/johnsons_apsp.py b/src/archunitpython/common/projection/cycles/johnsons_apsp.py index 91e0601..a9ab061 100644 --- a/src/archunitpython/common/projection/cycles/johnsons_apsp.py +++ b/src/archunitpython/common/projection/cycles/johnsons_apsp.py @@ -59,11 +59,11 @@ def _explore_neighbours(self, current_node: NumberNode) -> None: def _unblock(self, node: NumberNode) -> None: self._blocked = [n for n in self._blocked if n is not node] - to_remove: list[_BlockedBy] = [] + to_remove: set[_BlockedBy] = set() for blocker in self._blocked_map: if blocker.by is node: self._unblock(blocker.blocked) - to_remove.append(blocker) + to_remove.add(blocker) self._blocked_map = [b for b in self._blocked_map if b not in to_remove] def _is_part_of_current_start_cycle(self, current_node: NumberNode) -> bool: