mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
fix(producer): count live DOM, not source markup, for the short-band gate
R3 review finding: a string scan of compiled.html — however the regex is
tuned — cannot see elements a composition's own script creates at
runtime. The repo already has a production shape that hits this exactly:
style-10-prod's per-transcript-word caption generator builds one <span>
per word via document.createElement, measuring 2 source tags against
thousands of live nodes after init. That's the same unbounded-undercount
failure class as the earlier <img>/SVG counterexamples, but this one
has no static-scan fix — the elements simply don't exist as tags in the
string.
resolveCompositionElementCount() now prefers the live DOM size, queried
from the probe session that's already running for every render at this
point in the pipeline (its Chrome gets reused for capture on the common
single-worker path, so this costs one extra CDP evaluate, not a browser
launch) once that session's init sequence has completed — session.page
.evaluate(() => document.querySelectorAll("*").length) sees runtime-
generated DOM the source scan never could. countElementTags remains as
the fallback for the rare case with no initialized probe session
(evaluate throws, session absent, or not yet initialized).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7bb9e3cbf9
commit
def98f79c3
@@ -71,7 +71,11 @@ export interface RenderCaptureObservability {
|
||||
/** Worker count the resolver would have used absent the inversion; undefined if it never fired. */
|
||||
dePreInversionWorkers?: number;
|
||||
/**
|
||||
* Rough element count of the compiled composition (`countElementTags`).
|
||||
* Element count for the short-comp band gate (`resolveCompositionElementCount`):
|
||||
* the LIVE DOM size from the already-running probe session when one is
|
||||
* initialized, falling back to a static scan of the compiled HTML
|
||||
* (`countElementTags`) otherwise. Live is authoritative — a static scan
|
||||
* cannot see elements a composition's own script creates at runtime.
|
||||
*
|
||||
* Emitted on every render, not just inverted ones — this is the variable the
|
||||
* short-comp inversion band is gated on, and the fleet distribution of it is
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
countElementTags,
|
||||
envInt,
|
||||
mergeWorkerInitObservability,
|
||||
resolveCompositionElementCount,
|
||||
resolveDeShortBand,
|
||||
shouldPreferParallelDrawElement,
|
||||
shouldPreferSingleWorkerDrawElement,
|
||||
@@ -1896,6 +1897,46 @@ describe("shouldPreferSingleWorkerDrawElement (DE priority inversion)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveCompositionElementCount", () => {
|
||||
// Review finding (R3): a static scan of SOURCE markup cannot see DOM a
|
||||
// composition's own script creates at runtime (document.createElement) —
|
||||
// an unbounded undercount no regex can close. style-10-prod's real
|
||||
// per-transcript-word caption generator is exactly this shape: 2 source
|
||||
// tags, thousands of live nodes after init. These pin the fix: prefer the
|
||||
// live count from an initialized probe session, static scan only as
|
||||
// fallback.
|
||||
it("uses the live DOM count from an initialized probe session, ignoring the (much smaller) source scan", async () => {
|
||||
const session = { isInitialized: true, page: { evaluate: async () => 40001 } };
|
||||
expect(await resolveCompositionElementCount(session, "<div><span></span></div>")).toBe(40001);
|
||||
});
|
||||
|
||||
it("falls back to the static scan when there is no probe session", async () => {
|
||||
expect(await resolveCompositionElementCount(null, "<div><span></span></div>")).toBe(2);
|
||||
});
|
||||
|
||||
it("falls back to the static scan when the probe session is not yet initialized", async () => {
|
||||
const session = { isInitialized: false, page: { evaluate: async () => 999 } };
|
||||
expect(await resolveCompositionElementCount(session, "<div></div>")).toBe(1);
|
||||
});
|
||||
|
||||
it("falls back to the static scan when page.evaluate throws (detached frame, mid-navigation)", async () => {
|
||||
const session = {
|
||||
isInitialized: true,
|
||||
page: {
|
||||
evaluate: async () => {
|
||||
throw new Error("Execution context was destroyed");
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(await resolveCompositionElementCount(session, "<div><span></span></div>")).toBe(2);
|
||||
});
|
||||
|
||||
it("falls back to the static scan when evaluate resolves a non-finite value", async () => {
|
||||
const session = { isInitialized: true, page: { evaluate: async () => Number.NaN } };
|
||||
expect(await resolveCompositionElementCount(session, "<div></div>")).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("envInt", () => {
|
||||
afterEach(() => {
|
||||
delete process.env.HF_TEST_ENV_INT;
|
||||
|
||||
@@ -1270,9 +1270,13 @@ export function envInt(name: string, fallback: number): number {
|
||||
* literal closing marker (`</`, a void name at a word boundary, or `/>`), so
|
||||
* ordinary JS comparisons and divisions don't qualify — verified by test.
|
||||
*
|
||||
* These semantics are FROZEN while the short-band baseline is being read —
|
||||
* the fleet distribution recorded by the baseline release must be measured
|
||||
* by the same counter that later gates routing, or the baseline is invalid.
|
||||
* FALLBACK ONLY as of the live-DOM fix below — a string scan of the SOURCE
|
||||
* markup cannot see elements a composition's own script creates at runtime
|
||||
* (`document.createElement`), which is an unbounded undercount no regex can
|
||||
* close: `style-10-prod`'s per-transcript-word caption generator measures 2
|
||||
* source tags against thousands of live nodes after init (review finding).
|
||||
* `resolveCompositionElementCount` prefers the initialized probe session's
|
||||
* live count and uses this only when no such session exists.
|
||||
*/
|
||||
export function countElementTags(html: string): number {
|
||||
const matches = html.match(
|
||||
@@ -1281,6 +1285,41 @@ export function countElementTags(html: string): number {
|
||||
return matches === null ? 0 : matches.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Element count for the short-comp band's gate — prefers the LIVE DOM of the
|
||||
* probe session that's already running for every render at this point in the
|
||||
* pipeline (its Chrome is reused for capture on the common single-worker
|
||||
* path, so this costs one extra CDP round-trip, not a browser launch) over
|
||||
* the static `countElementTags` scan of `compiled.html`. The live count is
|
||||
* the only one that sees runtime-generated DOM — a composition whose script
|
||||
* builds its own elements after load (the caption-word-span pattern above)
|
||||
* is otherwise measured as near-empty regardless of how the static scanner
|
||||
* is tuned, admitting an arbitrarily large live DOM below the routing
|
||||
* ceiling (review finding, R3).
|
||||
*
|
||||
* These semantics are FROZEN while the short-band baseline is being read —
|
||||
* the fleet distribution recorded by the baseline release must be measured
|
||||
* by the same resolver that later gates routing, or the baseline is invalid.
|
||||
*/
|
||||
export async function resolveCompositionElementCount(
|
||||
probeSession: Pick<CaptureSession, "isInitialized" | "page"> | null,
|
||||
html: string,
|
||||
): Promise<number> {
|
||||
if (probeSession?.isInitialized) {
|
||||
try {
|
||||
const liveCount = await probeSession.page.evaluate(
|
||||
() => document.querySelectorAll("*").length,
|
||||
);
|
||||
if (typeof liveCount === "number" && Number.isFinite(liveCount)) return liveCount;
|
||||
} catch {
|
||||
// Probe page evaluate can fail (navigation mid-flight, detached frame,
|
||||
// page crash) — fall through to the static scan rather than block the
|
||||
// render on a routing-gate measurement.
|
||||
}
|
||||
}
|
||||
return countElementTags(html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Max-merge init telemetry across per-worker capture perf summaries — the
|
||||
* success-path channel for PARALLEL renders, whose worker console buffers
|
||||
@@ -2539,7 +2578,10 @@ async function executeRenderPipeline(input: {
|
||||
// 900 floor stands, unchanged.
|
||||
const deShortBandMinFrames = envInt("HF_DE_SHORT_MIN_FRAMES", 250);
|
||||
const deShortBandMaxElements = envInt("HF_DE_SHORT_MAX_ELEMENTS", 2500);
|
||||
const compositionElementCount = countElementTags(compiled.html);
|
||||
const compositionElementCount = await resolveCompositionElementCount(
|
||||
probeSession,
|
||||
compiled.html,
|
||||
);
|
||||
// HF_DE_SHORT_MAX_ELEMENTS=0 is the documented kill switch (symmetric
|
||||
// with HF_DE_SHORT_MIN_FRAMES=0, which disables via the predicate's own
|
||||
// minFrames > 0 guard). Gated explicitly here too — without it, a fired
|
||||
|
||||
Reference in New Issue
Block a user