Skip to content

perf(desktop): stop the statusbar re-rendering per streaming token - #72163

Merged
OutThisLife merged 1 commit into
mainfrom
bb/desktop-render-waste
Jul 26, 2026
Merged

perf(desktop): stop the statusbar re-rendering per streaming token#72163
OutThisLife merged 1 commit into
mainfrom
bb/desktop-render-waste

Conversation

@OutThisLife

Copy link
Copy Markdown
Collaborator

First fix found with the render-churn counters from #71925. Cuts total renders during multi-tab streaming by 72%.

The bug

The statusbar subscribed to $focusedSessionState — a projection of $sessionStates, which is republished on every message delta — but reads only three fields off it (busy, usage, turnStartedAt).

@nanostores/react's useStore bails out on reference equality, so a republished state object re-ran useStatusbarItems per token, rebuilding all ~9 statusbar item objects. StatusbarItemView wasn't memoized, so one genuinely-changed item (the running timer, ticking every second) re-rendered the entire bar. Each of those items carries a tooltip, which is why TooltipContent was the single worst component in the whole app at 2,048 wasted renders of 2,304.

The statusbar looked innocent — StatusbarSurface is already memo()'d with a comment explaining it's isolated so "any statusbar-only churn re-renders the bar alone." That isolation was real and still didn't help, because the churn was inside the boundary.

The fix

Adds useStoreSelector beside the existing useSessionSlice in lib/use-session-slice.ts — the same narrowing idea that file already documents, for a scalar instead of a keyed array. Subscribe to the store, but bail out unless the selected value actually changes.

Applied to the three fields the statusbar reads, plus memo() on StatusbarItemView. No behavior change; 30 shell tests pass.

Measured

Five concurrent streaming tabs, node scripts/perf/run.mjs render-churn --spawn --tiles 5 --tokens 240:

metric before after
total renders 78,385 21,864 (−72%)
wasted renders 10,432 6,121 (−41%)
TooltipContent 2,304 / 2,048 wasted 143 / 127 (−94%)
StatusbarItemView 2,174 / 1,446 wasted, 594ms 0

sidebar_renders stays at 6 with 0 wasted — the sidebar was already clean and this doesn't disturb it.

Note on the selector contract

select must return a primitive or a referentially stable value; returning a fresh object or array defeats the bail-out and reintroduces the churn. That's documented on the function. usage is passed through as an object deliberately — it's replaced wholesale rather than mutated and only changes when the backend reports new usage, so its reference is a valid bail-out key on its own.

Remaining waste (Block/Ct at 880/880 each, 542ms combined) is in the transcript renderer under minified component names — separate PR.

The statusbar subscribed to `$focusedSessionState` — a projection of
`$sessionStates`, which is republished on every message delta — but reads
only three fields off it. Every token therefore re-ran useStatusbarItems
and rebuilt all ~9 item objects, and since StatusbarItemView was not
memoized, one changed item (the running timer) re-rendered the whole bar.

Adds `useStoreSelector` beside the existing `useSessionSlice`, the same
narrowing idea for a scalar instead of a keyed array: subscribe to the
store, but bail out unless the selected value changes. Applies it to the
three fields the statusbar actually reads, and memoizes StatusbarItemView.

Measured over five concurrent streaming tabs (`render-churn`):

  total renders   78,385 -> 21,701   (-72%)
  wasted renders  10,432 ->  6,112   (-41%)
  TooltipContent   2,304 ->    143   (-94%)
  StatusbarItemView 2,174 ->      0
@github-actions

github-actions Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on 41dd895

ℹ️ Info

Desktop E2E visual evidence · View test artifacts · View job

1 visual diff.

inline evidence upload failed.

Failed to upload diff-665a0833239e-onboarding-overlay-diff.png with gh image (exit code 1): Error uploading /home/runner/work/_temp/e2e-evidence/diff-665a0833239e-onboarding-overlay-diff.png: step 0 (get upload token): uploadToken not found on repo page — do you have write access to NousResearch/hermes-agent? (or, if NousResearch enforces SAML SSO, authorize at https://github.com/orgs/NousResearch/sso)

@alt-glitch alt-glitch added type/perf Performance improvement or optimization comp/desktop Electron desktop app (apps/desktop/*) P3 Low — cosmetic, nice to have labels Jul 26, 2026
@OutThisLife
OutThisLife merged commit 8943c99 into main Jul 26, 2026
37 checks passed
@OutThisLife
OutThisLife deleted the bb/desktop-render-waste branch July 26, 2026 20:18
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
…nder-waste

perf(desktop): stop the statusbar re-rendering per streaming token
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
…ng token

ModelPickerOverlay subscribed to $focusedSessionState whole — a
projection of $sessionStates, republished on every message delta — to
read two fields that essentially never change (model, provider). The
overlay is mounted app-wide and unconditionally renders the un-memoized
ModelPickerDialog (closed), so the focused session's stream re-ran the
dialog's full hook body ~30x/s.

Same defect class and same fix as the statusbar (NousResearch#72163): select each
scalar through useStoreSelector so unchanged values bail out.
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
NousResearch#72163 narrowed $focusedSessionState but left two whole-store reads in
the same hook paying the same price:

- $subagentsBySession: only two COUNTS are rendered, but the whole-map
  subscription re-ran the hook (rebuilding all ~9 statusbar items) on
  every subagent progress tick in any session. Select the two scalars.
- $sessions: only one row's started_at is read, but any session-list
  write (title update, poll refresh, archive) re-ran the hook. Select
  the one scalar.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/desktop Electron desktop app (apps/desktop/*) P3 Low — cosmetic, nice to have type/perf Performance improvement or optimization

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants