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
+2
View File
@@ -98,6 +98,7 @@ export interface RenderObservabilityTelemetryPayload {
observabilityExtractCacheMisses?: number; observabilityExtractCacheMisses?: number;
observabilityInitDurationMs?: number; observabilityInitDurationMs?: number;
observabilityInitTweenCount?: number; observabilityInitTweenCount?: number;
observabilityInitElementCount?: number;
} }
function renderObservabilityEventProperties(props: RenderObservabilityTelemetryPayload) { function renderObservabilityEventProperties(props: RenderObservabilityTelemetryPayload) {
@@ -157,6 +158,7 @@ function renderObservabilityEventProperties(props: RenderObservabilityTelemetryP
observability_extract_cache_misses: props.observabilityExtractCacheMisses, observability_extract_cache_misses: props.observabilityExtractCacheMisses,
observability_init_duration_ms: props.observabilityInitDurationMs, observability_init_duration_ms: props.observabilityInitDurationMs,
observability_init_tween_count: props.observabilityInitTweenCount, observability_init_tween_count: props.observabilityInitTweenCount,
observability_init_element_count: props.observabilityInitElementCount,
}; };
} }
@@ -66,6 +66,7 @@ export function renderObservabilityTelemetryPayload(
observabilityExtractCacheMisses: extraction?.cacheMisses, observabilityExtractCacheMisses: extraction?.cacheMisses,
observabilityInitDurationMs: init?.initDurationMs, observabilityInitDurationMs: init?.initDurationMs,
observabilityInitTweenCount: init?.tweenCount, observabilityInitTweenCount: init?.tweenCount,
observabilityInitElementCount: init?.elementCount,
}; };
} }
+22 -3
View File
@@ -119,6 +119,8 @@ export interface CaptureSession {
initTelemetry?: { initTelemetry?: {
initDurationMs: number; initDurationMs: number;
tweenCount: number; tweenCount: number;
/** Live DOM element count at end of init — observational; see collectSessionInitTelemetry. */
elementCount: number;
}; };
capturePerf: { capturePerf: {
frames: number; frames: number;
@@ -406,8 +408,24 @@ function appendBrowserDiagnostic(session: CaptureSession, text: string): void {
async function collectSessionInitTelemetry( async function collectSessionInitTelemetry(
page: Page, page: Page,
initStart: number, initStart: number,
): Promise<{ initDurationMs: number; tweenCount: number }> { ): Promise<{ initDurationMs: number; tweenCount: number; elementCount: number }> {
const initDurationMs = Date.now() - initStart; const initDurationMs = Date.now() - initStart;
// Live DOM size, measured once the init sequence has completed so
// script-generated elements are present. This is the SAME quantity the
// short-comp routing gate wants, but measured here it is observational
// only — capture has already started, so it is far too late to route on.
// Its job is coverage: the routing gate can only read a live count on the
// ~17% of renders that get a probe session, which leaves the fleet
// element-count distribution unknowable for the rest (and hides exactly
// the dangerous shape — small source markup, huge runtime DOM). Every
// render reaches this path, so the distribution becomes readable even
// where the gate stays blind.
let elementCount = 0;
try {
elementCount = await page.evaluate(() => document.querySelectorAll("*").length);
} catch {
elementCount = 0;
}
let tweenCount = 0; let tweenCount = 0;
try { try {
tweenCount = await page.evaluate(() => { tweenCount = await page.evaluate(() => {
@@ -431,7 +449,7 @@ async function collectSessionInitTelemetry(
} catch { } catch {
tweenCount = 0; tweenCount = 0;
} }
return { initDurationMs, tweenCount }; return { initDurationMs, tweenCount, elementCount };
} }
async function recordSessionInitTelemetry( async function recordSessionInitTelemetry(
@@ -442,7 +460,7 @@ async function recordSessionInitTelemetry(
session.initTelemetry = telemetry; session.initTelemetry = telemetry;
appendBrowserDiagnostic( appendBrowserDiagnostic(
session, session,
`[FrameCapture:INIT] complete initDurationMs=${telemetry.initDurationMs} tweenCount=${telemetry.tweenCount}`, `[FrameCapture:INIT] complete initDurationMs=${telemetry.initDurationMs} tweenCount=${telemetry.tweenCount} elementCount=${telemetry.elementCount}`,
); );
} }
@@ -3788,6 +3806,7 @@ export function getCapturePerfSummary(session: CaptureSession): CapturePerfSumma
subTimelineWaitOutcome: session.subTimelineWaitOutcome, subTimelineWaitOutcome: session.subTimelineWaitOutcome,
initDurationMs: session.initTelemetry?.initDurationMs, initDurationMs: session.initTelemetry?.initDurationMs,
initTweenCount: session.initTelemetry?.tweenCount, initTweenCount: session.initTelemetry?.tweenCount,
initElementCount: session.initTelemetry?.elementCount,
warnings: cloneCaptureWarnings(session.warnings), warnings: cloneCaptureWarnings(session.warnings),
staticDedupReused: session.staticDedupCount ?? 0, staticDedupReused: session.staticDedupCount ?? 0,
staticDedupEnabled: session.staticDedupEnabled ?? false, staticDedupEnabled: session.staticDedupEnabled ?? false,
+8
View File
@@ -253,6 +253,14 @@ export interface CapturePerfSummary {
initDurationMs?: number; initDurationMs?: number;
/** GSAP tween count at init — the motion-axis signal for capture routing analysis. */ /** GSAP tween count at init — the motion-axis signal for capture routing analysis. */
initTweenCount?: number; initTweenCount?: number;
/**
* Live DOM element count at end of init. Observational counterpart to the
* short-comp routing gate's own count: the gate can only measure the ~17%
* of renders that get a probe session, so without this the fleet
* element-count distribution and any large-runtime-DOM tail stays
* invisible for the rest.
*/
initElementCount?: number;
/** Correctness warnings observed before or during capture. */ /** Correctness warnings observed before or during capture. */
warnings?: CaptureWarning[]; warnings?: CaptureWarning[];
/** /**
@@ -416,18 +416,33 @@ describe("init observability fallback (parallel workers)", () => {
const summary = makeRecorder().summary({ const summary = makeRecorder().summary({
lastBrowserConsole: ["[FrameCapture:NAV] page.goto start"], lastBrowserConsole: ["[FrameCapture:NAV] page.goto start"],
capture: { forceScreenshot: false, captureMode: "screenshot" }, 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", () => { it("max-merges console INIT lines over the fallback, matching multi-session semantics", () => {
const summary = makeRecorder().summary({ 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" }, 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", () => { it("stays undefined when neither source has anything", () => {
@@ -182,6 +182,15 @@ export interface RenderExtractionObservability {
export interface RenderInitObservability { export interface RenderInitObservability {
initDurationMs?: number; initDurationMs?: number;
tweenCount?: 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 { export interface RenderObservabilitySummary {
@@ -299,13 +308,17 @@ function summarizeInitObservability(
// let the console parse (same max semantics) refine it. // let the console parse (same max semantics) refine it.
let initDurationMs: number | undefined = fallback?.initDurationMs; let initDurationMs: number | undefined = fallback?.initDurationMs;
let tweenCount: number | undefined = fallback?.tweenCount; let tweenCount: number | undefined = fallback?.tweenCount;
let elementCount: number | undefined = fallback?.elementCount;
for (const line of lines) { for (const line of lines) {
if (!line.includes("[FrameCapture:INIT]")) continue; if (!line.includes("[FrameCapture:INIT]")) continue;
initDurationMs = maxReading(initDurationMs, readUnsignedIntAfter(line, "initDurationMs=")); initDurationMs = maxReading(initDurationMs, readUnsignedIntAfter(line, "initDurationMs="));
tweenCount = maxReading(tweenCount, readUnsignedIntAfter(line, "tweenCount=")); tweenCount = maxReading(tweenCount, readUnsignedIntAfter(line, "tweenCount="));
elementCount = maxReading(elementCount, readUnsignedIntAfter(line, "elementCount="));
} }
if (initDurationMs === undefined && tweenCount === undefined) return undefined; if (initDurationMs === undefined && tweenCount === undefined && elementCount === undefined) {
return { initDurationMs, tweenCount }; return undefined;
}
return { initDurationMs, tweenCount, elementCount };
} }
// fallow-ignore-next-line complexity // 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", () => { it("max-merges across workers and ignores workers that reported nothing", () => {
expect( expect(
mergeWorkerInitObservability([ 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", () => { it("returns undefined when no worker reported — summary.init must stay absent, not zeroed", () => {
expect(mergeWorkerInitObservability([])).toBeUndefined(); expect(mergeWorkerInitObservability([])).toBeUndefined();
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", () => { describe("countElementTags", () => {
@@ -1345,10 +1345,15 @@ export async function resolveCompositionElementCount(
* initializes before the timeline is fully wired, not an expected disagreement. * initializes before the timeline is fully wired, not an expected disagreement.
*/ */
export function mergeWorkerInitObservability( export function mergeWorkerInitObservability(
perfs: ReadonlyArray<{ initDurationMs?: number; initTweenCount?: number }>, perfs: ReadonlyArray<{
): { initDurationMs?: number; tweenCount?: number } | undefined { initDurationMs?: number;
initTweenCount?: number;
initElementCount?: number;
}>,
): { initDurationMs?: number; tweenCount?: number; elementCount?: number } | undefined {
let initDurationMs: number | undefined; let initDurationMs: number | undefined;
let tweenCount: number | undefined; let tweenCount: number | undefined;
let elementCount: number | undefined;
for (const perf of perfs) { for (const perf of perfs) {
if (perf.initDurationMs !== undefined) { if (perf.initDurationMs !== undefined) {
initDurationMs = initDurationMs =
@@ -1360,9 +1365,20 @@ export function mergeWorkerInitObservability(
tweenCount = tweenCount =
tweenCount === undefined ? perf.initTweenCount : Math.max(tweenCount, perf.initTweenCount); 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; if (initDurationMs === undefined && tweenCount === undefined && elementCount === undefined) {
return { initDurationMs, tweenCount }; return undefined;
}
return { initDurationMs, tweenCount, elementCount };
} }
/** /**