mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-12 07:09:59 +00:00
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:
co-authored by
Claude Opus 5
parent
5244dde5f1
commit
d74afc7b7d
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user