Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions packages/core/src/runtime/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1439,16 +1439,16 @@ export function initSandboxRuntimeModular(): void {
// second cache checking for itself would be told nothing changed.
let compositionTimingRevision = 0;

const queryCache = new Map<string, { revision: number; nodes: Element[] }>();
const queryCache = new Map<string, Element[]>();
const queryForTransport = (selector: string): Element[] => {
if (!compositionTimingObserver || renderCaptureSeekStarted)
return Array.from(document.querySelectorAll(selector));
if (recordsMoveTiming(compositionTimingObserver.takeRecords()))
invalidateCompositionTimingCaches();
const cached = queryCache.get(selector);
if (cached?.revision === compositionTimingRevision) return cached.nodes;
if (cached) return cached;
const nodes = Array.from(document.querySelectorAll(selector));
queryCache.set(selector, { revision: compositionTimingRevision, nodes });
queryCache.set(selector, nodes);
return nodes;
};

Expand All @@ -1461,6 +1461,7 @@ export function initSandboxRuntimeModular(): void {

const invalidateCompositionTimingCaches = () => {
compositionTimingRevision += 1;
queryCache.clear();
// The same records that stale the duration caches are what a parked
// transport is waiting for: a timing attribute edited, a sub-composition
// mounted, media metadata arriving. One signal, two readers.
Expand Down Expand Up @@ -2965,10 +2966,10 @@ 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>();
let neverStyledStarts = { revision: -1, starts: new Map<StylableElement, number>() };
let neverStyledStarts = { revision: -1, starts: new WeakMap<StylableElement, number>() };
const neverStyledStart = (el: StylableElement, revision: number): number => {
if (neverStyledStarts.revision !== revision)
neverStyledStarts = { revision, starts: new Map() };
neverStyledStarts = { revision, starts: new WeakMap() };
let start = neverStyledStarts.starts.get(el);
if (start === undefined) {
start = resolveStartForElement(el, Number.NaN);
Expand Down Expand Up @@ -3407,7 +3408,12 @@ export function initSandboxRuntimeModular(): void {
style.setProperty("visibility", shown, isVisibleNow ? "" : "important");
}
rawNode.toggleAttribute(CLIP_HIDDEN_ATTR, !isRuntimeStamped && !isVisibleNow);
if (!isRuntimeStamped && !isVisibleNow && !clipsStyledVisible.has(rawNode))
if (
!isRuntimeStamped &&
!isVisibleNow &&
!clipsStyledVisible.has(rawNode) &&
rawNode.isConnected
)
clipsNeverStyledVisible.add(rawNode);
const upcoming =
(hiddenImagesSkipped || mediaDeferred) &&
Expand Down Expand Up @@ -4672,6 +4678,8 @@ export function initSandboxRuntimeModular(): void {
stopSceneAnimations([host]);
const placeholder = parkScene(name);
if (placeholder) moveDisplayHide(host, placeholder);
for (const clip of clipsNeverStyledVisible)
if (!clip.isConnected) clipsNeverStyledVisible.delete(clip);
parkedNames.add(name);
releaseDetachedMedia();
webAudio.releaseDecodedExcept(webAudioMediaIn(document));
Expand Down
33 changes: 23 additions & 10 deletions packages/producer/src/services/coreRuntimeBrowser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1597,20 +1597,23 @@ describe("core runtime browser contract", () => {
const builtAsItRuns = (i: number) =>
`const tl = gsap.timeline({ paused: true }); tl.to('[data-composition-id="s${i}"] b', { opacity: 0.5, duration: 1 }); window.__timelines["s${i}"] = tl;`;
it.each([
["its timeline built as the script runs", builtAsItRuns],
["its timeline built as the script runs", builtAsItRuns, true],
["its timeline built as the script runs, scrubbed while paused", builtAsItRuns, false],
[
"its timeline driving one built after a font wait",
(i: number) =>
`const tl = gsap.timeline({ paused: true }); Promise.all([document.fonts.load("16px serif")]).then(() => { const inner = gsap.timeline({ paused: true }); inner.to('[data-composition-id="s${i}"] b', { opacity: 0.5, duration: 1 }); tl.add(inner.tweenFromTo(0, 1), 0); window.__timelines["s${i}"] = tl; });`,
true,
],
[
"a timeline built after a font wait to pose it once",
(i: number) =>
`const tl = gsap.timeline({ paused: true }); window.__timelines["s${i}"] = tl; Promise.all([document.fonts.load("16px serif")]).then(() => { const pose = gsap.timeline({ paused: true }); pose.to('[data-composition-id="s${i}"] b', { opacity: 0.5, duration: 1 }); pose.time(0.5); });`,
true,
],
])(
"lets go of each scene the preview mounts and unmounts while it plays, its captured audio included (%s)",
async (_, sceneScript) => {
] as const)(
"lets go of each scene the preview mounts and unmounts, its captured audio included (%s)",
async (_, sceneScript, plays) => {
const dir = mkdtempSync(resolve(tmpdir(), "scene-window-memory-"));
// Its own browser: Web Audio only captures media where the context may start without a gesture.
const audioBrowser = await puppeteer.launch({
Expand Down Expand Up @@ -1659,8 +1662,7 @@ describe("core runtime browser contract", () => {
await page.waitForFunction(() => window.__playerReady && window.__renderReady);
const cdp = await page.createCDPSession();
await cdp.send("Performance.enable");
// DOM nodes alive but out of the page, after a full collection.
const kept = async () => {
const keptOnce = async () => {
await cdp.send("HeapProfiler.collectGarbage");
const { metrics } = await cdp.send("Performance.getMetrics");
const inPage = await page.evaluate(() => {
Expand All @@ -1671,19 +1673,30 @@ describe("core runtime browser contract", () => {
});
return metrics.find((m) => m.name === "Nodes")!.value - inPage;
};
// DOM nodes alive but out of the page. One collection can leave a freed scene unswept;
// a held one survives every collection.
const kept = async () => {
let count = await keptOnce();
for (let round = 0; round < 5; round++) {
const next = await keptOnce();
if (next >= count) return next;
count = next;
}
return count;
};
const before = await kept();
await page.evaluate(async () => {
await page.evaluate(async (plays) => {
const player = window.__player as unknown as {
play: () => void;
seek: (time: number, options: { keepPlaying: boolean }) => void;
};
player.play();
if (plays) player.play();
for (const time of [2.5, 4.5, 6.5, 8.5, 10.5]) {
player.seek(time, { keepPlaying: true });
player.seek(time, { keepPlaying: plays });
await window.__hfWaitForSeekCompletion?.();
await new Promise((settle) => setTimeout(settle, 200));
}
});
}, plays);
// The window unmounts only once it has mounted what is ahead.
await page.waitForFunction(
() => document.querySelectorAll("[data-hf-parked-scene]").length >= 5,
Expand Down
Loading