Frame timing
Measures the time between animation frame callbacks and counts the delivered and the dropped frames, with a frame interval that agrees with the real refresh rate.
FrameTimingMonitor measures the time between two requestAnimationFrame callbacks. From each gap, it calculates the number of frames that the page dropped. The expected frame interval comes from the recent frames, thus the interval agrees with a 120 Hz display and with a 30 fps limit.
What it measures
The monitor gives two signals:
- The frame delta (
lag_frame_delta_histogram): the time between two animation frame callbacks. - The frames (
lag_frames): one delivered frame for each callback, and the approximate number of dropped frames of each gap.
The monitor answers this question: are the frames of the page late? A frame delta of more than one frame interval shows a frame that the browser did not deliver in time.
Compare it with Long animation frames in Chromium. LoAF measures the blocking during the production of a frame. This monitor measures the delivery of frames. If LoAF shows no blocking but this monitor shows dropped frames, the cause is outside the main thread, for example the compositor or the GPU. If the two show problems, the main thread blocks the frames.
How it works
- The monitor asks for an animation frame. In the callback, it keeps the frame timestamp, which
requestAnimationFramegives to the callback, and asks for the next frame. - From the second callback, it calculates the delta: the time between the frame timestamps of this callback and of the previous callback.
- It calculates the dropped frames:
max(1, round(delta / interval)) − 1. At 60 Hz, a gap of 33 ms is 1 dropped frame, and a gap of 100 ms is 5. - It reports the delta, the frame rate (
1000 / delta), the dropped frames and the interval that it used.
The frame timestamp is the start of the frame, not the time at which the callback starts. Thus a callback that starts late in its frame does not change the delta. If the callback gets no timestamp, for example from a shim of requestAnimationFrame, the monitor reads the clock (performance.now()).
The default frame interval is automatic. It is the fifth-shortest delta of the last 600 frames (approximately 10 s at 60 Hz). The monitor ignores a delta of less than 4 ms, because two callbacks in one frame are noise, not a refresh rate. Until the window has 5 deltas, the interval is 16.67 ms (60 Hz). A burst of jank does not raise the interval, because the short deltas stay in the window.
The fifth-shortest delta, not the shortest delta, prevents false dropped frames. For example, at 60 Hz, a late frame and then an on-time frame give one delta of 10.67 ms. With an interval of 10.67 ms, each normal delta of 16.67 ms is 1 dropped frame, for the next 600 frames. With the fifth-shortest delta, 4 short deltas in the window do not change the interval.
The factory records each delta in lag_frame_delta_histogram. It adds 1 to lag_frames with outcome="delivered", and the dropped frames to lag_frames with outcome="dropped". The class also gives getDroppedFrameRate(): the dropped frames divided by the expected frames (delivered plus dropped).
Browser support
The monitor uses requestAnimationFrame and cancelAnimationFrame. The research gives no versions for them. It gives these facts about the frame rate (clocks and timers):
- Most browsers pause animation frames in background tabs and in hidden iframes.
- Safari on iOS limits animation frames to 30 fps in Low Power Mode. On ProMotion displays, Safari limits them to approximately 60 fps by default.
- The Energy Saver mode of Chrome also limits the frame rate.
- Safari throttles animation frames in cross-origin iframes that the user did not interact with (browser support).
The automatic interval agrees with these limits, thus a 30 fps limit does not count as dropped frames.
Measurement validity
With measurement conditions, the monitor pauses while the page is hidden or frozen, because the browser does not deliver animation frames there. At the restart, the first callback has no previous frame, thus no gap spans the pause.
Each frame goes to a SampleValidator with a window of the length of the delta. The validator discards a frame whose gap overlaps a hidden, frozen or suspend interval. A delta of 5000 ms or more waits 2000 ms for late evidence of a suspend.
Metrics
| Metric | Kind | Unit | Attributes | Description |
|---|---|---|---|---|
lag_frame_delta_histogram | Histogram | ms | None | The time between two animation frame callbacks. |
lag_frames | Counter | {frame} | outcome | The number of delivered frames and the estimated number of dropped frames. |
The dropped-frame rate of a fleet is the rate of lag_frames{outcome="dropped"} divided by the rate of all lag_frames. The monitor sends no events of its own. The measurement conditions send a lag.stall event for each stall episode.
Configuration
function createInstrumentedFrameTiming(
deps : CoreDeps & FrameDeps,
conditions? : MeasurementConditions,
) : MonitorHandle<FrameTimingMonitor>;
The factory uses CoreDeps (logger, clock, meter) and FrameDeps (requestAnimationFrame, cancelAnimationFrame). Give conditions to pause the monitor while the page is hidden. The factory uses the automatic interval. The class also takes a fixed targetFps, which sets the interval to 1000 / targetFps.
import { metrics } from "@opentelemetry/api";
import { createBrowserDeps, createInstrumentedFrameTiming } from "@mark1russell7/lag";
const deps = createBrowserDeps(window, { logger : console, meter : metrics.getMeter("lag") });
if (deps.requestAnimationFrame && deps.cancelAnimationFrame) {
const frames = createInstrumentedFrameTiming({
...deps,
requestAnimationFrame : deps.requestAnimationFrame,
cancelAnimationFrame : deps.cancelAnimationFrame,
});
console.log(frames.monitor?.getFrameIntervalMs(), frames.monitor?.getDroppedFrameRate());
}
Cost
- Callbacks: one animation frame callback for each frame: approximately 60 each second at 60 Hz, and 120 at 120 Hz.
- CPU: the overhead benchmark measured approximately 14 ms of main-thread time each second for the loop alone, because the page then renders each frame. It used Chromium in the new headless mode on a desktop computer (the comment of
overhead/overhead.test.ts). - Records: one histogram value and one or two counter values for each frame.
- Memory: two arrays of at most 600 deltas each: the window in the sequence of the frames, and the same deltas in sorted sequence.
- Side effects: a loop of animation frames keeps the render steps of the browser active in each frame. It also makes the idle periods of the main thread shorter (clocks and timers).
Limits
- Frame starts, not presentation. The delta is the time between the frame timestamps of two callbacks. It does not measure when the frame was on the screen.
- The dropped frames are estimates. The monitor divides each gap by the interval. A frame interval that changes in less than 600 frames, for example a variable refresh rate, makes the estimate less accurate.
- The first frame gives no value. The first frame has no previous frame.
- The monitor changes the page. The loop of animation frames keeps the render steps active and makes the idle periods shorter. The idle availability monitor shows this effect.
Tests
Unit tests:
FrameTimingMonitor.test.ts: the first frame gives no report, an on-time frame has 0 dropped frames, and a 100 ms gap at 60 Hz has 5 dropped frames. A slightly late frame (17 ms) is on time. The monitor counts the dropped frames and the dropped-frame rate, andstop()cancels the pending frame. A fixedtargetFpssets the interval. One chain of frames afterstop()andstart()in the report function, and no gap across the pause.FrameTimingMonitor.test.ts, the automatic interval: it agrees with a 120 Hz display. It does not count a 30 fps limit as dropped frames. It keeps its value through a burst of jank, and it ignores deltas of less than 4 ms. One short delta, from a late frame and an on-time frame, gives no dropped frames. The delta comes from the frame timestamps, also when some callbacks start 6 ms late. Without a timestamp, the monitor reads the clock.FrameTimingMonitor.test.ts, the rules of the interval: 16.67 ms until the window has 5 deltas, and then the fifth-shortest delta. The interval follows a change from 60 Hz to 120 Hz at the fifth short delta, and from 120 Hz to 60 Hz after 600 frames.setup-all-monitors.test.ts: the monitor pauses while the page is hidden.
Browser tests (Vitest browser mode with Playwright, in Chromium, Firefox, WebKit and Chrome, unless an item names other browsers):
lag-monitors.test.ts: on the test page, the monitor records more than 5 frame deltas.cdp/visibility.test.tsandcdp/freeze.test.ts(Chromium in the new headless mode): the monitor records no frame delta while the page is hidden or frozen.
Source
FrameTimingMonitor.ts: the monitor and the automatic interval.instrumented/frame-timing.ts: the factory.