/** * @license * Copyright 2026 Vybestack LLC * SPDX-License-Identifier: Apache-2.0 */ /** * A memory sampler whose `heapUsed` is true under Bun. * * `process.memoryUsage()` is a Node-compatibility shim under Bun, and its * `heapUsed` field does not track the JavaScriptCore heap. Measured on bun * 1.3.14 against `bun:jsc`'s `heapSize()`: * * objects real JSC heap reported heapUsed * 200 K 20 MB 0 MB * 1 M 98 MB 53 MB * 3 M 287 MB 53 MB * * The heap tripled while `heapUsed` sat still. That is not a scale factor to * correct for, it is a number that does not move, so any trend built on it is * meaningless. A session could grow to many gigabytes while `/perf` reported * a small, flat heap. * * `rss`, `external`, and `heapTotal` come from the shim. `heapTotal` is * floored at the JSC heap size so it cannot be less than `heapUsed`; only * `heapUsed` is re-sourced from `bun:jsc`. * * LLxprt runs under Bun, so failure to load `bun:jsc` is a startup error rather * than a reason to silently fall back to the inaccurate compatibility value. * * `heapStats()` must not be used for periodic monitoring. It enumerates live * heap cells and allocates statistics while doing so. On a large, active heap, * that work can block the event loop and sharply increase resident memory. * `heapSize()` reads the same heap-size counter without enumerating the heap. * * Two measured properties shape the remaining behavior: * * 1. `extraMemorySize` from `heapStats()` must not be added to `heapSize`. * Retaining strings of a known logical size reported * `heapSize == extraMemorySize == logicalBytes`, so summing them yields * exactly twice the retained size. * * 2. `heapSize()` lags allocation until a sweep. Sampled immediately after * retaining 185 MB it read 67 MB; after `gcAndSweep()` it read 185 MB, * matching the logical size exactly. * * This sampler deliberately does not force a collection. It runs on a 60 s UI * tick, and a full GC against a multi-gigabyte heap is a user-visible pause. * A monitor must not perturb what it observes. The consequence is that * `heapUsed` is a floor that trails recent allocation and converges after the * next natural collection, which is sound for the trend this feeds. */ /** The subset of `bun:jsc` this module needs. */ interface JscHeapApi { heapSize: () => number; } /** * Bun populates `process.versions.bun`; Node leaves it undefined. * * Read through `globalThis` with optional chaining rather than touching * `process.versions` directly: this runs at module load, and suites that * substitute a partial `process` double would otherwise crash on import. * Mirrors the same defensive read in core's utils/runtime.ts. */ function isBunRuntime(): boolean { const versions = ( globalThis as { process?: { versions?: Record }; } ).process?.versions; const bunVersion = versions?.bun; return typeof bunVersion === 'string' && bunVersion.length > 0; } function isJscHeapApi(value: unknown): value is JscHeapApi { if (typeof value !== 'object' || value === null) { return false; } return 'heapSize' in value && typeof value.heapSize === 'function'; } /** Loads the synchronous JavaScriptCore heap API required by this Bun CLI. */ function loadJscHeapApi(): JscHeapApi { if (!isBunRuntime()) { throw new Error('LLxprt memory sampling requires Bun'); } const jsc = process.getBuiltinModule('bun:jsc'); if (!isJscHeapApi(jsc)) { throw new Error('bun:jsc heap size is unavailable'); } return jsc; } /** * Reads through `globalThis` rather than importing `node:process`. A memory * sampler must measure the real process, and sibling suites in this shard * replace the `node:process` module wholesale, which would otherwise make this * report another file's stub. */ function defaultBaseSampler(): NodeJS.MemoryUsage { const proc = ( globalThis as { process?: { memoryUsage?: () => NodeJS.MemoryUsage } } ).process; if (typeof proc?.memoryUsage !== 'function') { throw new Error('process.memoryUsage is unavailable in this runtime'); } return proc.memoryUsage(); } /** Resolved once: the runtime does not change under a running process. */ const jscHeapApi = loadJscHeapApi(); /** * Samples process memory, replacing `heapUsed` with JavaScriptCore's aggregate * heap size and flooring the shim's `heapTotal` at that value. * * Shaped as `NodeJS.MemoryUsage` so it drops into the existing * `MemoryTelemetryControllerOptions.memoryNow` and `MemoryMonitorPorts.memoryUsage` * seams without changing either contract. */ export function sampleMemoryUsage( baseSampler: () => NodeJS.MemoryUsage = defaultBaseSampler, ): NodeJS.MemoryUsage { const base = baseSampler(); const heapUsed = jscHeapApi.heapSize(); return { ...base, heapUsed, // Bun's base heapTotal is the shim value and can trail the JSC heap size. heapTotal: Math.max(base.heapTotal, heapUsed), }; }