camofox-browser/tests/unit/memoryPressure.test.js

238 lines
9.5 KiB
JavaScript

/**
* Tests for native memory pressure browser restart logic (#1032).
*
* The core decision logic from server.js is replicated here as a pure function
* to verify threshold checks, baseline tracking, and edge cases without
* needing a running browser.
*/
// ============================================================================
// Pure decision function (mirrors the setInterval logic in server.js)
// ============================================================================
/**
* Evaluate whether the browser should be restarted due to native memory pressure.
*
* @param {object} state - Mutable state object: { baseline, sessionsSize, browserAlive }
* @param {number} rssMb - Current RSS in MB
* @param {number} heapUsedMb - Current JS heap used in MB
* @param {number} thresholdMb - Growth threshold in MB
* @returns {'skip'|'baseline_set'|'ok'|'restart'} action to take
*/
function evaluateMemoryPressure(state, rssMb, heapUsedMb, thresholdMb, browserRssMb = null, browserRssThresholdMb = 1500) {
if (state.sessionsSize > 0 || !state.browserAlive) return 'skip';
if (browserRssMb !== null && browserRssMb >= browserRssThresholdMb) return 'browser_rss_restart';
const nativeMemMb = Math.round(rssMb - heapUsedMb);
if (state.baseline === null) {
state.baseline = nativeMemMb;
return 'baseline_set';
}
const growth = nativeMemMb - state.baseline;
if (growth >= thresholdMb) return 'restart';
return 'ok';
}
// ============================================================================
// Config parsing tests (mirrors the parseInt(env) || 300 pattern in config.js)
// ============================================================================
describe('NATIVE_MEM_RESTART_THRESHOLD_MB config parsing', () => {
// Replicate the exact parsing expression from lib/config.js line 74:
// nativeMemRestartThresholdMb: parseInt(process.env.NATIVE_MEM_RESTART_THRESHOLD_MB) || 300
function parseThreshold(envValue) {
return parseInt(envValue) || 300;
}
test('defaults to 300 when env var is undefined', () => {
expect(parseThreshold(undefined)).toBe(300);
});
test('defaults to 300 when env var is empty string', () => {
expect(parseThreshold('')).toBe(300);
});
test('reads numeric string', () => {
expect(parseThreshold('500')).toBe(500);
});
test('reads small value', () => {
expect(parseThreshold('50')).toBe(50);
});
test('falls back to 300 on non-numeric value', () => {
expect(parseThreshold('abc')).toBe(300);
});
test('falls back to 300 on zero (0 is falsy)', () => {
// parseInt('0') === 0 which is falsy, so || 300 kicks in.
// This is expected behavior — 0 threshold makes no sense.
expect(parseThreshold('0')).toBe(300);
});
test('reads negative value (parseInt parses it)', () => {
// parseInt('-100') === -100 which is truthy
// Negative threshold means restart immediately — not useful but not broken.
expect(parseThreshold('-100')).toBe(-100);
});
});
// ============================================================================
// Memory pressure decision logic
// ============================================================================
describe('evaluateMemoryPressure', () => {
test('skips when sessions are active', () => {
const state = { baseline: null, sessionsSize: 3, browserAlive: true };
expect(evaluateMemoryPressure(state, 500, 100, 200)).toBe('skip');
expect(state.baseline).toBeNull(); // baseline not touched
});
test('skips when browser is not alive', () => {
const state = { baseline: null, sessionsSize: 0, browserAlive: false };
expect(evaluateMemoryPressure(state, 500, 100, 200)).toBe('skip');
expect(state.baseline).toBeNull();
});
test('skips when sessions active AND browser dead', () => {
const state = { baseline: null, sessionsSize: 1, browserAlive: false };
expect(evaluateMemoryPressure(state, 500, 100, 200)).toBe('skip');
});
test('sets baseline on first idle check', () => {
const state = { baseline: null, sessionsSize: 0, browserAlive: true };
const result = evaluateMemoryPressure(state, 250, 100, 200);
expect(result).toBe('baseline_set');
expect(state.baseline).toBe(150); // 250 - 100
});
test('returns ok when growth is below threshold', () => {
const state = { baseline: 150, sessionsSize: 0, browserAlive: true };
// native = 300 - 100 = 200, growth = 200 - 150 = 50 < 200
expect(evaluateMemoryPressure(state, 300, 100, 200)).toBe('ok');
});
test('returns ok when growth equals threshold minus one', () => {
const state = { baseline: 150, sessionsSize: 0, browserAlive: true };
// native = 449 - 100 = 349, growth = 349 - 150 = 199 < 200
expect(evaluateMemoryPressure(state, 449, 100, 200)).toBe('ok');
});
test('returns restart when growth equals threshold exactly', () => {
const state = { baseline: 150, sessionsSize: 0, browserAlive: true };
// native = 450 - 100 = 350, growth = 350 - 150 = 200 >= 200
expect(evaluateMemoryPressure(state, 450, 100, 200)).toBe('restart');
});
test('returns restart when growth exceeds threshold', () => {
const state = { baseline: 147, sessionsSize: 0, browserAlive: true };
// Exact scenario from #1032: baseline 147, current 453
// native = 514 - 61 = 453, growth = 453 - 147 = 306 >= 200
expect(evaluateMemoryPressure(state, 514, 61, 200)).toBe('restart');
});
test('respects custom threshold', () => {
const state = { baseline: 100, sessionsSize: 0, browserAlive: true };
// native = 400 - 50 = 350, growth = 350 - 100 = 250
expect(evaluateMemoryPressure(state, 400, 50, 300)).toBe('ok'); // 250 < 300
expect(evaluateMemoryPressure(state, 450, 50, 300)).toBe('restart'); // 300 >= 300
});
test('handles zero baseline', () => {
const state = { baseline: 0, sessionsSize: 0, browserAlive: true };
// native = 250 - 50 = 200, growth = 200 - 0 = 200 >= 200
expect(evaluateMemoryPressure(state, 250, 50, 200)).toBe('restart');
});
test('handles negative growth (memory decreased)', () => {
const state = { baseline: 300, sessionsSize: 0, browserAlive: true };
// native = 250 - 50 = 200, growth = 200 - 300 = -100
expect(evaluateMemoryPressure(state, 250, 50, 200)).toBe('ok');
});
});
// ============================================================================
// Baseline lifecycle
// ============================================================================
describe('baseline lifecycle', () => {
test('baseline resets to null and reestablishes on next check', () => {
const state = { baseline: null, sessionsSize: 0, browserAlive: true };
// First check: baseline set
evaluateMemoryPressure(state, 250, 100, 200);
expect(state.baseline).toBe(150);
// Simulate browser close (reset)
state.baseline = null;
// Next check: new baseline established
evaluateMemoryPressure(state, 200, 80, 200);
expect(state.baseline).toBe(120);
});
test('does not update baseline on subsequent checks', () => {
const state = { baseline: null, sessionsSize: 0, browserAlive: true };
evaluateMemoryPressure(state, 250, 100, 200);
expect(state.baseline).toBe(150);
// Second check with different values — baseline should NOT change
evaluateMemoryPressure(state, 300, 100, 200);
expect(state.baseline).toBe(150); // unchanged
});
test('full churn cycle: baseline → growth → restart → new baseline', () => {
const state = { baseline: null, sessionsSize: 0, browserAlive: true };
// 1. Browser launches, first idle check sets baseline
expect(evaluateMemoryPressure(state, 250, 100, 200)).toBe('baseline_set');
expect(state.baseline).toBe(150);
// 2. Sessions come and go, sessions back to 0, memory grew
expect(evaluateMemoryPressure(state, 400, 100, 200)).toBe('ok'); // +150, under 200
// 3. More churn, memory keeps growing
expect(evaluateMemoryPressure(state, 500, 100, 200)).toBe('restart'); // +250, over 200
// 4. Browser killed, baseline resets
state.baseline = null;
state.browserAlive = false;
// 5. No browser — skip
expect(evaluateMemoryPressure(state, 200, 80, 200)).toBe('skip');
// 6. New browser launched on next request
state.browserAlive = true;
expect(evaluateMemoryPressure(state, 200, 80, 200)).toBe('baseline_set');
expect(state.baseline).toBe(120);
});
test('sessions becoming active prevents restart even at high memory', () => {
const state = { baseline: 100, sessionsSize: 0, browserAlive: true };
// Memory is over threshold
expect(evaluateMemoryPressure(state, 500, 50, 200)).toBe('restart');
// But if a session arrives before the interval fires, it skips
state.sessionsSize = 1;
expect(evaluateMemoryPressure(state, 500, 50, 200)).toBe('skip');
});
test('browser process tree RSS triggers idle restart before native baseline logic', () => {
const state = { baseline: null, sessionsSize: 0, browserAlive: true };
expect(evaluateMemoryPressure(state, 500, 100, 300, 2300, 1500)).toBe('browser_rss_restart');
expect(state.baseline).toBeNull();
});
test('browser process tree RSS below threshold falls through to native check', () => {
const state = { baseline: null, sessionsSize: 0, browserAlive: true };
expect(evaluateMemoryPressure(state, 500, 100, 300, 1200, 1500)).toBe('baseline_set');
expect(state.baseline).toBe(400);
});
test('browser process tree RSS is ignored while sessions are active', () => {
const state = { baseline: null, sessionsSize: 1, browserAlive: true };
expect(evaluateMemoryPressure(state, 500, 100, 300, 2300, 1500)).toBe('skip');
});
});