perf(resolution): adaptive pool engagement — projected-settle bar replaces the fixed 150k-ref gate for mid-run boot; tokio −23% (#1390)
The 9-language competitor matrix exposed tokio as the worst fresh-index gap: 77% of its wall was resolution running SEQUENTIALLY — 56k Rust refs sit under the fixed 150k pool gate while costing 36µs each (9× Go's 4µs/ref on prometheus). A ref-count gate can't see per-ref cost. After each sequential batch the loop now projects the remaining sequential settle from the measured rate and boots the pool mid-run when it clears 400ms. The switch rides machinery that already existed: pool boot is async and fan-out engages only when ready, admission order is mode-independent, and the #1320 edges-before-fanout invariant holds at every batch boundary regardless of when the pool arrives. Up-front engagement at >=150k refs is unchanged; 2-core/low-memory hosts still decline inside tryCreate's sizing; CODEGRAPH_NO_PARALLEL_RESOLVE still disables; downgrade permanence is preserved (one engage attempt per run). Measured (n=3, interleaved, caffeinated): tokio 3.06-3.12 → 2.40-2.57s (resolution 2,443→~1,330ms); express (tiny control) unchanged with zero engagements; dubbo unchanged (ref-count path). Gates: tokio + excalidraw adaptive-vs-sequential dumps byte-identical (87,302 / 89,903 rows), dubbo dump identical to the session baseline, suite 2,689 ×2 with CODEGRAPH_KERNEL_EXPECT=1. Known follow-up: sampling the rate mid-first- batch would close the remaining ~0.3s to the forced-engage ceiling. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
082ea65f3a
commit
1aa4de6eaa
+49
-6
@@ -1473,17 +1473,38 @@ export class ReferenceResolver {
|
||||
// costs zero wall-clock. Any failure downgrades to sequential permanently.
|
||||
let pool: ResolverPool | null = null;
|
||||
let poolReady = false;
|
||||
const tPoolStart = Date.now();
|
||||
if (parallel && total >= minRefsForPool()) {
|
||||
pool = ResolverPool.tryCreate(parallel.dbPath, this.projectRoot);
|
||||
pool?.ready().then(
|
||||
// True once pool creation has been attempted by EITHER engage site (the
|
||||
// up-front ref-count gate or the adaptive projection below) — a pool that
|
||||
// failed or was destroyed must stay down (downgrade is permanent), and
|
||||
// tryCreate's sizing probes shouldn't re-run every batch on hosts that
|
||||
// declined.
|
||||
let poolEngageTried = false;
|
||||
const createPool = (t0: number, why: string): ResolverPool | null => {
|
||||
poolEngageTried = true;
|
||||
if (!parallel) return null;
|
||||
const p = ResolverPool.tryCreate(parallel.dbPath, this.projectRoot);
|
||||
p?.ready().then(
|
||||
() => {
|
||||
poolReady = true;
|
||||
if (process.env.CODEGRAPH_SYNTH_TIMINGS) console.error(`[pool-timing] pool ready after ${Date.now() - tPoolStart}ms`);
|
||||
if (process.env.CODEGRAPH_SYNTH_TIMINGS) console.error(`[pool-timing] pool ready after ${Date.now() - t0}ms (${why})`);
|
||||
},
|
||||
() => { void pool?.destroy().catch(() => undefined); pool = null; }
|
||||
() => {
|
||||
void p.destroy().catch(() => undefined);
|
||||
if (pool === p) pool = null;
|
||||
}
|
||||
);
|
||||
return p;
|
||||
};
|
||||
if (parallel && total >= minRefsForPool()) {
|
||||
pool = createPool(Date.now(), 'ref-count');
|
||||
}
|
||||
// Adaptive engagement bar (see the batch-loop hook): projected remaining
|
||||
// sequential settle above this boots the pool mid-loop. Boot is async and
|
||||
// fan-out waits for ready, so a marginal engage costs background boot
|
||||
// only; the bar just needs to clear the fan-out's own overhead class.
|
||||
const ADAPTIVE_ENGAGE_SETTLE_MS = 400;
|
||||
let adaptiveSeqMs = 0;
|
||||
let adaptiveSeqRefs = 0;
|
||||
|
||||
// Process in PIPELINED batches (double-buffer). The enumeration is the
|
||||
// head of the pending set in rowid order; every ref a persisted batch
|
||||
@@ -1606,6 +1627,28 @@ export class ReferenceResolver {
|
||||
if (process.env.CODEGRAPH_SYNTH_TIMINGS) console.error(`[pool-timing] batch ${inFlight.mode}: ${batch.length} refs in ${Date.now() - tBatch}ms`);
|
||||
lp('settle', tBatch);
|
||||
|
||||
// Adaptive pool engagement: the fixed ref-count gate can't see PER-REF
|
||||
// cost, and settle rates differ ~9× by language (56k Rust refs cost
|
||||
// more sequential settle than 154k Go refs — 36µs vs 4µs measured on
|
||||
// tokio/prometheus). After each sequential batch, project the remaining
|
||||
// settle from the observed rate and boot the pool mid-loop when it
|
||||
// clears the bar. The loop already switches to fan-out only when the
|
||||
// async boot reports ready, admission order is mode-independent, and
|
||||
// 2-core/low-memory hosts still decline inside tryCreate's sizing —
|
||||
// so the switch changes wall-clock, never the graph.
|
||||
if (inFlight.mode === 'seq' && parallel && pool === null && !poolEngageTried) {
|
||||
adaptiveSeqMs += Date.now() - tBatch;
|
||||
adaptiveSeqRefs += batch.length;
|
||||
const remaining = total - processed - batch.length;
|
||||
const projectedMs = (adaptiveSeqMs / Math.max(1, adaptiveSeqRefs)) * Math.max(0, remaining);
|
||||
if (projectedMs >= ADAPTIVE_ENGAGE_SETTLE_MS) {
|
||||
if (process.env.CODEGRAPH_SYNTH_TIMINGS) {
|
||||
console.error(`[pool-timing] adaptive engage: projected ${Math.round(projectedMs)}ms sequential settle over ${remaining} remaining refs`);
|
||||
}
|
||||
pool = createPool(Date.now(), 'adaptive');
|
||||
}
|
||||
}
|
||||
|
||||
// WAL-valve backstop at the ONE pool-idle boundary of the double-buffer
|
||||
// (this batch settled, the next not yet fanned out): past the hard cap
|
||||
// the writer parks for a full backfill here, where the pool's readers
|
||||
|
||||
Reference in New Issue
Block a user