perf(native): defer NativeDatabase.openReadWrite until after change detection#939
perf(native): defer NativeDatabase.openReadWrite until after change detection#939carlos-alm merged 3 commits intomainfrom
Conversation
…etection setupPipeline eagerly opened a rusqlite connection (~60ms) before knowing whether any files changed. On no-op incremental builds this was entirely wasted — WASM only paid ~4ms for better-sqlite3. Defer NativeDatabase.openReadWrite + initSchema to tryNativeOrchestrator, which runs after the Rust orchestrator confirms files need rebuilding. Setup now always uses better-sqlite3 for the cheap metadata reads (schema mismatch check), and the native connection opens on-demand only when the Rust pipeline will actually run. Closes #934 Impact: 2 functions changed, 6 affected
|
Claude finished @carlos-alm's task —— View job Root Cause AnalysisProblem Being Solved: Setup phase was eagerly opening a NativeDatabase connection (~60ms) before knowing if any files changed. On no-op incremental builds, this expensive operation was pure waste since the build would exit early after change detection. Why This Approach: Deferring
Risk Assessment: Low risk. This is a pure optimization that maintains identical behavior while reducing wasted work. All error handling and fallback paths are preserved. Backlog Compliance
Code Quality Assessment✅ Proper database lifecycle management - Correctly closes better-sqlite3 before opening rusqlite The 60-line reduction comes from moving the native DB setup logic from Final Recommendation
Conditions for approval:
This PR directly supports Foundation principle P1 (always-current graph) by making incremental builds significantly faster, enabling more frequent rebuilds without user friction. |
Codegraph Impact Analysis2 functions changed → 6 callers affected across 5 files
|
Greptile SummaryThis PR defers Confidence Score: 5/5Safe to merge — focused optimization with no new correctness issues and prior concerns fully addressed. All prior P1 concerns (advisory-lock transfer semantics and defensive flag reset) have been resolved with clarifying inline comments. No new P0/P1 issues were found: advisory-lock acquire/release sequencing is consistent across the happy path, error-recovery, and no-op exit; schema initialization via better-sqlite3 is persisted before any raw close so the native connection reopens to a fully-migrated DB; the NativeDbProxy lifecycle (close() no-op, __lockPath release via closeDbPair) is correctly threaded through every exit path. Remaining observations are P2 or below. No files require special attention. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[buildGraph] --> B[setupPipeline\nopenDb better-sqlite3 ~4ms\ninitSchema\ncheckEngineSchemaMismatch]
B --> C{tryNativeOrchestrator}
C -->|shouldSkip| D[return undefined\nfall through to JS pipeline]
C -->|nativeAvailable| E[ctx.db.close raw\nacquireAdvisoryLock\nNativeDatabase.openReadWrite ~60ms\nnativeDb.initSchema\nNativeDbProxy → ctx.db\nnativeFirstProxy = true]
E -->|open failed| F[catch: releaseAdvisoryLock\nctx.db = openDb\nnativeFirstProxy = false\nfall through to JS pipeline]
E -->|open success| G[nativeDb.buildGraph]
G -->|earlyExit| H[closeDbPair\nreturn early-exit\nno-op build ✓]
G -->|build complete| I[setBuildMeta\nrunPostNativeStructure?\nrunPostNativeAnalysis?]
I --> J[closeDbPair\nreturn NativeTimingResult]
D --> K[runPipelineStages\ncollectFiles / detectChanges]
F --> K
K -->|no changes| L[earlyExit]
K -->|changes| M[parseFiles / insertNodes\nresolveImports / buildEdges\nbuildStructure / runAnalyses\nfinalize]
Reviews (2): Last reviewed commit: "fix: clarify advisory-lock transfer and ..." | Re-trigger Greptile |
| ctx.nativeDb = undefined; | ||
| ctx.nativeFirstProxy = false; |
There was a problem hiding this comment.
Redundant flag reset in error-recovery branch
ctx.nativeFirstProxy = false is set in the catch block, but true is only written on line 588 — which comes after the lines that can throw (openReadWrite, initSchema, and the three proxy assignments above it). If any of those lines throws, the assignment ctx.nativeFirstProxy = true was never reached, so resetting to false here is a no-op. The comment or the assignment should clarify whether this is a defensive guard (in case the ordering ever changes) or dead code.
There was a problem hiding this comment.
Fixed — added inline comment clarifying this is a defensive guard in case future refactors move the ctx.nativeFirstProxy = true assignment above the throwing lines.
| // Close better-sqlite3 before opening rusqlite to avoid WAL conflicts | ||
| ctx.db.close(); | ||
| acquireAdvisoryLock(ctx.dbPath); | ||
| ctx.nativeDb = native.NativeDatabase.openReadWrite(ctx.dbPath); |
There was a problem hiding this comment.
Advisory-lock ownership gap between
close() and proxy creation
ctx.db.close() calls the raw db.close() method, not the closeDb() helper, so the advisory lock that openDb wrote to ctx.db.__lockPath is not released. The lock file continues to hold the current PID. The very next line (acquireAdvisoryLock) then re-writes the same PID — which is a no-op in practice — and the proxy captures the path again.
This is functionally correct (lock is released exactly once when closeDbPair calls releaseAdvisoryLock(proxy.__lockPath) at the end), but the pattern is subtly different from every other place in the codebase that deliberately keeps the lock across a close/reopen pair. A short inline comment at the close() call site would help future readers understand that skipping closeDb here is intentional — the lock is being transferred to the proxy rather than released.
There was a problem hiding this comment.
Fixed — added inline comment at the ctx.db.close() call explaining that raw close() is intentional: the advisory lock is kept and transferred to the NativeDbProxy, not released here.
Impact: 1 functions changed, 5 affected
Summary
NativeDatabase.openReadWrite+initSchemafromsetupPipelinetotryNativeOrchestrator, saving ~60ms on every incremental build invocationbetter-sqlite3(~4ms) for the initial metadata reads (schema mismatch check)Closes #934
Test plan
codegraph build --engine native --timing— setup phase should drop from ~67ms to ~4ms on incremental buildscodegraph buildwith no changes) exit cleanly without opening native connection