mirror of
https://github.com/morluto/rea
synced 2026-10-26 22:28:27 -04:00
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:
@@ -33,6 +33,7 @@ import type {
|
||||
JavaScriptSourceMapOriginal,
|
||||
} from "./JavaScriptArtifactAnalysisTypes.js";
|
||||
import { analyzeJavaScriptJsonModule } from "./JavaScriptJsonModules.js";
|
||||
import { completeJavaScriptAnalysisSteps } from "./JavaScriptAnalysisControl.js";
|
||||
import { htmlArtifactReferences } from "../../domain/javascript/htmlArtifactReferences.js";
|
||||
|
||||
interface MutableArtifactAnalysis<
|
||||
@@ -56,34 +57,44 @@ interface ArtifactAnalysisContext<
|
||||
readonly projectSemantics: (
|
||||
file: JavaScriptArtifactFile,
|
||||
ir: JavaScriptSemanticIr,
|
||||
) => SemanticIr;
|
||||
) => Generator<void, SemanticIr>;
|
||||
}
|
||||
|
||||
/** Analyze every text source from the artifact without a result quota. */
|
||||
export const analyzeJavaScriptArtifactFiles = (
|
||||
fileSet: JavaScriptArtifactFileSet,
|
||||
): JavaScriptArtifactAnalysis =>
|
||||
analyzeArtifactFiles(fileSet, (_file, ir) => ir);
|
||||
analyzeArtifactFiles(fileSet, function* (_file, ir) {
|
||||
// The full IR needs no projection; one step keeps the shared protocol.
|
||||
yield;
|
||||
return ir;
|
||||
});
|
||||
|
||||
/** Project each file's full IR immediately, retaining only later module facts. */
|
||||
/**
|
||||
* Project each file's full IR immediately, retaining only later module facts.
|
||||
* Parsing, analysis and each projection pass run as separate steps, so control
|
||||
* messages and cancellation are handled within one large file as well.
|
||||
*/
|
||||
export const analyzeAndProjectJavaScriptArtifactFiles = async (
|
||||
fileSet: JavaScriptArtifactFileSet,
|
||||
projectSemantics: (
|
||||
file: JavaScriptArtifactFile,
|
||||
ir: JavaScriptSemanticIr,
|
||||
) => void,
|
||||
) => Iterator<void, void> | void,
|
||||
beforeFile?: (
|
||||
file: JavaScriptArtifactFile,
|
||||
completed: number,
|
||||
total: number,
|
||||
) => Promise<void>,
|
||||
signal?: AbortSignal,
|
||||
): Promise<JavaScriptModuleArtifactAnalysis> => {
|
||||
const state = emptyArtifactAnalysis<JavaScriptModuleSemanticIr>();
|
||||
const projectFileSemantics = (
|
||||
const projectFileSemantics = function* (
|
||||
file: JavaScriptArtifactFile,
|
||||
ir: JavaScriptSemanticIr,
|
||||
): JavaScriptModuleSemanticIr => {
|
||||
projectSemantics(file, ir);
|
||||
): Generator<void, JavaScriptModuleSemanticIr> {
|
||||
const projection = projectSemantics(file, ir);
|
||||
if (projection !== undefined) while (projection.next().done !== true) yield;
|
||||
const programScopes = ir.scopes.filter(({ kind }) => kind === "program");
|
||||
const programScopeId = programScopes[0]?.scopeId;
|
||||
const localNames = new Set(
|
||||
@@ -111,7 +122,10 @@ export const analyzeAndProjectJavaScriptArtifactFiles = async (
|
||||
// Let progress, cancellation and garbage collection run between files.
|
||||
await setImmediate();
|
||||
await beforeFile?.(file, index, fileSet.files.length);
|
||||
analyzeArtifactFile(file, context);
|
||||
await completeJavaScriptAnalysisSteps(
|
||||
analyzeArtifactFileSteps(file, context),
|
||||
signal,
|
||||
);
|
||||
}
|
||||
return finalizeArtifactAnalysis(state);
|
||||
};
|
||||
@@ -122,7 +136,10 @@ const analyzeArtifactFiles = <SemanticIr extends JavaScriptModuleSemanticIr>(
|
||||
): JavaScriptArtifactAnalysis<SemanticIr> => {
|
||||
const state = emptyArtifactAnalysis<SemanticIr>();
|
||||
const context = { state, projectSemantics };
|
||||
for (const file of fileSet.files) analyzeArtifactFile(file, context);
|
||||
for (const file of fileSet.files) {
|
||||
const steps = analyzeArtifactFileSteps(file, context);
|
||||
while (steps.next().done !== true);
|
||||
}
|
||||
return finalizeArtifactAnalysis(state);
|
||||
};
|
||||
|
||||
@@ -148,10 +165,12 @@ const finalizeArtifactAnalysis = <
|
||||
};
|
||||
};
|
||||
|
||||
const analyzeArtifactFile = <SemanticIr extends JavaScriptModuleSemanticIr>(
|
||||
function* analyzeArtifactFileSteps<
|
||||
SemanticIr extends JavaScriptModuleSemanticIr,
|
||||
>(
|
||||
file: JavaScriptArtifactFile,
|
||||
context: ArtifactAnalysisContext<SemanticIr>,
|
||||
): void => {
|
||||
): Generator<void, void> {
|
||||
const { state } = context;
|
||||
addStructuredObservations(file, state);
|
||||
if (file.kind === "html" && file.text.included)
|
||||
@@ -162,6 +181,7 @@ const analyzeArtifactFile = <SemanticIr extends JavaScriptModuleSemanticIr>(
|
||||
return;
|
||||
}
|
||||
const parsed = parseJavaScriptSource(file.text.value, file.path);
|
||||
yield;
|
||||
if (parsed === null) {
|
||||
const analysis = failedJavaScriptStaticAnalysis();
|
||||
state.files.push({ file, javascript: analysis, semantic: null });
|
||||
@@ -175,17 +195,19 @@ const analyzeArtifactFile = <SemanticIr extends JavaScriptModuleSemanticIr>(
|
||||
openReceiverFacts,
|
||||
);
|
||||
const staticFindings = findingCount(analysis);
|
||||
yield;
|
||||
const semantics =
|
||||
analysis.parse_status === "complete" || analysis.parse_status === "partial"
|
||||
? analyzeParsedJavaScriptSemantics(parsed)
|
||||
: null;
|
||||
const projected =
|
||||
semantics === null
|
||||
? null
|
||||
: yield* context.projectSemantics(file, semantics);
|
||||
state.files.push({
|
||||
file,
|
||||
javascript: analysis,
|
||||
semantic:
|
||||
semantics === null
|
||||
? null
|
||||
: { ir: context.projectSemantics(file, semantics) },
|
||||
semantic: projected === null ? null : { ir: projected },
|
||||
});
|
||||
state.visitedNodes += analysis.visited_ast_nodes;
|
||||
state.findings += staticFindings + (semantics?.moduleLinks.length ?? 0);
|
||||
@@ -194,7 +216,7 @@ const analyzeArtifactFile = <SemanticIr extends JavaScriptModuleSemanticIr>(
|
||||
0,
|
||||
);
|
||||
if (analysis.parse_status === "failed") state.parseFailures += 1;
|
||||
};
|
||||
}
|
||||
|
||||
const addSourceMap = (
|
||||
file: JavaScriptArtifactFile,
|
||||
|
||||
Reference in New Issue
Block a user