perf(javascript): reuse validated immutable graphs (#1112)

* perf(javascript): reuse validated immutable graphs

* fix(ci): follow the moved process CLI boundary test
This commit is contained in:
Ysopwnable authored and GitHub committed 2026-10-09 02:40:21 +08:00
1 parent 1f5be750bc
commit ec6db0f063
11 files changed
+582 -117

No files matched your search

@@ -10,7 +10,9 @@ describe("JavaScript analysis cancellation before publication", () => {
it.each([
"parse_javascript_source",
"build_javascript_application_graph",
"seal_javascript_application_graph",
"build_javascript_semantic_graph",
"seal_javascript_semantic_graph",
"validate_javascript_application_result",
"create_javascript_application_evidence",
"seal_javascript_application_result",
@@ -3,7 +3,7 @@ import { z } from "zod";
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
import {
analyzeJavaScriptApplicationInputSchema,
javascriptApplicationAnalysisResultSchema,
parseOwnedJavaScriptApplicationAnalysisSteps,
} from "../../domain/javascript/javascriptApplicationAnalysis.js";
import { AnalysisOutputError } from "../../domain/analysisErrorCore.js";
import { ArtifactOperationError } from "../../domain/artifactOperationError.js";
@@ -17,6 +17,7 @@ import { createOwnedJavaScriptApplicationEvidenceCooperatively } from "./JavaScr
import {
assertJavaScriptAnalysisActive,
checkpointJavaScriptAnalysis,
completeJavaScriptAnalysisSteps,
} from "./JavaScriptAnalysisControl.js";
import { reconstructJavaScriptArtifact } from "./JavaScriptArtifactReconstruction.js";
import { JAVASCRIPT_APPLICATION_PROVIDER } from "../InvestigationProviders.js";
@@ -63,14 +64,17 @@ export const analyzeJavaScriptApplicationValidated = async (
phase: "validate_javascript_application_result",
completed: 0,
total: 1,
message: "Validating the application analysis result",
message: "Validating result metadata and cross-graph bindings",
});
await checkpointJavaScriptAnalysis(options.signal);
const result = javascriptApplicationAnalysisResultSchema.parse({
...application,
summary,
limitations: reconstructed.graph.limitations,
});
const result = await completeJavaScriptAnalysisSteps(
parseOwnedJavaScriptApplicationAnalysisSteps({
...application,
summary,
limitations: reconstructed.graph.limitations,
}),
options.signal,
);
await options.progress?.report({
phase: "create_javascript_application_evidence",
completed: 0,
@@ -1,6 +1,7 @@
import type { ArtifactInventorySnapshot } from "../../artifacts/inventory/ArtifactInventory.js";
import {
createJavaScriptApplicationGraph,
createImmutableJavaScriptApplicationGraphSteps,
type JavaScriptApplicationGraph,
} from "../../domain/javascript/javascriptApplicationGraph.js";
import type { JavaScriptModuleArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
@@ -46,7 +47,26 @@ export const buildJavaScriptArtifactGraph = (
snapshot: ArtifactInventorySnapshot,
fileSet: JavaScriptArtifactFileSet,
analysis: JavaScriptModuleArtifactAnalysis,
): JavaScriptApplicationGraph => {
): JavaScriptApplicationGraph =>
createJavaScriptApplicationGraph(
buildJavaScriptArtifactGraphInput(snapshot, fileSet, analysis),
);
/** Build a validated application graph with cooperative immutable ownership. */
export const buildImmutableJavaScriptArtifactGraphSteps = (
snapshot: ArtifactInventorySnapshot,
fileSet: JavaScriptArtifactFileSet,
analysis: JavaScriptModuleArtifactAnalysis,
): Generator<void, JavaScriptApplicationGraph> =>
createImmutableJavaScriptApplicationGraphSteps(
buildJavaScriptArtifactGraphInput(snapshot, fileSet, analysis),
);
const buildJavaScriptArtifactGraphInput = (
snapshot: ArtifactInventorySnapshot,
fileSet: JavaScriptArtifactFileSet,
analysis: JavaScriptModuleArtifactAnalysis,
): unknown => {
const accumulator = new JavaScriptArtifactGraphAccumulator();
const root = createJavaScriptArtifactRootNode(accumulator, snapshot);
const context: JavaScriptArtifactGraphContext = {
@@ -74,7 +94,7 @@ export const buildJavaScriptArtifactGraph = (
addJavaScriptHtmlRoles(context);
addJavaScriptSourceMapOriginals(context);
const coverage = graphCoverage(context);
return createJavaScriptApplicationGraph({
return {
schema: "JavaScriptApplicationGraph",
root_node_ids:
packageRoots.length === 0
@@ -84,7 +104,7 @@ export const buildJavaScriptArtifactGraph = (
edges: accumulator.edges(),
coverage,
limitations: graphLimitations(context, coverage.status),
});
};
};
const graphCoverage = (context: JavaScriptArtifactGraphContext) => {
@@ -7,7 +7,7 @@ import type { JavaScriptSemanticGraph } from "../../domain/javascript/javascript
import type { ElectronBoundarySummary } from "../../domain/javascript/javascriptApplicationAnalysis.js";
import { analyzeAndProjectJavaScriptArtifactFiles } from "./JavaScriptArtifactAnalysis.js";
import { readJavaScriptArtifactFiles } from "../../artifacts/javascript/JavaScriptArtifactFiles.js";
import { buildJavaScriptArtifactGraph } from "./JavaScriptArtifactGraphBuilder.js";
import { buildImmutableJavaScriptArtifactGraphSteps } from "./JavaScriptArtifactGraphBuilder.js";
import {
javascriptArtifactReconstructionInputSchema,
type JavaScriptArtifactReconstructionInput,
@@ -16,7 +16,10 @@ import { scanCanonicalArtifactInventory } from "../../artifacts/inventory/Artifa
import { summarizeElectronBoundaries } from "./ElectronBoundaryAnalysis.js";
import { createJavaScriptSemanticGraphProjection } from "./JavaScriptSemanticGraphBuilder.js";
import type { ProgressReporter } from "../ProgressReporter.js";
import { checkpointJavaScriptAnalysis } from "./JavaScriptAnalysisControl.js";
import {
checkpointJavaScriptAnalysis,
completeJavaScriptAnalysisSteps,
} from "./JavaScriptAnalysisControl.js";
/** Application-layer result retaining local diagnostics outside the canonical graph. */
export interface JavaScriptArtifactReconstructionResult {
@@ -95,16 +98,36 @@ export const reconstructJavaScriptArtifact = async (
"build_javascript_application_graph",
"Constructing application and Electron boundary relationships",
);
const graph = buildJavaScriptArtifactGraph(snapshot, files, analysis);
const applicationGraphSteps = buildImmutableJavaScriptArtifactGraphSteps(
snapshot,
files,
analysis,
);
await reportPhase(
"seal_javascript_application_graph",
"Sealing the validated application graph",
);
const graph = await completeJavaScriptAnalysisSteps(
applicationGraphSteps,
signal,
);
await reportPhase(
"build_javascript_semantic_graph",
"Binding and validating static semantic relationships",
);
const semanticGraph = semanticProjection.finish(
const semanticGraphSteps = semanticProjection.finishImmutableSteps(
snapshot.manifest.root_sha256,
graph,
analysis,
);
await reportPhase(
"seal_javascript_semantic_graph",
"Sealing the validated semantic graph",
);
const semanticGraph = await completeJavaScriptAnalysisSteps(
semanticGraphSteps,
signal,
);
await checkpointJavaScriptAnalysis(signal);
return {
input_path: path,
@@ -1,5 +1,6 @@
import {
createJavaScriptSemanticGraph,
createImmutableJavaScriptSemanticGraphSteps,
type JavaScriptSemanticGraph,
type JavaScriptSemanticGraphNode,
} from "../../domain/javascript/javascriptSemanticGraph.js";
@@ -131,6 +132,11 @@ export interface JavaScriptSemanticGraphProjection {
applicationGraph: BuilderInput["applicationGraph"],
analysis: Pick<JavaScriptArtifactAnalysis, "truncated_scopes">,
) => JavaScriptSemanticGraph;
readonly finishImmutableSteps: (
rootArtifactSha256: string,
applicationGraph: BuilderInput["applicationGraph"],
analysis: Pick<JavaScriptArtifactAnalysis, "truncated_scopes">,
) => Generator<void, JavaScriptSemanticGraph>;
}
/** Accumulate semantic nodes while each source IR is still file-local. */
@@ -161,71 +167,90 @@ export const createJavaScriptSemanticGraphProjection =
if (state.fileNodesDropped) truncatedFiles += 1;
state.fileNodeBudget = null;
};
const finishWith = <Value>(
factory: (input: unknown) => Value,
rootArtifactSha256: string,
applicationGraph: BuilderInput["applicationGraph"],
analysis: Pick<JavaScriptArtifactAnalysis, "truncated_scopes">,
): Value => {
bindSemanticGraphApplicationNodes(state, applicationGraph);
if (state.roots.size === 0) addFallbackRoot(rootArtifactSha256, state);
const unknowns = [...state.unknowns.values()];
const graph = factory({
schema: "JavaScriptSemanticRelationGraph",
root_artifact_sha256: rootArtifactSha256,
application_graph_id: applicationGraph.graph_id,
root_node_ids: [...state.roots],
nodes: [...state.nodes.values()],
relations: [...state.relations.values()],
fingerprints,
unknowns,
coverage: {
status: truncatedFiles > 0 ? "partial" : "unknown",
truncated: truncatedFiles > 0,
omitted_nodes: truncatedFiles > 0 ? null : 0,
omitted_relations: truncatedFiles > 0 ? null : 0,
limits:
truncatedFiles > 0
? [
{
name: "semantic_graph_node_ceiling",
value: SEMANTIC_GRAPH_NODE_CEILING,
unit: "items" as const,
},
]
: [],
families: JAVASCRIPT_SEMANTIC_RELATION_FAMILIES.map((family) => ({
family,
status: semanticFamilyStatus(family, analysis),
retained_relations: [...state.relations.values()].filter(
(relation) =>
JAVASCRIPT_SEMANTIC_RELATION_FAMILY[relation.relation] ===
family,
).length,
omitted_relations: truncatedFiles > 0 ? null : 0,
unknown_ids: unknowns
.filter((unknown) => unknown.family === family)
.map(({ unknown_id: identifier }) => identifier),
})),
},
limitations: [
"The semantic graph contains static syntax observations and conservative relationship candidates; it does not claim runtime execution.",
"Local data flow does not claim control-flow-sensitive reaching definitions or arbitrary dynamic property resolution.",
"Promise ownership covers explicit unshadowed Promise construction, static factories, aggregation, chaining, and await syntax only.",
"Function fingerprints are static candidates; equal digests can remain ambiguous and do not prove behavioral equivalence.",
"Event extraction covers EventEmitter-style literal registrations, removals, and dispatch candidates; dynamic names remain unknown.",
"Timer extraction covers global or node:timers scheduling and exact local-handle cancellation.",
"Child-process extraction covers asynchronous node:child_process creation, literal argv/env/stdio options, exit/error listeners, and kill signals.",
"Configuration extraction covers process.env, process.argv, node:fs reads, and direct logical defaults.",
"Request extraction covers fetch, WebSocket, node:http/node:https construction, direct option fields, and exact local response consumers.",
"Boundary extraction covers unshadowed JSON/global coercions plus parse and validation method candidates.",
"Resource extraction covers built-in filesystem/network acquisition and exact local close/destroy/end handles.",
],
});
state.nodes.clear();
state.relations.clear();
state.unknowns.clear();
state.roots.clear();
fingerprints.length = 0;
return graph;
};
return {
projectFile: projectSource,
finish: (rootArtifactSha256, applicationGraph, analysis) => {
bindSemanticGraphApplicationNodes(state, applicationGraph);
if (state.roots.size === 0) addFallbackRoot(rootArtifactSha256, state);
const unknowns = [...state.unknowns.values()];
const graph = createJavaScriptSemanticGraph({
schema: "JavaScriptSemanticRelationGraph",
root_artifact_sha256: rootArtifactSha256,
application_graph_id: applicationGraph.graph_id,
root_node_ids: [...state.roots],
nodes: [...state.nodes.values()],
relations: [...state.relations.values()],
fingerprints,
unknowns,
coverage: {
status: truncatedFiles > 0 ? "partial" : "unknown",
truncated: truncatedFiles > 0,
omitted_nodes: truncatedFiles > 0 ? null : 0,
omitted_relations: truncatedFiles > 0 ? null : 0,
limits:
truncatedFiles > 0
? [
{
name: "semantic_graph_node_ceiling",
value: SEMANTIC_GRAPH_NODE_CEILING,
unit: "items" as const,
},
]
: [],
families: JAVASCRIPT_SEMANTIC_RELATION_FAMILIES.map((family) => ({
family,
status: semanticFamilyStatus(family, analysis),
retained_relations: [...state.relations.values()].filter(
(relation) =>
JAVASCRIPT_SEMANTIC_RELATION_FAMILY[relation.relation] ===
family,
).length,
omitted_relations: truncatedFiles > 0 ? null : 0,
unknown_ids: unknowns
.filter((unknown) => unknown.family === family)
.map(({ unknown_id: identifier }) => identifier),
})),
},
limitations: [
"The semantic graph contains static syntax observations and conservative relationship candidates; it does not claim runtime execution.",
"Local data flow does not claim control-flow-sensitive reaching definitions or arbitrary dynamic property resolution.",
"Promise ownership covers explicit unshadowed Promise construction, static factories, aggregation, chaining, and await syntax only.",
"Function fingerprints are static candidates; equal digests can remain ambiguous and do not prove behavioral equivalence.",
"Event extraction covers EventEmitter-style literal registrations, removals, and dispatch candidates; dynamic names remain unknown.",
"Timer extraction covers global or node:timers scheduling and exact local-handle cancellation.",
"Child-process extraction covers asynchronous node:child_process creation, literal argv/env/stdio options, exit/error listeners, and kill signals.",
"Configuration extraction covers process.env, process.argv, node:fs reads, and direct logical defaults.",
"Request extraction covers fetch, WebSocket, node:http/node:https construction, direct option fields, and exact local response consumers.",
"Boundary extraction covers unshadowed JSON/global coercions plus parse and validation method candidates.",
"Resource extraction covers built-in filesystem/network acquisition and exact local close/destroy/end handles.",
],
});
state.nodes.clear();
state.relations.clear();
state.unknowns.clear();
state.roots.clear();
fingerprints.length = 0;
return graph;
},
finish: (rootArtifactSha256, applicationGraph, analysis) =>
finishWith(
createJavaScriptSemanticGraph,
rootArtifactSha256,
applicationGraph,
analysis,
),
finishImmutableSteps: (rootArtifactSha256, applicationGraph, analysis) =>
finishWith(
createImmutableJavaScriptSemanticGraphSteps,
rootArtifactSha256,
applicationGraph,
analysis,
),
};
};