feat(engine): measure live DOM size on every render, not just probed ones

The short-comp routing gate can only read a live element count when a
probe session exists, and the first v0.7.83 data shows that is far rarer
than estimated: 17% of renders (86/503), not the ">=28%" the video-presence
proxy suggested. The other 83% fall back to a static source scan, which
is exactly blind to the shape that motivated the live count — small
markup, thousands of script-created nodes.

That leaves the fleet element-count distribution unknowable for most
renders, and the observed distribution is already surprising: p99 ~900,
max 1,420 against a 2,500 ceiling calibrated on 7k/20k/40k synthetic
nodes. Either the ceiling is close to irrelevant, or the large-DOM tail
is hiding in the 83% we cannot see. Both readings change what PR B
should do, and neither is decidable from probed renders alone (they are
a biased sample — they got a probe *because* they carry media or
unresolved compositions).

So measure it where every render already goes: capture-session init.
`collectSessionInitTelemetry` gains a querySelectorAll("*") count beside
the tween count it already collects, riding the same channel to
`observability_init_element_count`. This is observational only — capture
has begun, far too late to route on — and it deliberately does not feed
the gate. It answers the distribution question the gate cannot.

Coverage for this channel is proven rather than assumed: the tween-count
fix that shipped in v0.7.83 took the clamped-parallel bucket from 0/272
renders to 217/217, and 23.1% -> 100% overall.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vance Ingalls
2026-07-29 22:55:12 -07:00
co-authored by Claude Opus 5
parent 5244dde5f1
commit d74afc7b7d
8 changed files with 99 additions and 17 deletions
@@ -416,18 +416,33 @@ describe("init observability fallback (parallel workers)", () => {
const summary = makeRecorder().summary({
lastBrowserConsole: ["[FrameCapture:NAV] page.goto start"],
capture: { forceScreenshot: false, captureMode: "screenshot" },
initFallback: { initDurationMs: 850, tweenCount: 1200 },
initFallback: { initDurationMs: 850, tweenCount: 1200, elementCount: 3400 },
});
expect(summary.init).toEqual({ initDurationMs: 850, tweenCount: 1200 });
expect(summary.init).toEqual({ initDurationMs: 850, tweenCount: 1200, elementCount: 3400 });
});
it("max-merges console INIT lines over the fallback, matching multi-session semantics", () => {
const summary = makeRecorder().summary({
lastBrowserConsole: ["[FrameCapture:INIT] complete initDurationMs=1234 tweenCount=42"],
lastBrowserConsole: [
"[FrameCapture:INIT] complete initDurationMs=1234 tweenCount=42 elementCount=5000",
],
capture: { forceScreenshot: false, captureMode: "screenshot" },
initFallback: { initDurationMs: 850, tweenCount: 1200 },
initFallback: { initDurationMs: 850, tweenCount: 1200, elementCount: 3400 },
});
expect(summary.init).toEqual({ initDurationMs: 1234, tweenCount: 1200 });
expect(summary.init).toEqual({ initDurationMs: 1234, tweenCount: 1200, elementCount: 5000 });
});
// The single-session path has no structured fallback — it parses the console
// line only. This is the path that covers renders the routing gate cannot
// measure, so the element count must survive it.
it("parses elementCount from the console INIT line with no fallback at all", () => {
const summary = makeRecorder().summary({
lastBrowserConsole: [
"[FrameCapture:INIT] complete initDurationMs=90 tweenCount=7 elementCount=1420",
],
capture: { forceScreenshot: false, captureMode: "screenshot" },
});
expect(summary.init).toEqual({ initDurationMs: 90, tweenCount: 7, elementCount: 1420 });
});
it("stays undefined when neither source has anything", () => {
@@ -182,6 +182,15 @@ export interface RenderExtractionObservability {
export interface RenderInitObservability {
initDurationMs?: number;
tweenCount?: number;
/**
* Live DOM element count at end of capture-session init. Observational:
* measured after routing has already been decided, so it cannot gate — it
* exists because the routing gate's own count is only available on the
* ~17% of renders that get a probe session, leaving the fleet
* element-count distribution (and any large-runtime-DOM tail) unreadable
* for the rest.
*/
elementCount?: number;
}
export interface RenderObservabilitySummary {
@@ -299,13 +308,17 @@ function summarizeInitObservability(
// let the console parse (same max semantics) refine it.
let initDurationMs: number | undefined = fallback?.initDurationMs;
let tweenCount: number | undefined = fallback?.tweenCount;
let elementCount: number | undefined = fallback?.elementCount;
for (const line of lines) {
if (!line.includes("[FrameCapture:INIT]")) continue;
initDurationMs = maxReading(initDurationMs, readUnsignedIntAfter(line, "initDurationMs="));
tweenCount = maxReading(tweenCount, readUnsignedIntAfter(line, "tweenCount="));
elementCount = maxReading(elementCount, readUnsignedIntAfter(line, "elementCount="));
}
if (initDurationMs === undefined && tweenCount === undefined) return undefined;
return { initDurationMs, tweenCount };
if (initDurationMs === undefined && tweenCount === undefined && elementCount === undefined) {
return undefined;
}
return { initDurationMs, tweenCount, elementCount };
}
// fallow-ignore-next-line complexity
@@ -1936,17 +1936,25 @@ describe("shouldPreferSingleWorkerDrawElement (DE priority inversion)", () => {
it("max-merges across workers and ignores workers that reported nothing", () => {
expect(
mergeWorkerInitObservability([
{ initDurationMs: 400, initTweenCount: 900 },
{ initDurationMs: 400, initTweenCount: 900, initElementCount: 1200 },
{},
{ initDurationMs: 1250, initTweenCount: 880 },
{ initDurationMs: 1250, initTweenCount: 880, initElementCount: 1190 },
]),
).toEqual({ initDurationMs: 1250, tweenCount: 900 });
).toEqual({ initDurationMs: 1250, tweenCount: 900, elementCount: 1200 });
});
it("returns undefined when no worker reported — summary.init must stay absent, not zeroed", () => {
expect(mergeWorkerInitObservability([])).toBeUndefined();
expect(mergeWorkerInitObservability([{}, {}])).toBeUndefined();
});
it("surfaces an element count even when a worker reported nothing else", () => {
expect(mergeWorkerInitObservability([{ initElementCount: 4000 }])).toEqual({
initDurationMs: undefined,
tweenCount: undefined,
elementCount: 4000,
});
});
});
describe("countElementTags", () => {
@@ -1345,10 +1345,15 @@ export async function resolveCompositionElementCount(
* initializes before the timeline is fully wired, not an expected disagreement.
*/
export function mergeWorkerInitObservability(
perfs: ReadonlyArray<{ initDurationMs?: number; initTweenCount?: number }>,
): { initDurationMs?: number; tweenCount?: number } | undefined {
perfs: ReadonlyArray<{
initDurationMs?: number;
initTweenCount?: number;
initElementCount?: number;
}>,
): { initDurationMs?: number; tweenCount?: number; elementCount?: number } | undefined {
let initDurationMs: number | undefined;
let tweenCount: number | undefined;
let elementCount: number | undefined;
for (const perf of perfs) {
if (perf.initDurationMs !== undefined) {
initDurationMs =
@@ -1360,9 +1365,20 @@ export function mergeWorkerInitObservability(
tweenCount =
tweenCount === undefined ? perf.initTweenCount : Math.max(tweenCount, perf.initTweenCount);
}
// Max across workers: every worker loads the same composition, so they
// should agree — max is defensive against a worker sampled before its
// init script finished populating the DOM.
if (perf.initElementCount !== undefined) {
elementCount =
elementCount === undefined
? perf.initElementCount
: Math.max(elementCount, perf.initElementCount);
}
}
if (initDurationMs === undefined && tweenCount === undefined) return undefined;
return { initDurationMs, tweenCount };
if (initDurationMs === undefined && tweenCount === undefined && elementCount === undefined) {
return undefined;
}
return { initDurationMs, tweenCount, elementCount };
}
/**