Skip to content

feat(desktop): state diagnostics — render + store churn counters - #71925

Merged
OutThisLife merged 2 commits into
mainfrom
bb/desktop-state-diagnostics
Jul 26, 2026
Merged

feat(desktop): state diagnostics — render + store churn counters#71925
OutThisLife merged 2 commits into
mainfrom
bb/desktop-state-diagnostics

Conversation

@OutThisLife

Copy link
Copy Markdown
Collaborator

Instrumentation for diagnosing renderer state churn, built ahead of the streaming/multi-tab performance work. It turns "I think the sidebar re-renders while an agent is typing" into a number.

Two dev-only counters, plus a perf scenario that drives them:

window.__RENDER_COUNTS__   what re-rendered, attributed to props / state / parent
window.__ATOM_CHURN__      which store published it, and whether it mattered

The wasted column in each is the fix list — components that re-rendered with neither changed props nor changed hook state, and stores that published a value deep-equal to the last one. @nanostores/react bails out on reference equality only, so the latter re-renders every subscriber for nothing; that's the "preserve reference identity on no-ops" rule in apps/desktop/AGENTS.md, now measurable.

Both are inert until start(), so the idle cost is one no-op branch per commit and per notify.

First result

Against 5 concurrent streaming tabs (render-churn --spawn --tiles 5 --tokens 240), reproducible across runs:

sidebar_renders          6
sidebar_wasted           0
wasted_renders      10,432
total_renders       78,385
commits              1,566
wasted_notifies          0

The sidebar hypothesis is refuted — 6 renders across the whole run, all on genuine busy/needsInput edges, none wasted. The stableArray guards on $workingSessionIds / $attentionSessionIds (store/session-states.ts:236-259) are holding. $sessionStates notified 1,200 times across 10 listeners with zero wasted notifies, so the publishing side is honest too.

The real cost is in components re-rendering on a parent commit without their own inputs changing: TooltipContent 2,048 wasted of 2,304, StatusbarItemView 1,446 of 2,174, Block and Ct 880 of 880 each. That's the next PR, not this one.

Why bippy

React 19.2 removed injectProfilingHooks from react-dom (grep -c → 0), so the entire mark* profiling family is dead on this stack and onCommitFiberRoot is the only channel left.

<Profiler> cannot answer the question: React invokes onRender for every Profiler in a committed tree including subtrees that bailed out, and a bailed-out subtree still reports nonzero actualDuration, so there's no safe threshold. Counting those callbacks would have "proven" the sidebar hypothesis true. This uses didFiberRender instead.

react-scan ships the right idea in its react-scan/lite subpath, but lite's only imports are bippy and bippy/source, while the package pulls ~217 transitive deps and floats react-grab/react-doctor on latest (non-reproducible installs; its main entry currently breaks Vite — upstream #448, #467). Taking bippy directly: MIT, zero dependencies.

Import order

main.tsx imports the entry statically, above react-dom. react-dom captures the devtools hook at module init, not at createRoot:

order _renderers commits
bippy first 1 1
react-dom first 0 0

A dynamic import behind import.meta.env.DEV resolves a microtask too late. Production exclusion is therefore a build-time alias to a no-op module rather than tree-shaking, since a static side-effect import can't be eliminated. Verified against the built bundle: bippy, __RENDER_COUNTS__, __ATOM_CHURN__, traverseRenderedFibers, didFiberRender all → 0 occurrences.

Adds two dev-only counters that attribute re-renders and store
notifications during an interaction, so a perf claim can be answered with
a number instead of a hunch:

  window.__RENDER_COUNTS__  what re-rendered, and why (props/state/parent)
  window.__ATOM_CHURN__     which store published it, and whether it mattered

The `wasted` column in each is the fix list — components that re-rendered
with no changed input, and stores that published a value equal to the
last one. Both are inert until start(), so idle cost is one branch per
commit and per notify.

React 19.2 removed injectProfilingHooks from react-dom, so the mark*
profiling family is unavailable and onCommitFiberRoot is the only channel
left. <Profiler> can't answer the question either: React invokes onRender
for every Profiler in a committed tree including subtrees that bailed
out, and a bailed-out subtree still reports nonzero actualDuration. This
uses bippy's didFiberRender instead. bippy over react-scan because
react-scan/lite is a thin wrapper over it while the package pulls ~217
transitive deps and floats two on latest.

main.tsx imports the entry statically above react-dom because react-dom
captures the devtools hook at module init — a late install reports
renderers=0 and observes zero commits. Production exclusion is handled by
a build-time alias to a no-op module rather than tree-shaking, since a
static side-effect import can't be eliminated.
Drives the same synthetic multi-tab streaming workload as `multitab`
(publishSessionState per session per flush, no backend, no credits) but
reports render attribution instead of frame pacing — sidebar_renders,
wasted_renders, and wasted_notifies.

Answers 'does the sidebar re-render while an agent is typing' directly.
@OutThisLife
OutThisLife requested a review from a team July 26, 2026 11:11
@github-actions

github-actions Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on 37ac8ae

ℹ️ 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 enabled auto-merge July 26, 2026 19:11
@OutThisLife
OutThisLife merged commit 41f2196 into main Jul 26, 2026
35 checks passed
@OutThisLife
OutThisLife deleted the bb/desktop-state-diagnostics branch July 26, 2026 19:12
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
…ate-diagnostics

feat(desktop): state diagnostics — render + store churn counters
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.

3 participants