Improve gpu cpu monitor efficiency.

This commit is contained in:
Jaret Burkett
2026-08-14 17:14:00 -06:00
parent 695b0baccf
commit 247cb45c3e
2 changed files with 40 additions and 11 deletions

View File

@@ -32,9 +32,17 @@ export async function GET(request: Request) {
cleanup();
return;
}
unsubscribe = monitor.subscribe((sample: MonitorSample) => {
unsubscribe = monitor.subscribe((sample: MonitorSample, serialized: string) => {
// desiredSize goes ever more negative when the consumer stopped
// reading. If the abort for a vanished client never propagates (dev
// HMR reloads, proxy quirks), this is the backstop that keeps dead
// subscribers from accumulating and eating CPU forever.
if (controller.desiredSize !== null && controller.desiredSize < -120) {
cleanup();
return;
}
try {
send('sample', sample);
controller.enqueue(encoder.encode(`event: sample\ndata: ${serialized}\n\n`));
} catch {
// Client is gone but abort hasn't fired yet
cleanup();

View File

@@ -31,6 +31,8 @@ const NV_WATCHDOG_MS = 15_000;
// lines of one iteration arrive together; iterations are MONITOR_TICK_MS
// apart, so this can never bleed into the next batch).
const NV_BATCH_FLUSH_MS = 100;
// Temperature refresh is decoupled from the tick (see refreshCpuTemp)
const CPU_TEMP_REFRESH_MS = 5000;
function parseGpuLine(line: string): GpuInfo | null {
const [
@@ -78,7 +80,8 @@ function parseGpuLine(line: string): GpuInfo | null {
};
}
type Subscriber = (sample: MonitorSample) => void;
// Serialized once per tick and shared, so N subscribers don't stringify N times
type Subscriber = (sample: MonitorSample, serialized: string) => void;
class SystemMonitor {
private isMac = os.platform() === 'darwin';
@@ -100,6 +103,8 @@ class SystemMonitor {
private tickInFlight = false;
private lastCpuTemp = 0;
private cpuTempInFlight = false;
private lastCpuTempAt = 0;
private cpuStatic: { name: string; cores: number } | null = null;
start(): void {
if (this.isMac) {
@@ -172,9 +177,10 @@ class SystemMonitor {
if (this.history.length > MONITOR_HISTORY_LENGTH) {
this.history.splice(0, this.history.length - MONITOR_HISTORY_LENGTH);
}
const serialized = JSON.stringify(sample);
for (const subscriber of this.subscribers) {
try {
subscriber(sample);
subscriber(sample, serialized);
} catch (error) {
console.error('Monitor: subscriber failed:', error);
}
@@ -185,7 +191,14 @@ class SystemMonitor {
// CPU (mirrors /api/cpu exactly)
// -------------------------------------------------------------------------
private async sampleCpu(): Promise<CpuInfo> {
const cpuInfoRaw = await si.cpu(); // static info, cached by systeminformation
// si.cpu() shells out to lscpu/dmidecode on every call — at tick cadence
// that alone was eating a measurable slice of a core. Name and core count
// never change, so resolve them exactly once.
if (!this.cpuStatic) {
const cpuInfoRaw = await si.cpu();
this.cpuStatic = { name: `${cpuInfoRaw.manufacturer} ${cpuInfoRaw.brand}`, cores: cpuInfoRaw.cores };
}
const cpuStatic = this.cpuStatic;
if (this.isMac) {
try {
@@ -194,8 +207,8 @@ class SystemMonitor {
const ramData = ms.getRAMUsageSync();
const cpuData = ms.getCpuDataSync();
return {
name: `${cpuInfoRaw.manufacturer} ${cpuInfoRaw.brand}`,
cores: cpuInfoRaw.cores,
name: cpuStatic.name,
cores: cpuStatic.cores,
temperature: cpuData.temperature || 0,
totalMemory: ramData.total / (1024 * 1024),
availableMemory: ramData.free / (1024 * 1024),
@@ -213,8 +226,8 @@ class SystemMonitor {
this.refreshCpuTemp();
const [memoryData, load] = await Promise.all([si.mem(), si.currentLoad()]);
return {
name: `${cpuInfoRaw.manufacturer} ${cpuInfoRaw.brand}`,
cores: cpuInfoRaw.cores,
name: cpuStatic.name,
cores: cpuStatic.cores,
temperature: this.lastCpuTemp,
totalMemory: memoryData.total / (1024 * 1024),
availableMemory: memoryData.available / (1024 * 1024),
@@ -224,7 +237,11 @@ class SystemMonitor {
}
private refreshCpuTemp(): void {
if (this.cpuTempInFlight) return;
// si.cpuTemperature() execs `sensors`, which can take ~1s of slow SMBus
// polling — refreshed back-to-back it becomes a permanently resident
// child. Every few seconds is plenty for a temperature.
if (this.cpuTempInFlight || Date.now() - this.lastCpuTempAt < CPU_TEMP_REFRESH_MS) return;
this.lastCpuTempAt = Date.now();
this.cpuTempInFlight = true;
si.cpuTemperature()
.then(t => {
@@ -464,7 +481,11 @@ export function startMonitor(): SystemMonitor {
const g = globalThis as unknown as { __aiToolkitSystemMonitor?: SystemMonitor };
if (!g.__aiToolkitSystemMonitor) {
g.__aiToolkitSystemMonitor = new SystemMonitor();
g.__aiToolkitSystemMonitor.start();
// Escape hatch: with AI_TOOLKIT_DISABLE_MONITOR=1 the monitor object
// exists (the SSE route stays functional) but never samples anything.
if (process.env.AI_TOOLKIT_DISABLE_MONITOR !== '1') {
g.__aiToolkitSystemMonitor.start();
}
}
return g.__aiToolkitSystemMonitor;
}