From 9194bc5fdc16b3caf6cc30d1d4b984bb6709e9c3 Mon Sep 17 00:00:00 2001 From: Matte Date: Sat, 27 Sep 2025 16:42:29 +0200 Subject: [PATCH 1/2] Add test case for graph with high-degree node - also fixed bug in another Performance test: elapsed -> end - start --- specs/dep_graph_spec.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/specs/dep_graph_spec.js b/specs/dep_graph_spec.js index ce59c00..3ad0533 100644 --- a/specs/dep_graph_spec.js +++ b/specs/dep_graph_spec.js @@ -541,7 +541,20 @@ describe("DepGraph Performance", function () { var start = new Date().getTime(); g.overallOrder(); var end = new Date().getTime(); - expect(start - end).toBeLessThan(1000); + expect(end - start).toBeLessThan(1000); + }); + + it("should construct very large graph with high-degree node in a reasonable amount of time", function () { + var start = new Date().getTime(); + var g = new DepGraph(); + // Create a graph with 100000 nodes, all depending on the same hub + g.addNode("hub"); + for (var i = 0; i < 100000; i++) { + g.addNode(i.toString()); + g.addDependency(i.toString(), "hub"); + } + var end = new Date().getTime(); + expect(end - start).toBeLessThan(1000); }); }); From 06b78863ca57cb30b3bfcc39657dc0cd358a53a6 Mon Sep 17 00:00:00 2001 From: Matte Date: Sat, 27 Sep 2025 16:48:59 +0200 Subject: [PATCH 2/2] Using Sets instead of Arrays for adjacency lists - Preserve behavior since Sets respect insertion order - Improve performance when constructing graphs with high-degree nodes - Create Array copies in a few places to preserve behavior --- lib/dep_graph.js | 50 +++++++++++++++++------------------------------- 1 file changed, 18 insertions(+), 32 deletions(-) diff --git a/lib/dep_graph.js b/lib/dep_graph.js index 9444532..b10cbf5 100644 --- a/lib/dep_graph.js +++ b/lib/dep_graph.js @@ -8,7 +8,7 @@ * Detects cycles and throws an Error if one is detected (unless the "circular" * parameter is "true" in which case it ignores them). * - * @param edges The edges to DFS through (this is a Map of node to Array of nodes) + * @param edges The edges to DFS through (this is a Map of node to Set of nodes) * @param leavesOnly Whether to only return "leaf" nodes (ones who have no edges) * @param result An array in which the results will be populated * @param circular A boolean to allow circular dependencies @@ -45,7 +45,7 @@ function createDFS(edges, leavesOnly, result, circular) { inCurrentPath.add(node); currentPath.push(node); - var nodeEdges = edges.get(node); + var nodeEdges = [...edges.get(node)]; // (push edges onto the todo stack in reverse order to be order-compatible with the old DFS implementation) for (var i = nodeEdges.length - 1; i >= 0; i--) { todo.push({ node: nodeEdges[i], processed: false }); @@ -57,7 +57,7 @@ function createDFS(edges, leavesOnly, result, circular) { currentPath.pop(); inCurrentPath.delete(node); visited.add(node); - if (!leavesOnly || edges.get(node).length === 0) { + if (!leavesOnly || edges.get(node).size === 0) { result.push(node); } } @@ -70,8 +70,8 @@ function createDFS(edges, leavesOnly, result, circular) { */ var DepGraph = (exports.DepGraph = function DepGraph(opts) { this.nodes = new Map(); // Node -> Node/Data - this.outgoingEdges = new Map(); // Node -> [Dependency Node] - this.incomingEdges = new Map(); // Node -> [Dependant Node] + this.outgoingEdges = new Map(); // Node -> {Dependency Node} + this.incomingEdges = new Map(); // Node -> {Dependant Node} this.circular = opts && !!opts.circular; // Allows circular deps }); DepGraph.prototype = { @@ -92,8 +92,8 @@ DepGraph.prototype = { } else { this.nodes.set(node, node); } - this.outgoingEdges.set(node, []); - this.incomingEdges.set(node, []); + this.outgoingEdges.set(node, new Set()); + this.incomingEdges.set(node, new Set()); } }, /** @@ -106,10 +106,7 @@ DepGraph.prototype = { this.incomingEdges.delete(node); [this.incomingEdges, this.outgoingEdges].forEach(function (edgeList) { edgeList.forEach(function (v) { - var idx = v.indexOf(node); - if (idx >= 0) { - v.splice(idx, 1); - } + v.delete(node); }); }); } @@ -151,31 +148,20 @@ DepGraph.prototype = { if (!this.hasNode(to)) { throw new Error("Node does not exist: " + to); } - if (this.outgoingEdges.get(from).indexOf(to) === -1) { - this.outgoingEdges.get(from).push(to); - } - if (this.incomingEdges.get(to).indexOf(from) === -1) { - this.incomingEdges.get(to).push(from); - } + this.outgoingEdges.get(from).add(to); + this.incomingEdges.get(to).add(from); return true; }, /** * Remove a dependency between two nodes. */ removeDependency: function (from, to) { - var idx; if (this.hasNode(from)) { - idx = this.outgoingEdges.get(from).indexOf(to); - if (idx >= 0) { - this.outgoingEdges.get(from).splice(idx, 1); - } + this.outgoingEdges.get(from).delete(to); } if (this.hasNode(to)) { - idx = this.incomingEdges.get(to).indexOf(from); - if (idx >= 0) { - this.incomingEdges.get(to).splice(idx, 1); - } + this.incomingEdges.get(to).delete(from); } }, /** @@ -187,8 +173,8 @@ DepGraph.prototype = { var result = new DepGraph(); source.nodes.forEach(function (v, n) { result.nodes.set(n, v); - result.outgoingEdges.set(n, source.outgoingEdges.get(n).slice(0)); - result.incomingEdges.set(n, source.incomingEdges.get(n).slice(0)); + result.outgoingEdges.set(n, new Set(source.outgoingEdges.get(n))); + result.incomingEdges.set(n, new Set(source.incomingEdges.get(n))); }); result.circular = source.circular; return result; @@ -200,7 +186,7 @@ DepGraph.prototype = { */ directDependenciesOf: function (node) { if (this.hasNode(node)) { - return this.outgoingEdges.get(node).slice(0); + return [...this.outgoingEdges.get(node)]; } else { throw new Error("Node does not exist: " + node); } @@ -212,7 +198,7 @@ DepGraph.prototype = { */ directDependantsOf: function (node) { if (this.hasNode(node)) { - return this.incomingEdges.get(node).slice(0); + return [...this.incomingEdges.get(node)]; } else { throw new Error("Node does not exist: " + node); } @@ -303,7 +289,7 @@ DepGraph.prototype = { // run a DFS starting at these points to get the order keys .filter(function (node) { - return self.incomingEdges.get(node).length === 0; + return self.incomingEdges.get(node).size === 0; }) .forEach(function (n) { DFS(n); @@ -331,7 +317,7 @@ DepGraph.prototype = { entryNodes: function () { var self = this; return Array.from(this.nodes.keys()).filter(function (node) { - return self.incomingEdges.get(node).length === 0; + return self.incomingEdges.get(node).size === 0; }); }, };