Historical recipe — not current setup guidance. Measurements and implementation links below belong to their original workload and revision. Deployment labels and defaults may be obsolete. Use the reviewed performance recipes; browse historical evidence.

AVPlumber · Mixer optimization cookbook

Browser frame ticks staggered at 60 fps

Implementation links are pinned to develop at c20464a5 (2026-10-09); read against develop at 8fa15c92 on 2026-10-13 and still current. Measurements retain their original workload and scope. Watch the current 192-input L4 demo.

Current scope. Staggering remains opt-in. The runnable-thread measurements below belong to the stated 60 fps workload; zero deadlines before and after does not establish extra admission capacity.

At 60 fps every browser window of every worker started its frame at the same instant. Once per 16,666 us the count of runnable threads spiked to about 57 on 16 vCPUs for about 2 ms, against about 6 for the rest of the period; the one-minute load average floated between about 9 and 23 while CPU use stayed at 53 %. DMA_BROWSER_STAGGER_FRAMES=1 starts each browser worker's frames at its own point of the period: the spike's peak fell to 22.9, the mean runnable count from 14.3 to 9.8 and the threads waiting for a CPU from 4.55 to 1.30, with 0 missed deadlines before and after. The one-minute load average is not a capacity gauge for this workload.

Status: Opt-in, measured on L4
Runnable threads against the phase of the browsers' 16,666 us clock, before and with the stagger Two panels on one scale, each the runnable-thread count folded on 16,666,000 ns of the monotonic clock in 50 bins of 0.33 ms. Before: about 6 runnable threads for most of the period and one spike right after the period start, 51.1 in the second bin, peak 57 at 0.67 ms, 1.67 ms wide at half height, far above the 16 vCPUs. With the stagger: five bumps, one per browser worker, each just after one of the phases the rule gives the workers, 0, 1/8, 1/4, 1/2 and 3/4 of the period; the highest is 22.9, the others 12 to 16, over a floor of 6.4. Before · every 60 fps window of every worker ticks at phase 0 60 0 16 vCPUs 0 ms 4 8 12 16.666 ms · next tick peak 57 · 1.7 ms wide at half height about 6 runnable for the rest of the period With the stagger · each worker ticks at its own phase 60 0 16 vCPUs 0 ms 4 8 12 16.666 ms · next tick phase 0 1/8 1/4 1/2 3/4 peak 22.9 Before: mean 14.3 runnable, more than 16 for 27.9 % of the time; 4.55 threads waiting for a CPU on average. With the stagger: mean 9.8, more than 16 for 15.0 % of the time; 1.30 waiting.
Measured: the runnable-thread count, read at Poisson-timed instants (100 a second on average) for 20 minutes per run and folded on 16,666,000 ns of CLOCK_MONOTONIC, in 50 bins of 0.33 ms; phase 0 is a multiple of that period. Both panels have the same scale. The dashed marks in the second panel are not measured: they are the phases the rule gives the five workers (0, 1/8, 1/4, 1/2 and 3/4 of the period). Red tint: more runnable threads than vCPUs.

The symptom

On the 88-source 60 fps show, the one-minute load average floated between about 9 and 23 on 16 vCPUs while CPU use stayed at 53 %, with no missed deadline and no dropped output. A load average above the CPU count reads as an overloaded host; half the CPUs idle says the opposite.

What was measured

The show: 88 sources at 1080p60, HLG 10-bit 4:2:2: 44 NVDEC file decodes, 4 raw v210 uploads and 40 browser windows (36 test pages and 4 key pages) in 5 browser workers of 8 windows; 256 scenes, 14 aux buses and 17 encoded outputs.

A passive sampler read the kernel's runnable-thread count (nr_running in /proc/loadavg) for 20 minutes at Poisson-timed instants, 100 a second on average: random spacing cannot alias with a frame clock, which a fixed-rate sampler can. It also read each thread's schedstat at the start and the end, which gives the time every thread ran and the time it waited for a CPU. Before the fix:

Why the load average floats

Two things move the number, and only one of them is load.

  1. The artefact: the kernel samples, it does not average. The load average is built from one instantaneous count of runnable threads every LOAD_FREQ = 5 × HZ + 1 ticks, 5.001 s on this kernel (CONFIG_HZ = 1000). The measured update period is 5.001 s; the update instants rule out 5.000, 5.002 and 5.004 s. Against a 16,666 us period, each sampling instant falls 1.2 ms later in the period than the one before, so the sampling walks through the spike once every 69.4 s (the periodogram of the kernel's samples peaks at 68.5 s). The kernel's point samples ranged from 0 to 78 (sd 15.9), while the real mean of each 5 s ranged from 11.7 to 19.0 (sd 1.6). Their correlation is 0.13; the point samples follow the fold curve at their own phase instead (correlation 0.81). load1 read 8.8 to 23.6; a one-minute average of the same data, taken over time, stays between 13.1 and 16.4.
  2. The real part: two clocks that slide past each other. The browsers' period is 16,666 us and the mixer's frame is 16,666.67 us, so the browsers' spike slides across the mixer's frame once per 417 s. While it lands on the mixer's own per-frame work, more threads really do wait: the real one-minute average rises from about 13.3 to about 16.2 for roughly 1.5 minutes.
The kernel's point samples against the real 5-second means, first 60 samples of the run before the fix Sixty consecutive samples, 5.001 s apart. The kernel's single counts of runnable threads swing between 0 and 77 and peak about every 69.4 s, when the sampling instant walks through the spike. The mean runnable count over the same 5 seconds stays between 12.0 and 16.2. Over the whole 20 minutes the point samples have a standard deviation of 15.9, the real means 1.6, and their correlation is 0.13. Before the fix · what the kernel sampled for its load average, and the real mean one count of runnable threads, taken by the kernel every 5.001 s mean runnable count over the same 5 s 80 0 16 vCPUs 69.4 s: the sampling instant walks through the spike once 0 s 60 120 180 240 300 s Point samples over the 20 minutes: 0 to 78, sd 15.9. They made load1 read 8.8 to 23.6. Real 5 s means: 11.7 to 19.0, sd 1.6. A one-minute average of them stays between 13.1 and 16.4.
Measured, first 60 of the 236 samples of the run before the fix. Red: the kernel's point samples, recovered from successive load1 readings by undoing the kernel's one-minute decay, so they carry the rounding of load1's two decimals (a sample that came out just below zero is drawn at 0). Blue: the mean of the sampler's own readings over the same 5 s. The figures under the plot are over the whole 20 minutes.

What to read instead. The one-minute load average is not a capacity gauge for a frame-locked workload: it is a point sample that beats against the frame clock. Read load15, CPU pressure (PSI, /proc/pressure/cpu), the compositors' missed deadlines, or a runnable count averaged over time. How to see it on a host has the commands.

The cause in Chromium

Read from the source of Electron 44.5.1 and Chromium 152; not traced on the host.

A 60 fps window sets its rate before its display exists, and its interval equals the default. The flag stays unset, the display never gets the window's timebase, and Chromium keeps timebase zero for it: every 60 fps window of every process ticks on multiples of 16,666 us of CLOCK_MONOTONIC. At 25, 30 and 50 fps the interval differs from the default, so each window ticks from the moment its rate was set and the windows are already apart.

The fix and its switch

DMA_BROWSER_STAGGER_FRAMES=1 in the browser service, off by default; docker-compose/mixer/compose.yaml passes the variable through. With it on:

Before and after

Two runs of 20 minutes on the same show: one before the fix, and 20 quiet minutes on the final build with the switch on. An earlier build that spaced the five workers evenly (index / 5) measured mean 10.05, peak 22.9 and 1.38 waiting, the same within noise. That run is not a clean one: the show was being cut during it (the program changed slot, which is why its counters cover 58,920 frames of the 72,000), and it shows 3 missed program deadlines that the two quiet runs do not have. Whether cutting or the build caused them was not separated; the final build's quiet run has none.

20 minutes, 88 sources at 60 fpsBeforeWith the stagger
Runnable threads, mean14.39.8
Runnable threads, p50 / p95 / p99 / most9 / 51 / 67 / 1038 / 23 / 32 / 62
Time with more than 16 runnable27.9 %15.0 %
Spike on the 16,666 us clock, peak / floor57.0 / 6.222.9 / 6.4
Component at 60.0024 Hz / at 60.000 Hz, amplitude12.75 / 4.492.03 / 1.21
load1 as the kernel reports it, range8.8 to 23.67.5 to 13.8
Seconds with load1 above 1629.2 %0 %
load1 if averaged over time, range13.1 to 16.49.5 to 9.9
Real 5 s mean of the runnable count, range11.7 to 19.08.9 to 11.6
PSI cpu “some”16.4 %7.9 %
Host CPU52.8 %50.7 %
All threads: cores running / threads waiting8.18 / 4.557.87 / 1.30
Browser container: running / waiting2.86 / 2.252.69 / 0.33
Mixer: running / waiting5.13 / 2.225.05 / 0.94
Wake-up delay of a normal thread, p50 / p95 / p990.09 / 0.21 / 0.60 ms0.09 / 0.14 / 0.31 ms
Program frames / missed deadlines / output drops72,060 / 0 / 072,000 / 0 / 0
GPU / encoder / VRAM at the end of the run66 % / 76 % / 15,762 MiB66 % / 69 % / 15,108 MiB

Who stopped waiting, per thread group (cores running / threads waiting, from schedstat; thread names as the kernel shows them, with a number written as #):

Thread groupBeforeWith the stagger
Browser: Compositor, one per window0.594 / 1.0180.491 / 0.081
Browser: threads named electron1.389 / 0.5321.318 / 0.093
Browser: VizCompositorTh, one per worker0.657 / 0.2960.630 / 0.049
Mixer: aux_aux#_comp, 12 threads0.528 / 0.2510.530 / 0.157
Mixer: decode_#0.668 / 0.1660.660 / 0.047
Mixer: udp-tx0.181 / 0.1390.183 / 0.054

The work is the same and its cost barely moved: 8.18 cores running before, 7.87 after. What fell is the waiting. The compositors also counted fewer repeated input frames with the stagger on: 0.552 per program frame against 0.816 on the program, and 291.9 against 580.7 a minute on the multiviewer, with 444.8 against 981.8 discards a minute there. These are counted, not explained: no run isolated the cause.

How to see it on a host

Still open

At a glance

Where in the code

Measured on the L4 host (one NVIDIA L4, 16 vCPUs; kernel 7.0, CONFIG_HZ = 1000) with the 88-source 60 fps show: 44 NVDEC decodes, 4 raw v210 uploads and 40 browser windows in 5 workers. Two runs of 20 minutes, before and with the switch on.