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>
This commit is contained in:
Colby Mchenry
2026-07-20 20:11:31 -05:00
committed by GitHub
co-authored by Claude Fable 5
parent 3c1f30ab48
commit 27c3c55436
3 changed files with 79 additions and 5 deletions
+49 -4
View File
@@ -1,5 +1,6 @@
/**
* Memory headroom for worker-pool sizing — cgroup-honest on Linux.
* Memory headroom for worker-pool sizing — cgroup-honest on Linux,
* reclaim-honest on macOS.
*
* `os.freemem()` reads /proc/meminfo, which inside a container reports the
* HOST's (or VM's) memory, not the cgroup's — the same blindness os.cpus()
@@ -7,8 +8,19 @@
* kernel-scale index in a 7GB-capped container (migration plan §7a.1:
* oom_kill=5, six ~1GB workers at true 8-core concurrency), so pool sizing
* combines a CPU term with the memory headroom this module reports.
*
* On macOS `os.freemem()` has the OPPOSITE failure: it counts only
* `free_count` pages, and macOS deliberately keeps RAM full of reclaimable
* cache — a mostly-idle 64GB machine reads ~1GB "free", so the memory term
* capped the resolver pool at 2 workers where the CPU term allowed 6
* (measured on the dubbo warm-wall bench: resolution settle 3.0s at 2
* workers vs 1.9s at 6, ~1.52s of init wall). `darwinMemoryAvailable`
* reports what Activity Monitor calls available — free + inactive +
* speculative + purgeable pages — the same reclaimable-inclusive convention
* the Linux branch uses by crediting `inactive_file` back.
*/
import { execFileSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
@@ -66,13 +78,46 @@ export function cgroupMemoryAvailable(): number | null {
return null;
}
/**
* Reclaimable-inclusive available memory on macOS, or null elsewhere / on
* any parse failure (→ callers fall back to `os.freemem()`). Reads
* `/usr/bin/vm_stat` — the stable public interface over host_statistics64 —
* once per call (pool sizing runs it once per init; a few ms). Never throws.
*/
export function darwinMemoryAvailable(): number | null {
if (process.platform !== 'darwin') return null;
try {
const out = execFileSync('/usr/bin/vm_stat', { encoding: 'utf8', timeout: 2000 });
const pageMatch = /page size of (\d+) bytes/.exec(out);
const pageSize = pageMatch ? Number.parseInt(pageMatch[1]!, 10) : 16384;
const count = (label: string): number => {
const m = new RegExp(`^${label}:\\s+(\\d+)`, 'm').exec(out);
return m ? Number.parseInt(m[1]!, 10) : 0;
};
const pages =
count('Pages free') +
count('Pages inactive') +
count('Pages speculative') +
count('Pages purgeable');
const bytes = pages * pageSize;
return bytes > 0 && Number.isFinite(bytes) ? bytes : null;
} catch {
return null;
}
}
/**
* The budget pool sizing divides: the smaller of system free memory and the
* cgroup headroom (when contained). Conservative by construction — both
* numbers shrink as the process itself grows.
* cgroup headroom (when contained), with the macOS reclaimable-inclusive
* reading replacing the too-small darwin `freemem`. Conservative by
* construction — every number shrinks as the process itself grows.
*/
export function memoryBudgetBytes(): number {
const free = os.freemem();
const cgroup = cgroupMemoryAvailable();
return cgroup === null ? free : Math.min(free, cgroup);
if (cgroup !== null) return Math.min(free, cgroup);
// darwinAvailable ⊇ free by construction (the sum includes free pages);
// max() guards a hypothetical undercounting parse.
const darwin = darwinMemoryAvailable();
return darwin === null ? free : Math.max(free, darwin);
}