Files
codegraph/__tests__/resolver-pool-sizing.test.ts
T
27c3c55436 perf(resolution): darwin-honest memory budget — vm_stat-based availability unstrangles the resolver pool on macOS (#1388)
Post-R7b store-arc round 1, found by the dubbo warm-wall decomposition
(the cbm bar): resolution's loop-stage profile showed settle=3.0s — the
main thread idling on TWO resolver workers on an 11-core Mac. Pool sizing
logged `size=2 (budget=1068MB)`: memoryBudgetBytes() falls back to
os.freemem() when uncontained, and macOS keeps RAM deliberately full of
reclaimable cache, so freemem reads ~1GB on a mostly-idle 64GB machine.
The memory term then capped the pool at 2 where the CPU term allowed 6 —
the macOS sibling of §7a.1's os.cpus() cpuset-blindness (that round fixed
the CPU term; this fixes the memory term).

Fix: darwinMemoryAvailable() reads /usr/bin/vm_stat once per sizing call
and reports free + inactive + speculative + purgeable pages — what
Activity Monitor calls available, the same reclaimable-inclusive
convention the Linux branch already uses by crediting inactive_file back.
Parse failure → null → freemem fallback; Linux/cgroup and Windows paths
untouched.

Measured (dubbo 4,402 files, warm, caffeinated, n=3 each): pool now
self-sizes to 6 (budget 5.7-6.3GB) — wall 8.62-8.83s vs 9.67-10.87s
baseline, resolution phase 6.9→5.3s, loop settle 3.0→1.9s. Matches the
CODEGRAPH_RESOLVE_WORKERS=6 probe exactly (probe-before-build). Dumps
byte-identical pool-6 vs sequential (441,270 lines). Second consumer
unblocked: the cFnPtr LRU cache cap no longer spuriously degrades to 128
on Macs (its full-cache tier is worth ~60s at kernel scale).

Suite: resolver-pool-sizing gains a darwin-gated reclaimable-pages test +
an off-darwin null pin; full suite 2,689 green ×2 with
CODEGRAPH_KERNEL_EXPECT=1.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 20:11:31 -05:00

109 lines
4.3 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 * as os from 'os';
import { ResolverPool } from '../src/resolution/resolver-pool';
import {
cgroupMemoryAvailable,
darwinMemoryAvailable,
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 gets NO pool — sequential measured faster there (§7a.1: 853s vs 1150s)', () => {
expect(size({ availableParallelism: 2, memoryBudget: 6 * GB })).toBeNull();
expect(size({ availableParallelism: 3, 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);
}
});
it.runIf(process.platform === 'darwin')(
'darwin: available memory counts reclaimable pages, not just free_count',
() => {
const v = darwinMemoryAvailable();
// vm_stat exists on every macOS; a null here means the parse broke.
expect(v).not.toBeNull();
expect(Number.isFinite(v!)).toBe(true);
// The sum includes the free pages freemem() counts, so it can only be
// larger (modulo TOCTOU drift between the two reads — allow slack).
expect(v!).toBeGreaterThanOrEqual(os.freemem() * 0.5);
// And the budget must ride it (the 2-worker strangulation regression:
// a mostly-idle Mac read ~1GB free and halved the resolver pool).
expect(memoryBudgetBytes()).toBeGreaterThanOrEqual(v! * 0.5);
}
);
it.runIf(process.platform !== 'darwin')(
'darwinMemoryAvailable is null off-macOS and never throws',
() => {
expect(darwinMemoryAvailable()).toBeNull();
}
);
});