mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-26 19:44:51 +02:00
[eric] telemetry: memory sensor reports cap crossings and leak-shaped growth, silent on healthy sessions
This commit is contained in:
@@ -1478,6 +1478,10 @@ function createWindow() {
|
||||
// Frozen-but-not-crashed is the silent class no crash log sees; Chromium's own unresponsive
|
||||
// signal costs nothing and the report fires from the renderer AFTER it recovers.
|
||||
let wedgeStartedAt = 0;
|
||||
try {
|
||||
const { startMemorySensor } = require('./memorySensor');
|
||||
startMemorySensor(app, () => mainWindow);
|
||||
} catch (e) { console.warn('[diag] memory sensor unavailable:', e && e.message); }
|
||||
mainWindow.webContents.on('unresponsive', () => {
|
||||
wedgeStartedAt = Date.now();
|
||||
console.error('[diag][main] renderer unresponsive');
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// 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';
|
||||
|
||||
const SAMPLE_MS = 60_000;
|
||||
// Crossed once, reported once: a leak is a trend, not a per-minute alarm.
|
||||
const TOTAL_MB_CAP = 3000;
|
||||
const GROWTH_MB_PER_MIN = 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 };
|
||||
@@ -0,0 +1,27 @@
|
||||
// The leak detector's math, pinned: a flat session reports nothing, a straight climb is caught.
|
||||
const assert = require('node:assert/strict');
|
||||
const { test } = require('node:test');
|
||||
const { slopeMbPerMin, totalMb, GROWTH_MB_PER_MIN } = require('./memorySensor');
|
||||
|
||||
test('a flat memory profile has no slope, so nothing is ever reported', () => {
|
||||
assert.ok(Math.abs(slopeMbPerMin([900, 905, 898, 902, 900, 903, 899, 901, 900, 902])) < 1, 'jitter is not a trend');
|
||||
});
|
||||
|
||||
test('a steady climb is caught above the growth threshold', () => {
|
||||
const climbing = Array.from({ length: 10 }, (_, i) => 800 + i * 60);
|
||||
assert.ok(slopeMbPerMin(climbing) >= GROWTH_MB_PER_MIN, 'a 60MB/min climb must exceed the threshold');
|
||||
});
|
||||
|
||||
test('a single spike is not a leak', () => {
|
||||
const spike = [900, 900, 900, 900, 2000, 900, 900, 900, 900, 900];
|
||||
assert.ok(slopeMbPerMin(spike) < GROWTH_MB_PER_MIN, 'one spike must not read as a trend');
|
||||
});
|
||||
|
||||
test('too few samples never guesses', () => {
|
||||
assert.equal(slopeMbPerMin([900, 2000, 3000]), 0);
|
||||
});
|
||||
|
||||
test('totals sum every process in MB', () => {
|
||||
assert.equal(totalMb([{ memory: { workingSetSize: 1024 * 500 } }, { memory: { workingSetSize: 1024 * 300 } }]), 800);
|
||||
assert.equal(totalMb([{}, { memory: {} }]), 0);
|
||||
});
|
||||
@@ -152,6 +152,12 @@ contextBridge.exposeInMainWorld('openswarm', {
|
||||
openApplication: (name) => ipcRenderer.invoke('open-application', name),
|
||||
getUpdateStatus: () => ipcRenderer.invoke('get-update-status'),
|
||||
getCrashRecoveryInfo: () => ipcRenderer.invoke('get-crash-recovery-info'),
|
||||
// Threshold-crossing memory alerts (cap or leak-shaped growth); silent on a healthy session.
|
||||
onMemoryAlert: (cb) => {
|
||||
const listener = (_e, info) => cb(info);
|
||||
ipcRenderer.on('diag:memory', listener);
|
||||
return () => ipcRenderer.removeListener('diag:memory', listener);
|
||||
},
|
||||
// Fires after the renderer RECOVERS from a Chromium-detected freeze, with how long it was wedged.
|
||||
onWedge: (cb) => {
|
||||
const listener = (_e, info) => cb(info);
|
||||
|
||||
@@ -67,9 +67,13 @@ export function installUxSignals(): () => void {
|
||||
report('process', 'wedge_recovered', { wedge_ms: info?.ms ?? -1, source: 'chromium' });
|
||||
});
|
||||
const offObserver = installWedgeObserver();
|
||||
const offMem = (bridge.openswarm as { onMemoryAlert?: (cb: (i: Record<string, number | string>) => void) => () => void } | undefined)?.onMemoryAlert?.((info) => {
|
||||
report('process', 'memory_alert', info);
|
||||
});
|
||||
return () => {
|
||||
window.removeEventListener('click', onClick, true);
|
||||
offWedge?.();
|
||||
offObserver();
|
||||
offMem?.();
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user