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
@@ -1109,7 +1109,6 @@ let deParallelRouterTrialFiredThisProcess = false;
|
|||||||
* resetting outside a test process where many independent test cases share
|
* resetting outside a test process where many independent test cases share
|
||||||
* one imported module instance.
|
* one imported module instance.
|
||||||
*/
|
*/
|
||||||
// fallow-ignore-next-line unused-export
|
|
||||||
export function __resetDeParallelRouterTrialStateForTests(): void {
|
export function __resetDeParallelRouterTrialStateForTests(): void {
|
||||||
deParallelRouterTrialManagedByUs = false;
|
deParallelRouterTrialManagedByUs = false;
|
||||||
deParallelRouterTrialFiredThisProcess = false;
|
deParallelRouterTrialFiredThisProcess = false;
|
||||||
|
|||||||
@@ -3786,6 +3786,8 @@ export function getCapturePerfSummary(session: CaptureSession): CapturePerfSumma
|
|||||||
p95TotalMs: percentileOf(session.capturePerf.frameMs, 0.95),
|
p95TotalMs: percentileOf(session.capturePerf.frameMs, 0.95),
|
||||||
p99TotalMs: percentileOf(session.capturePerf.frameMs, 0.99),
|
p99TotalMs: percentileOf(session.capturePerf.frameMs, 0.99),
|
||||||
subTimelineWaitOutcome: session.subTimelineWaitOutcome,
|
subTimelineWaitOutcome: session.subTimelineWaitOutcome,
|
||||||
|
initDurationMs: session.initTelemetry?.initDurationMs,
|
||||||
|
initTweenCount: session.initTelemetry?.tweenCount,
|
||||||
warnings: cloneCaptureWarnings(session.warnings),
|
warnings: cloneCaptureWarnings(session.warnings),
|
||||||
staticDedupReused: session.staticDedupCount ?? 0,
|
staticDedupReused: session.staticDedupCount ?? 0,
|
||||||
staticDedupEnabled: session.staticDedupEnabled ?? false,
|
staticDedupEnabled: session.staticDedupEnabled ?? false,
|
||||||
|
|||||||
@@ -241,6 +241,18 @@ export interface CapturePerfSummary {
|
|||||||
p99TotalMs: number;
|
p99TotalMs: number;
|
||||||
/** Sub-composition timeline wait outcome (absent pre-init). */
|
/** Sub-composition timeline wait outcome (absent pre-init). */
|
||||||
subTimelineWaitOutcome?: SubTimelineWaitOutcome;
|
subTimelineWaitOutcome?: SubTimelineWaitOutcome;
|
||||||
|
/**
|
||||||
|
* Session init telemetry, mirrored from the `[FrameCapture:INIT]` console
|
||||||
|
* line so PARALLEL workers report it too: worker sessions' console buffers
|
||||||
|
* only propagate to the orchestrator on failure, which left the
|
||||||
|
* multi-worker path — the short-comp band's entire population — with 0%
|
||||||
|
* coverage of the motion axis (`observability_init_tween_count`) in fleet
|
||||||
|
* telemetry. Riding the perf summary reuses the one channel that already
|
||||||
|
* flows back per worker on success.
|
||||||
|
*/
|
||||||
|
initDurationMs?: number;
|
||||||
|
/** GSAP tween count at init — the motion-axis signal for capture routing analysis. */
|
||||||
|
initTweenCount?: number;
|
||||||
/** Correctness warnings observed before or during capture. */
|
/** Correctness warnings observed before or during capture. */
|
||||||
warnings?: CaptureWarning[];
|
warnings?: CaptureWarning[];
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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;
|
return digits > 0 ? value : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function summarizeInitObservability(lines: string[]): RenderInitObservability | undefined {
|
/** Max of two optional readings — multiple worker/session INIT records can appear; keep the worst. */
|
||||||
let initDurationMs: number | undefined;
|
function maxReading(current: number | undefined, next: number | undefined): number | undefined {
|
||||||
let tweenCount: 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) {
|
for (const line of lines) {
|
||||||
if (!line.includes("[FrameCapture:INIT]")) continue;
|
if (!line.includes("[FrameCapture:INIT]")) continue;
|
||||||
const duration = readUnsignedIntAfter(line, "initDurationMs=");
|
initDurationMs = maxReading(initDurationMs, readUnsignedIntAfter(line, "initDurationMs="));
|
||||||
const tweens = readUnsignedIntAfter(line, "tweenCount=");
|
tweenCount = maxReading(tweenCount, 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (initDurationMs === undefined && tweenCount === undefined) return undefined;
|
if (initDurationMs === undefined && tweenCount === undefined) return undefined;
|
||||||
return { initDurationMs, tweenCount };
|
return { initDurationMs, tweenCount };
|
||||||
@@ -402,6 +408,8 @@ export class RenderObservabilityRecorder {
|
|||||||
summary(input: {
|
summary(input: {
|
||||||
lastBrowserConsole: string[];
|
lastBrowserConsole: string[];
|
||||||
capture: RenderCaptureObservability;
|
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;
|
extraction?: RenderExtractionObservability;
|
||||||
compositionHash?: string;
|
compositionHash?: string;
|
||||||
}): RenderObservabilitySummary {
|
}): RenderObservabilitySummary {
|
||||||
@@ -416,7 +424,7 @@ export class RenderObservabilityRecorder {
|
|||||||
browserDiagnostics: summarizeBrowserDiagnostics(input.lastBrowserConsole),
|
browserDiagnostics: summarizeBrowserDiagnostics(input.lastBrowserConsole),
|
||||||
capture: { ...input.capture },
|
capture: { ...input.capture },
|
||||||
extraction: input.extraction ? { ...input.extraction } : undefined,
|
extraction: input.extraction ? { ...input.extraction } : undefined,
|
||||||
init: summarizeInitObservability(input.lastBrowserConsole),
|
init: summarizeInitObservability(input.lastBrowserConsole, input.initFallback),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import {
|
|||||||
shouldRetryViaPinnedFallback,
|
shouldRetryViaPinnedFallback,
|
||||||
countElementTags,
|
countElementTags,
|
||||||
envInt,
|
envInt,
|
||||||
|
mergeWorkerInitObservability,
|
||||||
shouldPreferParallelDrawElement,
|
shouldPreferParallelDrawElement,
|
||||||
shouldPreferSingleWorkerDrawElement,
|
shouldPreferSingleWorkerDrawElement,
|
||||||
shouldStreamParallelCapture,
|
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", () => {
|
describe("countElementTags", () => {
|
||||||
it("counts closing tags", () => {
|
it("counts closing tags", () => {
|
||||||
expect(countElementTags("<div><span>a</span></div>")).toBe(2);
|
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;
|
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
|
* DE priority inversion predicate: should an AUTO-resolved multi-worker render
|
||||||
* drop to single-worker verified drawElement streaming?
|
* drop to single-worker verified drawElement streaming?
|
||||||
@@ -3600,6 +3629,7 @@ async function executeRenderPipeline(input: {
|
|||||||
const observabilitySummary = observability.summary({
|
const observabilitySummary = observability.summary({
|
||||||
lastBrowserConsole,
|
lastBrowserConsole,
|
||||||
capture: captureObservability,
|
capture: captureObservability,
|
||||||
|
initFallback: mergeWorkerInitObservability(dedupPerfs),
|
||||||
extraction: extractionObservability,
|
extraction: extractionObservability,
|
||||||
compositionHash,
|
compositionHash,
|
||||||
});
|
});
|
||||||
@@ -3761,6 +3791,7 @@ async function executeRenderPipeline(input: {
|
|||||||
const observabilitySummary = observability.summary({
|
const observabilitySummary = observability.summary({
|
||||||
lastBrowserConsole,
|
lastBrowserConsole,
|
||||||
capture: captureObservability,
|
capture: captureObservability,
|
||||||
|
initFallback: mergeWorkerInitObservability(dedupPerfs),
|
||||||
extraction: extractionObservability,
|
extraction: extractionObservability,
|
||||||
compositionHash,
|
compositionHash,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user