fix(javascript): yield during per-file projection and graph construction (#1525)

analyze_javascript_application ran several long phases with no event-loop
turn, so MCP ping and cancellation waited: each file's parse, analysis and
semantic projection; building every application node and relationship
before the application graph's first sealing step; and whole-graph schema
parses inside both immutable graph factories.

- Each file runs as steps (parse, static analysis, semantic analysis, then
  each projection pass), driven by the existing time-sliced checkpoint.
- Application graph input construction yields between phases, per module
  link and per file of findings.
- The pipeline transfers its privately built graph inputs to new sealing
  steps that validate the graph's own fields synchronously and elements,
  content checks and the digest in steps. The public copying factories
  keep their synchronous capture.

Results are unchanged: graph IDs and the complete result digest match main
on five inputs including Obsidian's 23 MB obsidian.asar. On the issue's
1,200-function fixture the longest event-loop block falls from about
290 ms to 90 ms, and ping after abort from up to 286 ms to at most 56 ms.
On obsidian.asar it falls from 10.9 s to 5.4 s; the rest is one analyzer
stage of a single very large file.

Refs #1462

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: N0zoM1z0 <161784452+N0zoM1z0@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-10-10 03:14:37 -07:00
1 parent 72b3209f3a
commit 5fdb0636b4
11 files changed
+438 -83

No files matched your search

@@ -1,6 +1,6 @@
import {
createJavaScriptSemanticGraph,
createImmutableJavaScriptSemanticGraphSteps,
sealTransferredJavaScriptSemanticGraphSteps,
type JavaScriptSemanticGraph,
} from "../../domain/javascript/javascriptSemanticGraph.js";
import type {
@@ -130,6 +130,11 @@ export interface JavaScriptSemanticGraphProjection {
file: JavaScriptArtifactFile,
ir: JavaScriptSemanticIr,
) => void;
/** Project one file in steps between its projection passes. */
readonly projectFileSteps: (
file: JavaScriptArtifactFile,
ir: JavaScriptSemanticIr,
) => Generator<void, void>;
readonly finish: (
rootArtifactSha256: string,
applicationGraph: BuilderInput["applicationGraph"],
@@ -146,10 +151,10 @@ export const createJavaScriptSemanticGraphProjection =
const state = emptyState({ nodes: [] });
const fingerprints: JavaScriptSemanticFingerprint[] = [];
let truncatedFiles = 0;
const projectSource = (
const projectSourceSteps = function* (
file: JavaScriptArtifactFile,
ir: JavaScriptSemanticIr,
): void => {
): Generator<void, void> {
if (state.nodes.size >= SEMANTIC_GRAPH_NODE_CEILING) {
truncatedFiles += 1;
return;
@@ -161,7 +166,7 @@ export const createJavaScriptSemanticGraphProjection =
remainingTreeBudget,
);
state.fileNodesDropped = false;
fingerprints.push(...projectFile(file, ir, state));
fingerprints.push(...(yield* projectFileSteps(file, ir, state)));
// Nodes were dropped only if the budget actually blocked creation. A file
// that exactly fills its share ends with a zero budget without dropping
// anything, so the remaining budget alone cannot decide truncation.
@@ -198,7 +203,8 @@ export const createJavaScriptSemanticGraphProjection =
evidence_contexts: state.evidenceContexts.contexts,
nodes: [...state.nodes.values()],
relations,
fingerprints,
// The input is transferred to the factory; `fingerprints` is cleared below.
fingerprints: [...fingerprints],
unknowns,
coverage: {
status: truncatedFiles > 0 ? "partial" : "unknown",
@@ -246,7 +252,11 @@ export const createJavaScriptSemanticGraphProjection =
return graph;
};
return {
projectFile: projectSource,
projectFile: (file, ir) => {
const steps = projectSourceSteps(file, ir);
while (steps.next().done !== true);
},
projectFileSteps: projectSourceSteps,
finish: (rootArtifactSha256, applicationGraph) =>
finishWith(
createJavaScriptSemanticGraph,
@@ -255,18 +265,19 @@ export const createJavaScriptSemanticGraphProjection =
),
finishImmutableSteps: (rootArtifactSha256, applicationGraph) =>
finishWith(
createImmutableJavaScriptSemanticGraphSteps,
sealTransferredJavaScriptSemanticGraphSteps,
rootArtifactSha256,
applicationGraph,
),
};
};
const projectFile = (
function* projectFileSteps(
file: JavaScriptArtifactFile,
ir: JavaScriptSemanticIr,
state: BuilderState,
): JavaScriptSemanticFingerprint[] => {
): Generator<void, JavaScriptSemanticFingerprint[]> {
yield;
const moduleNode = retainNode(state, file, {
kind: "module",
roleKey: "module",
@@ -328,6 +339,7 @@ const projectFile = (
callableOwnerAt,
callSiteAt: createSemanticCallSiteLookup(ir, callSiteNodes),
};
yield;
projectDefinitionsAndReferences(definitions);
const context: FileContext = {
...definitions,
@@ -335,26 +347,20 @@ const projectFile = (
definitions.referenceNodes,
),
};
projectSemanticValues(context);
projectSemanticObjects(context);
projectCalls(context);
projectSemanticPromises(context);
projectSemanticEvents(context);
projectSemanticTimers(context);
projectSemanticChildProcesses(context);
projectSemanticConfiguration(context);
projectSemanticRequests(context);
projectSemanticBoundaries(context);
projectSemanticResources(context);
projectSemanticClosureCaptures(context);
projectSemanticFrontiers(context);
// Each pass completes before the next starts, so yielding between passes
// keeps projection order and results unchanged.
for (const project of FILE_PROJECTION_PASSES) {
yield;
project(context);
}
yield;
return projectSemanticFunctionFingerprints(
file,
ir,
callableNodes,
state.evidenceContexts,
);
};
}
const createReturnSiteNodes = (
file: JavaScriptArtifactFile,
@@ -543,3 +549,20 @@ const createArgumentNode = (
: `argument ${String(argument.index)}`,
functionNodeId: callNode.function_node_id,
});
/** Per-file projection passes, run in this order with a yield between each. */
const FILE_PROJECTION_PASSES: readonly ((context: FileContext) => void)[] = [
projectSemanticValues,
projectSemanticObjects,
projectCalls,
projectSemanticPromises,
projectSemanticEvents,
projectSemanticTimers,
projectSemanticChildProcesses,
projectSemanticConfiguration,
projectSemanticRequests,
projectSemanticBoundaries,
projectSemanticResources,
projectSemanticClosureCaptures,
projectSemanticFrontiers,
];