mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
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.
132 lines
4.1 KiB
TypeScript
132 lines
4.1 KiB
TypeScript
// Dev-only nanostores churn counter — the state-side companion to
|
|
// `render-counter.ts`. Render counts tell you WHAT re-rendered; this tells you
|
|
// WHICH ATOM pushed the update, and whether the push was worth making.
|
|
//
|
|
// The headline metric is `wasted`: notifications whose new value is deep-equal
|
|
// to the old one. `@nanostores/react`'s `useStore` bails out on REFERENCE
|
|
// equality only (`snapshotRef.current === value`), so publishing a fresh array
|
|
// or object with identical contents re-renders every subscriber for nothing.
|
|
// That is the exact failure `apps/desktop/AGENTS.md` names: "Preserve reference
|
|
// identity on no-ops."
|
|
//
|
|
// `listeners` is read from the store's own `lc` (listener count) at notify
|
|
// time, because `onNotify` fires even when a store has zero subscribers — a
|
|
// raw notify count over-reports. `notifies x listeners` is the real fan-out.
|
|
|
|
import { onNotify, type Store } from 'nanostores'
|
|
|
|
export interface AtomChurn {
|
|
/** Times the store notified its listeners. */
|
|
notifies: number
|
|
/**
|
|
* ...of those, how many pushed a value deep-equal to the previous one.
|
|
* Pure waste: every subscriber re-rendered and nothing actually changed.
|
|
*/
|
|
wasted: number
|
|
/** Peak listener count seen at notify time (`store.lc`). */
|
|
peakListeners: number
|
|
/** Sum of listeners across notifications — the true re-render fan-out. */
|
|
fanout: number
|
|
}
|
|
|
|
const churn = new Map<string, AtomChurn>()
|
|
const unsubscribes: Array<() => void> = []
|
|
let recording = false
|
|
|
|
const blank = (): AtomChurn => ({ fanout: 0, notifies: 0, peakListeners: 0, wasted: 0 })
|
|
|
|
/** Structural equality, depth-capped so a long transcript array doesn't make
|
|
* the instrumentation itself the bottleneck. Beyond the cap we compare by
|
|
* reference, which under-reports waste rather than inventing it. */
|
|
function equal(a: unknown, b: unknown, depth = 0): boolean {
|
|
if (Object.is(a, b)) {
|
|
return true
|
|
}
|
|
|
|
if (depth > 3 || typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) {
|
|
return false
|
|
}
|
|
|
|
if (Array.isArray(a) !== Array.isArray(b)) {
|
|
return false
|
|
}
|
|
|
|
const ka = Object.keys(a as object)
|
|
const kb = Object.keys(b as object)
|
|
|
|
if (ka.length !== kb.length) {
|
|
return false
|
|
}
|
|
|
|
return ka.every(k => equal((a as Record<string, unknown>)[k], (b as Record<string, unknown>)[k], depth + 1))
|
|
}
|
|
|
|
/** Watch one store. Call at module scope for each atom you want attributed. */
|
|
export function watchAtom(name: string, store: Store) {
|
|
const off = onNotify(store, ({ oldValue }) => {
|
|
if (!recording) {
|
|
return
|
|
}
|
|
|
|
const entry = churn.get(name) ?? blank()
|
|
// `lc` is nanostores' listener count — see node_modules/nanostores/atom/index.js.
|
|
const listeners = (store as unknown as { lc?: number }).lc ?? 0
|
|
const next = (store as unknown as { value?: unknown }).value
|
|
|
|
entry.notifies += 1
|
|
entry.fanout += listeners
|
|
entry.peakListeners = Math.max(entry.peakListeners, listeners)
|
|
|
|
if (equal(oldValue, next)) {
|
|
entry.wasted += 1
|
|
}
|
|
|
|
churn.set(name, entry)
|
|
})
|
|
|
|
unsubscribes.push(off)
|
|
|
|
return off
|
|
}
|
|
|
|
/** Rows sorted by wasted notifications, then fan-out — the fix list, in order. */
|
|
function report(limit = 40) {
|
|
return [...churn.entries()]
|
|
.map(([name, c]) => ({ name, ...c }))
|
|
.sort((a, b) => b.wasted - a.wasted || b.fanout - a.fanout)
|
|
.slice(0, limit)
|
|
}
|
|
|
|
declare global {
|
|
interface Window {
|
|
__ATOM_CHURN__?: {
|
|
churn: Map<string, AtomChurn>
|
|
clear: () => void
|
|
start: () => void
|
|
stop: () => void
|
|
recording: () => boolean
|
|
report: (limit?: number) => Array<AtomChurn & { name: string }>
|
|
get: (name: string) => AtomChurn | undefined
|
|
/** Names of every watched store, whether or not it has notified. */
|
|
watched: () => string[]
|
|
}
|
|
}
|
|
}
|
|
|
|
if (typeof window !== 'undefined' && !window.__ATOM_CHURN__) {
|
|
window.__ATOM_CHURN__ = {
|
|
churn,
|
|
clear: () => churn.clear(),
|
|
get: name => churn.get(name),
|
|
recording: () => recording,
|
|
report,
|
|
start: () => {
|
|
churn.clear()
|
|
recording = true
|
|
},
|
|
stop: () => {
|
|
recording = false
|
|
},
|
|
watched: () => [...churn.keys()]
|
|
}
|
|
}
|