diff --git a/scripts/update-incremental-report.ts b/scripts/update-incremental-report.ts
index bce3e734..3a047a84 100644
--- a/scripts/update-incremental-report.ts
+++ b/scripts/update-incremental-report.ts
@@ -161,6 +161,55 @@ for (const engineKey of ['native', 'wasm']) {
md += `| Full build | ${formatMs(e.fullBuildMs)} |\n`;
md += `| No-op rebuild | ${e.noopRebuildMs != null ? formatMs(e.noopRebuildMs) : 'n/a'} |\n`;
md += `| 1-file rebuild | ${e.oneFileRebuildMs != null ? formatMs(e.oneFileRebuildMs) : 'n/a'} |\n\n`;
+
+ // 1-file rebuild phase breakdown — skipped when phases are unavailable (older
+ // benchmark entries that predate per-phase tracking, or failed runs).
+ const ph = e.oneFilePhases;
+ if (ph && typeof ph === 'object') {
+ md += `1-file rebuild phase breakdown (${engineKey})
\n\n`;
+ md += '| Phase | Time |\n';
+ md += '|-------|-----:|\n';
+ // Core Rust pipeline phases (present for both engines)
+ const corePhases = [
+ ['setup', 'setupMs'],
+ ['collect', 'collectMs'],
+ ['detect', 'detectMs'],
+ ['parse', 'parseMs'],
+ ['insert', 'insertMs'],
+ ['resolve', 'resolveMs'],
+ ['edges', 'edgesMs'],
+ ['structure', 'structureMs'],
+ ['roles', 'rolesMs'],
+ ];
+ for (const [label, key] of corePhases) {
+ if (ph[key] != null) md += `| ${label} | ${formatMs(ph[key])} |\n`;
+ }
+ // Native-only JS post-pass phases (only present when engine=native)
+ if (engineKey === 'native') {
+ const nativePostPhases = [
+ ['gap detect + backfill', 'gapDetectMs'],
+ ['CHA expansion', 'chaMs'],
+ ['this/super dispatch', 'thisDispatchMs'],
+ ['role reclassify', 'reclassifyMs'],
+ ['technique backfill', 'techniqueBackfillMs'],
+ ];
+ for (const [label, key] of nativePostPhases) {
+ if (ph[key] != null) md += `| ${label} | ${formatMs(ph[key])} |\n`;
+ }
+ }
+ // Analysis phases (present for both engines)
+ const analysisPhases = [
+ ['ast', 'astMs'],
+ ['complexity', 'complexityMs'],
+ ['cfg', 'cfgMs'],
+ ['dataflow', 'dataflowMs'],
+ ['finalize', 'finalizeMs'],
+ ];
+ for (const [label, key] of analysisPhases) {
+ if (ph[key] != null) md += `| ${label} | ${formatMs(ph[key])} |\n`;
+ }
+ md += '\n \n\n';
+ }
}
const r = latest.resolve;
diff --git a/src/domain/graph/builder/stages/native-orchestrator.ts b/src/domain/graph/builder/stages/native-orchestrator.ts
index 33a3f62d..e43fabf8 100644
--- a/src/domain/graph/builder/stages/native-orchestrator.ts
+++ b/src/domain/graph/builder/stages/native-orchestrator.ts
@@ -716,7 +716,7 @@ async function runPostNativeThisDispatch(
changedFiles: string[] | undefined,
isFullBuild: boolean,
): Promise<{ elapsedMs: number; targetIds: Set; affectedFiles: Set }> {
- const t0 = Date.now();
+ const t0 = performance.now();
const targetIds = new Set();
// Files containing endpoints of newly inserted edges — lets the caller scope
// role re-classification to the nodes whose fan-in/out actually changed.
@@ -948,7 +948,15 @@ async function runPostNativeThisDispatch(
(symbols as { _tree?: unknown; _langId?: unknown })._langId = undefined;
}
- return { elapsedMs: Date.now() - t0, targetIds, affectedFiles };
+ return { elapsedMs: performance.now() - t0, targetIds, affectedFiles };
+}
+
+interface PostPassTimings {
+ gapDetectMs: number;
+ chaMs: number;
+ thisDispatchMs: number;
+ reclassifyMs: number;
+ techniqueBackfillMs: number;
}
/** Format timing result from native orchestrator phases + JS post-processing. */
@@ -956,7 +964,7 @@ function formatNativeTimingResult(
p: Record,
structurePatchMs: number,
analysisTiming: { astMs: number; complexityMs: number; cfgMs: number; dataflowMs: number },
- thisDispatchMs: number,
+ postPass: PostPassTimings,
): BuildResult {
return {
phases: {
@@ -969,7 +977,11 @@ function formatNativeTimingResult(
edgesMs: +(p.edgesMs ?? 0).toFixed(1),
structureMs: +((p.structureMs ?? 0) + structurePatchMs).toFixed(1),
rolesMs: +(p.rolesMs ?? 0).toFixed(1),
- thisDispatchMs: +thisDispatchMs.toFixed(1),
+ gapDetectMs: +postPass.gapDetectMs.toFixed(1),
+ chaMs: +postPass.chaMs.toFixed(1),
+ thisDispatchMs: +postPass.thisDispatchMs.toFixed(1),
+ reclassifyMs: +postPass.reclassifyMs.toFixed(1),
+ techniqueBackfillMs: +postPass.techniqueBackfillMs.toFixed(1),
astMs: +(analysisTiming.astMs ?? 0).toFixed(1),
complexityMs: +(analysisTiming.complexityMs ?? 0).toFixed(1),
cfgMs: +(analysisTiming.cfgMs ?? 0).toFixed(1),
@@ -1508,8 +1520,14 @@ export async function tryNativeOrchestrator(
ctx.db = openDb(ctx.dbPath);
ctx.nativeFirstProxy = false;
} else if (!ctx.nativeFirstProxy && !handoffWalAfterNativeBuild(ctx)) {
- // DB reopen failed — return partial result
- return formatNativeTimingResult(p, 0, analysisTiming, 0);
+ // DB reopen failed — return partial result (no post-pass phases completed)
+ return formatNativeTimingResult(p, 0, analysisTiming, {
+ gapDetectMs: 0,
+ chaMs: 0,
+ thisDispatchMs: 0,
+ reclassifyMs: 0,
+ techniqueBackfillMs: 0,
+ });
}
}
@@ -1531,6 +1549,7 @@ export async function tryNativeOrchestrator(
// gated below.
const removedCount = result.removedCount ?? 0;
const changedCount = result.changedCount ?? 0;
+ const gapDetectStart = performance.now();
const gap = detectDroppedLanguageGap(ctx);
if (
result.isFullBuild ||
@@ -1541,6 +1560,7 @@ export async function tryNativeOrchestrator(
) {
await backfillNativeDroppedFiles(ctx, gap);
}
+ const gapDetectMs = performance.now() - gapDetectStart;
// Phase 8.5: expand CHA call edges (interface dispatch → concrete implementations).
// Returns the affected files so role re-classification below can be scoped to
@@ -1549,11 +1569,13 @@ export async function tryNativeOrchestrator(
// Function-as-object-property methods (`fn.method = function() {}`) are extracted
// natively by the Rust engine (#1432) and resolved in-build by its edge builder, so
// no WASM re-parse post-pass is needed for them. `Foo.prototype.bar = fn` likewise.
+ const chaStart = performance.now();
const { newEdgeCount: chaEdgeCount, affectedFiles: chaAffectedFiles } = runPostNativeCha(
ctx.db as unknown as BetterSqlite3Database,
// null = full build (scan all call→method edges); array = incremental (gate queries decide scope)
result.isFullBuild ? null : (result.changedFiles ?? null),
);
+ const chaMs = performance.now() - chaStart;
// Phase 8.5: this/super dispatch — hybrid WASM re-parse to resolve call sites
// whose raw receiver info the Rust pipeline does not persist to DB.
@@ -1576,6 +1598,7 @@ export async function tryNativeOrchestrator(
// files restores correctness without re-running the classifier over the
// whole graph (which cost ~130ms per build on codegraph itself and was a
// major part of the v3.12.0 native full-build benchmark regression).
+ let reclassifyMs = 0;
if (chaEdgeCount > 0 || thisDispatchTargetIds.size > 0) {
const affectedFiles = [...new Set([...chaAffectedFiles, ...thisDispatchAffectedFiles])];
// When edges were inserted but all their endpoint nodes have null `file`
@@ -1584,6 +1607,7 @@ export async function tryNativeOrchestrator(
// case — scoped classification with an empty set would be a no-op, leaving
// roles stale for those nodes.
const scopedFiles = affectedFiles.length > 0 ? affectedFiles : null;
+ const reclassifyStart = performance.now();
try {
const { classifyNodeRoles } = (await import('../../../../features/structure.js')) as {
classifyNodeRoles: (
@@ -1600,13 +1624,16 @@ export async function tryNativeOrchestrator(
} catch (err) {
debug(`Post-pass role re-classification failed: ${toErrorMessage(err)}`);
}
+ reclassifyMs = performance.now() - reclassifyStart;
}
// Backfill the `technique` column on `calls` edges written by the Rust
// orchestrator, which does not write the column. Runs after all edge-writing
// phases (including the WASM dropped-language backfill, CHA post-pass, and
// this/super dispatch) so every new edge in this build cycle gets a label.
+ const techniqueBackfillStart = performance.now();
backfillEdgeTechniquesAfterNativeOrchestrator(ctx.db, !!result.isFullBuild, result.changedFiles);
+ const techniqueBackfillMs = performance.now() - techniqueBackfillStart;
// Re-count nodes/edges now that all edge-writing post-passes have run: the
// Rust orchestrator captured its counts before the JS post-passes added
@@ -1651,5 +1678,11 @@ export async function tryNativeOrchestrator(
}
closeDbPair({ db: ctx.db, nativeDb: ctx.nativeDb });
- return formatNativeTimingResult(p, structurePatchMs, analysisTiming, thisDispatchMs);
+ return formatNativeTimingResult(p, structurePatchMs, analysisTiming, {
+ gapDetectMs,
+ chaMs,
+ thisDispatchMs,
+ reclassifyMs,
+ techniqueBackfillMs,
+ });
}
diff --git a/src/types.ts b/src/types.ts
index d7f97da6..8e1f46fc 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1268,8 +1268,16 @@ export interface BuildResult {
edgesMs: number;
structureMs: number;
rolesMs: number;
+ /** Wall-clock time for the CHA expansion post-pass (native path only). */
+ chaMs?: number;
/** Wall-clock time for the this/super dispatch WASM post-pass (native path only). */
thisDispatchMs?: number;
+ /** Wall-clock time for the dropped-language gap detection + backfill (native path only). */
+ gapDetectMs?: number;
+ /** Wall-clock time for role re-classification after JS edge-writing post-passes (native path only). */
+ reclassifyMs?: number;
+ /** Wall-clock time for the technique-column backfill on native-written edges (native path only). */
+ techniqueBackfillMs?: number;
astMs: number;
complexityMs: number;
cfgMs: number;