fix(perf): guard post-native passes on 1-file incremental rebuilds#1493
fix(perf): guard post-native passes on 1-file incremental rebuilds#1493carlos-alm wants to merge 2 commits into
Conversation
…e 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
|
Claude finished @carlos-alm's task —— View job PR Review: fix(perf): guard post-native passes on 1-file incremental rebuilds
Root Cause AnalysisProblem Being Solved: Native 1-file incremental rebuilds were 11–14x slower than WASM (~1901ms vs ~171ms), with ~1823ms unaccounted by phase timings. Two JS post-native passes executed unconditionally even when they had zero work:
Why This Approach: The fix is the minimal correct change. For optimization (1): the redundant Risk Assessment: Low. Both changes are pure skips of work that was already no-ops in the common 1-file incremental case:
One subtle point: Code QualityChange 1: backfill gate ( Before: if (result.isFullBuild || removedCount > 0 || changedCount > 0 || gap.missingAbs.length > 0 || gap.staleRel.length > 0) {
await backfillNativeDroppedFiles(ctx, gap);
}After: if (gap.missingAbs.length > 0 || gap.staleRel.length > 0) {
await backfillNativeDroppedFiles(ctx, gap);
}
const backfillHappened = gap.missingAbs.length > 0 || gap.staleRel.length > 0;✓ Correct — the old conditions were purely redundant; the function always no-oped on empty gaps anyway.
const backfillHappened = gap.missingAbs.length > 0 || gap.staleRel.length > 0;
if (backfillHappened) {
await backfillNativeDroppedFiles(ctx, gap);
}This avoids evaluating the same boolean expression twice. Not a bug — just a readability nit. Consistent with the code's style elsewhere (e.g. the Change 2: COUNT(*) gate ( const postPassWroteData = backfillHappened || chaEdgeCount > 0 || thisDispatchTargetIds.size > 0;
if (postPassWroteData) { ... }✓ All three sources of new data are correctly tracked. Removed variables ( Comment quality: Updated comments accurately describe the new behavior and rationale, including the Configuration Change DetectionNo linting rules, test timeouts, coverage thresholds, or TypeScript flags were modified. ✓ Test IntegrityNo test files changed. ✓ The behavior being gated (calling backfill or COUNT scan) is an internal optimization — existing integration tests covering full-build and incremental correctness still exercise the code paths when gaps exist. Backlog Compliance
Final Recommendation
Optional cleanup (not blocking): Hoist the // native-orchestrator.ts ~L1535
const backfillHappened = gap.missingAbs.length > 0 || gap.staleRel.length > 0;
if (backfillHappened) {
await backfillNativeDroppedFiles(ctx, gap);
} |
Greptile SummaryGuards two post-native passes that were running unconditionally on every incremental build —
Confidence Score: 5/5Safe to merge — both guards are structurally correct and the follow-up hoist resolves the previously flagged duplicate expression. The backfill gate narrows a call that had a no-op internal early-exit into one that is never made at all when the gap is empty; no correctness is lost. The postPassWroteData condition covers all three edge-writing passes (backfill, CHA, this-dispatch) and correctly excludes the technique-column backfill which only updates existing rows and cannot affect COUNT(*). The backfillHappened hoist writes the boolean expression exactly once, eliminating the out-of-sync maintenance risk. No files require special attention. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[tryNativeOrchestrator] --> B[detectDroppedLanguageGap]
B --> C{gap non-empty?\nmissingAbs > 0\nor staleRel > 0}
C -- "backfillHappened = true" --> D[backfillNativeDroppedFiles]
C -- "backfillHappened = false\n(skip)" --> E
D --> E[runPostNativeCha\n→ chaEdgeCount]
E --> F[runPostNativeThisDispatch\n→ thisDispatchTargetIds]
F --> G[backfillEdgeTechniquesAfterNativeOrchestrator\nalways runs — column update only]
G --> H{postPassWroteData?\nbackfillHappened OR\nchaEdgeCount > 0 OR\nthisDispatchTargetIds.size > 0}
H -- yes --> I["COUNT(*) nodes + edges\nupdate build_meta if changed"]
H -- "no (1-file quiet incremental)" --> J[use Rust orchestrator counts as-is]
I --> K[info log + return result]
J --> K
Reviews (3): Last reviewed commit: "refactor(perf): hoist backfillHappened b..." | Re-trigger Greptile |
| 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; |
There was a problem hiding this comment.
backfillHappened re-evaluates the same boolean expression that already guards the await on the line above. If the condition ever changes (e.g. a third field is added to gap), both sites must be updated in sync. Hoist the variable before the if so the condition is written exactly once.
| 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; | |
| const gapDetectStart = performance.now(); | |
| const gap = detectDroppedLanguageGap(ctx); | |
| const backfillHappened = gap.missingAbs.length > 0 || gap.staleRel.length > 0; | |
| if (backfillHappened) { | |
| await backfillNativeDroppedFiles(ctx, gap); | |
| } | |
| const gapDetectMs = performance.now() - gapDetectStart; |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Fixed — hoisted backfillHappened before the if so the boolean expression is written exactly once. The variable declaration now serves as the if guard directly.
Codegraph Impact Analysis1 functions changed → 5 callers affected across 5 files
|
…xpression 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.
|
Addressed: hoisted |
Native 1-file incremental rebuilds were 11–14x slower than WASM (1901ms vs 171ms) with ~1823ms unaccounted in phase timings. Two post-native passes ran unconditionally even when they had no work to do:
backfillNativeDroppedFilescall gate: Previously called wheneverchangedCount > 0(i.e. any incremental with changes), even whendetectDroppedLanguageGapreturned an empty gap.backfillNativeDroppedFilesalready has an internal early-exit for empty gaps, but removing the spurious outer conditions (isFullBuild || removedCount > 0 || changedCount > 0) eliminates the wasted call entirely. Now only called whengap.missingAbs.length > 0 || gap.staleRel.length > 0.Node/edge
COUNT(*)re-count: Ran unconditionally after all post-passes, even when none of them wrote any new edges.COUNT(*)over 50K+ edge tables is non-trivial, especially via theNativeDbProxynapi-rs round-trip. Now gated onpostPassWroteData— true only when the backfill, CHA expansion, or this/super dispatch actually inserted edges. On a typical 1-file incremental (no class hierarchy, no WASM-only files), all three are zero — the scan is skipped entirely.Combined with the related fixes already in this branch stack (phase timings visibility #1434, CHA incremental scoping #1441, symbolsOnly inline #1437, threshold #1435), native 1-file incremental should approach the expected ~43ms from v3.9.6.
Closes #1454