Files
codegraph/__tests__/resolver-pool-sizing.test.ts
T
b8833fec57 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>
2026-07-17 07:54:31 -05:00

80 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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);
}
});
});