diff --git a/src/livekit/BackgroundEffectsBench.tsx b/src/livekit/BackgroundEffectsBench.tsx
index d67c01325..530e787ea 100644
--- a/src/livekit/BackgroundEffectsBench.tsx
+++ b/src/livekit/BackgroundEffectsBench.tsx
@@ -69,6 +69,10 @@ interface Run {
buckets: Bucket[];
/** Worst observed delay on a 100ms timer: the main thread's own stutter. */
worstTimerLagMs: number;
+ /** The longest the picture simply stopped, in milliseconds. */
+ worstFrameGapMs: number;
+ /** From asking for the run to the first frame on screen. */
+ firstFrameMs: number | null;
frames: number;
seconds: number;
}
@@ -225,8 +229,10 @@ export const BackgroundEffectsBench: FC = () => {
{run.effect}: mean {run.meanFps.toFixed(1)}fps, worst{" "}
{run.worstFps.toFixed(1)}fps
- {run.startupMs !== null && <> , started in {run.startupMs}ms>}, main
- thread stalled up to {run.worstTimerLagMs}ms
+ {run.startupMs !== null && <>, pipeline ready in {run.startupMs}ms>}
+ , first frame at {run.firstFrameMs ?? "?"}ms, picture stopped for up
+ to {run.worstFrameGapMs}ms, main thread stalled up to{" "}
+ {run.worstTimerLagMs}ms
))}
{runs.length > 0 && (
@@ -255,16 +261,20 @@ async function countFrames(
| "worstFps"
| "buckets"
| "worstTimerLagMs"
+ | "worstFrameGapMs"
+ | "firstFrameMs"
| "frames"
| "seconds"
>
> {
return new Promise((resolve) => {
const buckets: Bucket[] = [];
+ const startedAt = performance.now();
let frames = 0;
let inBucket = 0;
- let started: number | null = null;
- let bucketStarted = 0;
+ let firstFrameMs: number | null = null;
+ let lastFrameAt = startedAt;
+ let worstFrameGapMs = 0;
// A phone that is struggling stalls its main thread, and a timer is the
// cheapest witness to that: Safari has no long-task observer.
@@ -279,19 +289,28 @@ async function countFrames(
lastTick = now;
}, 100);
+ // On the clock, not on the frames. Closing a bucket only when a frame
+ // arrives cannot report a stretch with no frames in it at all — the first
+ // measurement on a phone spent fourteen seconds inside one bucket and
+ // reported it as a seventh of a frame per second rather than as a freeze.
+ const bucketer = window.setInterval(() => {
+ const at = Math.round((performance.now() - startedAt) / 1000);
+ buckets.push({ at, fps: inBucket / bucketSeconds });
+ inBucket = 0;
+ onProgress(at);
+ }, bucketSeconds * 1000);
+
const finish = (): void => {
window.clearInterval(ticker);
- const elapsed =
- started === null ? 0 : (performance.now() - started) / 1000;
+ window.clearInterval(bucketer);
+ const elapsed = (performance.now() - startedAt) / 1000;
resolve({
meanFps: elapsed > 0 ? frames / elapsed : 0,
- worstFps: buckets.length
- ? Math.min(...buckets.map((b) => b.fps))
- : elapsed > 0
- ? frames / elapsed
- : 0,
+ worstFps: buckets.length ? Math.min(...buckets.map((b) => b.fps)) : 0,
buckets,
worstTimerLagMs,
+ worstFrameGapMs: Math.round(worstFrameGapMs),
+ firstFrameMs: firstFrameMs === null ? null : Math.round(firstFrameMs),
frames,
seconds: Math.round(elapsed),
});
@@ -299,37 +318,33 @@ async function countFrames(
const onFrame = (): void => {
const now = performance.now();
- if (started === null) {
- started = now;
- bucketStarted = now;
- }
+ firstFrameMs ??= now - startedAt;
+ worstFrameGapMs = Math.max(worstFrameGapMs, now - lastFrameAt);
+ lastFrameAt = now;
frames += 1;
inBucket += 1;
- if (now - bucketStarted >= bucketSeconds * 1000) {
- buckets.push({
- at: Math.round((now - started) / 1000),
- fps: inBucket / ((now - bucketStarted) / 1000),
- });
- onProgress(Math.round((now - started) / 1000));
- inBucket = 0;
- bucketStarted = now;
- }
- if (now - started >= seconds * 1000) return finish();
+ if (now - startedAt >= seconds * 1000) return finish();
element.requestVideoFrameCallback(onFrame);
};
if (!("requestVideoFrameCallback" in element)) {
window.clearInterval(ticker);
+ window.clearInterval(bucketer);
resolve({
meanFps: 0,
worstFps: 0,
buckets: [],
worstTimerLagMs: 0,
+ worstFrameGapMs: 0,
+ firstFrameMs: null,
frames: 0,
seconds: 0,
});
return;
}
element.requestVideoFrameCallback(onFrame);
+ // A freeze can outlast the run: without this, a device that never presents
+ // another frame never finishes and reports nothing at all.
+ window.setTimeout(finish, seconds * 1000 + 2000);
});
}