perf(studio): stop the manual-edit loop from running while the preview is paused (#4333)

* perf(studio): stop the manual-edit reapply loop from running while paused

* fix(studio): judge nested timelines by their parent, not the player alone

* fix(studio): bound the paused-ancestor walk and ignore unknown parents

* style(studio): format the manual-edit tests
This commit is contained in:
Miguel Ángel
2026-09-23 11:10:11 -04:00
committed by GitHub
parent aabfba5445
commit df97c599ee
2 changed files with 117 additions and 0 deletions
@@ -474,6 +474,106 @@ describe("studio manual edits", () => {
expect(frames).toHaveLength(0);
});
it("does not reapply every frame while the player is paused over unpaused scene timelines", () => {
const window = new Window();
const frames: FrameRequestCallback[] = [];
let playing = false;
const previewWindow = window as unknown as Parameters<
typeof installStudioManualEditSeekReapply
>[0] & {
__player: Record<string, unknown>;
__timelines: Record<string, Record<string, unknown>>;
requestAnimationFrame: (callback: FrameRequestCallback) => number;
};
previewWindow.requestAnimationFrame = (callback: FrameRequestCallback) => {
frames.push(callback);
return frames.length;
};
previewWindow.__player = {
play: () => {
playing = true;
},
isPlaying: () => playing,
};
// A GSAP child of a paused master: not paused itself, inactive, time left.
const master = { paused: () => true };
previewWindow.__timelines = {
scene0: {
parent: master,
play: () => {},
paused: () => false,
isActive: () => false,
time: () => 0,
duration: () => 5,
},
};
expect(installStudioManualEditSeekReapply(previewWindow, () => {})).toBe(true);
expect(frames).toHaveLength(0);
(previewWindow.__player.play as () => void)();
expect(frames).toHaveLength(1);
playing = false;
frames.shift()?.(16);
expect(frames).toHaveLength(0);
});
it("tolerates a timeline whose parent chain loops", () => {
const window = new Window();
const previewWindow = window as unknown as Parameters<
typeof installStudioManualEditSeekReapply
>[0] & {
__timelines: Record<string, Record<string, unknown>>;
requestAnimationFrame: (callback: FrameRequestCallback) => number;
};
previewWindow.requestAnimationFrame = () => 1;
const loop: Record<string, unknown> = { paused: () => false };
loop.parent = loop;
previewWindow.__timelines = {
scene0: {
parent: loop,
play: () => {},
paused: () => false,
time: () => 0,
duration: () => 5,
},
};
expect(() => installStudioManualEditSeekReapply(previewWindow, () => {})).not.toThrow();
});
it("keeps reapplying while Studio drives a timeline directly and the player reads idle", () => {
const window = new Window();
const frames: FrameRequestCallback[] = [];
let paused = true;
const previewWindow = window as unknown as Parameters<
typeof installStudioManualEditSeekReapply
>[0] & {
__player: Record<string, unknown>;
__timeline: Record<string, unknown>;
requestAnimationFrame: (callback: FrameRequestCallback) => number;
};
previewWindow.requestAnimationFrame = (callback: FrameRequestCallback) => {
frames.push(callback);
return frames.length;
};
previewWindow.__player = { play: () => {}, isPlaying: () => false };
previewWindow.__timeline = {
play: () => {
paused = false;
},
paused: () => paused,
isActive: () => !paused,
time: () => 1,
duration: () => 5,
};
expect(installStudioManualEditSeekReapply(previewWindow, () => {})).toBe(true);
(previewWindow.__timeline.play as () => void)();
frames.shift()?.(16);
expect(frames).toHaveLength(1);
});
it("stops playback reapply after an unpaused timeline has completed", () => {
const window = new Window();
const frames: FrameRequestCallback[] = [];
@@ -120,6 +120,21 @@ function hasRemainingTimelineTime(owner: Record<string, unknown>): boolean {
return time < duration;
}
function hasPausedAncestor(owner: Record<string, unknown>): boolean {
let ancestor = owner.parent;
for (let depth = 0; depth < 32 && ancestor && typeof ancestor === "object"; depth++) {
const node = ancestor as Record<string, unknown>;
const paused = node.paused;
try {
if (typeof paused === "function" && paused.call(node)) return true;
} catch {
return false;
}
ancestor = node.parent;
}
return false;
}
function isTimelinePlaying(owner: Record<string, unknown> | undefined): boolean {
if (!owner) return false;
const isPlaying = owner.isPlaying;
@@ -148,6 +163,8 @@ function isTimelinePlaying(owner: Record<string, unknown> | undefined): boolean
}
}
// A GSAP child of a paused timeline reads unpaused with time left; only its ancestors know.
if (hasPausedAncestor(owner)) return false;
return hasRemainingTimelineTime(owner);
}