perf(resolution): worker connection recycling — WAL-depth writes-under-readers fix, superphase −11.4% at 8c (#1362)
The §7a.6 anomaly probed to its mechanism with five discriminating runs (§7a.7 table): main-thread B-tree writes triple under attached readers because READERS PIN WAL checkpoint progress — the deep WAL taxes every writer page operation (deletes 42.6s pool-off vs 118.8s pool-4 on identical hardware; an aggressive 64MB valve recovers the writes but overpays +129s in full-park folds; the v2 cache resurrection was falsified — long-tail name traffic is uncacheable at any capacity). Fix: workers close and reopen their read-only connections every 8 batches at the double-buffer's worker-idle boundary (ResolverPool.recycleWorkers + QueryBuilder.rebind + a cadence call). Reopens are sub-millisecond, resolver caches survive (only prepared statements re-prepare), and the existing checkpoints advance instead of parking. Failed recycle downgrades to sequential, same as a failed fan-out. Measured (8c pool-4, linux v7.2-rc2, cadence 25 → 8 iterated): resolution superphase 715.0 → 633.6s (−11.4%), envelope best 14.8min, recreate 59.7 → 45.3s. Byte-neutral everywhere: git dumps byte-identical old-vs-new, linux dump sha 6dd1185b reproduced (10,446,478 lines), counts 2,049,153/6,413,518, suite 2517 green. 2c unchanged by construction (no pool → no recycling). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
5955d04c97
commit
971a5a0483
@@ -39,15 +39,19 @@ let resolver: ReferenceResolver | null = null;
|
||||
|
||||
type InMessage =
|
||||
| { type: 'open'; dbPath: string; projectRoot: string }
|
||||
| { type: 'recycle'; id: number }
|
||||
| { type: 'resolve'; id: number; refs: UnresolvedReference[] }
|
||||
| { type: 'synth'; id: number; pass: string }
|
||||
| { type: 'close' };
|
||||
|
||||
let dbPath: string | null = null;
|
||||
|
||||
port.on('message', (msg: InMessage) => {
|
||||
try {
|
||||
switch (msg.type) {
|
||||
case 'open': {
|
||||
const tOpen = Date.now();
|
||||
dbPath = msg.dbPath;
|
||||
const created = createDatabase(msg.dbPath, { readOnly: true });
|
||||
db = created.db;
|
||||
db.pragma('busy_timeout = 5000');
|
||||
@@ -60,6 +64,25 @@ port.on('message', (msg: InMessage) => {
|
||||
port.postMessage({ type: 'ready' });
|
||||
break;
|
||||
}
|
||||
case 'recycle': {
|
||||
// Close and reopen the read-only connection so the WAL checkpoints
|
||||
// the writer runs can advance past this reader's mark (see
|
||||
// QueryBuilder.rebind). Everything above the connection survives —
|
||||
// the resolver keeps its warm caches; prepared statements re-prepare
|
||||
// lazily. Runs only at the pool-idle boundary, so no query is in
|
||||
// flight on this connection.
|
||||
if (!queries || !dbPath) throw new Error('resolver-worker: recycle before open');
|
||||
try {
|
||||
db?.close();
|
||||
} catch { /* already closed */ }
|
||||
const reopened = createDatabase(dbPath, { readOnly: true });
|
||||
db = reopened.db;
|
||||
db.pragma('busy_timeout = 5000');
|
||||
db.pragma('cache_size = -32000');
|
||||
queries.rebind(db);
|
||||
port.postMessage({ type: 'recycled', id: msg.id });
|
||||
break;
|
||||
}
|
||||
case 'resolve': {
|
||||
if (!resolver) throw new Error('resolver-worker: resolve before open');
|
||||
const tRes = Date.now();
|
||||
|
||||
Reference in New Issue
Block a user