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:
co-authored by
Claude Fable 5
parent
3c1f30ab48
commit
27c3c55436
@@ -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.5–2s 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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user