mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-12 15:20:13 +00:00
feat(producer): surface init telemetry from parallel workers — the band's missing motion axis
The routing surface the short-comp benchmarks validated is (motion x DOM size x frames). After the baseline release, fleet telemetry carries DOM size (composition_element_count) and frames on every render — but the motion proxy, observability_init_tween_count, has 0% coverage on the exact renders the band routes: parallel workers' console buffers (and so the [FrameCapture:INIT] line the summary parses) only propagate to the orchestrator on FAILURE. Single-worker screenshot renders report it; the multi-worker clamp bucket never does. Verified against 7d of fleet data: 35k screenshot renders carry tween counts, 0 of 9,600 band renders. Fix rides the one channel parallel workers already return on success — the per-worker CapturePerfSummary. Sessions record initTelemetry on every init path; the perf summary now carries it; the orchestrator max-merges across workers (same multi-session semantics the console parser uses) and feeds it to the observability summary as a structured fallback, console lines still refining when present. With this, every band render carries full coordinates — (elements, tweens, frames, path, speed) — which buys two reads: regressing wild DE speed against element count on the existing 900+ inversions validates the bench's 0.50ms/element slope BEFORE the routing flip, and any post-flip misroute can be reproduced locally by feeding its telemetry row straight into gen-crossover-comp's knobs (--movers ~ tween count, --static ~ element count) and re-benching. (Also drops a now-stale fallow suppression in render.ts — the test-only reset export it guarded gained real test importers, so the issue it suppressed no longer exists and the gate flags the leftover.) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e9de2fa14f
commit
23854f7c6a
@@ -407,3 +407,34 @@ describe("RenderObservabilityRecorder", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("init observability fallback (parallel workers)", () => {
|
||||
const makeRecorder = () =>
|
||||
new RenderObservabilityRecorder({ renderJobId: "render-par", pipelineStartMs: Date.now() });
|
||||
|
||||
it("uses the structured fallback when the console has no INIT line — the parallel success path", () => {
|
||||
const summary = makeRecorder().summary({
|
||||
lastBrowserConsole: ["[FrameCapture:NAV] page.goto start"],
|
||||
capture: { forceScreenshot: false, captureMode: "screenshot" },
|
||||
initFallback: { initDurationMs: 850, tweenCount: 1200 },
|
||||
});
|
||||
expect(summary.init).toEqual({ initDurationMs: 850, tweenCount: 1200 });
|
||||
});
|
||||
|
||||
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"],
|
||||
capture: { forceScreenshot: false, captureMode: "screenshot" },
|
||||
initFallback: { initDurationMs: 850, tweenCount: 1200 },
|
||||
});
|
||||
expect(summary.init).toEqual({ initDurationMs: 1234, tweenCount: 1200 });
|
||||
});
|
||||
|
||||
it("stays undefined when neither source has anything", () => {
|
||||
const summary = makeRecorder().summary({
|
||||
lastBrowserConsole: [],
|
||||
capture: { forceScreenshot: false, captureMode: "screenshot" },
|
||||
});
|
||||
expect(summary.init).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -270,20 +270,26 @@ function readUnsignedIntAfter(line: string, prefix: string): number | undefined
|
||||
return digits > 0 ? value : undefined;
|
||||
}
|
||||
|
||||
function summarizeInitObservability(lines: string[]): RenderInitObservability | undefined {
|
||||
let initDurationMs: number | undefined;
|
||||
let tweenCount: number | undefined;
|
||||
/** Max of two optional readings — multiple worker/session INIT records can appear; keep the worst. */
|
||||
function maxReading(current: number | undefined, next: number | undefined): number | undefined {
|
||||
if (next === undefined) return current;
|
||||
return current === undefined ? next : Math.max(current, next);
|
||||
}
|
||||
|
||||
function summarizeInitObservability(
|
||||
lines: string[],
|
||||
fallback?: RenderInitObservability,
|
||||
): RenderInitObservability | undefined {
|
||||
// Console parsing only sees THIS process's session buffer, so parallel
|
||||
// workers' INIT lines never reach it — their init telemetry arrives
|
||||
// structured via the per-worker perf summaries instead. Seed with that and
|
||||
// let the console parse (same max semantics) refine it.
|
||||
let initDurationMs: number | undefined = fallback?.initDurationMs;
|
||||
let tweenCount: number | undefined = fallback?.tweenCount;
|
||||
for (const line of lines) {
|
||||
if (!line.includes("[FrameCapture:INIT]")) continue;
|
||||
const duration = readUnsignedIntAfter(line, "initDurationMs=");
|
||||
const tweens = readUnsignedIntAfter(line, "tweenCount=");
|
||||
// Multiple worker/session INIT records can appear; keep the worst observed startup cost.
|
||||
if (duration !== undefined) {
|
||||
initDurationMs = initDurationMs === undefined ? duration : Math.max(initDurationMs, duration);
|
||||
}
|
||||
if (tweens !== undefined) {
|
||||
tweenCount = tweenCount === undefined ? tweens : Math.max(tweenCount, tweens);
|
||||
}
|
||||
initDurationMs = maxReading(initDurationMs, readUnsignedIntAfter(line, "initDurationMs="));
|
||||
tweenCount = maxReading(tweenCount, readUnsignedIntAfter(line, "tweenCount="));
|
||||
}
|
||||
if (initDurationMs === undefined && tweenCount === undefined) return undefined;
|
||||
return { initDurationMs, tweenCount };
|
||||
@@ -402,6 +408,8 @@ export class RenderObservabilityRecorder {
|
||||
summary(input: {
|
||||
lastBrowserConsole: string[];
|
||||
capture: RenderCaptureObservability;
|
||||
/** Structured init telemetry from per-worker perf summaries — the only success-path channel parallel workers have (their console buffers propagate on failure only). */
|
||||
initFallback?: RenderInitObservability;
|
||||
extraction?: RenderExtractionObservability;
|
||||
compositionHash?: string;
|
||||
}): RenderObservabilitySummary {
|
||||
@@ -416,7 +424,7 @@ export class RenderObservabilityRecorder {
|
||||
browserDiagnostics: summarizeBrowserDiagnostics(input.lastBrowserConsole),
|
||||
capture: { ...input.capture },
|
||||
extraction: input.extraction ? { ...input.extraction } : undefined,
|
||||
init: summarizeInitObservability(input.lastBrowserConsole),
|
||||
init: summarizeInitObservability(input.lastBrowserConsole, input.initFallback),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
shouldRetryViaPinnedFallback,
|
||||
countElementTags,
|
||||
envInt,
|
||||
mergeWorkerInitObservability,
|
||||
shouldPreferParallelDrawElement,
|
||||
shouldPreferSingleWorkerDrawElement,
|
||||
shouldStreamParallelCapture,
|
||||
@@ -1761,6 +1762,23 @@ describe("shouldPreferSingleWorkerDrawElement (DE priority inversion)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("mergeWorkerInitObservability", () => {
|
||||
it("max-merges across workers and ignores workers that reported nothing", () => {
|
||||
expect(
|
||||
mergeWorkerInitObservability([
|
||||
{ initDurationMs: 400, initTweenCount: 900 },
|
||||
{},
|
||||
{ initDurationMs: 1250, initTweenCount: 880 },
|
||||
]),
|
||||
).toEqual({ initDurationMs: 1250, tweenCount: 900 });
|
||||
});
|
||||
|
||||
it("returns undefined when no worker reported — summary.init must stay absent, not zeroed", () => {
|
||||
expect(mergeWorkerInitObservability([])).toBeUndefined();
|
||||
expect(mergeWorkerInitObservability([{}, {}])).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("countElementTags", () => {
|
||||
it("counts closing tags", () => {
|
||||
expect(countElementTags("<div><span>a</span></div>")).toBe(2);
|
||||
|
||||
@@ -1266,6 +1266,35 @@ export function countElementTags(html: string): number {
|
||||
return matches === null ? 0 : matches.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Max-merge init telemetry across per-worker capture perf summaries — the
|
||||
* success-path channel for PARALLEL renders, whose worker console buffers
|
||||
* (and so the `[FrameCapture:INIT]` line) only propagate on failure. Max
|
||||
* matches summarizeInitObservability's own multi-session semantics: keep the
|
||||
* worst observed startup cost, and tween count is per-composition so any
|
||||
* worker's reading is the reading.
|
||||
*/
|
||||
export function mergeWorkerInitObservability(
|
||||
perfs: ReadonlyArray<{ initDurationMs?: number; initTweenCount?: number }>,
|
||||
): { initDurationMs?: number; tweenCount?: number } | undefined {
|
||||
let initDurationMs: number | undefined;
|
||||
let tweenCount: number | undefined;
|
||||
for (const perf of perfs) {
|
||||
if (perf.initDurationMs !== undefined) {
|
||||
initDurationMs =
|
||||
initDurationMs === undefined
|
||||
? perf.initDurationMs
|
||||
: Math.max(initDurationMs, perf.initDurationMs);
|
||||
}
|
||||
if (perf.initTweenCount !== undefined) {
|
||||
tweenCount =
|
||||
tweenCount === undefined ? perf.initTweenCount : Math.max(tweenCount, perf.initTweenCount);
|
||||
}
|
||||
}
|
||||
if (initDurationMs === undefined && tweenCount === undefined) return undefined;
|
||||
return { initDurationMs, tweenCount };
|
||||
}
|
||||
|
||||
/**
|
||||
* DE priority inversion predicate: should an AUTO-resolved multi-worker render
|
||||
* drop to single-worker verified drawElement streaming?
|
||||
@@ -3600,6 +3629,7 @@ async function executeRenderPipeline(input: {
|
||||
const observabilitySummary = observability.summary({
|
||||
lastBrowserConsole,
|
||||
capture: captureObservability,
|
||||
initFallback: mergeWorkerInitObservability(dedupPerfs),
|
||||
extraction: extractionObservability,
|
||||
compositionHash,
|
||||
});
|
||||
@@ -3761,6 +3791,7 @@ async function executeRenderPipeline(input: {
|
||||
const observabilitySummary = observability.summary({
|
||||
lastBrowserConsole,
|
||||
capture: captureObservability,
|
||||
initFallback: mergeWorkerInitObservability(dedupPerfs),
|
||||
extraction: extractionObservability,
|
||||
compositionHash,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user