Files
hyperframes/packages/engine/src/services/riskFrameCapture.test.ts
T
Miguel Ángel c50918277a perf(engine): fast capture stays on for frames a shadow, blur or clip effect does not touch (#6318)
* perf(engine): fast capture stays on for frames a shadow, blur or clip effect does not touch

A single drop-shadow, blur, backdrop-filter or blend element, or one clip-path or mask
tween, sent the whole render to screenshot capture, 2-3x slower than drawElement. The two
init gates now name the frames the effect can show on. Only those frames capture by
screenshot, in a second browser opened on the first of them; every other frame keeps
drawElement.

* fix(engine): scan effects in a throwaway session; hand risk frames over only where captured

The effect scan seeked the render's own page to the end, leaving its timeline state ahead of
the render. It now runs in a throwaway session with the render's own seek. A hidden effect
element with a visible descendant counts as shown. Per-frame capture is opt-in per caller
(deFrameFallback), so disk, segmented, chunk and parallel-worker sessions keep the
whole-render fallback, and a route that cannot hand frames over drops drawElement up front.

* fix(engine): a timeline callback keeps the whole-render fallback; trim comment share

A tl.call or onComplete can add an effect's class to any element mid-render, so the elements
found at the start are not the only ones it can reach. Such a timeline is unbounded and keeps
the whole-render screenshot fallback.

* style(engine): shorten the live-frame scan comment

* perf(engine): after a timeline callback, not before, every frame may show the effect

A callback can add an effect's class to any element, so only the frames from the first
callback on lose their element bounds. Frames before it keep the scanned bounds, so a late
callback no longer sends the whole render to screenshot capture.

* perf(engine): open the second screenshot browser only when memory is available for it

Per-frame capture needs a second browser (479 MB measured at 1080p). When the render would
leave less than 1.5 GB available after it, the render keeps the whole-render screenshot
fallback instead. getAvailableMemoryMb moves from the CLI to the engine's system-memory
module, which already owns the host's memory readings; os.freemem() reads 367 MB on this
49 GB Mac, so the guard uses free, inactive, speculative and purgeable pages.

* fix(engine): budget the whole 1 GB per-frame capture adds, not just the second browser

The render's process tree goes from 1002 to 2001 MB at 1080p: the screenshot browser (479 MB)
plus a drawElement browser that is itself about 500 MB larger than a screenshot one. The guard
budgeted 500 MB. It now budgets the 1 GB measured, scaled by output pixels.
2026-10-25 19:15:18 +00:00

37 lines
1.3 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { captureFrameToBufferPipelined, captureFramesBatchPipelined } from "./frameCapture.js";
import { isDrawElementCaptureError } from "./drawElementCaptureError.js";
import type { CaptureSession } from "./frameCapture.js";
// A drawElement path handed a risk frame must fail the attempt before touching the page, so a
// caller that ignores deRiskFrames re-renders by screenshot instead of shipping damaged frames.
const session = {
deRiskFrames: new Set([5]),
isInitialized: true,
options: { fps: { num: 30, den: 1 }, width: 1920, height: 1080 },
page: new Proxy(
{},
{
get() {
throw new Error("the page must not be touched");
},
},
),
} as unknown as CaptureSession;
describe("drawElement capture of a risk frame", () => {
it("is refused by the per-frame pipelined path", async () => {
const failure = await captureFrameToBufferPipelined(session, 5, 5 / 30).catch((e) => e);
expect(isDrawElementCaptureError(failure)).toBe(true);
});
it("is refused by the batch path when any frame of the batch is a risk frame", async () => {
const failure = await captureFramesBatchPipelined(
session,
[3, 4, 5, 6],
[0.1, 0.13, 0.17, 0.2],
).catch((e) => e);
expect(isDrawElementCaptureError(failure)).toBe(true);
});
});