Skip to content

Commit a770c23

Browse files
authored
perf(native): defer NativeDatabase.openReadWrite until after change detection (#939)
* perf(native): defer NativeDatabase.openReadWrite until after change detection 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 * fix: clarify advisory-lock transfer and defensive flag reset (#939) Impact: 1 functions changed, 5 affected
1 parent b4b84d2 commit a770c23

1 file changed

Lines changed: 27 additions & 35 deletions

File tree

src/domain/graph/builder/pipeline.ts

Lines changed: 27 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -133,38 +133,15 @@ function setupPipeline(ctx: PipelineContext): void {
133133
const native = enginePref !== 'wasm' ? loadNative() : null;
134134
ctx.nativeAvailable = !!native?.NativeDatabase;
135135

136-
// When native is available, use a NativeDbProxy backed by a single rusqlite
137-
// connection. This eliminates the dual-connection WAL corruption problem.
138-
// The Rust orchestrator handles the full pipeline; the proxy is used for any
139-
// JS post-processing (e.g. structure fallback on large builds).
140-
if (ctx.nativeAvailable && native?.NativeDatabase) {
141-
try {
142-
const dir = path.dirname(ctx.dbPath);
143-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
144-
acquireAdvisoryLock(ctx.dbPath);
145-
ctx.nativeDb = native.NativeDatabase.openReadWrite(ctx.dbPath);
146-
ctx.nativeDb.initSchema();
147-
const proxy = new NativeDbProxy(ctx.nativeDb);
148-
proxy.__lockPath = `${ctx.dbPath}.lock`;
149-
ctx.db = proxy as unknown as typeof ctx.db;
150-
ctx.nativeFirstProxy = true;
151-
} catch (err) {
152-
warn(`NativeDatabase setup failed, falling back to better-sqlite3: ${toErrorMessage(err)}`);
153-
try {
154-
ctx.nativeDb?.close();
155-
} catch {
156-
/* ignore */
157-
}
158-
ctx.nativeDb = undefined;
159-
ctx.nativeFirstProxy = false;
160-
releaseAdvisoryLock(`${ctx.dbPath}.lock`);
161-
ctx.db = openDb(ctx.dbPath);
162-
initSchema(ctx.db);
163-
}
164-
} else {
165-
ctx.db = openDb(ctx.dbPath);
166-
initSchema(ctx.db);
167-
}
136+
// Always use better-sqlite3 for setup — it's cheap (~4ms) and only needed
137+
// for metadata reads (schema mismatch check). NativeDatabase.openReadWrite
138+
// is deferred to tryNativeOrchestrator, saving ~60ms on incremental builds
139+
// where the Rust orchestrator handles the full pipeline, and avoiding the
140+
// cost entirely on no-op builds that exit before reaching the orchestrator.
141+
const dir = path.dirname(ctx.dbPath);
142+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
143+
ctx.db = openDb(ctx.dbPath);
144+
initSchema(ctx.db);
168145

169146
ctx.config = loadConfig(ctx.rootDir);
170147
ctx.incremental =
@@ -589,15 +566,26 @@ async function tryNativeOrchestrator(
589566
return undefined;
590567
}
591568

592-
// In native-first mode, nativeDb is already open from setupPipeline.
593-
// Otherwise, open it on demand (deferred to skip overhead on no-op rebuilds).
569+
// Open NativeDatabase on demand — deferred from setupPipeline to skip the
570+
// ~60ms cost on no-op/early-exit builds. Close the better-sqlite3 connection
571+
// first to avoid dual-connection WAL corruption.
594572
if (!ctx.nativeDb && ctx.nativeAvailable) {
595573
const native = loadNative();
596574
if (native?.NativeDatabase) {
597575
try {
576+
// Close better-sqlite3 before opening rusqlite to avoid WAL conflicts.
577+
// Uses raw close() instead of closeDb() intentionally — the advisory lock
578+
// is kept and transferred to the NativeDbProxy below, not released here.
579+
ctx.db.close();
580+
acquireAdvisoryLock(ctx.dbPath);
598581
ctx.nativeDb = native.NativeDatabase.openReadWrite(ctx.dbPath);
599582
ctx.nativeDb.initSchema();
600-
ctx.nativeDb.exec('PRAGMA wal_checkpoint(TRUNCATE)');
583+
// Replace ctx.db with a NativeDbProxy so post-native JS fallback
584+
// (structure, analysis) can use it without reopening better-sqlite3.
585+
const proxy = new NativeDbProxy(ctx.nativeDb);
586+
proxy.__lockPath = `${ctx.dbPath}.lock`;
587+
ctx.db = proxy as unknown as typeof ctx.db;
588+
ctx.nativeFirstProxy = true;
601589
} catch (err) {
602590
warn(`NativeDatabase setup failed, falling back to JS: ${toErrorMessage(err)}`);
603591
try {
@@ -606,6 +594,10 @@ async function tryNativeOrchestrator(
606594
debug(`tryNativeOrchestrator: close failed during fallback: ${toErrorMessage(e)}`);
607595
}
608596
ctx.nativeDb = undefined;
597+
ctx.nativeFirstProxy = false; // defensive: reset in case future refactors move the assignment above throwing lines
598+
releaseAdvisoryLock(`${ctx.dbPath}.lock`);
599+
// Reopen better-sqlite3 for JS pipeline fallback
600+
ctx.db = openDb(ctx.dbPath);
609601
}
610602
}
611603
}

0 commit comments

Comments
 (0)