You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(core): emit dev-only pipeline stage timings and surface them in the benchmark
The block-memo path runs four stages per content change (parse →
transform → build → render) and every optimization decision should start
from that split. measureStage() wraps each stage and emits one
performance.measure per execution (ai-markdown:stage:*), so the DevTools
Performance panel picks them up with zero wiring and the BlockMemoCompare
story's new 'Pipeline stages' panel aggregates avg/Σ/count/max per stage.
Design points, each the survivor of a review finding:
- PIPELINE_STAGES tuple is the single source of truth: the union type,
the emitted names (hoisted table, no per-call allocation), and the
panel's display order all derive from it — a new stage cannot be
silently omitted from the display.
- measureStage(stage, fn) is single-callable: the stage name is a typed
argument and the start/end pairing is unforgettable by construction.
- The ENABLED gate builds on the shared DEV constant plus a ONE-TIME
capability probe of the exact calls made (options-object measure +
clearMeasures) — environments with only legacy measure overloads gate
off permanently instead of paying a swallowed throw per stage per token.
- Entries are cleared from the global User Timing buffer immediately
after emission: live observers still receive every entry (delivery is
queued at creation) while the buffer never grows from render work —
~4 entries/token in ANY consuming app's dev build, observer or not.
Buffer readers (console getEntriesByType, late buffered observers) see
nothing by design; documented in docs/streaming-and-performance.md
('Built-in stage timing') together with the always-on-in-dev tradeoff.
- Production builds fold ENABLED to false; unbundled Node SSR pays one
boolean per stage.
Story side: useRenderProfiler gains observeStages (page-wide measures —
enabled ONLY on the block-memo side per page to keep attribution honest;
IsolatedSide enables it for memo mode).
entry per pipeline stage per content change, named:
213
+
214
+
```
215
+
ai-markdown:stage:parse # unified.parse over the full document
216
+
ai-markdown:stage:transform # remark/rehype transformer run
217
+
ai-markdown:stage:build # block-plan construction
218
+
ai-markdown:stage:render # per-block render with cache lookup
219
+
```
220
+
221
+
This is how to answer "which stage eats the budget" without guessing —
222
+
the numbers map 1:1 onto the (1)/(2)/(3) split above. Two supported ways
223
+
to read them:
224
+
225
+
-**DevTools Performance panel**: record a session; the measures appear in
226
+
the User Timing track. No wiring needed — emission is always on in dev,
227
+
which is a deliberate choice traded against a few `performance.*` calls
228
+
per token.
229
+
-**A live `PerformanceObserver`** for `entryTypes: ['measure']`, filtering
230
+
by the `ai-markdown:stage:` prefix (the built-in Storybook benchmark's
231
+
"Pipeline stages" panel does exactly this).
232
+
233
+
Delivery semantics to know before wiring your own reader: each entry is
234
+
**cleared from the global User Timing buffer immediately after emission**,
235
+
so the buffer never grows from render work. Already-registered observers
236
+
still receive every entry (delivery is queued at creation), but
237
+
`performance.getEntriesByType('measure')` from the console and
238
+
late-attached `buffered: true` observers will see nothing — attach your
239
+
observer before streaming starts. Production builds emit nothing and pay
240
+
one boolean check per stage.
241
+
208
242
### Validating output equivalence
209
243
210
244
If you suspect `blockMemoEnabled: true` is producing different output than `false`, the library's test suite includes a `byteEquivalence.test.tsx` harness that asserts byte-identical HTML across every plugin permutation. Failures should be reported as bugs.
{label: 'max base (ms)',value: fmt(snapshot.base.max)},
128
140
],
129
141
},
142
+
// Pipeline stage timings — present only when this profiler observes the
143
+
// dev-only `ai-markdown:stage:*` measures (block-memo side). Ordered by
144
+
// the shared PIPELINE_STAGES tuple so a stage added to the pipeline
145
+
// cannot be silently omitted from the panel.
146
+
...(stages.length===0
147
+
? []
148
+
: [
149
+
{
150
+
title: 'Pipeline stages (dev-only)',
151
+
hint:
152
+
'per-execution timings from ai-markdown:stage:* performance measures, emitted by the block-memo path only. parse + transform run on every content change regardless of caching — they are the incremental-parse opportunity; render is where block-memo saves. render also fires on cache-hit-only re-renders, so its avg skews low by design.',
153
+
fields: stages,
154
+
},
155
+
]),
130
156
{
131
157
title: 'Component renders',
132
158
hint: 'spy customComponents count function-body invocations per tag (react-scan-style). Block-memo skips re-invoking components inside cached subtrees → totals diverge cleanly between paths.',
0 commit comments