mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
perf(studio): films with thousands of timed clips stop re-reading every clip each frame (#6805)
* perf(studio): films with thousands of timed clips stop re-reading every clip each frame * perf(studio): drop the added comment lines the ratchet counts
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import { initSandboxRuntimeModular } from "./init";
|
||||
import { createMockTimeline, resetRuntimeFixtureDom } from "./runtimeSeekFixture.test-helpers";
|
||||
|
||||
afterEach(() => {
|
||||
window.__hfRuntimeTeardown?.();
|
||||
vi.restoreAllMocks();
|
||||
resetRuntimeFixtureDom();
|
||||
});
|
||||
|
||||
it("reads a far clip's start once while playing, not on every frame", () => {
|
||||
resetRuntimeFixtureDom();
|
||||
let now = 0;
|
||||
let id = 0;
|
||||
const callbacks = new Map<number, FrameRequestCallback>();
|
||||
vi.spyOn(performance, "now").mockImplementation(() => now);
|
||||
vi.spyOn(window, "requestAnimationFrame").mockImplementation((callback) => {
|
||||
callbacks.set(++id, callback);
|
||||
return id;
|
||||
});
|
||||
vi.spyOn(window, "cancelAnimationFrame").mockImplementation((key) => {
|
||||
callbacks.delete(key);
|
||||
});
|
||||
const step = (frames: number) => {
|
||||
for (let i = 0; i < frames; i++) {
|
||||
now += 33;
|
||||
const pending = [...callbacks.values()];
|
||||
callbacks.clear();
|
||||
for (const callback of pending) callback(now);
|
||||
}
|
||||
};
|
||||
const words = Array.from(
|
||||
{ length: 300 },
|
||||
(_, i) => `<span id="w${i}" class="clip" data-start="${5 + i * 0.1}" data-duration="1"></span>`,
|
||||
);
|
||||
document.body.innerHTML = `<div data-composition-id="main" data-root="true" data-duration="60">${words.join("")}</div>`;
|
||||
window.__timelines = { main: createMockTimeline(60) };
|
||||
initSandboxRuntimeModular();
|
||||
const player = window.__player!;
|
||||
const far = document.getElementById("w299")!;
|
||||
const startReads = vi.fn();
|
||||
const getAttribute = far.getAttribute.bind(far);
|
||||
far.getAttribute = (name: string) => {
|
||||
if (name === "data-start") startReads();
|
||||
return getAttribute(name);
|
||||
};
|
||||
|
||||
player.play();
|
||||
step(5);
|
||||
const afterWarmUp = startReads.mock.calls.length;
|
||||
step(60);
|
||||
|
||||
expect(player.isPlaying()).toBe(true);
|
||||
expect(startReads.mock.calls.length - afterWarmUp).toBe(0);
|
||||
});
|
||||
@@ -2948,12 +2948,23 @@ export function initSandboxRuntimeModular(): void {
|
||||
// A clip's first visible style pass is slow; playing, each hidden clip gets it once just before its start.
|
||||
const clipsNeverStyledVisible = new Set<StylableElement>();
|
||||
const clipsStyledVisible = new WeakSet<Element>();
|
||||
const styleClipsDueVisibleOnce = (t: number) => {
|
||||
let neverStyledStarts = { revision: -1, starts: new Map<StylableElement, number>() };
|
||||
const neverStyledStart = (el: StylableElement, revision: number): number => {
|
||||
if (neverStyledStarts.revision !== revision)
|
||||
neverStyledStarts = { revision, starts: new Map() };
|
||||
let start = neverStyledStarts.starts.get(el);
|
||||
if (start === undefined) {
|
||||
start = resolveStartForElement(el, Number.NaN);
|
||||
neverStyledStarts.starts.set(el, start);
|
||||
}
|
||||
return start;
|
||||
};
|
||||
const styleClipsDueVisibleOnce = (t: number, revision: number) => {
|
||||
if (clipsNeverStyledVisible.size === 0 || !clock.isPlaying()) return;
|
||||
const clips: StylableElement[] = [];
|
||||
for (const el of clipsNeverStyledVisible) {
|
||||
const start = resolveStartForElement(el, Number.NaN);
|
||||
if (el.isConnected && el.hasAttribute(CLIP_HIDDEN_ATTR) && !(start <= t + LOOKAHEAD_SECONDS))
|
||||
const start = neverStyledStart(el, revision);
|
||||
if (!(start <= t + LOOKAHEAD_SECONDS) && el.isConnected && el.hasAttribute(CLIP_HIDDEN_ATTR))
|
||||
continue;
|
||||
clipsNeverStyledVisible.delete(el);
|
||||
if (el.isConnected && el.hasAttribute(CLIP_HIDDEN_ATTR) && start > t) clips.push(el);
|
||||
@@ -5290,7 +5301,7 @@ export function initSandboxRuntimeModular(): void {
|
||||
const revision = opts?.revision ?? readCompositionTimingRevision();
|
||||
if (revision !== tweensRenderedThrough.revision)
|
||||
tweensRenderedThrough = { revision, time: Number.NEGATIVE_INFINITY };
|
||||
styleClipsDueVisibleOnce(t);
|
||||
styleClipsDueVisibleOnce(t, revision);
|
||||
const firstRenders = t > tweensRenderedThrough.time;
|
||||
// Only clips holding a tween that renders for the first time on this seek need their authored style read.
|
||||
const firstRendered =
|
||||
|
||||
@@ -167,3 +167,24 @@ describe("live DOM manifest hydration", () => {
|
||||
expect(findTimelineDomNodeForClip(doc, clip({ start: 9 }), 0)?.id).toBe("stable");
|
||||
});
|
||||
});
|
||||
|
||||
describe("selector occurrence indexes while hydrating many clips", () => {
|
||||
it("queries the document once for a class many clips share, not once per clip", () => {
|
||||
const count = 200;
|
||||
const spans = Array.from(
|
||||
{ length: count },
|
||||
(_, i) =>
|
||||
`<span class="clip w" data-start="${i}" data-duration="1" data-track-index="0"></span>`,
|
||||
).join("");
|
||||
const doc = documentWith(spans);
|
||||
const query = vi.spyOn(doc, "querySelectorAll");
|
||||
const clips = Array.from({ length: count }, (_, i) => clip({ tagName: "span", start: i }));
|
||||
|
||||
const elements = buildTimelineElementsFromClips(clips, doc);
|
||||
|
||||
expect(elements).toHaveLength(count);
|
||||
expect(query.mock.calls.filter(([selector]) => selector === ".w").length).toBeLessThanOrEqual(
|
||||
1,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
* an argument, so each is callable — and readable — on its own.
|
||||
*/
|
||||
|
||||
import { withSelectorIndexPass } from "../../utils/sourceScopedSelectorIndex";
|
||||
import { createTimelineDomNodeResolver, findClipElementById } from "../lib/timelineElementHelpers";
|
||||
import { usePlayerStore } from "../store/playerStore";
|
||||
import type { TimelineElement, DomClipChild, SubCompositionHostState } from "../store/playerStore";
|
||||
@@ -214,15 +215,17 @@ export function buildTimelineElementsFromClips(
|
||||
iframeDoc: Document | null,
|
||||
): TimelineElement[] {
|
||||
const resolveHost = iframeDoc ? createTimelineDomNodeResolver(iframeDoc) : null;
|
||||
return clips.map((clip, index) => {
|
||||
const hostEl = resolveHost?.(clip, index) ?? null;
|
||||
return createTimelineElementFromManifestClip({
|
||||
clip,
|
||||
fallbackIndex: index,
|
||||
doc: iframeDoc,
|
||||
hostEl,
|
||||
const build = () =>
|
||||
clips.map((clip, index) => {
|
||||
const hostEl = resolveHost?.(clip, index) ?? null;
|
||||
return createTimelineElementFromManifestClip({
|
||||
clip,
|
||||
fallbackIndex: index,
|
||||
doc: iframeDoc,
|
||||
hostEl,
|
||||
});
|
||||
});
|
||||
});
|
||||
return iframeDoc ? withSelectorIndexPass(iframeDoc, build) : build();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
import type { TimelineElement } from "../store/playerStore";
|
||||
import type { ClipManifestClip, IframeWindow, TimelineLike } from "./playbackTypes";
|
||||
import { withSelectorIndexPass } from "../../utils/sourceScopedSelectorIndex";
|
||||
import { resolveCssStackingContextId } from "@hyperframes/core/runtime/stacking-context";
|
||||
import { readClipTiming } from "@hyperframes/core/composition-contract";
|
||||
import { readMasterFxChain } from "@hyperframes/core/audio-groups";
|
||||
@@ -234,6 +235,14 @@ export function parseTimelineFromDOM(
|
||||
doc: Document,
|
||||
rootDuration: number,
|
||||
timelines?: Readonly<Record<string, TimelineLike>>,
|
||||
): TimelineElement[] {
|
||||
return withSelectorIndexPass(doc, () => scanTimelineFromDOM(doc, rootDuration, timelines));
|
||||
}
|
||||
|
||||
function scanTimelineFromDOM(
|
||||
doc: Document,
|
||||
rootDuration: number,
|
||||
timelines?: Readonly<Record<string, TimelineLike>>,
|
||||
): TimelineElement[] {
|
||||
const rootComp = doc.querySelector("[data-composition-id]");
|
||||
const nodes = doc.querySelectorAll("[data-start]");
|
||||
|
||||
Reference in New Issue
Block a user