perf(javascript): index version relationships once per comparison (#1347)

relationshipSignature rebuilt a node map and scanned every graph edge
for each side of each matched pair, so comparison cost grew as
pairs x (nodes + edges). Build node and adjacency indexes once per
graph in ItemContext; signatures, sorting and output are unchanged.
At 4000 matched modules, end-to-end compare: 5.7s -> 2.9s (2 edges
per node) and 10.4s -> 5.8s (6 edges per node); 8000: 3.3x and 2.5x.
Output JSON is byte-identical on the base commit.
This commit is contained in:
keemsisi authored and GitHub committed 2026-10-10 02:07:30 +08:00
1 parent 3f59a84bd1
commit b1e85ad711
2 files changed
+232 -27

No files matched your search

@@ -27,6 +27,19 @@ interface ItemContext {
readonly rightNativeEvidence: readonly Evidence[];
readonly pairByLeft: ReadonlyMap<string, string>;
readonly pairByRight: ReadonlyMap<string, string>;
readonly leftRelationships: RelationshipIndex;
readonly rightRelationships: RelationshipIndex;
}
interface IndexedRelationship {
readonly direction: "in" | "out";
readonly relation: ApplicationEdge["relation"];
readonly neighborId: string;
}
interface RelationshipIndex {
readonly nodeById: ReadonlyMap<string, ApplicationNode>;
readonly adjacency: ReadonlyMap<string, readonly IndexedRelationship[]>;
}
type OneSidedNode =
@@ -53,7 +66,10 @@ type ComparisonItemSemantic<
/** Classify matched, unmatched, and ambiguous nodes without inventing absence. */
export const classifyJavaScriptApplicationVersions = (
matching: ApplicationVersionMatchingProjection,
context: Omit<ItemContext, "pairByLeft" | "pairByRight">,
context: Omit<
ItemContext,
"pairByLeft" | "pairByRight" | "leftRelationships" | "rightRelationships"
>,
): ApplicationVersionItemProjection => {
const pairByLeft = new Map(
matching.pairs.map(({ left, right }) => [
@@ -67,7 +83,13 @@ export const classifyJavaScriptApplicationVersions = (
`pair:${left.node_id}:${right.node_id}`,
]),
);
const fullContext: ItemContext = { ...context, pairByLeft, pairByRight };
const fullContext: ItemContext = {
...context,
pairByLeft,
pairByRight,
leftRelationships: indexRelationships(context.leftGraph),
rightRelationships: indexRelationships(context.rightGraph),
};
const leftItems = matching.unmatchedLeft.map((node) => {
const candidates = matching.candidatesForLeft.get(node.node_id) ?? [];
return unmatchedItem(
@@ -339,37 +361,45 @@ const properties = (node: ApplicationNode) =>
compareUnicodeCodePoints(canonicalJson(left), canonicalJson(right)),
);
const indexRelationships = (
graph: JavaScriptApplicationGraph,
): RelationshipIndex => {
const adjacency = new Map<string, IndexedRelationship[]>();
const add = (nodeId: string, relationship: IndexedRelationship): void => {
const existing = adjacency.get(nodeId);
if (existing === undefined) adjacency.set(nodeId, [relationship]);
else existing.push(relationship);
};
for (const edge of graph.edges) {
add(edge.source_node_id, {
direction: "out",
relation: edge.relation,
neighborId: edge.target_node_id,
});
if (edge.target_node_id !== edge.source_node_id)
add(edge.target_node_id, {
direction: "in",
relation: edge.relation,
neighborId: edge.source_node_id,
});
}
return {
nodeById: new Map(graph.nodes.map((value) => [value.node_id, value])),
adjacency,
};
};
const relationshipSignature = (
node: ApplicationNode,
side: "left" | "right",
context: ItemContext,
): string => {
const graph = side === "left" ? context.leftGraph : context.rightGraph;
const { nodeById, adjacency } =
side === "left" ? context.leftRelationships : context.rightRelationships;
const paired = side === "left" ? context.pairByLeft : context.pairByRight;
const nodeById = new Map(graph.nodes.map((value) => [value.node_id, value]));
const signatures = graph.edges.flatMap((edge) => {
if (edge.source_node_id === node.node_id)
return [
edgeSignature({
direction: "out",
relation: edge.relation,
neighborId: edge.target_node_id,
paired,
nodeById,
}),
];
if (edge.target_node_id === node.node_id)
return [
edgeSignature({
direction: "in",
relation: edge.relation,
neighborId: edge.source_node_id,
paired,
nodeById,
}),
];
return [];
});
const signatures = (adjacency.get(node.node_id) ?? []).map((relationship) =>
edgeSignature({ ...relationship, paired, nodeById }),
);
return canonicalJson(signatures.sort(compareUnicodeCodePoints));
};