Merge pull request #72212 from NousResearch/bb/desktop-transcript-renders

fix(desktop): make render-churn measure streaming, not boot churn
This commit is contained in:
brooklyn! 2026-07-26 16:00:17 -05:00 committed by GitHub
commit f695ce3461
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 80 additions and 7 deletions

View file

@ -107,6 +107,43 @@ const drive = (chunk, intervalMs, totalTokens) => `
})()
`
/** Wait until the renderer stops committing on its own, so the recording window
* captures STREAMING cost and not whatever boot/hydration work happened to
* still be in flight. Returns `quiet:N` once commits hold still for `quietMs`.
*
* If it returns `timeout:...` the app never went idle at all with tiles
* marked busy and NO driver running, that means something is ticking on its
* own. The report of what rendered during the wait is attached so the culprit
* is named rather than guessed at. */
const quiesce = (quietMs, timeoutMs) => `
(async () => {
const rc = window.__RENDER_COUNTS__
rc.start()
const deadline = Date.now() + ${timeoutMs}
const startedAt = Date.now()
let last = -1
let stableSince = Date.now()
while (Date.now() < deadline) {
await new Promise(r => setTimeout(r, 100))
const n = rc.commits()
if (n !== last) { last = n; stableSince = Date.now(); continue }
if (Date.now() - stableSince >= ${quietMs}) { rc.stop(); return 'quiet:' + n }
}
const idle = {
commits: last,
seconds: (Date.now() - startedAt) / 1000,
top: rc.report(8),
// Who OWNS the update? The component whose own hook state changed with
// no changed props is the root of a churn cascade; everything under it
// is collateral. Naming it is the difference between fixing the cause
// and memoizing a symptom.
owners: rc.report(200).filter(r => r.stateChanged > 0 && r.propsChanged === 0).slice(0, 8)
}
rc.stop()
return 'timeout:' + JSON.stringify(idle)
})()
`
const START = `
(() => {
window.__RENDER_COUNTS__.start()
@ -173,9 +210,11 @@ export default {
await sleep(350)
}
await sleep(1000)
// Start recording AFTER mount so the numbers are steady-state streaming
// cost, not one-off mount cost.
// Let the app go quiet before recording, so boot/hydration commits that
// happen to still be in flight don't land in the streaming window. This is
// what makes runs comparable — a fixed sleep let 2-4x of hydration churn
// leak in depending on machine load.
const settle = await cdp.eval(quiesce(600, 15000))
await cdp.eval(START)
await cdp.eval(drive(chunk, intervalMs, tokens))
await sleep(tokens * intervalMs + 1500)
@ -208,6 +247,9 @@ export default {
detail: {
tiles,
tokens,
// 'quiet:N' = the app went idle before recording (comparable run).
// 'timeout:N' = it never did, so boot churn is mixed into the numbers.
settle,
sidebar: sidebarRows,
topRenders: data.renders.slice(0, 15),
topAtoms: data.atoms.slice(0, 15)

View file

@ -27,9 +27,15 @@ export interface RenderRecord {
/** ...of those, how many had changed hook state (useState/useMemo/store). */
stateChanged: number
/**
* ...of those, how many had NEITHER changed props nor changed state. These
* re-rendered purely because a parent did the wasted work a `memo()` or a
* narrower store subscription would eliminate.
* ...of those, how many consumed a context whose value changed. `memo()`
* cannot block these the fix is to narrow or split the provider, not to
* add a memo boundary.
*/
contextChanged: number
/**
* ...of those, how many had NEITHER changed props, changed state, nor a
* changed context. These re-rendered purely because a parent did the
* wasted work a `memo()` or a narrower store subscription would eliminate.
*/
wasted: number
/** Sum of `actualDuration` across counted renders, in ms. */
@ -41,6 +47,7 @@ let commits = 0
let recording = false
const blank = (): RenderRecord => ({
contextChanged: 0,
propsChanged: 0,
renders: 0,
stateChanged: 0,
@ -84,6 +91,25 @@ function stateChanged(fiber: Fiber): boolean {
return false
}
/** Did any consumed context value change? A `memo()` cannot block a re-render
* caused by context, so distinguishing this from a parent-driven render is
* the difference between "add a memo" and "split the provider". */
function contextChanged(fiber: Fiber): boolean {
let dep = fiber.dependencies?.firstContext
while (dep) {
const context = dep.context as { _currentValue?: unknown } | undefined
if (context && 'memoizedValue' in dep && !Object.is(dep.memoizedValue, context._currentValue)) {
return true
}
dep = dep.next
}
return false
}
function record(fiber: Fiber) {
const name = getDisplayName(fiber)
@ -94,6 +120,7 @@ function record(fiber: Fiber) {
const entry = counts.get(name) ?? blank()
const props = propsChanged(fiber)
const state = stateChanged(fiber)
const context = contextChanged(fiber)
entry.renders += 1
entry.totalMs += fiber.actualDuration ?? 0
@ -106,7 +133,11 @@ function record(fiber: Fiber) {
entry.stateChanged += 1
}
if (!props && !state) {
if (context) {
entry.contextChanged += 1
}
if (!props && !state && !context) {
entry.wasted += 1
}