Skip to content

Commit f711a7a

Browse files
authored
perf(native): fix incremental rebuild regression (#882) (#888)
* perf(native): fix incremental rebuild regression by lowering orchestrator version gate The v3.9.1 native addon includes all incremental purge fixes from PR #865, but the orchestrator version gate was raised to < 3.10.0 in PR #867 (because the v3.9.0 prebuilt binaries were built before those fixes). This forced the slower JS pipeline path for all v3.9.x addon versions, causing: - No-op rebuild: 6ms → 15ms (+150%) — JS collectFiles + detectChanges slower than Rust orchestrator's equivalent - 1-file rebuild: 527ms → 757ms (+44%) — JS pipeline adds nativeDb open/close cycles + JS-side resolve/edges instead of single Rust pass Fix: lower the gate from < 3.10.0 to < 3.9.1, re-enabling the Rust orchestrator for v3.9.1+ addon binaries. Additionally, skip nativeDb open/close cycles in the JS pipeline fallback for small incremental builds (≤5 files). The WAL checkpoint + connection churn (~5-10ms) exceeds bulk-insert savings for a handful of files. Closes #882 * refactor: extract smallFilesThreshold to DEFAULTS.build config (#888) Replace hardcoded `<= 5` threshold across pipeline stages with `config.build.smallFilesThreshold` from DEFAULTS, consolidating five scattered magic numbers into one configurable constant. * fix: use allSymbols.size instead of fileSymbols.size for smallIncremental guard ctx.fileSymbols is an empty Map when the smallIncremental check runs (populated later inside insertNodes). This caused every non-full build to be treated as small incremental, skipping reopenNativeDb for all incremental builds. Use ctx.allSymbols.size which parseFiles populates before this check.
1 parent 4d264b2 commit f711a7a

7 files changed

Lines changed: 21 additions & 13 deletions

File tree

src/domain/graph/builder/pipeline.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,10 @@ interface NativeOrchestratorResult {
234234
function shouldSkipNativeOrchestrator(ctx: PipelineContext): string | null {
235235
if (process.env.CODEGRAPH_FORCE_JS_PIPELINE === '1') return 'CODEGRAPH_FORCE_JS_PIPELINE=1';
236236
if (ctx.forceFullRebuild) return 'forceFullRebuild';
237-
const orchestratorBuggy = !!ctx.engineVersion && semverCompare(ctx.engineVersion, '3.10.0') < 0;
237+
// v3.9.0 addon had buggy incremental purge (wrong SQL on analysis tables,
238+
// scoped removal over-detection). Fixed in v3.9.1 by PR #865. Gate on
239+
// < 3.9.1 so v3.9.1+ uses the fast Rust orchestrator path.
240+
const orchestratorBuggy = !!ctx.engineVersion && semverCompare(ctx.engineVersion, '3.9.1') < 0;
238241
if (orchestratorBuggy) return `buggy addon ${ctx.engineVersion}`;
239242
if (ctx.engineName !== 'native') return `engine=${ctx.engineName}`;
240243
return null;
@@ -636,10 +639,13 @@ async function runPipelineStages(ctx: PipelineContext): Promise<void> {
636639

637640
await parseFiles(ctx);
638641

639-
// Temporarily reopen nativeDb for insertNodes — it uses the WAL checkpoint
640-
// guard internally (same pattern as feature modules). Closed again before
641-
// resolveImports/buildEdges which don't yet have the guard (#709).
642-
if (ctx.nativeAvailable && ctx.engineName === 'native') {
642+
// For small incremental builds (≤smallFilesThreshold files), skip the nativeDb open/close
643+
// cycle for insertNodes — the WAL checkpoint + connection churn (~5-10ms)
644+
// exceeds the napi bulk-insert savings on a handful of files. The JS
645+
// fallback path inside insertNodes handles this case efficiently.
646+
const smallIncremental =
647+
!ctx.isFullBuild && ctx.allSymbols.size <= ctx.config.build.smallFilesThreshold;
648+
if (ctx.nativeAvailable && ctx.engineName === 'native' && !smallIncremental) {
643649
reopenNativeDb(ctx, 'insertNodes');
644650
}
645651

@@ -657,7 +663,8 @@ async function runPipelineStages(ctx: PipelineContext): Promise<void> {
657663

658664
// Reopen nativeDb for feature modules (ast, cfg, complexity, dataflow)
659665
// which use suspendJsDb/resumeJsDb WAL checkpoint before native writes.
660-
if (ctx.nativeAvailable) {
666+
// Skip for small incremental builds — same rationale as insertNodes above.
667+
if (ctx.nativeAvailable && !smallIncremental) {
661668
reopenNativeDb(ctx, 'analyses');
662669
if (ctx.nativeDb && ctx.engineOpts) {
663670
ctx.engineOpts.nativeDb = ctx.nativeDb;

src/domain/graph/builder/stages/build-edges.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -712,7 +712,7 @@ function loadNodes(ctx: PipelineContext): { rows: QueryNodeRow[]; scoped: boolea
712712
const nodeKindFilter = `kind IN ('function','method','class','interface','struct','type','module','enum','trait','record','constant')`;
713713

714714
// Gate: only scope for small incremental on large codebases
715-
if (!isFullBuild && fileSymbols.size <= 5) {
715+
if (!isFullBuild && fileSymbols.size <= ctx.config.build.smallFilesThreshold) {
716716
const existingFileCount = (
717717
db.prepare("SELECT COUNT(*) as c FROM nodes WHERE kind = 'file'").get() as { c: number }
718718
).c;

src/domain/graph/builder/stages/build-structure.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export async function buildStructure(ctx: PipelineContext): Promise<void> {
3737
// For small incremental builds on large codebases, use a fast path that
3838
// updates only the changed files' metrics via targeted SQL instead of
3939
// loading ALL definitions from DB (~8ms) and recomputing ALL metrics (~15ms).
40-
// Gate: ≤5 changed files AND significantly more existing files (>20) to
40+
// Gate: ≤smallFilesThreshold changed files AND significantly more existing files (>20) to
4141
// avoid triggering on small test fixtures where directory metrics matter.
4242
const useNativeReads = ctx.engineName === 'native' && !!ctx.nativeDb;
4343
const existingFileCount = !isFullBuild
@@ -52,7 +52,7 @@ export async function buildStructure(ctx: PipelineContext): Promise<void> {
5252
const useSmallIncrementalFastPath =
5353
!isFullBuild &&
5454
changedFileList != null &&
55-
changedFileList.length <= 5 &&
55+
changedFileList.length <= ctx.config.build.smallFilesThreshold &&
5656
existingFileCount > 20;
5757

5858
if (!isFullBuild && !useSmallIncrementalFastPath) {

src/domain/graph/builder/stages/finalize.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ export async function finalize(ctx: PipelineContext): Promise<void> {
258258
// immediately after build.
259259
const pair = { db: ctx.db, nativeDb: ctx.nativeDb };
260260
const isTempDir = path.resolve(rootDir).startsWith(path.resolve(tmpdir()));
261-
if (!isFullBuild && allSymbols.size <= 5 && !isTempDir) {
261+
if (!isFullBuild && allSymbols.size <= ctx.config.build.smallFilesThreshold && !isTempDir) {
262262
closeDbPairDeferred(pair);
263263
} else {
264264
closeDbPair(pair);

src/domain/graph/builder/stages/resolve-imports.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,14 @@ function buildReexportMap(ctx: PipelineContext): void {
3434

3535
/**
3636
* Find barrel files related to changed files for scoped re-parsing.
37-
* For small incremental builds (<=5 files), only barrels that re-export from
37+
* For small incremental builds (<=smallFilesThreshold files), only barrels that re-export from
3838
* or are imported by the changed files. For larger changes, all barrels.
3939
*/
4040
function findBarrelCandidates(ctx: PipelineContext): Array<{ file: string }> {
4141
const { db, fileSymbols, rootDir, aliases } = ctx;
4242
const changedRelPaths = new Set<string>(fileSymbols.keys());
4343

44-
const SMALL_CHANGE_THRESHOLD = 5;
45-
if (changedRelPaths.size <= SMALL_CHANGE_THRESHOLD) {
44+
if (changedRelPaths.size <= ctx.config.build.smallFilesThreshold) {
4645
const allBarrelFiles = new Set(
4746
(
4847
db

src/infrastructure/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export const DEFAULTS = {
2323
incremental: true,
2424
dbPath: '.codegraph/graph.db',
2525
driftThreshold: 0.2,
26+
smallFilesThreshold: 5,
2627
},
2728
query: {
2829
defaultDepth: 3,

src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1085,6 +1085,7 @@ export interface CodegraphConfig {
10851085
incremental: boolean;
10861086
dbPath: string;
10871087
driftThreshold: number;
1088+
smallFilesThreshold: number;
10881089
};
10891090

10901091
query: {

0 commit comments

Comments
 (0)