mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
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.
152 lines
3.8 KiB
TypeScript
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])
|
|
})
|