Skip to content

Commit 8b3aa3d

Browse files
committed
fix(native): add post-pass phase timings to result.phases
Times each JS post-pass in tryNativeOrchestrator and exposes the measurements in BuildResult.phases: - gapDetectMs — dropped-language gap detection + backfill - chaMs — CHA expansion (interface dispatch) - thisDispatchMs — this/super dispatch WASM re-parse (was already tracked but now properly named alongside the rest) - reclassifyMs — scoped role re-classification after edge insertion - techniqueBackfillMs — technique-column UPDATE on native-written edges Previously only thisDispatchMs was reported, causing wall-clock vs phaseSum to diverge by 1.1s+ on 1-file rebuilds and making benchmark regressions undiagnosable from committed history. Updates update-incremental-report.ts to render the new phases in a collapsible details block under each engine's 1-file rebuild section. Closes #1434
1 parent 3db5d8c commit 8b3aa3d

3 files changed

Lines changed: 102 additions & 6 deletions

File tree

scripts/update-incremental-report.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,55 @@ for (const engineKey of ['native', 'wasm']) {
161161
md += `| Full build | ${formatMs(e.fullBuildMs)} |\n`;
162162
md += `| No-op rebuild | ${e.noopRebuildMs != null ? formatMs(e.noopRebuildMs) : 'n/a'} |\n`;
163163
md += `| 1-file rebuild | ${e.oneFileRebuildMs != null ? formatMs(e.oneFileRebuildMs) : 'n/a'} |\n\n`;
164+
165+
// 1-file rebuild phase breakdown — skipped when phases are unavailable (older
166+
// benchmark entries that predate per-phase tracking, or failed runs).
167+
const ph = e.oneFilePhases;
168+
if (ph && typeof ph === 'object') {
169+
md += `<details><summary>1-file rebuild phase breakdown (${engineKey})</summary>\n\n`;
170+
md += '| Phase | Time |\n';
171+
md += '|-------|-----:|\n';
172+
// Core Rust pipeline phases (present for both engines)
173+
const corePhases = [
174+
['setup', 'setupMs'],
175+
['collect', 'collectMs'],
176+
['detect', 'detectMs'],
177+
['parse', 'parseMs'],
178+
['insert', 'insertMs'],
179+
['resolve', 'resolveMs'],
180+
['edges', 'edgesMs'],
181+
['structure', 'structureMs'],
182+
['roles', 'rolesMs'],
183+
];
184+
for (const [label, key] of corePhases) {
185+
if (ph[key] != null) md += `| ${label} | ${formatMs(ph[key])} |\n`;
186+
}
187+
// Native-only JS post-pass phases (only present when engine=native)
188+
if (engineKey === 'native') {
189+
const nativePostPhases = [
190+
['gap detect + backfill', 'gapDetectMs'],
191+
['CHA expansion', 'chaMs'],
192+
['this/super dispatch', 'thisDispatchMs'],
193+
['role reclassify', 'reclassifyMs'],
194+
['technique backfill', 'techniqueBackfillMs'],
195+
];
196+
for (const [label, key] of nativePostPhases) {
197+
if (ph[key] != null) md += `| ${label} | ${formatMs(ph[key])} |\n`;
198+
}
199+
}
200+
// Analysis phases (present for both engines)
201+
const analysisPhases = [
202+
['ast', 'astMs'],
203+
['complexity', 'complexityMs'],
204+
['cfg', 'cfgMs'],
205+
['dataflow', 'dataflowMs'],
206+
['finalize', 'finalizeMs'],
207+
];
208+
for (const [label, key] of analysisPhases) {
209+
if (ph[key] != null) md += `| ${label} | ${formatMs(ph[key])} |\n`;
210+
}
211+
md += '\n</details>\n\n';
212+
}
164213
}
165214

166215
const r = latest.resolve;

src/domain/graph/builder/stages/native-orchestrator.ts

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -577,7 +577,11 @@ function runPostNativeCha(
577577
AND INSTR(tgt.name, '.') > 0
578578
AND src.file IN (${ph})`,
579579
)
580-
.all(...chunk) as Array<{ source_id: number; method_name: string; caller_file: string | null }>;
580+
.all(...chunk) as Array<{
581+
source_id: number;
582+
method_name: string;
583+
caller_file: string | null;
584+
}>;
581585
rows.push(...chunkRows);
582586
}
583587
callToMethods = rows;
@@ -933,12 +937,20 @@ async function runPostNativeThisDispatch(
933937
return { elapsedMs: Date.now() - t0, targetIds, affectedFiles };
934938
}
935939

940+
interface PostPassTimings {
941+
gapDetectMs: number;
942+
chaMs: number;
943+
thisDispatchMs: number;
944+
reclassifyMs: number;
945+
techniqueBackfillMs: number;
946+
}
947+
936948
/** Format timing result from native orchestrator phases + JS post-processing. */
937949
function formatNativeTimingResult(
938950
p: Record<string, number>,
939951
structurePatchMs: number,
940952
analysisTiming: { astMs: number; complexityMs: number; cfgMs: number; dataflowMs: number },
941-
thisDispatchMs: number,
953+
postPass: PostPassTimings,
942954
): BuildResult {
943955
return {
944956
phases: {
@@ -951,7 +963,11 @@ function formatNativeTimingResult(
951963
edgesMs: +(p.edgesMs ?? 0).toFixed(1),
952964
structureMs: +((p.structureMs ?? 0) + structurePatchMs).toFixed(1),
953965
rolesMs: +(p.rolesMs ?? 0).toFixed(1),
954-
thisDispatchMs: +thisDispatchMs.toFixed(1),
966+
gapDetectMs: +postPass.gapDetectMs.toFixed(1),
967+
chaMs: +postPass.chaMs.toFixed(1),
968+
thisDispatchMs: +postPass.thisDispatchMs.toFixed(1),
969+
reclassifyMs: +postPass.reclassifyMs.toFixed(1),
970+
techniqueBackfillMs: +postPass.techniqueBackfillMs.toFixed(1),
955971
astMs: +(analysisTiming.astMs ?? 0).toFixed(1),
956972
complexityMs: +(analysisTiming.complexityMs ?? 0).toFixed(1),
957973
cfgMs: +(analysisTiming.cfgMs ?? 0).toFixed(1),
@@ -1490,8 +1506,14 @@ export async function tryNativeOrchestrator(
14901506
ctx.db = openDb(ctx.dbPath);
14911507
ctx.nativeFirstProxy = false;
14921508
} else if (!ctx.nativeFirstProxy && !handoffWalAfterNativeBuild(ctx)) {
1493-
// DB reopen failed — return partial result
1494-
return formatNativeTimingResult(p, 0, analysisTiming, 0);
1509+
// DB reopen failed — return partial result (no post-pass phases completed)
1510+
return formatNativeTimingResult(p, 0, analysisTiming, {
1511+
gapDetectMs: 0,
1512+
chaMs: 0,
1513+
thisDispatchMs: 0,
1514+
reclassifyMs: 0,
1515+
techniqueBackfillMs: 0,
1516+
});
14951517
}
14961518
}
14971519

@@ -1513,6 +1535,7 @@ export async function tryNativeOrchestrator(
15131535
// gated below.
15141536
const removedCount = result.removedCount ?? 0;
15151537
const changedCount = result.changedCount ?? 0;
1538+
const gapDetectStart = performance.now();
15161539
const gap = detectDroppedLanguageGap(ctx);
15171540
if (
15181541
result.isFullBuild ||
@@ -1523,6 +1546,7 @@ export async function tryNativeOrchestrator(
15231546
) {
15241547
await backfillNativeDroppedFiles(ctx, gap);
15251548
}
1549+
const gapDetectMs = performance.now() - gapDetectStart;
15261550

15271551
// Phase 8.5: expand CHA call edges (interface dispatch → concrete implementations).
15281552
// Returns the affected files so role re-classification below can be scoped to
@@ -1531,11 +1555,13 @@ export async function tryNativeOrchestrator(
15311555
// Function-as-object-property methods (`fn.method = function() {}`) are extracted
15321556
// natively by the Rust engine (#1432) and resolved in-build by its edge builder, so
15331557
// no WASM re-parse post-pass is needed for them. `Foo.prototype.bar = fn` likewise.
1558+
const chaStart = performance.now();
15341559
const { newEdgeCount: chaEdgeCount, affectedFiles: chaAffectedFiles } = runPostNativeCha(
15351560
ctx.db as unknown as BetterSqlite3Database,
15361561
// null = full build (scan all call→method edges); array = incremental (gate queries decide scope)
15371562
result.isFullBuild ? null : (result.changedFiles ?? null),
15381563
);
1564+
const chaMs = performance.now() - chaStart;
15391565

15401566
// Phase 8.5: this/super dispatch — hybrid WASM re-parse to resolve call sites
15411567
// whose raw receiver info the Rust pipeline does not persist to DB.
@@ -1558,6 +1584,7 @@ export async function tryNativeOrchestrator(
15581584
// files restores correctness without re-running the classifier over the
15591585
// whole graph (which cost ~130ms per build on codegraph itself and was a
15601586
// major part of the v3.12.0 native full-build benchmark regression).
1587+
let reclassifyMs = 0;
15611588
if (chaEdgeCount > 0 || thisDispatchTargetIds.size > 0) {
15621589
const affectedFiles = [...new Set([...chaAffectedFiles, ...thisDispatchAffectedFiles])];
15631590
// When edges were inserted but all their endpoint nodes have null `file`
@@ -1566,6 +1593,7 @@ export async function tryNativeOrchestrator(
15661593
// case — scoped classification with an empty set would be a no-op, leaving
15671594
// roles stale for those nodes.
15681595
const scopedFiles = affectedFiles.length > 0 ? affectedFiles : null;
1596+
const reclassifyStart = performance.now();
15691597
try {
15701598
const { classifyNodeRoles } = (await import('../../../../features/structure.js')) as {
15711599
classifyNodeRoles: (
@@ -1582,13 +1610,16 @@ export async function tryNativeOrchestrator(
15821610
} catch (err) {
15831611
debug(`Post-pass role re-classification failed: ${toErrorMessage(err)}`);
15841612
}
1613+
reclassifyMs = performance.now() - reclassifyStart;
15851614
}
15861615

15871616
// Backfill the `technique` column on `calls` edges written by the Rust
15881617
// orchestrator, which does not write the column. Runs after all edge-writing
15891618
// phases (including the WASM dropped-language backfill, CHA post-pass, and
15901619
// this/super dispatch) so every new edge in this build cycle gets a label.
1620+
const techniqueBackfillStart = performance.now();
15911621
backfillEdgeTechniquesAfterNativeOrchestrator(ctx.db, !!result.isFullBuild, result.changedFiles);
1622+
const techniqueBackfillMs = performance.now() - techniqueBackfillStart;
15921623

15931624
// Re-count nodes/edges now that all edge-writing post-passes have run: the
15941625
// Rust orchestrator captured its counts before the JS post-passes added
@@ -1633,5 +1664,11 @@ export async function tryNativeOrchestrator(
16331664
}
16341665

16351666
closeDbPair({ db: ctx.db, nativeDb: ctx.nativeDb });
1636-
return formatNativeTimingResult(p, structurePatchMs, analysisTiming, thisDispatchMs);
1667+
return formatNativeTimingResult(p, structurePatchMs, analysisTiming, {
1668+
gapDetectMs,
1669+
chaMs,
1670+
thisDispatchMs,
1671+
reclassifyMs,
1672+
techniqueBackfillMs,
1673+
});
16371674
}

src/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1268,8 +1268,18 @@ export interface BuildResult {
12681268
edgesMs: number;
12691269
structureMs: number;
12701270
rolesMs: number;
1271+
/** Wall-clock time for the prototype-method post-pass (native path only). */
1272+
protoMethodsMs?: number;
1273+
/** Wall-clock time for the CHA expansion post-pass (native path only). */
1274+
chaMs?: number;
12711275
/** Wall-clock time for the this/super dispatch WASM post-pass (native path only). */
12721276
thisDispatchMs?: number;
1277+
/** Wall-clock time for the dropped-language gap detection + backfill (native path only). */
1278+
gapDetectMs?: number;
1279+
/** Wall-clock time for role re-classification after JS edge-writing post-passes (native path only). */
1280+
reclassifyMs?: number;
1281+
/** Wall-clock time for the technique-column backfill on native-written edges (native path only). */
1282+
techniqueBackfillMs?: number;
12731283
astMs: number;
12741284
complexityMs: number;
12751285
cfgMs: number;

0 commit comments

Comments
 (0)