perf(desktop): stop idle-session event-health polling from re-rendering the whole shell - #1988

Merged
Astro-Han merged 4 commits into
mainfrom
perf/desktop-idle-event-health-polling
Aug 3, 2026
Merged

perf(desktop): stop idle-session event-health polling from re-rendering the whole shell#1988
Astro-Han merged 4 commits into
mainfrom
perf/desktop-idle-event-health-polling

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

useSessionEventHealthPolling re-rendered the whole AppShell every 5s for the entire lifetime of any open session, idle or not — the only periodic work the app did while idle, and it changed nothing visible.

Four commits, one root cause:

  1. Gate the polling. Once a session no longer expects an event stream there is nothing to observe: deriveSessionEventStreamStatus short-circuits to closed and shouldRefreshStaleSessionEventStream never fires. The interval and the visibilitychange listener are now gated on sessionExpectsEventStream.
  2. Take health out of rendered state.sessionEventHealthBySession lived in AppShellSessionUiState, whose only purpose is to force a render. Nothing renders it — the sole reader is the polling effect via sessionEventHealthBySessionRef. It now lives in a controller-owned ref, so neither the probes nor the per-SessionEvent writes (app-shell-effects.ts:380, one extra full render per streamed event) notify React. setSessionEventHealthBySession keeps its updater signature, so all four call sites are untouched; clearSessionUiState drops the session from the ref alongside the rendered maps.
  3. Drop the probe the gate left behind. The gate initially sat after a one-off evaluate(), so settling into idle still wrote one snapshot. That write cannot do anything — expected is false on this path, and neither status nor staleSince has a renderer consumer (the sidebar's staleSessionIds comes from stale-sessions.ts, an unrelated backend/slug classifier). markSessionEventStreamClosed already records the closed stream when the subscription goes away, so an idle session now costs nothing at all.
  4. Comment and type cleanup left over from step 2.

Gating alone would have fixed the idle symptom, but only step 2 removes the per-event renders during a stream. Note that "skip unchanged writes" is not a valid alternative to either: the ref is only synced on write, so skipping writes would strand refreshRequestedAt and turn the 10s refresh cooldown into a refresh every tick.

Closes#1979

Verification

  • npm --workspace apps/desktop run test:dist — 1348 pass, 0 fail.
  • npm --workspace apps/desktop run typecheck — clean (preload, main, renderer, storybook).
  • npm run format / npm run lint — clean.
  • npm --workspace apps/desktop run e2e — 58 passed, including session-health-notice.spec.ts.

New coverage in app-shell-effect-stability-contract.test.ts, driven by a fake clock added to the existing fake DOM (observable window.setInterval / document.addEventListener, controllable Date.now):

  • an idle active session arms no interval, registers no visibilitychange listener, and writes nothing at all — before or after the clock advances;
  • a session going active → running → active re-arms and then disarms both the interval and the listener;
  • a running session goes stale past the threshold, refreshes sessions and messages exactly once, and re-probes when the window becomes visible again.

Each was verified by mutation, not just by passing: removing the gate, moving the gate back after evaluate, making health writes notify again, dropping the ref cleanup in clearSessionUiState, and no-oping the interval callback each turn the corresponding test red. The round-trip case specifically covers the one failure mode this gate introduces — a session that starts running but never re-arms; shrinking the dep array to [activeId] leaves every other case green and fails only that one.

Not run: no screenshot or Storybook evidence — this PR removes renders and changes no pixels.

Review focus

clearSessionUiState now mutates the health ref in addition to replaceState. That is the one place where the ref's lifecycle could drift from the rendered maps, and it is covered by drops event-stream health along with the rest of a cleared session.

Known pre-existing behavior, not addressed here

If a session sits idle past the 15s threshold and then starts running before a fresh SessionEvent or sessions:changed observation reaches the renderer, the first evaluate reads stale observation timestamps, derives stale, and fires one extra refresh. The inputs to that first evaluate are bit-identical before and after this PR — evaluateSessionEventStreamSnapshot never advances subscribedAt / lastEventAt / lastChangedAt, so the idle ticks this PR removes never affected it. The refresh is idempotent and self-healing. Out of scope; worth a separate issue if it ever shows up in practice.

Related

`useSessionEventHealthPolling` armed a 5s interval for any active session,
but once a session no longer expects an event stream the tick has nothing
to observe: `deriveSessionEventStreamStatus` short-circuits to `closed` and
`shouldRefreshStaleSessionEventStream` never fires. Every tick still wrote a
snapshot carrying a fresh `checkedAt`, which only ever cleared the session UI
state controller's reference check and forced a full AppShell render — the
only periodic work the app did while idle.
Gate the interval (and the visibilitychange listener) on
`sessionExpectsEventStream`. The one-off `evaluate` before the gate still
records the transition into `closed`, and both inputs to `expected` are
already effect deps, so a session that starts running re-arms on its own.
Refs #1979
…tate
`sessionEventHealthBySession` lived in `AppShellSessionUiState`, whose only
purpose is to force an AppShell render on change. Nothing renders it — the
sole reader is the polling effect, through `sessionEventHealthBySessionRef` —
yet every probe wrote a snapshot with a fresh `checkedAt`, and each SessionEvent
did too, so a stream forced one extra full render per event.
Move it to a controller-owned ref. `setSessionEventHealthBySession` keeps its
updater signature, so all four call sites are untouched; `clearSessionUiState`
drops the session from the ref alongside the rendered maps. The `MissingSessionUiMapKey`
guard keeps the remaining maps exhaustive.
Refs #1979
The gate sat after a one-off `evaluate()`, so settling into idle still wrote
one snapshot. That write cannot do anything: `expected` is false on this path,
so the status derives to `closed` and no refresh is ever requested, and neither
`status` nor `staleSince` has a renderer consumer (the sidebar's
`staleSessionIds` comes from `stale-sessions.ts`, an unrelated backend/slug
classifier). `markSessionEventStreamClosed` already records the closed stream
when the subscription goes away.
Moving the gate above `evaluate` makes an idle session cost nothing at all.
The test asserted that dead write, which would have failed a smaller correct
implementation; it now asserts zero writes.
Also cover the failure mode this gate introduces: a session that starts running
must re-arm. Neither single-status case caught it — shrinking the dep array to
`[activeId]` kept them green. The round-trip case fails on that mutation.
Refs #1979
… move
The turn-transient comment still listed event-stream health among the maps
`clearAppShellSessionUiStateForSession` clears; it now lives in a controller
ref and is cleared by `clearSessionUiState`. Point at the ref instead, and drop
the interface preamble that described a field no longer declared there — the
ref's own comment already carries that rationale.
Narrow `omitSessionKey` back to `Record<string, unknown>`; widening it to
`object` for the ref accepted arrays and non-record objects, which would return
silently unchanged.
@Astro-Han
Astro-Han marked this pull request as ready for review August 3, 2026 10:55
@Astro-Han
Astro-Han merged commit d671e06 into mainAug 3, 2026
11 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf(desktop): stop idle-session event-health polling from re-rendering the whole shell

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

perf(desktop): stop idle-session event-health polling from re-rendering the whole shell - #1988

Merged
Astro-Han merged 4 commits into
mainfrom
perf/desktop-idle-event-health-polling
Aug 3, 2026
Merged

perf(desktop): stop idle-session event-health polling from re-rendering the whole shell#1988
Astro-Han merged 4 commits into
mainfrom
perf/desktop-idle-event-health-polling

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

useSessionEventHealthPolling re-rendered the whole AppShell every 5s for the entire lifetime of any open session, idle or not — the only periodic work the app did while idle, and it changed nothing visible.

Four commits, one root cause:

  1. Gate the polling. Once a session no longer expects an event stream there is nothing to observe: deriveSessionEventStreamStatus short-circuits to closed and shouldRefreshStaleSessionEventStream never fires. The interval and the visibilitychange listener are now gated on sessionExpectsEventStream.
  2. Take health out of rendered state.sessionEventHealthBySession lived in AppShellSessionUiState, whose only purpose is to force a render. Nothing renders it — the sole reader is the polling effect via sessionEventHealthBySessionRef. It now lives in a controller-owned ref, so neither the probes nor the per-SessionEvent writes (app-shell-effects.ts:380, one extra full render per streamed event) notify React. setSessionEventHealthBySession keeps its updater signature, so all four call sites are untouched; clearSessionUiState drops the session from the ref alongside the rendered maps.
  3. Drop the probe the gate left behind. The gate initially sat after a one-off evaluate(), so settling into idle still wrote one snapshot. That write cannot do anything — expected is false on this path, and neither status nor staleSince has a renderer consumer (the sidebar's staleSessionIds comes from stale-sessions.ts, an unrelated backend/slug classifier). markSessionEventStreamClosed already records the closed stream when the subscription goes away, so an idle session now costs nothing at all.
  4. Comment and type cleanup left over from step 2.

Gating alone would have fixed the idle symptom, but only step 2 removes the per-event renders during a stream. Note that "skip unchanged writes" is not a valid alternative to either: the ref is only synced on write, so skipping writes would strand refreshRequestedAt and turn the 10s refresh cooldown into a refresh every tick.

Closes#1979

Verification

  • npm --workspace apps/desktop run test:dist — 1348 pass, 0 fail.
  • npm --workspace apps/desktop run typecheck — clean (preload, main, renderer, storybook).
  • npm run format / npm run lint — clean.
  • npm --workspace apps/desktop run e2e — 58 passed, including session-health-notice.spec.ts.

New coverage in app-shell-effect-stability-contract.test.ts, driven by a fake clock added to the existing fake DOM (observable window.setInterval / document.addEventListener, controllable Date.now):

  • an idle active session arms no interval, registers no visibilitychange listener, and writes nothing at all — before or after the clock advances;
  • a session going active → running → active re-arms and then disarms both the interval and the listener;
  • a running session goes stale past the threshold, refreshes sessions and messages exactly once, and re-probes when the window becomes visible again.

Each was verified by mutation, not just by passing: removing the gate, moving the gate back after evaluate, making health writes notify again, dropping the ref cleanup in clearSessionUiState, and no-oping the interval callback each turn the corresponding test red. The round-trip case specifically covers the one failure mode this gate introduces — a session that starts running but never re-arms; shrinking the dep array to [activeId] leaves every other case green and fails only that one.

Not run: no screenshot or Storybook evidence — this PR removes renders and changes no pixels.

Review focus

clearSessionUiState now mutates the health ref in addition to replaceState. That is the one place where the ref's lifecycle could drift from the rendered maps, and it is covered by drops event-stream health along with the rest of a cleared session.

Known pre-existing behavior, not addressed here

If a session sits idle past the 15s threshold and then starts running before a fresh SessionEvent or sessions:changed observation reaches the renderer, the first evaluate reads stale observation timestamps, derives stale, and fires one extra refresh. The inputs to that first evaluate are bit-identical before and after this PR — evaluateSessionEventStreamSnapshot never advances subscribedAt / lastEventAt / lastChangedAt, so the idle ticks this PR removes never affected it. The refresh is idempotent and self-healing. Out of scope; worth a separate issue if it ever shows up in practice.

Related

`useSessionEventHealthPolling` armed a 5s interval for any active session,
but once a session no longer expects an event stream the tick has nothing
to observe: `deriveSessionEventStreamStatus` short-circuits to `closed` and
`shouldRefreshStaleSessionEventStream` never fires. Every tick still wrote a
snapshot carrying a fresh `checkedAt`, which only ever cleared the session UI
state controller's reference check and forced a full AppShell render — the
only periodic work the app did while idle.
Gate the interval (and the visibilitychange listener) on
`sessionExpectsEventStream`. The one-off `evaluate` before the gate still
records the transition into `closed`, and both inputs to `expected` are
already effect deps, so a session that starts running re-arms on its own.
Refs #1979
…tate
`sessionEventHealthBySession` lived in `AppShellSessionUiState`, whose only
purpose is to force an AppShell render on change. Nothing renders it — the
sole reader is the polling effect, through `sessionEventHealthBySessionRef` —
yet every probe wrote a snapshot with a fresh `checkedAt`, and each SessionEvent
did too, so a stream forced one extra full render per event.
Move it to a controller-owned ref. `setSessionEventHealthBySession` keeps its
updater signature, so all four call sites are untouched; `clearSessionUiState`
drops the session from the ref alongside the rendered maps. The `MissingSessionUiMapKey`
guard keeps the remaining maps exhaustive.
Refs #1979
The gate sat after a one-off `evaluate()`, so settling into idle still wrote
one snapshot. That write cannot do anything: `expected` is false on this path,
so the status derives to `closed` and no refresh is ever requested, and neither
`status` nor `staleSince` has a renderer consumer (the sidebar's
`staleSessionIds` comes from `stale-sessions.ts`, an unrelated backend/slug
classifier). `markSessionEventStreamClosed` already records the closed stream
when the subscription goes away.
Moving the gate above `evaluate` makes an idle session cost nothing at all.
The test asserted that dead write, which would have failed a smaller correct
implementation; it now asserts zero writes.
Also cover the failure mode this gate introduces: a session that starts running
must re-arm. Neither single-status case caught it — shrinking the dep array to
`[activeId]` kept them green. The round-trip case fails on that mutation.
Refs #1979
… move
The turn-transient comment still listed event-stream health among the maps
`clearAppShellSessionUiStateForSession` clears; it now lives in a controller
ref and is cleared by `clearSessionUiState`. Point at the ref instead, and drop
the interface preamble that described a field no longer declared there — the
ref's own comment already carries that rationale.
Narrow `omitSessionKey` back to `Record<string, unknown>`; widening it to
`object` for the ref accepted arrays and non-record objects, which would return
silently unchanged.
@Astro-Han
Astro-Han marked this pull request as ready for review August 3, 2026 10:55
@Astro-Han
Astro-Han merged commit d671e06 into mainAug 3, 2026
11 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf(desktop): stop idle-session event-health polling from re-rendering the whole shell

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

perf(desktop): stop idle-session event-health polling from re-rendering the whole shell - #1988

Merged
Astro-Han merged 4 commits into
mainfrom
perf/desktop-idle-event-health-polling
Aug 3, 2026
Merged

perf(desktop): stop idle-session event-health polling from re-rendering the whole shell#1988
Astro-Han merged 4 commits into
mainfrom
perf/desktop-idle-event-health-polling

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

useSessionEventHealthPolling re-rendered the whole AppShell every 5s for the entire lifetime of any open session, idle or not — the only periodic work the app did while idle, and it changed nothing visible.

Four commits, one root cause:

  1. Gate the polling. Once a session no longer expects an event stream there is nothing to observe: deriveSessionEventStreamStatus short-circuits to closed and shouldRefreshStaleSessionEventStream never fires. The interval and the visibilitychange listener are now gated on sessionExpectsEventStream.
  2. Take health out of rendered state.sessionEventHealthBySession lived in AppShellSessionUiState, whose only purpose is to force a render. Nothing renders it — the sole reader is the polling effect via sessionEventHealthBySessionRef. It now lives in a controller-owned ref, so neither the probes nor the per-SessionEvent writes (app-shell-effects.ts:380, one extra full render per streamed event) notify React. setSessionEventHealthBySession keeps its updater signature, so all four call sites are untouched; clearSessionUiState drops the session from the ref alongside the rendered maps.
  3. Drop the probe the gate left behind. The gate initially sat after a one-off evaluate(), so settling into idle still wrote one snapshot. That write cannot do anything — expected is false on this path, and neither status nor staleSince has a renderer consumer (the sidebar's staleSessionIds comes from stale-sessions.ts, an unrelated backend/slug classifier). markSessionEventStreamClosed already records the closed stream when the subscription goes away, so an idle session now costs nothing at all.
  4. Comment and type cleanup left over from step 2.

Gating alone would have fixed the idle symptom, but only step 2 removes the per-event renders during a stream. Note that "skip unchanged writes" is not a valid alternative to either: the ref is only synced on write, so skipping writes would strand refreshRequestedAt and turn the 10s refresh cooldown into a refresh every tick.

Closes#1979

Verification

  • npm --workspace apps/desktop run test:dist — 1348 pass, 0 fail.
  • npm --workspace apps/desktop run typecheck — clean (preload, main, renderer, storybook).
  • npm run format / npm run lint — clean.
  • npm --workspace apps/desktop run e2e — 58 passed, including session-health-notice.spec.ts.

New coverage in app-shell-effect-stability-contract.test.ts, driven by a fake clock added to the existing fake DOM (observable window.setInterval / document.addEventListener, controllable Date.now):

  • an idle active session arms no interval, registers no visibilitychange listener, and writes nothing at all — before or after the clock advances;
  • a session going active → running → active re-arms and then disarms both the interval and the listener;
  • a running session goes stale past the threshold, refreshes sessions and messages exactly once, and re-probes when the window becomes visible again.

Each was verified by mutation, not just by passing: removing the gate, moving the gate back after evaluate, making health writes notify again, dropping the ref cleanup in clearSessionUiState, and no-oping the interval callback each turn the corresponding test red. The round-trip case specifically covers the one failure mode this gate introduces — a session that starts running but never re-arms; shrinking the dep array to [activeId] leaves every other case green and fails only that one.

Not run: no screenshot or Storybook evidence — this PR removes renders and changes no pixels.

Review focus

clearSessionUiState now mutates the health ref in addition to replaceState. That is the one place where the ref's lifecycle could drift from the rendered maps, and it is covered by drops event-stream health along with the rest of a cleared session.

Known pre-existing behavior, not addressed here

If a session sits idle past the 15s threshold and then starts running before a fresh SessionEvent or sessions:changed observation reaches the renderer, the first evaluate reads stale observation timestamps, derives stale, and fires one extra refresh. The inputs to that first evaluate are bit-identical before and after this PR — evaluateSessionEventStreamSnapshot never advances subscribedAt / lastEventAt / lastChangedAt, so the idle ticks this PR removes never affected it. The refresh is idempotent and self-healing. Out of scope; worth a separate issue if it ever shows up in practice.

Related

`useSessionEventHealthPolling` armed a 5s interval for any active session,
but once a session no longer expects an event stream the tick has nothing
to observe: `deriveSessionEventStreamStatus` short-circuits to `closed` and
`shouldRefreshStaleSessionEventStream` never fires. Every tick still wrote a
snapshot carrying a fresh `checkedAt`, which only ever cleared the session UI
state controller's reference check and forced a full AppShell render — the
only periodic work the app did while idle.
Gate the interval (and the visibilitychange listener) on
`sessionExpectsEventStream`. The one-off `evaluate` before the gate still
records the transition into `closed`, and both inputs to `expected` are
already effect deps, so a session that starts running re-arms on its own.
Refs #1979
…tate
`sessionEventHealthBySession` lived in `AppShellSessionUiState`, whose only
purpose is to force an AppShell render on change. Nothing renders it — the
sole reader is the polling effect, through `sessionEventHealthBySessionRef` —
yet every probe wrote a snapshot with a fresh `checkedAt`, and each SessionEvent
did too, so a stream forced one extra full render per event.
Move it to a controller-owned ref. `setSessionEventHealthBySession` keeps its
updater signature, so all four call sites are untouched; `clearSessionUiState`
drops the session from the ref alongside the rendered maps. The `MissingSessionUiMapKey`
guard keeps the remaining maps exhaustive.
Refs #1979
The gate sat after a one-off `evaluate()`, so settling into idle still wrote
one snapshot. That write cannot do anything: `expected` is false on this path,
so the status derives to `closed` and no refresh is ever requested, and neither
`status` nor `staleSince` has a renderer consumer (the sidebar's
`staleSessionIds` comes from `stale-sessions.ts`, an unrelated backend/slug
classifier). `markSessionEventStreamClosed` already records the closed stream
when the subscription goes away.
Moving the gate above `evaluate` makes an idle session cost nothing at all.
The test asserted that dead write, which would have failed a smaller correct
implementation; it now asserts zero writes.
Also cover the failure mode this gate introduces: a session that starts running
must re-arm. Neither single-status case caught it — shrinking the dep array to
`[activeId]` kept them green. The round-trip case fails on that mutation.
Refs #1979
… move
The turn-transient comment still listed event-stream health among the maps
`clearAppShellSessionUiStateForSession` clears; it now lives in a controller
ref and is cleared by `clearSessionUiState`. Point at the ref instead, and drop
the interface preamble that described a field no longer declared there — the
ref's own comment already carries that rationale.
Narrow `omitSessionKey` back to `Record<string, unknown>`; widening it to
`object` for the ref accepted arrays and non-record objects, which would return
silently unchanged.
@Astro-Han
Astro-Han marked this pull request as ready for review August 3, 2026 10:55
@Astro-Han
Astro-Han merged commit d671e06 into mainAug 3, 2026
11 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf(desktop): stop idle-session event-health polling from re-rendering the whole shell

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

perf(desktop): stop idle-session event-health polling from re-rendering the whole shell - #1988

Merged
Astro-Han merged 4 commits into
mainfrom
perf/desktop-idle-event-health-polling
Aug 3, 2026
Merged

perf(desktop): stop idle-session event-health polling from re-rendering the whole shell#1988
Astro-Han merged 4 commits into
mainfrom
perf/desktop-idle-event-health-polling

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

useSessionEventHealthPolling re-rendered the whole AppShell every 5s for the entire lifetime of any open session, idle or not — the only periodic work the app did while idle, and it changed nothing visible.

Four commits, one root cause:

  1. Gate the polling. Once a session no longer expects an event stream there is nothing to observe: deriveSessionEventStreamStatus short-circuits to closed and shouldRefreshStaleSessionEventStream never fires. The interval and the visibilitychange listener are now gated on sessionExpectsEventStream.
  2. Take health out of rendered state.sessionEventHealthBySession lived in AppShellSessionUiState, whose only purpose is to force a render. Nothing renders it — the sole reader is the polling effect via sessionEventHealthBySessionRef. It now lives in a controller-owned ref, so neither the probes nor the per-SessionEvent writes (app-shell-effects.ts:380, one extra full render per streamed event) notify React. setSessionEventHealthBySession keeps its updater signature, so all four call sites are untouched; clearSessionUiState drops the session from the ref alongside the rendered maps.
  3. Drop the probe the gate left behind. The gate initially sat after a one-off evaluate(), so settling into idle still wrote one snapshot. That write cannot do anything — expected is false on this path, and neither status nor staleSince has a renderer consumer (the sidebar's staleSessionIds comes from stale-sessions.ts, an unrelated backend/slug classifier). markSessionEventStreamClosed already records the closed stream when the subscription goes away, so an idle session now costs nothing at all.
  4. Comment and type cleanup left over from step 2.

Gating alone would have fixed the idle symptom, but only step 2 removes the per-event renders during a stream. Note that "skip unchanged writes" is not a valid alternative to either: the ref is only synced on write, so skipping writes would strand refreshRequestedAt and turn the 10s refresh cooldown into a refresh every tick.

Closes#1979

Verification

  • npm --workspace apps/desktop run test:dist — 1348 pass, 0 fail.
  • npm --workspace apps/desktop run typecheck — clean (preload, main, renderer, storybook).
  • npm run format / npm run lint — clean.
  • npm --workspace apps/desktop run e2e — 58 passed, including session-health-notice.spec.ts.

New coverage in app-shell-effect-stability-contract.test.ts, driven by a fake clock added to the existing fake DOM (observable window.setInterval / document.addEventListener, controllable Date.now):

  • an idle active session arms no interval, registers no visibilitychange listener, and writes nothing at all — before or after the clock advances;
  • a session going active → running → active re-arms and then disarms both the interval and the listener;
  • a running session goes stale past the threshold, refreshes sessions and messages exactly once, and re-probes when the window becomes visible again.

Each was verified by mutation, not just by passing: removing the gate, moving the gate back after evaluate, making health writes notify again, dropping the ref cleanup in clearSessionUiState, and no-oping the interval callback each turn the corresponding test red. The round-trip case specifically covers the one failure mode this gate introduces — a session that starts running but never re-arms; shrinking the dep array to [activeId] leaves every other case green and fails only that one.

Not run: no screenshot or Storybook evidence — this PR removes renders and changes no pixels.

Review focus

clearSessionUiState now mutates the health ref in addition to replaceState. That is the one place where the ref's lifecycle could drift from the rendered maps, and it is covered by drops event-stream health along with the rest of a cleared session.

Known pre-existing behavior, not addressed here

If a session sits idle past the 15s threshold and then starts running before a fresh SessionEvent or sessions:changed observation reaches the renderer, the first evaluate reads stale observation timestamps, derives stale, and fires one extra refresh. The inputs to that first evaluate are bit-identical before and after this PR — evaluateSessionEventStreamSnapshot never advances subscribedAt / lastEventAt / lastChangedAt, so the idle ticks this PR removes never affected it. The refresh is idempotent and self-healing. Out of scope; worth a separate issue if it ever shows up in practice.

Related

`useSessionEventHealthPolling` armed a 5s interval for any active session,
but once a session no longer expects an event stream the tick has nothing
to observe: `deriveSessionEventStreamStatus` short-circuits to `closed` and
`shouldRefreshStaleSessionEventStream` never fires. Every tick still wrote a
snapshot carrying a fresh `checkedAt`, which only ever cleared the session UI
state controller's reference check and forced a full AppShell render — the
only periodic work the app did while idle.
Gate the interval (and the visibilitychange listener) on
`sessionExpectsEventStream`. The one-off `evaluate` before the gate still
records the transition into `closed`, and both inputs to `expected` are
already effect deps, so a session that starts running re-arms on its own.
Refs #1979
…tate
`sessionEventHealthBySession` lived in `AppShellSessionUiState`, whose only
purpose is to force an AppShell render on change. Nothing renders it — the
sole reader is the polling effect, through `sessionEventHealthBySessionRef` —
yet every probe wrote a snapshot with a fresh `checkedAt`, and each SessionEvent
did too, so a stream forced one extra full render per event.
Move it to a controller-owned ref. `setSessionEventHealthBySession` keeps its
updater signature, so all four call sites are untouched; `clearSessionUiState`
drops the session from the ref alongside the rendered maps. The `MissingSessionUiMapKey`
guard keeps the remaining maps exhaustive.
Refs #1979
The gate sat after a one-off `evaluate()`, so settling into idle still wrote
one snapshot. That write cannot do anything: `expected` is false on this path,
so the status derives to `closed` and no refresh is ever requested, and neither
`status` nor `staleSince` has a renderer consumer (the sidebar's
`staleSessionIds` comes from `stale-sessions.ts`, an unrelated backend/slug
classifier). `markSessionEventStreamClosed` already records the closed stream
when the subscription goes away.
Moving the gate above `evaluate` makes an idle session cost nothing at all.
The test asserted that dead write, which would have failed a smaller correct
implementation; it now asserts zero writes.
Also cover the failure mode this gate introduces: a session that starts running
must re-arm. Neither single-status case caught it — shrinking the dep array to
`[activeId]` kept them green. The round-trip case fails on that mutation.
Refs #1979
… move
The turn-transient comment still listed event-stream health among the maps
`clearAppShellSessionUiStateForSession` clears; it now lives in a controller
ref and is cleared by `clearSessionUiState`. Point at the ref instead, and drop
the interface preamble that described a field no longer declared there — the
ref's own comment already carries that rationale.
Narrow `omitSessionKey` back to `Record<string, unknown>`; widening it to
`object` for the ref accepted arrays and non-record objects, which would return
silently unchanged.
@Astro-Han
Astro-Han marked this pull request as ready for review August 3, 2026 10:55
@Astro-Han
Astro-Han merged commit d671e06 into mainAug 3, 2026
11 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf(desktop): stop idle-session event-health polling from re-rendering the whole shell

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

perf(desktop): stop idle-session event-health polling from re-rendering the whole shell - #1988

Merged
Astro-Han merged 4 commits into
mainfrom
perf/desktop-idle-event-health-polling
Aug 3, 2026
Merged

perf(desktop): stop idle-session event-health polling from re-rendering the whole shell#1988
Astro-Han merged 4 commits into
mainfrom
perf/desktop-idle-event-health-polling

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

useSessionEventHealthPolling re-rendered the whole AppShell every 5s for the entire lifetime of any open session, idle or not — the only periodic work the app did while idle, and it changed nothing visible.

Four commits, one root cause:

  1. Gate the polling. Once a session no longer expects an event stream there is nothing to observe: deriveSessionEventStreamStatus short-circuits to closed and shouldRefreshStaleSessionEventStream never fires. The interval and the visibilitychange listener are now gated on sessionExpectsEventStream.
  2. Take health out of rendered state.sessionEventHealthBySession lived in AppShellSessionUiState, whose only purpose is to force a render. Nothing renders it — the sole reader is the polling effect via sessionEventHealthBySessionRef. It now lives in a controller-owned ref, so neither the probes nor the per-SessionEvent writes (app-shell-effects.ts:380, one extra full render per streamed event) notify React. setSessionEventHealthBySession keeps its updater signature, so all four call sites are untouched; clearSessionUiState drops the session from the ref alongside the rendered maps.
  3. Drop the probe the gate left behind. The gate initially sat after a one-off evaluate(), so settling into idle still wrote one snapshot. That write cannot do anything — expected is false on this path, and neither status nor staleSince has a renderer consumer (the sidebar's staleSessionIds comes from stale-sessions.ts, an unrelated backend/slug classifier). markSessionEventStreamClosed already records the closed stream when the subscription goes away, so an idle session now costs nothing at all.
  4. Comment and type cleanup left over from step 2.

Gating alone would have fixed the idle symptom, but only step 2 removes the per-event renders during a stream. Note that "skip unchanged writes" is not a valid alternative to either: the ref is only synced on write, so skipping writes would strand refreshRequestedAt and turn the 10s refresh cooldown into a refresh every tick.

Closes#1979

Verification

  • npm --workspace apps/desktop run test:dist — 1348 pass, 0 fail.
  • npm --workspace apps/desktop run typecheck — clean (preload, main, renderer, storybook).
  • npm run format / npm run lint — clean.
  • npm --workspace apps/desktop run e2e — 58 passed, including session-health-notice.spec.ts.

New coverage in app-shell-effect-stability-contract.test.ts, driven by a fake clock added to the existing fake DOM (observable window.setInterval / document.addEventListener, controllable Date.now):

  • an idle active session arms no interval, registers no visibilitychange listener, and writes nothing at all — before or after the clock advances;
  • a session going active → running → active re-arms and then disarms both the interval and the listener;
  • a running session goes stale past the threshold, refreshes sessions and messages exactly once, and re-probes when the window becomes visible again.

Each was verified by mutation, not just by passing: removing the gate, moving the gate back after evaluate, making health writes notify again, dropping the ref cleanup in clearSessionUiState, and no-oping the interval callback each turn the corresponding test red. The round-trip case specifically covers the one failure mode this gate introduces — a session that starts running but never re-arms; shrinking the dep array to [activeId] leaves every other case green and fails only that one.

Not run: no screenshot or Storybook evidence — this PR removes renders and changes no pixels.

Review focus

clearSessionUiState now mutates the health ref in addition to replaceState. That is the one place where the ref's lifecycle could drift from the rendered maps, and it is covered by drops event-stream health along with the rest of a cleared session.

Known pre-existing behavior, not addressed here

If a session sits idle past the 15s threshold and then starts running before a fresh SessionEvent or sessions:changed observation reaches the renderer, the first evaluate reads stale observation timestamps, derives stale, and fires one extra refresh. The inputs to that first evaluate are bit-identical before and after this PR — evaluateSessionEventStreamSnapshot never advances subscribedAt / lastEventAt / lastChangedAt, so the idle ticks this PR removes never affected it. The refresh is idempotent and self-healing. Out of scope; worth a separate issue if it ever shows up in practice.

Related

`useSessionEventHealthPolling` armed a 5s interval for any active session,
but once a session no longer expects an event stream the tick has nothing
to observe: `deriveSessionEventStreamStatus` short-circuits to `closed` and
`shouldRefreshStaleSessionEventStream` never fires. Every tick still wrote a
snapshot carrying a fresh `checkedAt`, which only ever cleared the session UI
state controller's reference check and forced a full AppShell render — the
only periodic work the app did while idle.
Gate the interval (and the visibilitychange listener) on
`sessionExpectsEventStream`. The one-off `evaluate` before the gate still
records the transition into `closed`, and both inputs to `expected` are
already effect deps, so a session that starts running re-arms on its own.
Refs #1979
…tate
`sessionEventHealthBySession` lived in `AppShellSessionUiState`, whose only
purpose is to force an AppShell render on change. Nothing renders it — the
sole reader is the polling effect, through `sessionEventHealthBySessionRef` —
yet every probe wrote a snapshot with a fresh `checkedAt`, and each SessionEvent
did too, so a stream forced one extra full render per event.
Move it to a controller-owned ref. `setSessionEventHealthBySession` keeps its
updater signature, so all four call sites are untouched; `clearSessionUiState`
drops the session from the ref alongside the rendered maps. The `MissingSessionUiMapKey`
guard keeps the remaining maps exhaustive.
Refs #1979
The gate sat after a one-off `evaluate()`, so settling into idle still wrote
one snapshot. That write cannot do anything: `expected` is false on this path,
so the status derives to `closed` and no refresh is ever requested, and neither
`status` nor `staleSince` has a renderer consumer (the sidebar's
`staleSessionIds` comes from `stale-sessions.ts`, an unrelated backend/slug
classifier). `markSessionEventStreamClosed` already records the closed stream
when the subscription goes away.
Moving the gate above `evaluate` makes an idle session cost nothing at all.
The test asserted that dead write, which would have failed a smaller correct
implementation; it now asserts zero writes.
Also cover the failure mode this gate introduces: a session that starts running
must re-arm. Neither single-status case caught it — shrinking the dep array to
`[activeId]` kept them green. The round-trip case fails on that mutation.
Refs #1979
… move
The turn-transient comment still listed event-stream health among the maps
`clearAppShellSessionUiStateForSession` clears; it now lives in a controller
ref and is cleared by `clearSessionUiState`. Point at the ref instead, and drop
the interface preamble that described a field no longer declared there — the
ref's own comment already carries that rationale.
Narrow `omitSessionKey` back to `Record<string, unknown>`; widening it to
`object` for the ref accepted arrays and non-record objects, which would return
silently unchanged.
@Astro-Han
Astro-Han marked this pull request as ready for review August 3, 2026 10:55
@Astro-Han
Astro-Han merged commit d671e06 into mainAug 3, 2026
11 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf(desktop): stop idle-session event-health polling from re-rendering the whole shell

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

perf(desktop): stop idle-session event-health polling from re-rendering the whole shell - #1988

Merged
Astro-Han merged 4 commits into
mainfrom
perf/desktop-idle-event-health-polling
Aug 3, 2026
Merged

perf(desktop): stop idle-session event-health polling from re-rendering the whole shell#1988
Astro-Han merged 4 commits into
mainfrom
perf/desktop-idle-event-health-polling

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

useSessionEventHealthPolling re-rendered the whole AppShell every 5s for the entire lifetime of any open session, idle or not — the only periodic work the app did while idle, and it changed nothing visible.

Four commits, one root cause:

  1. Gate the polling. Once a session no longer expects an event stream there is nothing to observe: deriveSessionEventStreamStatus short-circuits to closed and shouldRefreshStaleSessionEventStream never fires. The interval and the visibilitychange listener are now gated on sessionExpectsEventStream.
  2. Take health out of rendered state.sessionEventHealthBySession lived in AppShellSessionUiState, whose only purpose is to force a render. Nothing renders it — the sole reader is the polling effect via sessionEventHealthBySessionRef. It now lives in a controller-owned ref, so neither the probes nor the per-SessionEvent writes (app-shell-effects.ts:380, one extra full render per streamed event) notify React. setSessionEventHealthBySession keeps its updater signature, so all four call sites are untouched; clearSessionUiState drops the session from the ref alongside the rendered maps.
  3. Drop the probe the gate left behind. The gate initially sat after a one-off evaluate(), so settling into idle still wrote one snapshot. That write cannot do anything — expected is false on this path, and neither status nor staleSince has a renderer consumer (the sidebar's staleSessionIds comes from stale-sessions.ts, an unrelated backend/slug classifier). markSessionEventStreamClosed already records the closed stream when the subscription goes away, so an idle session now costs nothing at all.
  4. Comment and type cleanup left over from step 2.

Gating alone would have fixed the idle symptom, but only step 2 removes the per-event renders during a stream. Note that "skip unchanged writes" is not a valid alternative to either: the ref is only synced on write, so skipping writes would strand refreshRequestedAt and turn the 10s refresh cooldown into a refresh every tick.

Closes#1979

Verification

  • npm --workspace apps/desktop run test:dist — 1348 pass, 0 fail.
  • npm --workspace apps/desktop run typecheck — clean (preload, main, renderer, storybook).
  • npm run format / npm run lint — clean.
  • npm --workspace apps/desktop run e2e — 58 passed, including session-health-notice.spec.ts.

New coverage in app-shell-effect-stability-contract.test.ts, driven by a fake clock added to the existing fake DOM (observable window.setInterval / document.addEventListener, controllable Date.now):

  • an idle active session arms no interval, registers no visibilitychange listener, and writes nothing at all — before or after the clock advances;
  • a session going active → running → active re-arms and then disarms both the interval and the listener;
  • a running session goes stale past the threshold, refreshes sessions and messages exactly once, and re-probes when the window becomes visible again.

Each was verified by mutation, not just by passing: removing the gate, moving the gate back after evaluate, making health writes notify again, dropping the ref cleanup in clearSessionUiState, and no-oping the interval callback each turn the corresponding test red. The round-trip case specifically covers the one failure mode this gate introduces — a session that starts running but never re-arms; shrinking the dep array to [activeId] leaves every other case green and fails only that one.

Not run: no screenshot or Storybook evidence — this PR removes renders and changes no pixels.

Review focus

clearSessionUiState now mutates the health ref in addition to replaceState. That is the one place where the ref's lifecycle could drift from the rendered maps, and it is covered by drops event-stream health along with the rest of a cleared session.

Known pre-existing behavior, not addressed here

If a session sits idle past the 15s threshold and then starts running before a fresh SessionEvent or sessions:changed observation reaches the renderer, the first evaluate reads stale observation timestamps, derives stale, and fires one extra refresh. The inputs to that first evaluate are bit-identical before and after this PR — evaluateSessionEventStreamSnapshot never advances subscribedAt / lastEventAt / lastChangedAt, so the idle ticks this PR removes never affected it. The refresh is idempotent and self-healing. Out of scope; worth a separate issue if it ever shows up in practice.

Related

`useSessionEventHealthPolling` armed a 5s interval for any active session,
but once a session no longer expects an event stream the tick has nothing
to observe: `deriveSessionEventStreamStatus` short-circuits to `closed` and
`shouldRefreshStaleSessionEventStream` never fires. Every tick still wrote a
snapshot carrying a fresh `checkedAt`, which only ever cleared the session UI
state controller's reference check and forced a full AppShell render — the
only periodic work the app did while idle.
Gate the interval (and the visibilitychange listener) on
`sessionExpectsEventStream`. The one-off `evaluate` before the gate still
records the transition into `closed`, and both inputs to `expected` are
already effect deps, so a session that starts running re-arms on its own.
Refs #1979
…tate
`sessionEventHealthBySession` lived in `AppShellSessionUiState`, whose only
purpose is to force an AppShell render on change. Nothing renders it — the
sole reader is the polling effect, through `sessionEventHealthBySessionRef` —
yet every probe wrote a snapshot with a fresh `checkedAt`, and each SessionEvent
did too, so a stream forced one extra full render per event.
Move it to a controller-owned ref. `setSessionEventHealthBySession` keeps its
updater signature, so all four call sites are untouched; `clearSessionUiState`
drops the session from the ref alongside the rendered maps. The `MissingSessionUiMapKey`
guard keeps the remaining maps exhaustive.
Refs #1979
The gate sat after a one-off `evaluate()`, so settling into idle still wrote
one snapshot. That write cannot do anything: `expected` is false on this path,
so the status derives to `closed` and no refresh is ever requested, and neither
`status` nor `staleSince` has a renderer consumer (the sidebar's
`staleSessionIds` comes from `stale-sessions.ts`, an unrelated backend/slug
classifier). `markSessionEventStreamClosed` already records the closed stream
when the subscription goes away.
Moving the gate above `evaluate` makes an idle session cost nothing at all.
The test asserted that dead write, which would have failed a smaller correct
implementation; it now asserts zero writes.
Also cover the failure mode this gate introduces: a session that starts running
must re-arm. Neither single-status case caught it — shrinking the dep array to
`[activeId]` kept them green. The round-trip case fails on that mutation.
Refs #1979
… move
The turn-transient comment still listed event-stream health among the maps
`clearAppShellSessionUiStateForSession` clears; it now lives in a controller
ref and is cleared by `clearSessionUiState`. Point at the ref instead, and drop
the interface preamble that described a field no longer declared there — the
ref's own comment already carries that rationale.
Narrow `omitSessionKey` back to `Record<string, unknown>`; widening it to
`object` for the ref accepted arrays and non-record objects, which would return
silently unchanged.
@Astro-Han
Astro-Han marked this pull request as ready for review August 3, 2026 10:55
@Astro-Han
Astro-Han merged commit d671e06 into mainAug 3, 2026
11 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf(desktop): stop idle-session event-health polling from re-rendering the whole shell

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

perf(desktop): stop idle-session event-health polling from re-rendering the whole shell - #1988

Merged
Astro-Han merged 4 commits into
mainfrom
perf/desktop-idle-event-health-polling
Aug 3, 2026
Merged

perf(desktop): stop idle-session event-health polling from re-rendering the whole shell#1988
Astro-Han merged 4 commits into
mainfrom
perf/desktop-idle-event-health-polling

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

useSessionEventHealthPolling re-rendered the whole AppShell every 5s for the entire lifetime of any open session, idle or not — the only periodic work the app did while idle, and it changed nothing visible.

Four commits, one root cause:

  1. Gate the polling. Once a session no longer expects an event stream there is nothing to observe: deriveSessionEventStreamStatus short-circuits to closed and shouldRefreshStaleSessionEventStream never fires. The interval and the visibilitychange listener are now gated on sessionExpectsEventStream.
  2. Take health out of rendered state.sessionEventHealthBySession lived in AppShellSessionUiState, whose only purpose is to force a render. Nothing renders it — the sole reader is the polling effect via sessionEventHealthBySessionRef. It now lives in a controller-owned ref, so neither the probes nor the per-SessionEvent writes (app-shell-effects.ts:380, one extra full render per streamed event) notify React. setSessionEventHealthBySession keeps its updater signature, so all four call sites are untouched; clearSessionUiState drops the session from the ref alongside the rendered maps.
  3. Drop the probe the gate left behind. The gate initially sat after a one-off evaluate(), so settling into idle still wrote one snapshot. That write cannot do anything — expected is false on this path, and neither status nor staleSince has a renderer consumer (the sidebar's staleSessionIds comes from stale-sessions.ts, an unrelated backend/slug classifier). markSessionEventStreamClosed already records the closed stream when the subscription goes away, so an idle session now costs nothing at all.
  4. Comment and type cleanup left over from step 2.

Gating alone would have fixed the idle symptom, but only step 2 removes the per-event renders during a stream. Note that "skip unchanged writes" is not a valid alternative to either: the ref is only synced on write, so skipping writes would strand refreshRequestedAt and turn the 10s refresh cooldown into a refresh every tick.

Closes#1979

Verification

  • npm --workspace apps/desktop run test:dist — 1348 pass, 0 fail.
  • npm --workspace apps/desktop run typecheck — clean (preload, main, renderer, storybook).
  • npm run format / npm run lint — clean.
  • npm --workspace apps/desktop run e2e — 58 passed, including session-health-notice.spec.ts.

New coverage in app-shell-effect-stability-contract.test.ts, driven by a fake clock added to the existing fake DOM (observable window.setInterval / document.addEventListener, controllable Date.now):

  • an idle active session arms no interval, registers no visibilitychange listener, and writes nothing at all — before or after the clock advances;
  • a session going active → running → active re-arms and then disarms both the interval and the listener;
  • a running session goes stale past the threshold, refreshes sessions and messages exactly once, and re-probes when the window becomes visible again.

Each was verified by mutation, not just by passing: removing the gate, moving the gate back after evaluate, making health writes notify again, dropping the ref cleanup in clearSessionUiState, and no-oping the interval callback each turn the corresponding test red. The round-trip case specifically covers the one failure mode this gate introduces — a session that starts running but never re-arms; shrinking the dep array to [activeId] leaves every other case green and fails only that one.

Not run: no screenshot or Storybook evidence — this PR removes renders and changes no pixels.

Review focus

clearSessionUiState now mutates the health ref in addition to replaceState. That is the one place where the ref's lifecycle could drift from the rendered maps, and it is covered by drops event-stream health along with the rest of a cleared session.

Known pre-existing behavior, not addressed here

If a session sits idle past the 15s threshold and then starts running before a fresh SessionEvent or sessions:changed observation reaches the renderer, the first evaluate reads stale observation timestamps, derives stale, and fires one extra refresh. The inputs to that first evaluate are bit-identical before and after this PR — evaluateSessionEventStreamSnapshot never advances subscribedAt / lastEventAt / lastChangedAt, so the idle ticks this PR removes never affected it. The refresh is idempotent and self-healing. Out of scope; worth a separate issue if it ever shows up in practice.

Related

`useSessionEventHealthPolling` armed a 5s interval for any active session,
but once a session no longer expects an event stream the tick has nothing
to observe: `deriveSessionEventStreamStatus` short-circuits to `closed` and
`shouldRefreshStaleSessionEventStream` never fires. Every tick still wrote a
snapshot carrying a fresh `checkedAt`, which only ever cleared the session UI
state controller's reference check and forced a full AppShell render — the
only periodic work the app did while idle.
Gate the interval (and the visibilitychange listener) on
`sessionExpectsEventStream`. The one-off `evaluate` before the gate still
records the transition into `closed`, and both inputs to `expected` are
already effect deps, so a session that starts running re-arms on its own.
Refs #1979
…tate
`sessionEventHealthBySession` lived in `AppShellSessionUiState`, whose only
purpose is to force an AppShell render on change. Nothing renders it — the
sole reader is the polling effect, through `sessionEventHealthBySessionRef` —
yet every probe wrote a snapshot with a fresh `checkedAt`, and each SessionEvent
did too, so a stream forced one extra full render per event.
Move it to a controller-owned ref. `setSessionEventHealthBySession` keeps its
updater signature, so all four call sites are untouched; `clearSessionUiState`
drops the session from the ref alongside the rendered maps. The `MissingSessionUiMapKey`
guard keeps the remaining maps exhaustive.
Refs #1979
The gate sat after a one-off `evaluate()`, so settling into idle still wrote
one snapshot. That write cannot do anything: `expected` is false on this path,
so the status derives to `closed` and no refresh is ever requested, and neither
`status` nor `staleSince` has a renderer consumer (the sidebar's
`staleSessionIds` comes from `stale-sessions.ts`, an unrelated backend/slug
classifier). `markSessionEventStreamClosed` already records the closed stream
when the subscription goes away.
Moving the gate above `evaluate` makes an idle session cost nothing at all.
The test asserted that dead write, which would have failed a smaller correct
implementation; it now asserts zero writes.
Also cover the failure mode this gate introduces: a session that starts running
must re-arm. Neither single-status case caught it — shrinking the dep array to
`[activeId]` kept them green. The round-trip case fails on that mutation.
Refs #1979
… move
The turn-transient comment still listed event-stream health among the maps
`clearAppShellSessionUiStateForSession` clears; it now lives in a controller
ref and is cleared by `clearSessionUiState`. Point at the ref instead, and drop
the interface preamble that described a field no longer declared there — the
ref's own comment already carries that rationale.
Narrow `omitSessionKey` back to `Record<string, unknown>`; widening it to
`object` for the ref accepted arrays and non-record objects, which would return
silently unchanged.
@Astro-Han
Astro-Han marked this pull request as ready for review August 3, 2026 10:55
@Astro-Han
Astro-Han merged commit d671e06 into mainAug 3, 2026
11 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf(desktop): stop idle-session event-health polling from re-rendering the whole shell

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

perf(desktop): stop idle-session event-health polling from re-rendering the whole shell - #1988

Merged
Astro-Han merged 4 commits into
mainfrom
perf/desktop-idle-event-health-polling
Aug 3, 2026
Merged

perf(desktop): stop idle-session event-health polling from re-rendering the whole shell#1988
Astro-Han merged 4 commits into
mainfrom
perf/desktop-idle-event-health-polling

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

useSessionEventHealthPolling re-rendered the whole AppShell every 5s for the entire lifetime of any open session, idle or not — the only periodic work the app did while idle, and it changed nothing visible.

Four commits, one root cause:

  1. Gate the polling. Once a session no longer expects an event stream there is nothing to observe: deriveSessionEventStreamStatus short-circuits to closed and shouldRefreshStaleSessionEventStream never fires. The interval and the visibilitychange listener are now gated on sessionExpectsEventStream.
  2. Take health out of rendered state.sessionEventHealthBySession lived in AppShellSessionUiState, whose only purpose is to force a render. Nothing renders it — the sole reader is the polling effect via sessionEventHealthBySessionRef. It now lives in a controller-owned ref, so neither the probes nor the per-SessionEvent writes (app-shell-effects.ts:380, one extra full render per streamed event) notify React. setSessionEventHealthBySession keeps its updater signature, so all four call sites are untouched; clearSessionUiState drops the session from the ref alongside the rendered maps.
  3. Drop the probe the gate left behind. The gate initially sat after a one-off evaluate(), so settling into idle still wrote one snapshot. That write cannot do anything — expected is false on this path, and neither status nor staleSince has a renderer consumer (the sidebar's staleSessionIds comes from stale-sessions.ts, an unrelated backend/slug classifier). markSessionEventStreamClosed already records the closed stream when the subscription goes away, so an idle session now costs nothing at all.
  4. Comment and type cleanup left over from step 2.

Gating alone would have fixed the idle symptom, but only step 2 removes the per-event renders during a stream. Note that "skip unchanged writes" is not a valid alternative to either: the ref is only synced on write, so skipping writes would strand refreshRequestedAt and turn the 10s refresh cooldown into a refresh every tick.

Closes#1979

Verification

  • npm --workspace apps/desktop run test:dist — 1348 pass, 0 fail.
  • npm --workspace apps/desktop run typecheck — clean (preload, main, renderer, storybook).
  • npm run format / npm run lint — clean.
  • npm --workspace apps/desktop run e2e — 58 passed, including session-health-notice.spec.ts.

New coverage in app-shell-effect-stability-contract.test.ts, driven by a fake clock added to the existing fake DOM (observable window.setInterval / document.addEventListener, controllable Date.now):

  • an idle active session arms no interval, registers no visibilitychange listener, and writes nothing at all — before or after the clock advances;
  • a session going active → running → active re-arms and then disarms both the interval and the listener;
  • a running session goes stale past the threshold, refreshes sessions and messages exactly once, and re-probes when the window becomes visible again.

Each was verified by mutation, not just by passing: removing the gate, moving the gate back after evaluate, making health writes notify again, dropping the ref cleanup in clearSessionUiState, and no-oping the interval callback each turn the corresponding test red. The round-trip case specifically covers the one failure mode this gate introduces — a session that starts running but never re-arms; shrinking the dep array to [activeId] leaves every other case green and fails only that one.

Not run: no screenshot or Storybook evidence — this PR removes renders and changes no pixels.

Review focus

clearSessionUiState now mutates the health ref in addition to replaceState. That is the one place where the ref's lifecycle could drift from the rendered maps, and it is covered by drops event-stream health along with the rest of a cleared session.

Known pre-existing behavior, not addressed here

If a session sits idle past the 15s threshold and then starts running before a fresh SessionEvent or sessions:changed observation reaches the renderer, the first evaluate reads stale observation timestamps, derives stale, and fires one extra refresh. The inputs to that first evaluate are bit-identical before and after this PR — evaluateSessionEventStreamSnapshot never advances subscribedAt / lastEventAt / lastChangedAt, so the idle ticks this PR removes never affected it. The refresh is idempotent and self-healing. Out of scope; worth a separate issue if it ever shows up in practice.

Related

`useSessionEventHealthPolling` armed a 5s interval for any active session,
but once a session no longer expects an event stream the tick has nothing
to observe: `deriveSessionEventStreamStatus` short-circuits to `closed` and
`shouldRefreshStaleSessionEventStream` never fires. Every tick still wrote a
snapshot carrying a fresh `checkedAt`, which only ever cleared the session UI
state controller's reference check and forced a full AppShell render — the
only periodic work the app did while idle.
Gate the interval (and the visibilitychange listener) on
`sessionExpectsEventStream`. The one-off `evaluate` before the gate still
records the transition into `closed`, and both inputs to `expected` are
already effect deps, so a session that starts running re-arms on its own.
Refs #1979
…tate
`sessionEventHealthBySession` lived in `AppShellSessionUiState`, whose only
purpose is to force an AppShell render on change. Nothing renders it — the
sole reader is the polling effect, through `sessionEventHealthBySessionRef` —
yet every probe wrote a snapshot with a fresh `checkedAt`, and each SessionEvent
did too, so a stream forced one extra full render per event.
Move it to a controller-owned ref. `setSessionEventHealthBySession` keeps its
updater signature, so all four call sites are untouched; `clearSessionUiState`
drops the session from the ref alongside the rendered maps. The `MissingSessionUiMapKey`
guard keeps the remaining maps exhaustive.
Refs #1979
The gate sat after a one-off `evaluate()`, so settling into idle still wrote
one snapshot. That write cannot do anything: `expected` is false on this path,
so the status derives to `closed` and no refresh is ever requested, and neither
`status` nor `staleSince` has a renderer consumer (the sidebar's
`staleSessionIds` comes from `stale-sessions.ts`, an unrelated backend/slug
classifier). `markSessionEventStreamClosed` already records the closed stream
when the subscription goes away.
Moving the gate above `evaluate` makes an idle session cost nothing at all.
The test asserted that dead write, which would have failed a smaller correct
implementation; it now asserts zero writes.
Also cover the failure mode this gate introduces: a session that starts running
must re-arm. Neither single-status case caught it — shrinking the dep array to
`[activeId]` kept them green. The round-trip case fails on that mutation.
Refs #1979
… move
The turn-transient comment still listed event-stream health among the maps
`clearAppShellSessionUiStateForSession` clears; it now lives in a controller
ref and is cleared by `clearSessionUiState`. Point at the ref instead, and drop
the interface preamble that described a field no longer declared there — the
ref's own comment already carries that rationale.
Narrow `omitSessionKey` back to `Record<string, unknown>`; widening it to
`object` for the ref accepted arrays and non-record objects, which would return
silently unchanged.
@Astro-Han
Astro-Han marked this pull request as ready for review August 3, 2026 10:55
@Astro-Han
Astro-Han merged commit d671e06 into mainAug 3, 2026
11 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf(desktop): stop idle-session event-health polling from re-rendering the whole shell

1 participant

@Astro-Han