feat(resolution): memory-aware, cgroup-honest worker-pool sizing + CODEGRAPH_RESOLVE_WORKERS (#1333)

Pool sizing used os.cpus().length, which enumerates the HOST's CPUs: inside
a 2-CPU cpuset it sized 6 resolver workers (the §7a.1 false-'sequential'
premise) and 8 parse workers, and at true 8-core concurrency six ~1GB
workers OOM-killed a 7GB container (oom_kill=5) mid-synthesis — sizing had
no memory term and no override knob.

resolvePoolSize (pure, matrix-tested): explicit CODEGRAPH_RESOLVE_WORKERS
override (0 disables, cap 16); CPU term max(2, min(availableParallelism-1,
6)) — cpuset-honest, floored at 2 so true 2-core boxes keep pooled
synthesis's ~2×; memory term floor(budget*0.7 / clamp(0.2*dbSize, 256MB,
1.5GB)) with budget = min(freemem, cgroup v2/v1 headroom). Parse pool's
core input switches to availableParallelism. Dev machines are unchanged
(still 6 workers); the 8c/7GB kernel-scale container now sizes 4.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-07-17 07:54:31 -05:00
committed by GitHub
co-authored by Claude Fable 5
parent 6e52295ceb
commit b8833fec57
5 changed files with 202 additions and 5 deletions
+79
View File
@@ -0,0 +1,79 @@
/**
* Resolver-pool sizing (§7a.1 P1.2): cgroup-honest CPU term + memory-aware
* cap + the CODEGRAPH_RESOLVE_WORKERS override. resolvePoolSize is pure —
* these pin the whole decision matrix, including the two failure modes the
* measurement round exposed: os.cpus() cpuset-blindness (6 workers inside a
* 2-CPU container) and memory-blind sizing (six ~1GB workers OOM-killing a
* 7GB container at true 8-core concurrency).
*/
import { describe, it, expect } from 'vitest';
import { ResolverPool } from '../src/resolution/resolver-pool';
import { cgroupMemoryAvailable, memoryBudgetBytes } from '../src/resolution/memory-budget';
const GB = 1024 * 1024 * 1024;
const MB = 1024 * 1024;
function size(opts: Partial<Parameters<typeof ResolverPool.resolvePoolSize>[0]>): number | null {
return ResolverPool.resolvePoolSize({
availableParallelism: 8,
memoryBudget: 16 * GB,
dbSizeBytes: 200 * MB,
...opts,
});
}
describe('ResolverPool.resolvePoolSize', () => {
it('big dev box: CPU-capped at the long-standing 6', () => {
expect(size({})).toBe(6);
expect(size({ availableParallelism: 11 })).toBe(6);
});
it('true 2-core box keeps a 2-worker pool (the pooled-synthesis 2×)', () => {
expect(size({ availableParallelism: 2, memoryBudget: 6 * GB })).toBe(2);
});
it('kernel-scale DB in a 7GB container: memory term shrinks the pool below the OOM line', () => {
// 4.6GB DB → ~940MB/worker estimate; 5.5GB headroom × 0.7 ≈ 3.85GB → 4 workers.
const s = size({ availableParallelism: 8, memoryBudget: 5.5 * GB, dbSizeBytes: 4.6 * GB });
expect(s).toBe(4);
expect(s!).toBeLessThan(6);
});
it('per-worker estimate is floored (small DBs) and capped (huge DBs)', () => {
// Small DB: floor 256MB/worker — memory cap = 16GB*0.7/256MB = 43 → CPU wins.
expect(size({ dbSizeBytes: 10 * MB })).toBe(6);
// Monster DB: cap 1.5GB/worker — 16GB*0.7/1.5GB = 7 → CPU still wins at 6.
expect(size({ dbSizeBytes: 40 * GB })).toBe(6);
// Same monster DB, tight memory: 4GB*0.7/1.5GB = 1 → below 2 → no pool.
expect(size({ dbSizeBytes: 40 * GB, memoryBudget: 4 * GB })).toBeNull();
});
it('starved memory disables the pool entirely', () => {
expect(size({ memoryBudget: 512 * MB, dbSizeBytes: 4 * GB })).toBeNull();
});
it('CODEGRAPH_RESOLVE_WORKERS overrides everything: 0 disables, values clamp at 16', () => {
expect(size({ explicit: '0' })).toBeNull();
expect(size({ explicit: '3', memoryBudget: 512 * MB })).toBe(3); // override skips the memory term
expect(size({ explicit: '64' })).toBe(16);
expect(size({ explicit: 'nonsense' })).toBe(6); // unparseable → computed path
});
});
describe('memory budget helpers', () => {
it('memoryBudgetBytes is positive and finite on every platform', () => {
const b = memoryBudgetBytes();
expect(b).toBeGreaterThan(0);
expect(Number.isFinite(b)).toBe(true);
});
it('cgroupMemoryAvailable is null when uncontained (non-Linux) and never throws', () => {
const v = cgroupMemoryAvailable();
if (process.platform !== 'linux') {
expect(v).toBeNull();
} else {
// Containerized CI: either uncontained (null) or a sane byte count.
expect(v === null || (v >= 0 && Number.isFinite(v))).toBe(true);
}
});
});