Skip to the content

All monitors

The monitors of @mark1russell7/lag with their signals, browser APIs, browser support and cost, how setupAllMonitors selects them, and which monitor answers which question.

Each monitor measures one signal of the health of the main thread, of the page or of the device. setupAllMonitors(createBrowserDeps(window, options)) starts each monitor whose dependencies are available in the browser. You can also start one monitor alone, through its factory (createInstrumented...).

The monitors

MonitorSignalBrowser APIBrowsersCost
DriftLagThe blocked time in each window of approximately 100 msA chain of setTimeout(5)AllApproximately 175 timer callbacks each second (Chromium), 64 (Firefox and WebKit on Windows)
MacrotaskLagThe wait of a setTimeout(0) from a message tasksetInterval, MessageChannel, setTimeoutAll3 callbacks each 5 s
Scheduling fairnessThe latency of a timeout, a message and a microtaskMessageChannel, queueMicrotask, setTimeoutAll with MessageChannel and queueMicrotask5 callbacks each 5 s
Worker lagThe wait of each worker heartbeat, and hangs of 5 s or moreWeb Worker, performance.timeOrigin, IndexedDB, fetch with keepaliveAll (timeOrigin: Chromium 62, Firefox 53, Safari 15)1 heartbeat and 1 ack each second, 8 sync exchanges each 60 s
Peer hang watchThe pages that closed during a hang, seen from another page of the originBroadcastChannel, navigator.locksAll current versions1 message each second and 1 held lock for a visible page
Shared livenessThe duration of each block of 50 ms or more, from shared memorySharedArrayBuffer, AtomicsCross-origin-isolated pages: Chromium 92, Firefox 79, Safari 15.2200 reads each second in the worker
Long animation framesThe blocking duration and the duration of each frame of 50 ms or morePerformanceObserver (long-animation-frame)Chromium 1231 observer
Event TimingThe duration and the phases of each interaction event of 16 ms or morePerformanceObserver (event)Chromium 96, Firefox 144, Safari 26.21 observer
Layout shiftThe score of each layout shiftPerformanceObserver (layout-shift)Chromium 771 observer
Page-view vitalsINP, CLS, LCP, FCP and TTFB of each page viewPerformanceObserver (5 entry types, 7 with soft navigations)Chromium: all five. Firefox and Safari: all except CLS5 observers
Page-view contextThe page-view ID for the worker and for crash reportsThe worker, window.crashReportWorker: all. Crash-report context: Chromium 1451 message and 1 set() for each page view
Frame timingThe time between animation frames, and the dropped framesrequestAnimationFrameAll1 callback each frame
Idle availabilityThe idle time and the gaps between idle callbacksrequestIdleCallbackChromium 47, Firefox 551 callback for each idle period
MemoryThe used memorymeasureUserAgentSpecificMemory(), performance.memoryChromium only (89 with isolation)1 sample each 30 s
Compute pressureThe CPU pressure state of the devicePressureObserverChromium 125, desktop only1 observer
Garbage collection signalThe count of garbage collectionsFinalizationRegistryChromium 84, Firefox 79, Safari 14.11 object, 1 callback for each collection
LifecycleThe lifecycle transitions of the pagevisibilitychange, freeze, resume, pagehide, pageshow, focus, blurAll (freeze and resume: Chromium 68)7 listeners
Timer throttlingThe rounds in which the browser throttled the timerssetTimeoutAll6 timeouts each 10 s
Clock reliabilityThe resolution of performance.now()performance.now()All1 short loop for each page
Clock driftThe skew between the clocks, suspends and clock stepsDate.now(), performance.now(), performance.timeOriginAll1 callback each second
Browser reportsIntervention and deprecation reportsReportingObserverChromium 69 (two types), Firefox 149 (deprecations)1 observer

"All" means that the monitor starts in Chromium, Firefox and Safari. The versions come from browser support. The research gives no versions for the timer APIs, MessageChannel, requestAnimationFrame, IndexedDB and the lifecycle events. The costs come from the code. The timer counts of DriftLag are estimates from the measured timer steps (experiment E3). Each page gives the details.

The overhead benchmark (overhead/overhead.test.ts) measures all monitors together on an idle page, in Chromium in the new headless mode. Its budgets are 3% of one core, 210 timer callbacks and 340 wake-ups each second. The comment of the test gives 22 ms to 25 ms of main-thread time each second for all monitors, on a desktop computer. Measured alone, three loops use most of the budget each second: DriftLag 11 ms, frame timing 14 ms and idle availability 3 ms. Refer to the testing strategy.

Which monitor answers which question

"Lag" is not one quantity. Select the monitor for your question:

QuestionMonitorWhy
How long does an event that arrives at a random time wait for the main thread?Worker lag: the delivery delayThe heartbeats come on the schedule of the worker. During a long block, many heartbeats wait, and each records its own wait.
How much of each 100 ms was the main thread blocked?DriftLagEach block in a window adds its length to the lag of the window.
Which script blocked a frame?Long animation frames, only in ChromiumThe browser names the scripts of each long frame.
How long does an interaction wait for the next paint?Event Timing and page-view vitals (INP)The browser measures each interaction to the next paint, in three phases.
Did the main thread hang, also until the page closed?Worker lag: hangs and the hang journalThe worker detects a hang while it continues. A later page reports a hang that the page did not survive, except in WebKit and Safari.
Did a page close during a hang, also in WebKit and Safari?Peer hang watchAnother open page of the origin gets the Web Lock of the hung page when the page ends. It needs a second page of the origin.
How long was each block, measured from outside without messages?Shared liveness, only in cross-origin-isolated pagesThe worker reads a counter in shared memory.
Is the task queue long at this time?MacrotaskLag and scheduling fairnessA zero-delay timeout and a message wait behind the tasks in the queue.
Are the frames late?Frame timingIt measures the time between animation frames.
Does the main thread have free time?Idle availabilityIt measures the idle periods that the browser gives.
Does the page move its content, and how fast does it load?Layout shift and page-view vitalsCLS, LCP, FCP and TTFB for each page view, with the rules of web-vitals.
Is the device busy, or does the page use much memory?Compute pressure, memory and the garbage collection signalThey give the context of the system for a lag.
Are the samples valid?Lifecycle, timer throttling, clock reliability and clock driftThey give the page state, the throttling, the clock resolution and the clock discontinuities.
Did the browser block an action of the page?Browser reportsChromium sends intervention reports to the page.
Which page view hung or crashed?Page-view contextThe worker and the crash reports get the page-view ID before a hang.

The thesis explains why these quantities are different, and the event loop explains the mechanism.

How setupAllMonitors selects the monitors

setupAllMonitors(deps) makes a MonitorRegistry and adds the monitors in this sequence. It adds a monitor only when its dependencies are in deps:

StepMonitorDependencies
1Lifecycledocument and window (always necessary)
2Page-view vitalsPerformanceObserver and the lifecycle
3Measurement conditionsnone (always)
4DriftLag and MacrotaskLagthe timer functions (always necessary). MacrotaskLag uses MessageChannel where it is available.
5Timer throttlingthe timer functions (always necessary)
6Long animation frames, Event Timing and layout shiftPerformanceObserver
7Frame timingrequestAnimationFrame and cancelAnimationFrame
8Idle availabilityrequestIdleCallback and cancelIdleCallback
9Scheduling fairnessMessageChannel and queueMicrotask
10MemorymemorySource
11Worker lagworker and performance. Without performance, the setup logs a warning and skips the monitor.
12Shared livenessSharedArrayBuffer and worker
12bPeer hang watchBroadcastChannel, locks, wallClock and the lifecycle
13Compute pressurePressureObserver
14Garbage collection signalFinalizationRegistry
15Clock reliability and clock driftperformance. Clock drift also uses wallClock.
16Browser reportsReportingObserver
17Page-view contextthe page-view vitals, and the worker monitor, the peer hang watch or crashReport

The necessary dependencies are logger, clock, meter, the four timer functions, document and window. All other dependency groups are optional.

The setup also shares these parts between the monitors:

  • One absolute clock for the page, made from performance. It reads timeOrigin one time.
  • One page ID for the hang journal of the worker and for the peer hang watch (deps.pageId, or a new random ID).
  • One set of measurement conditions for the timer-driven monitors.
  • An event sink that adds lag.page_view.id to each event, when the page-view vitals operate.
  • In a cross-origin-isolated page with a worker: a SharedArrayBuffer, and a DriftLag timer that beats the liveness counter.

Each factory has an error boundary. If the construction of a monitor fails, its handle has no monitor, and the logger gets the warning Failed to create the "<name>" monitor. The other monitors start. A monitor with an observer starts also when the browser does not have its entry type: it logs a warning and gets no entries. For example, Long animation frames start in Firefox, but they get no entries there.

The browser adapter

createBrowserDeps(window, options) gets the dependencies from the browser globals. It examines each optional API, and it binds each browser function to its object. These rules apply:

  • SharedArrayBuffer only when crossOriginIsolated is true and the option sharedMemory is not false.
  • The hang journal in IndexedDB only with a worker, and only when the option hangJournal is not false.
  • crashReport only when window.crashReport has a set() function and the option crashReportContext is not false.
  • BroadcastChannel and locks only when the browser has both, and only when the option peerHangWatch is not false. locks.request is bound to navigator.locks.
  • memorySource only when the browser has performance.memory or measureUserAgentSpecificMemory().
  • pressureSources with the default ["cpu"].

An example

import { init } from "@mark1russell7/otel-ts";
import { createBrowserDeps, createOtelEventSink, createOtelLoggerAdapter, setupAllMonitors } from "@mark1russell7/lag";
import { createLagWorker } from "@mark1russell7/lag/worker";

const otel = init({ serviceName : "shop", endpoint : "http://localhost:4318" });
const worker = createLagWorker();
const monitors = setupAllMonitors(createBrowserDeps(window, {
    logger : createOtelLoggerAdapter(otel.getLogger("lag")),
    meter : otel.getMeter("lag"),
    events : createOtelEventSink(otel.getLogger("lag-events")),
    worker,
}));
// Record the values that wait for a checkpoint, before each flush
otel.onBeforeFlush(() => monitors.flush());

// The monitors that started in this browser
const started = monitors.registry.getAll().filter(handle => handle.monitor !== undefined);
console.log(started.map(handle => handle.name));

AllMonitorHandles gives the registry, stop(), flush() and one accessor for each monitor, for example driftLag or workerMonitor. An accessor gives undefined for a monitor that did not start. stop() stops the monitors in the reverse sequence of their start. flush() records the values that wait for a checkpoint (the page-view vitals). Connect it to the before-flush hook of the exporter: refer to lifecycle.

To keep a monitor off, remove its dependency before the setup:

import { metrics } from "@opentelemetry/api";
import { createBrowserDeps, setupAllMonitors } from "@mark1russell7/lag";

const deps = createBrowserDeps(window, { logger : console, meter : metrics.getMeter("lag") });
// Without its dependencies, the idle monitor does not start
const { requestIdleCallback : _idle, cancelIdleCallback : _cancelIdle, ...withoutIdle } = deps;
const monitors = setupAllMonitors(withoutIdle);
console.log(monitors.idleMonitor === undefined);

Measurement conditions

The timer-driven monitors share one MeasurementConditions. It has two functions:

  • pauseWhileHidden(monitor) stops a monitor while the page is hidden or frozen, and starts it again when the page is visible.
  • The SampleValidator discards each sample whose window overlaps a hidden, frozen or suspend interval. It holds a sample of 5000 ms or more for 2000 ms, until it knows if the sample was a hang or a suspend.
MonitorPauses while hiddenSample validator
DriftLag, MacrotaskLag, scheduling fairnessyesyes
Worker lag (the delivery delay), shared livenessyesyes
Frame timing, idle availabilityyesyes
All other monitorsnono

Two monitors give the evidence of a suspend. The worker gives a heartbeat for which the worker timer was 5 s late or more. The clock-drift monitor gives a forward jump of the wall clock of 1 s or more. Measurement validity explains the rules. The conditions record these metrics:

MetricKindUnitAttributesDescription
lag_samples_discardedCounter{sample}reasonThe number of samples that a monitor did not record because the measurement window was not valid.
lag_stallsCounter{stall}kindThe number of stall episodes: very long samples (5000 ms or more) of all monitors whose windows overlap count as one episode. A hang has no evidence of a suspend. A suspend has evidence that the system stopped.
lag_stall_duration_histogramHistogrammskindThe duration of each stall episode: its longest sample.

They also send a lag.stall event for each stall episode, with the attributes kind (hang or suspend) and duration_ms.

Source

lag: Main-thread responsiveness monitoring for browser apps, exported as OpenTelemetry metrics.

To change a page, edit its file in packages/site/content/. The writing style guide tells you how.

An AI model (Claude, from Anthropic) wrote most of the text and the code of this site and of the library, under the direction of the author. The tests and the STE linter examine them. The writing standard gives the reason for this note.