From 498ee2150a3e89b0fd3bcd1e558177997260f751 Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Fri, 12 Jun 2026 19:12:20 -0600 Subject: [PATCH 1/2] fix(perf): guard post-native passes against unnecessary work on 1-file incremental rebuilds On 1-file native incremental builds, two JS post-passes ran unconditionally even when they had no work to do: - `backfillNativeDroppedFiles`: called whenever changedCount > 0, even when detectDroppedLanguageGap returned an empty gap. Gate now checks gap.missingAbs.length > 0 || gap.staleRel.length > 0 directly, matching backfillNativeDroppedFiles's own internal early-exit guard. - Node/edge COUNT(*) re-count: ran unconditionally after all post-passes even when none of them wrote any edges. COUNT(*) over 50K+ edge tables is non-trivial, especially via the NativeDbProxy napi-rs round-trip. Now gated on postPassWroteData (backfill | CHA edges | this-dispatch edges). Closes #1454 --- .../builder/stages/native-orchestrator.ts | 54 +++++++++---------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/src/domain/graph/builder/stages/native-orchestrator.ts b/src/domain/graph/builder/stages/native-orchestrator.ts index c16213a7..18fe478e 100644 --- a/src/domain/graph/builder/stages/native-orchestrator.ts +++ b/src/domain/graph/builder/stages/native-orchestrator.ts @@ -1524,28 +1524,18 @@ export async function tryNativeOrchestrator( // stale native binaries). WASM handles those — backfill via WASM so both // engines process the same file set (#967). // - // Detect the gap once (fs walk + 2 DB queries, ~20–30ms) and use it for - // both gating and the backfill itself. On dirty incrementals/full builds - // the orchestrator signals trigger backfill, so the walk happens once - // (instead of redundantly inside backfill). On quiet incrementals we - // still pay the walk so we can detect brand-new files in dropped-language - // extensions — a gap that the orchestrator's `detect_removed_files` - // filter (#1070) leaves open (#1083, #1091). The pre-check is cheap - // because the expensive part (WASM re-parse of the missing set) is - // gated below. - const removedCount = result.removedCount ?? 0; - const changedCount = result.changedCount ?? 0; + // Detect the gap once (fs walk + 2 DB queries) and use it for both gating + // and the backfill itself. On quiet incrementals we still pay the walk so + // we can detect brand-new files in dropped-language extensions — a gap that + // the orchestrator's `detect_removed_files` filter (#1070) leaves open + // (#1083, #1091). The pre-check is cheap because the expensive part (WASM + // re-parse of the missing set) is gated below. const gapDetectStart = performance.now(); const gap = detectDroppedLanguageGap(ctx); - if ( - result.isFullBuild || - removedCount > 0 || - changedCount > 0 || - gap.missingAbs.length > 0 || - gap.staleRel.length > 0 - ) { + if (gap.missingAbs.length > 0 || gap.staleRel.length > 0) { await backfillNativeDroppedFiles(ctx, gap); } + const backfillHappened = gap.missingAbs.length > 0 || gap.staleRel.length > 0; const gapDetectMs = performance.now() - gapDetectStart; // Phase 8.5: expand CHA call edges (interface dispatch → concrete implementations). @@ -1624,19 +1614,27 @@ export async function tryNativeOrchestrator( // 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 // edges, so both its summary and build_meta under-report (#1452). + // + // Fast path: skip the COUNT(*) scan when no post-pass wrote any edges. + // COUNT(*) on large tables (50K+ edges) is non-trivial, especially via the + // NativeDbProxy napi-rs round-trip. When all post-passes were no-ops, the + // Rust orchestrator's counts are still accurate — no re-count needed. let finalNodeCount = result.nodeCount ?? 0; let finalEdgeCount = result.edgeCount ?? 0; - try { - const counts = (ctx.db as unknown as BetterSqlite3Database) - .prepare('SELECT (SELECT COUNT(*) FROM nodes) AS n, (SELECT COUNT(*) FROM edges) AS e') - .get() as { n: number; e: number }; - if (counts.n !== finalNodeCount || counts.e !== finalEdgeCount) { - finalNodeCount = counts.n; - finalEdgeCount = counts.e; - setBuildMeta(ctx.db, { node_count: finalNodeCount, edge_count: finalEdgeCount }); + const postPassWroteData = backfillHappened || chaEdgeCount > 0 || thisDispatchTargetIds.size > 0; + if (postPassWroteData) { + try { + const counts = (ctx.db as unknown as BetterSqlite3Database) + .prepare('SELECT (SELECT COUNT(*) FROM nodes) AS n, (SELECT COUNT(*) FROM edges) AS e') + .get() as { n: number; e: number }; + if (counts.n !== finalNodeCount || counts.e !== finalEdgeCount) { + finalNodeCount = counts.n; + finalEdgeCount = counts.e; + setBuildMeta(ctx.db, { node_count: finalNodeCount, edge_count: finalEdgeCount }); + } + } catch (err) { + debug(`Post-pass node/edge re-count failed: ${toErrorMessage(err)}`); } - } catch (err) { - debug(`Post-pass node/edge re-count failed: ${toErrorMessage(err)}`); } info( `Native build orchestrator completed: ${finalNodeCount} nodes, ${finalEdgeCount} edges, ${result.fileCount ?? 0} files`, From 304a1584539620342e35fccf95b2df6a8243201b Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Fri, 12 Jun 2026 19:55:06 -0600 Subject: [PATCH 2/2] refactor(perf): hoist backfillHappened before if to avoid duplicate expression Greptile suggested hoisting the backfillHappened variable declaration above the conditional that guards backfillNativeDroppedFiles, so the boolean expression is written exactly once. Previously the condition was evaluated in both the if-guard and the const declaration on the following line. --- src/domain/graph/builder/stages/native-orchestrator.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/domain/graph/builder/stages/native-orchestrator.ts b/src/domain/graph/builder/stages/native-orchestrator.ts index 18fe478e..e6537b6c 100644 --- a/src/domain/graph/builder/stages/native-orchestrator.ts +++ b/src/domain/graph/builder/stages/native-orchestrator.ts @@ -1532,10 +1532,10 @@ export async function tryNativeOrchestrator( // re-parse of the missing set) is gated below. const gapDetectStart = performance.now(); const gap = detectDroppedLanguageGap(ctx); - if (gap.missingAbs.length > 0 || gap.staleRel.length > 0) { + const backfillHappened = gap.missingAbs.length > 0 || gap.staleRel.length > 0; + if (backfillHappened) { await backfillNativeDroppedFiles(ctx, gap); } - const backfillHappened = gap.missingAbs.length > 0 || gap.staleRel.length > 0; const gapDetectMs = performance.now() - gapDetectStart; // Phase 8.5: expand CHA call edges (interface dispatch → concrete implementations).