perf(native): fix 1238% incremental rebuild regression#856
Conversation
…dundant structure phase The native orchestrator's post-processing unconditionally ran a full JS structure rebuild (changedFilePaths=null) and loaded all nodes from DB after every native build, regardless of whether the Rust fast-path already handled structure. For a 1-file change on a 567-file codebase, this turned a 42ms rebuild into 562ms. Three fixes: 1. Rust reports `structureHandled: bool` in BuildPipelineResult so the JS side knows whether the small-incremental fast path ran 2. When Rust handled structure, JS skips the structure phase entirely and scopes the DB reconstruction query to changed files only 3. When Rust didn't handle structure (full/large incremental), JS now passes changedFiles to buildStructureFn instead of null, matching the JS pipeline's medium-incremental path behavior
|
Claude finished @carlos-alm's task —— View job PR Review for #856: perf(native): fix 1238% incremental rebuild regressionReview Progress
Root Cause AnalysisProblem Being Solved: The native incremental build pipeline had a 1238% performance regression (42ms → 562ms) for 1-file incremental builds introduced in v3.9.0 by PR #807. The root cause was post-native processing unconditionally running a full JS structure rebuild and loading all nodes from the database even when the Rust fast-path already handled structure for small incremental builds. Why This Approach: The fix adds a Risk Assessment: Low risk. The change is surgical and well-contained. Both engines are tested for parity, and the fix only affects the coordination between native and JS processing phases. Backlog Compliance
Code Quality Assessment✅ Strengths
✅ Implementation Quality
✅ Edge Case Handling
🔍 Performance Logic ValidationThe fast path thresholds are well-chosen:
The performance improvement logic is sound:
Expected improvement: ~92% reduction (562ms → ~42ms) aligns with eliminating the redundant full structure phase. Critical ConcernsNone identified. This is a clean performance optimization that:
Final Recommendation
Next Steps: Monitor CI benchmark to confirm the ~92% improvement is achieved in practice. |
Greptile SummaryThis PR fixes the 1238% native incremental rebuild regression introduced in v3.9.0 by removing two expensive unconditional operations that ran after every native build: a full-graph structure rebuild ( Key changes:
One P2 concern identified: For pure file-deletion builds (no content changes), Confidence Score: 4/5Safe to merge for the vast majority of use-cases; one narrow edge case (removal-only builds with fast path) may leave orphaned directory nodes that persist until the next full rebuild. All prior review concerns are resolved. The regression fix is logically correct and well-scoped. One P2 finding remains: the
Important Files Changed
Sequence DiagramsequenceDiagram
participant JS as pipeline.ts (JS)
participant Rust as build_pipeline.rs (Rust)
participant DB as SQLite DB
JS->>Rust: buildGraph(rootDir, config, aliases, opts)
Rust->>Rust: collect + detect changes
alt No changes (early exit)
Rust-->>JS: { earlyExit: true, structureHandled: true }
JS-->>JS: return early-exit
else Small incremental (≤5 changed, >20 existing)
Rust->>DB: purge changed/removed files
Rust->>DB: parse + insert nodes
Rust->>DB: resolve imports + build edges
Rust->>DB: update_changed_file_metrics (fast path)
Rust-->>JS: { structureHandled: true, changedFiles: [...] }
JS->>DB: reconstructFileSymbolsFromDb(changedFiles only)
Note over JS,DB: Scoped load — avoids full graph scan
JS->>JS: runPostNativeAnalysis(changedFiles)
else Large incremental or full build
Rust->>DB: purge + parse + insert + edges
Note over Rust: Skips structure phase
Rust-->>JS: { structureHandled: false, structureScope: [...] }
JS->>DB: reconstructFileSymbolsFromDb(all files)
JS->>JS: runPostNativeStructure(structureScope ?? changedFiles)
JS->>JS: runPostNativeAnalysis(changedFiles)
end
Reviews (3): Last reviewed commit: "fix(native): resolve use-after-move comp..." | Re-trigger Greptile |
src/domain/graph/builder/pipeline.ts
Outdated
| // Full builds need null (rebuild everything). Incremental builds pass the | ||
| // changed file list so buildStructure only updates those files' metrics | ||
| // and contains edges — matching the JS pipeline's medium-incremental path. | ||
| const changedFilePaths = isFullBuild ? null : (changedFiles ?? null); |
There was a problem hiding this comment.
Empty-array
changedFiles passes through ?? and silences structure updates for removal-only builds
When use_fast_path = false and the only change is file removals (no content-changed files), the Rust pipeline sets analysis_scope = Some([]) and the result carries changedFiles = []. The ?? operator only falls back to null for undefined or null — not for an empty array — so changedFilePaths becomes [] and buildStructureFn is called with an empty list. This may skip all structure updates, leaving stale directory-containment edges for the removed files.
Pre-PR this code always passed null (full rebuild), so this edge case never triggered. A defensive fix:
| const changedFilePaths = isFullBuild ? null : (changedFiles ?? null); | |
| const changedFilePaths = isFullBuild || !changedFiles?.length ? null : changedFiles; |
There was a problem hiding this comment.
Fixed. Applied the suggested guard — changedFilePaths now falls back to null when changedFiles is an empty array (removal-only builds), matching the pre-PR behavior of always doing a full structure rebuild in that case.
const changedFilePaths = isFullBuild || !changedFiles?.length ? null : changedFiles;
Codegraph Impact Analysis6 functions changed → 6 callers affected across 5 files
|
…ucture scope - Guard against empty changedFiles array passing through ?? operator in runPostNativeStructure, which would skip structure updates for removal-only builds (behavioral regression vs pre-PR) - Remove noisy eprintln! that fires on every large-incremental build with a misleading message about running JS pipeline - Add structureScope field to BuildPipelineResult containing the full changed_files list (including reverse-dep files) so the JS structure fallback path can update metrics for files whose edges changed even though their content didn't
|
All three Greptile findings addressed in 1716f4d: 1. 2. 3. Empty-array |
changed_files was moved into changed_file_list on line 544 and then referenced again for structure_scope on line 607, which Rust rejects as a use-after-move. Reuse changed_file_list.clone() which already holds the correct None/Some(changed_files) value.
|
Fixed the use-after-move compile error identified in the Greptile summary.
Commit: 9f83fd4 |
Summary
Fixes the native 1-file incremental rebuild regression from 42ms → 562ms (1238%) introduced in v3.9.0 by PR #807.
Root cause: The post-native-orchestrator processing unconditionally ran a full JS structure rebuild (
changedFilePaths=null) and loaded all nodes from DB after every native build, even when the Rust fast-path already handled structure for small incremental builds.Fixes:
BuildPipelineResultnow reportsstructureHandled: bool— true when the small-incremental fast path ran (≤5 changed files, >20 existing)changedFilestobuildStructureFn(matching the JS pipeline's medium-incremental path) instead ofnull(full rebuild)Expected improvement: Native 1-file incremental should return to ~42ms range (from 562ms). WASM pipeline is unaffected (uses separate JS stage pipeline).
Both engines tested: All incremental parity, build parity, pipeline, and scoped-rebuild tests pass.
Test plan
tsc --noEmit)tests/builder/pipeline.test.ts— 4/4 passtests/integration/incremental-parity.test.ts— all passtests/integration/incremental-edge-parity.test.ts— all passtests/integration/scoped-rebuild.test.ts— all pass