fix: use vm_stat for accurate macOS memory percentage

This commit is contained in:
Jarrod Watts
2026-04-04 14:29:25 +11:00
parent d1c2babac2
commit 9113b138f1
2 changed files with 89 additions and 6 deletions

View File

@@ -1,13 +1,50 @@
import os from 'node:os';
import { execFileSync } from 'node:child_process';
import type { MemoryInfo } from './types.js';
type MemoryReader = () => { totalBytes: number; freeBytes: number };
let readMemory: MemoryReader = () => ({
export function parseVmStat(
output: string,
): { pageSize: number; active: number; wired: number } | null {
const pageSizeMatch = output.match(/page size of (\d+) bytes/);
if (!pageSizeMatch) return null;
const activeMatch = output.match(/Pages active:\s+(\d+)/);
const wiredMatch = output.match(/Pages wired down:\s+(\d+)/);
if (!activeMatch || !wiredMatch) return null;
return {
pageSize: Number(pageSizeMatch[1]),
active: Number(activeMatch[1]),
wired: Number(wiredMatch[1]),
};
}
const readDefaultMemory: MemoryReader = () => ({
totalBytes: os.totalmem(),
freeBytes: os.freemem(),
});
const readMacOSMemory: MemoryReader = () => {
try {
const output = execFileSync('/usr/bin/vm_stat', {
encoding: 'utf8',
timeout: 5000,
});
const parsed = parseVmStat(output);
if (!parsed) return readDefaultMemory();
const totalBytes = os.totalmem();
const usedBytes = (parsed.active + parsed.wired) * parsed.pageSize;
return { totalBytes, freeBytes: totalBytes - usedBytes };
} catch {
return readDefaultMemory();
}
};
let readMemory: MemoryReader =
process.platform === 'darwin' ? readMacOSMemory : readDefaultMemory;
export async function getMemoryUsage(): Promise<MemoryInfo | null> {
try {
const { totalBytes, freeBytes } = readMemory();
@@ -51,8 +88,5 @@ export function formatBytes(bytes: number): string {
}
export function _setMemoryReaderForTests(reader: MemoryReader | null): void {
readMemory = reader ?? (() => ({
totalBytes: os.totalmem(),
freeBytes: os.freemem(),
}));
readMemory = reader ?? (process.platform === 'darwin' ? readMacOSMemory : readDefaultMemory);
}

View File

@@ -1,6 +1,6 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { _setMemoryReaderForTests, formatBytes, getMemoryUsage } from '../dist/memory.js';
import { _setMemoryReaderForTests, formatBytes, getMemoryUsage, parseVmStat } from '../dist/memory.js';
test('getMemoryUsage returns coarse system RAM usage with clamped values', async () => {
_setMemoryReaderForTests(() => ({
@@ -28,6 +28,55 @@ test('getMemoryUsage returns null when memory lookup fails', async () => {
assert.equal(memoryUsage, null);
});
test('parseVmStat parses vm_stat output correctly', () => {
const output = `Mach Virtual Memory Statistics: (page size of 16384 bytes)
Pages free: 2588951.
Pages active: 253775.
Pages inactive: 246498.
Pages speculative: 35498.
Pages throttled: 0.
Pages wired down: 195488.
Pages purgeable: 14994.
"Translation faults": 894498389.
Pages copy-on-write: 26696671.
Pages zero filled: 416498538.
Pages reactivated: 13299498.
Pages purged: 5765498.
File-backed pages: 156498.
Anonymous pages: 343775.
Pages stored in compressor: 498775.
Pages occupied by compressor: 115488.
Decompressions: 42498775.
Compressions: 58498775.
Pageins: 84498775.
Pageouts: 1498775.
Swapins: 0.
Swapouts: 0.`;
const result = parseVmStat(output);
assert.deepEqual(result, { pageSize: 16384, active: 253775, wired: 195488 });
});
test('parseVmStat returns null for empty string', () => {
assert.equal(parseVmStat(''), null);
});
test('parseVmStat returns null for malformed output', () => {
assert.equal(parseVmStat('not valid vm_stat output'), null);
});
test('macOS memory calculation: 16GB total, active+wired via vm_stat → 43%', async () => {
const totalBytes = 16 * 1024 ** 3;
const usedBytes = (253775 + 195488) * 16384;
_setMemoryReaderForTests(() => ({
totalBytes,
freeBytes: totalBytes - usedBytes,
}));
const memoryUsage = await getMemoryUsage();
assert.equal(memoryUsage.usedPercent, 43);
assert.equal(memoryUsage.usedBytes, usedBytes);
});
test('formatBytes formats human-readable units for memory line display', () => {
assert.equal(formatBytes(0), '0 B');
assert.equal(formatBytes(512), '512 B');