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 CPU is per painted frame
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. These are content- and browser-build-specific measurements. Reducing animation damage changes the workload; it is not a free saving for arbitrary moving pages.
Chromium's cost for a browser window follows the frames the window delivers, not what the page shows: about 3.5 ms of CPU for every painted frame, spread over the renderer, the GPU process and the browser's main process, about 0.2 core per window at 60 paints a second. The demo's browser test page slid its marker without pause, so all 29 windows painted at 60 fps. It now rests 4 s and slides 2 s, as a real graphic does, each window from its own point of the cycle; and the browser image moved from Electron 41 to 44.5.1. Electron went from 5.90 to 2.83 cores and host idle from 28 to 50 % on the 68-input 60 fps show.
Status: Implemented; historical test deploymentLater audit: the per-frame figure is two costs. About 1.8 ms follows each painted frame; the rest is paid on every tick of every window, whether it paints or not. See Per tick and per paint below.
- ~3.5 msChromium CPU per delivered frame per window, whatever the page: about 0.2 core at 60 paints/s
- 5.90 → 2.83Electron cores, after the rested test page and then Electron 44 (two sequential runs)
- 28 → 50 %host CPU idle on the 68-input 60 fps show
- 1,748 → 594browser paints per second once the page rests two thirds of the time
Where a window's CPU goes
For every frame a window delivers, three Chromium processes do work: the renderer paints the damaged page (about 3 % of a core per window at 60 paints/s), the GPU process composites it in VizCompositor and draws through ANGLE's GL (about 12 %), and the browser's main process handles the paint event, passes the DMA-BUF descriptor over the socket and takes the mixer's release ack (about 6 %; see Browser frame hand-off). Together about 3.5 ms of CPU per delivered frame, which at 60 paints/s is about 0.2 core per window (3 + 12 + 6 % = 21 %, computed). The cost tracks delivered frames, not page complexity. Part of the GPU process's share is a copy of every frame into the capture buffer and a poll for its end; a patched Chromium on the L4 paints straight into the buffer the mixer reads instead (Chromium paints into the buffer the mixer reads).
Chromium paints only on damage, so a page that does not change delivers no frame and pays none of the per-frame work. It is not free: its window is still woken on every tick (Per tick and per paint). On the same show a static key page painting 3 times a second took about 7 % of a core, and the always-sliding test page at 60 paints/s 21–25 %. The number of Electron workers does not enter: the work is per window per frame, so grouping windows into fewer, bigger workers would not reduce it and would concentrate VizCompositor load in fewer GPU processes (rejected idea 9). That leaves two levers, and this page pulls both: fewer delivered frames when nothing on the page moves, and a cheaper Chromium.
This is not the shortcut the cookbook rules out. Every window is still an independent 60 fps input: while its marker moves it paints 60 frames a second and the mixer imports, stamps and composites every one; the sender, the chain and the ring are unchanged. What changed is the content: a graphic that holds still two thirds of the time, which is what real lower thirds, bugs and tickers do. Running browser sources at 30 fps in a 60 fps show, which halves the delivered rate of a page that is moving, stays rejected (idea 10).
Measured at 68 inputs, 60 fps
Same 68-input 60 fps show (18 NVDEC, 29 browser sources, 17 raw NV12 files and 4 browser key pages) on the T4 test host (16 vCPU), same avplumber build. Three states, measured as two sequential pairs: the columns are not additive.
| Metric | Always sliding, Electron 41 | Rests 2/3, Electron 41 | Rests 2/3, Electron 44 |
|---|---|---|---|
| Electron CPU | 5.90 cores | 3.25 cores | 2.83 cores |
| Host CPU idle | 28 % | 49 % | 50.3 % |
| avplumber CPU | 5.08 cores | 4.61 cores | 4.58 cores |
| Browser paints/s | 1,748 | 594 | – |
| GPU utilisation | ~83 % | 62 % | – |
The first pair, columns 1 and 2, is the test-page change; the rested page ran with 0 missed program deadlines. The second pair is Electron 41 against 44 on the rested page, measured as its own two runs, so its Electron 41 baseline is not column 2: it read 3.30 cores, 47.7 % idle, avplumber 4.62 cores and CPU pressure (PSI some) 17.6 %, against 2.83 cores, 50.3 %, 4.58 cores and 16.4 % on Electron 44: Electron CPU −14 %, no regression. The gap between that baseline and column 2, 0.05 core and 1.3 points of idle, is within run-to-run noise. PSI was sampled only in the second pair (how it is read).
What changed
- The test page rests two thirds of the time. Before, the marker of
browser_alpha.htmlran a CSS keyframe animation, 4 s each way, without pause, so every window painted every frame. Now the marker'stransformhas a 2 s transition, and a timer toggles its target every 6 s: slide 2 s, rest 4 s. It moves ontransform, notleft, which snaps to whole pixels and lays out on the main thread each frame. Each window starts its cycle at (index × 0.618 mod 1) × 6 s, where index is the number in the source's id (browser_007is 7), whichprepare_demoembeds in the page as a data attribute; golden-ratio steps spread any count of windows evenly over the cycle, so 9–11 of the 29 slide at any moment instead of all of them. Paints fell from 1,748 to 594 a second, Electron from 5.90 to 3.25 cores, avplumber from 5.08 to 4.61, GPU utilisation from about 83 to 62 %, host idle rose from 28 to 49 %, with 0 missed program deadlines. - Electron 44.5.1 (Chromium 152) on Fedora 44. Electron 41 (Chromium 146) and Fedora 42 were both end of life. On the rested page, Electron CPU fell from 3.30 to 2.83 cores (−14 %), host idle rose from 47.7 to 50.3 %, avplumber went from 4.62 to 4.58 cores and CPU pressure (PSI some) from 17.6 to 16.4 %, with no regression. Electron 42 and later no longer download the Electron binary in the package's npm postinstall, so the image build runs
node node_modules/electron/install.jsafternpm ci. The image needs only Electron's runtime libraries; the NVIDIA userspace comes from the host through the container runtime. The GPU-process crash on recreating all windows is the same on Electron 41 and 44; its fix is on Keeping Vulkan out of the GPU process.
Per tick and per paint: a later audit on the L4
A 20-minute capture of every browser thread's run time and wake-ups, on the 88-source 60 fps show on the L4 host: 40 windows in 5 workers on Electron 44, 36 test pages and 4 key pages, 864 paints a second in all, 2.86 cores in the browser container. Dividing one by the other gives 3.3 ms per painted frame, close to the figure this page starts with. The capture splits it:
| Part of the 2.86 cores | Per event | Cores |
|---|---|---|
| Per painted frame, 864 a second | 1.5–1.9 ms | 1.30–1.65 |
| Per tick of each window, 2,400 a second, painting or not | 0.45–0.59 ms | 1.07–1.41 |
| Page-specific: the ticker's script loop and five pages that grow | – | 0.13 |
The split is computed, not measured directly: it is a fit across the five workers, whose paint rates differ only by their key page, so it rests on five points and a long extrapolation to zero paints. One measured fact shows the tick cost without the fit: a key page that paints 2 times a second and a test page that paints 20.3 times a second cost the same on the renderer's Compositor thread, 12.05 against 12.18 thousandths of a core.
- Every window is woken 60 times a second, for about 0.5 ms each time, although 1,536 of the 2,400 window-ticks a second paint nothing. That is 1.1–1.4 cores, 37–49 % of the container. Resting the test page two thirds of the time cut only the per-frame part. At 60 fps these ticks also all fell on one instant until the opt-in stagger (Browser frame ticks staggered at 60 fps).
- A painted frame is relayed, not drawn, on the CPU: three processes, nine threads and about 25 wake-ups for each one, whatever the page shows.
- The pixels are on the GPU. The renderers' raster threads use 0.002 of the 2.86 cores. A 152×152 window costs the same CPU per delivered frame as a 1920×1080 one within noise: 1.5–1.6 ms against 1.4 ms.
- One page is heavier than it needs to be. The ticker moves its crawl from a
requestAnimationFrameloop, which costs 0.047 core more than the same motion driven by the compositor. - Idea 9 rests on less than it says. 36–44 % of the cost in the GPU and browser processes is not per delivered frame. Whether that part is per window or per worker cannot be told from this capture, because every worker holds 8 windows (rejected idea 9).
Two things are open. Why a resting window ticks at all is not proven: 0.7–0.9 core is spent on ticks that paint nothing, and one test decides whether the page or the browser asks for them, an empty window beside a test page. And five pages' Compositor threads cost more the longer they run, 0.08 core in all after 4.5 hours of uptime; no data here explains it.
At a glance
- What
Two changes outside the mixer. The demo's browser test page,
demos/mixer/browser_alpha.html, rests 4 s and slides 2 s, each window from its own point of the cycle. The browser image builds Electron 44.5.1 on Fedora 44. No avplumber, mixer Python or browser-sender change. - Why
With the always-sliding page, 29 windows painting 60 times a second made Electron the largest CPU consumer of the 60 fps show: 5.90 cores against avplumber's 5.08, the host 28 % idle and the GPU at about 83 %. Chromium's work is per delivered frame, so the page had to deliver fewer frames when nothing moves, and Chromium had to get cheaper for the frames it does deliver.
- Latency
0 frames added: nothing in the frame path changed, the same sender, chain, ring and
smooth_timestampsconfiguration (smooth_timestampsnode reference). A resting page delivers no paint; the compositor repeats its last frame for that tick, as it does for any still key page. The marker moves on a compositor transform, so its motion is not delayed by main-thread layout. Cut and program latency were not re-measured in these runs. - Side effects
The demo's browser tiles hold still two thirds of the time, so the demo shows less motion than before, and a browser-heavy scene loads the browser path less than the always-sliding page did: on average about 20 paints/s per window (594 over 29, computed) instead of 60. Every window is still a 60 fps input while it moves.
Electron 44 is six Chromium majors up from 41 (146 → 152). Behaviour changes ride along; the one met so far, the GPU-process crash when all windows are recreated, exists on both versions and has its own page.
- Cost
- 0 nodes, 0 queues, 0 new threads; VRAM not re-measured.
- Code: +15/−5 lines in the test page; 1 line in
package.json(and the lockfile it regenerates, ~380 lines); +2/−1 and 2 changed lines in the Dockerfile. - One browser image rebuild; the Electron binary is fetched by the explicit
install.jsstep.
- Risk
Low. The per-process split (3, 12 and 6 %) and the paint-rate costs are from one host and one show. The −14 % from Electron 44 is one pair of runs, with about 0.05 core of run-to-run noise on the baseline. An image build that drops the
install.jsstep has no Electron binary, since Electron 42 and later no longer fetch it in postinstall. Nothing on this page changes what the mixer does with a frame. - Switch it off
No runtime switch; the two are independent. Restore the marker's
animation: slide 4s linear infinite alternateand its keyframes inbrowser_alpha.html: every window then paints at 60 fps again. Pin"electron"indeps/dma-browser/package.jsonback to41.3.0and rebuild the browser image.
Where in the code
- demos/mixer/browser_alpha.html: why the marker rests, why it moves on
transform, and the 2 s transition; source the 6 s cycle and the golden-ratio offset from the id's number. - demos/mixer/prepare_demo.py:
page_urlembeds the page with its data attributes; source the alpha pattern loadsbrowser_alpha.htmlwith the source's id. - docker-compose/images/dma-browser/Dockerfile:
npm ci, thennode node_modules/electron/install.js; source and sourcefedora:44for the build and runtime stages. - deps/dma-browser/package.json:
"electron": "44.5.1". - Docs: dma-browser README names the Electron and Chromium versions.
Source links are pinned to the reviewed revision.
Measured on the T4 test host (16 vCPU) with the 68-input 60 fps show: 18 NVDEC, 29 browser sources, 17 raw NV12 files and 4 browser key pages. The per-window shares are Chromium CPU per window on that show; Electron and avplumber figures are whole-process totals.