hermes-agent/apps/desktop/electron/stream-throttle.test.ts
Brooklyn Nicholson 8ccb4c2cee perf(desktop): scope background-throttling opt-out to live streaming
The process-wide disable-background-timer-throttling /
disable-backgrounding-occluded-windows switches plus a static
backgroundThrottling: false on every chat window pinned each renderer's
document.visibilityState to 'visible' for the life of the window. Every
visibility-gated backstop poll and clock tick in the renderer became an
always-on timer: an idle, minimized Hermes burned ~20% CPU around the
clock, on battery too.

Throttling is now a runtime dial. A small controller (stream-throttle.ts)
rides the merged hermes:active-work reports the quit guard already
receives: while any turn is in flight every chat window gets
setBackgroundThrottling(false) — a live answer keeps painting while
blurred, occluded, or minimized, exactly as before — and once all turns
settle (plus a 5s trailing window so the final flush lands at full
cadence) Chromium's default throttling returns and hidden windows go
quiet.

disable-renderer-backgrounding stays: process priority only, no timer
semantics, and it keeps hidden streaming fast.
2026-07-31 00:38:11 -05:00

152 lines
3.8 KiB
TypeScript

import assert from 'node:assert/strict'
import { test } from 'vitest'
import { createStreamThrottle, type ThrottleWindowLike } from './stream-throttle'
function makeTimers() {
const pending = new Map<number, () => void>()
let nextId = 1
return {
clearTimeout: (handle: unknown) => {
pending.delete(handle as number)
},
fire() {
const jobs = [...pending.values()]
pending.clear()
for (const job of jobs) {
job()
}
},
get pendingCount() {
return pending.size
},
setTimeout: (fn: () => void, _ms: number) => {
const id = nextId++
pending.set(id, fn)
return id
}
}
}
function makeWindow() {
const calls: boolean[] = []
const listeners = new Map<string, () => void>()
let destroyed = false
const win = {
calls,
close() {
destroyed = true
listeners.get('closed')?.()
},
isDestroyed: () => destroyed,
on(event: string, fn: () => void) {
listeners.set(event, fn)
},
webContents: {
isDestroyed: () => destroyed,
setBackgroundThrottling(allowed: boolean) {
calls.push(allowed)
}
}
}
return win
}
test('registering a window applies the current throttle state immediately', () => {
const timers = makeTimers()
const throttle = createStreamThrottle(timers)
const idle = makeWindow()
throttle.register(idle)
// Idle default: throttling allowed.
assert.deepEqual(idle.calls, [true])
throttle.update(true)
const late = makeWindow()
throttle.register(late)
// A window created mid-stream starts unthrottled.
assert.deepEqual(late.calls, [false])
})
test('a turn in flight unthrottles every chat window; settling re-throttles after the trailing delay', () => {
const timers = makeTimers()
const throttle = createStreamThrottle(timers)
const win = makeWindow()
throttle.register(win)
throttle.update(true)
assert.deepEqual(win.calls, [true, false])
assert.equal(throttle.isUnthrottled(), true)
// Turn ends: not re-throttled synchronously — the tail flush needs full
// cadence — only after the trailing timer fires.
throttle.update(false)
assert.deepEqual(win.calls, [true, false])
assert.equal(throttle.isUnthrottled(), true)
timers.fire()
assert.deepEqual(win.calls, [true, false, true])
assert.equal(throttle.isUnthrottled(), false)
})
test('a new turn during the trailing window cancels the pending re-throttle', () => {
const timers = makeTimers()
const throttle = createStreamThrottle(timers)
const win = makeWindow()
throttle.register(win)
throttle.update(true)
throttle.update(false)
assert.equal(timers.pendingCount, 1)
// Busy again before the delay elapses: stay unthrottled, timer cancelled.
throttle.update(true)
assert.equal(timers.pendingCount, 0)
assert.equal(throttle.isUnthrottled(), true)
// The cancelled timer firing late must be a no-op.
timers.fire()
assert.equal(throttle.isUnthrottled(), true)
})
test('repeated busy reports do not re-apply or stack timers', () => {
const timers = makeTimers()
const throttle = createStreamThrottle(timers)
const win = makeWindow()
throttle.register(win)
throttle.update(true)
throttle.update(true)
throttle.update(true)
assert.deepEqual(win.calls, [true, false])
throttle.update(false)
throttle.update(false)
assert.equal(timers.pendingCount, 1)
})
test('closed and destroyed windows drop out without throwing', () => {
const timers = makeTimers()
const throttle = createStreamThrottle(timers)
const closedWin = makeWindow()
throttle.register(closedWin)
closedWin.close()
const gone: ThrottleWindowLike & { on?: never } = {
isDestroyed: () => true,
webContents: null
}
throttle.register(gone)
throttle.update(true)
// Only the registration-time call landed; nothing after close.
assert.deepEqual(closedWin.calls, [true])
})