Skip to content

Commit 9b48391

Browse files
fix(studio): prevent negative timeline latency telemetry (heygen-com#2905)
1 parent 4677e8c commit 9b48391

2 files changed

Lines changed: 69 additions & 3 deletions

File tree

packages/studio/src/player/components/useTimelinePerformanceTelemetry.test.ts

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,28 @@
11
// @vitest-environment happy-dom
22

3-
import { describe, expect, it } from "vitest";
4-
import { summarizeTimelinePerformance } from "./useTimelinePerformanceTelemetry";
3+
import { act, createElement } from "react";
4+
import { createRoot } from "react-dom/client";
5+
import { afterEach, describe, expect, it, vi } from "vitest";
6+
7+
const trackStudioTimelinePerformance = vi.hoisted(() => vi.fn());
8+
9+
vi.mock("../../telemetry/events", () => ({
10+
trackStudioTimelinePerformance,
11+
}));
12+
13+
import {
14+
summarizeTimelinePerformance,
15+
useTimelinePerformanceTelemetry,
16+
} from "./useTimelinePerformanceTelemetry";
17+
18+
Reflect.set(globalThis, "IS_REACT_ACT_ENVIRONMENT", true);
19+
20+
afterEach(() => {
21+
trackStudioTimelinePerformance.mockReset();
22+
vi.useRealTimers();
23+
vi.restoreAllMocks();
24+
vi.unstubAllGlobals();
25+
});
526

627
describe("summarizeTimelinePerformance", () => {
728
it("reports raw mounted work and p95 scroll timings", () => {
@@ -52,3 +73,48 @@ describe("summarizeTimelinePerformance", () => {
5273
).toBeNull();
5374
});
5475
});
76+
77+
describe("useTimelinePerformanceTelemetry", () => {
78+
it("measures callback delivery when the animation-frame timestamp predates the scroll", () => {
79+
vi.useFakeTimers();
80+
let frameCallback: FrameRequestCallback | undefined;
81+
vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) => {
82+
frameCallback = callback;
83+
return 1;
84+
});
85+
vi.stubGlobal("cancelAnimationFrame", vi.fn());
86+
87+
let recordTimelineScroll: ((scroll: HTMLDivElement) => void) | undefined;
88+
function Probe() {
89+
recordTimelineScroll = useTimelinePerformanceTelemetry({
90+
totalClipCount: 1,
91+
totalRowCount: 1,
92+
zoomMode: "manual",
93+
}).recordTimelineScroll;
94+
return null;
95+
}
96+
97+
const root = createRoot(document.createElement("div"));
98+
act(() => root.render(createElement(Probe)));
99+
100+
try {
101+
vi.spyOn(performance, "now")
102+
.mockReturnValueOnce(100)
103+
.mockReturnValueOnce(108)
104+
.mockReturnValueOnce(500);
105+
106+
recordTimelineScroll?.(document.createElement("div"));
107+
frameCallback?.(99);
108+
vi.advanceTimersByTime(400);
109+
110+
expect(trackStudioTimelinePerformance).toHaveBeenCalledWith(
111+
expect.objectContaining({
112+
scroll_frame_latency_p95_ms: 8,
113+
scroll_frame_latency_max_ms: 8,
114+
}),
115+
);
116+
} finally {
117+
act(() => root.unmount());
118+
}
119+
});
120+
});

packages/studio/src/player/components/useTimelinePerformanceTelemetry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ export function useTimelinePerformanceTelemetry(context: TimelinePerformanceCont
8787
state.pendingScrollStartedAt = now;
8888
state.frameRequest = requestAnimationFrame((frameAt) => {
8989
state.frameRequest = 0;
90-
state.frameLatencies.push(frameAt - state.pendingScrollStartedAt);
90+
state.frameLatencies.push(performance.now() - state.pendingScrollStartedAt);
9191
if (state.previousFrameAt !== null) {
9292
state.frameIntervals.push(frameAt - state.previousFrameAt);
9393
}

0 commit comments

Comments
 (0)