Files
openswarm/electron/memorySensor.js
T

72 lines
2.8 KiB
JavaScript

// Memory/compute overload sensor: the quiet death nobody reports, where RSS climbs until macOS
// kills a renderer or the fans spin up. Idle-scheduled, unref'd, and it EMITS ONLY on threshold
// crossings, so a healthy session ships nothing at all.
'use strict';
// Overridable so support can ask a user to run with a tighter cap, and so the wire is testable
// without allocating gigabytes on someone's machine.
const SAMPLE_MS = Number(process.env.OSW_MEM_SAMPLE_MS || 60_000);
// Crossed once, reported once: a leak is a trend, not a per-minute alarm.
const TOTAL_MB_CAP = Number(process.env.OSW_MEM_CAP_MB || 3000);
const GROWTH_MB_PER_MIN = Number(process.env.OSW_MEM_GROWTH_MB || 40);
const GROWTH_WINDOW = 10;
let p_timer = null;
let p_history = [];
let p_capReported = false;
let p_growthReported = false;
function totalMb(metrics) {
let kb = 0;
for (const m of metrics) kb += (m.memory && m.memory.workingSetSize) || 0;
return Math.round(kb / 1024);
}
/** Least-squares slope in MB/min over the sample window; a straight climb is the leak signature. */
function slopeMbPerMin(history) {
const n = history.length;
if (n < 4) return 0;
const meanX = (n - 1) / 2;
const meanY = history.reduce((a, b) => a + b, 0) / n;
let num = 0;
let den = 0;
for (let i = 0; i < n; i += 1) {
num += (i - meanX) * (history[i] - meanY);
den += (i - meanX) * (i - meanX);
}
return den === 0 ? 0 : num / den;
}
function startMemorySensor(app, getMainWindow) {
if (p_timer) return;
p_timer = setInterval(() => {
let metrics;
try { metrics = app.getAppMetrics(); } catch (_) { return; }
const mb = totalMb(metrics);
p_history.push(mb);
if (p_history.length > GROWTH_WINDOW) p_history.shift();
const slope = slopeMbPerMin(p_history);
const send = (reason, extra) => {
const win = getMainWindow();
if (win && !win.isDestroyed()) {
try { win.webContents.send('diag:memory', { reason, total_mb: mb, procs: metrics.length, slope_mb_min: Math.round(slope), ...extra }); } catch (_) {}
}
console.error('[diag][memory]', reason, 'total_mb=' + mb, 'procs=' + metrics.length, 'slope=' + Math.round(slope));
};
if (!p_capReported && mb >= TOTAL_MB_CAP) { p_capReported = true; send('cap_crossed', {}); }
if (p_capReported && mb < TOTAL_MB_CAP * 0.8) p_capReported = false;
if (!p_growthReported && p_history.length >= GROWTH_WINDOW && slope >= GROWTH_MB_PER_MIN) {
p_growthReported = true;
send('growth_suspect', { window_min: GROWTH_WINDOW });
}
}, SAMPLE_MS);
p_timer.unref?.();
}
function stopMemorySensor() {
if (p_timer) { clearInterval(p_timer); p_timer = null; }
p_history = [];
}
module.exports = { startMemorySensor, stopMemorySensor, slopeMbPerMin, totalMb, TOTAL_MB_CAP, GROWTH_MB_PER_MIN };