Historical evidence, superseded guidance. This is the earlier article, preserved with its original workload, measurements and revision links. Its defaults, branch/deployment labels and recommendations are not current. Read the current recipe.

AVPlumber · Mixer optimization cookbook

Input, decode and wake-ups: 502 to 234 % of a core

Measured on 13 and 14 October 2026 on the 16 vCPU NVIDIA L4 host that runs the demo, one build at a time. The evening builds ran the full show on a second L4 host first; the later ones passed the unit tests and smoke tests, with light GPU smokes on the demo host, before they went into the show. One change, the HEVC table clears, is on develop since 2026-10-13. The others are on avplumber branches that are not merged to develop yet and run in the demo show; each section names its branch, and source links are pinned to the branch commits. Watch the current 192-input L4 demo.

On the 192-source show at 1080p30 the mixer process read 502 % of one vCPU in 1,186 threads after the raw-upload, colour-tag and compositor changes of 13 October. Ten more changes, aimed at what each source's frames cost on their way from the file to the compositors, took it to about 234 % in 889 threads, with no missed deadline and no dropped output at any step. The whole stack, browser included, then ran fifteen minutes confined to 8 vCPUs without missing a deadline, about half of them busy.

Status: on branches, running in the demo show; one change on develop
The mixer's CPU after each build that went onto the show Percent of one vCPU for the whole mixer process: baseline 502; HEVC table clears 471; resident raw sources 335; signalled inputs 322; input hops 265; event loop without self wake-ups 263; Program compositors look for frames every 8 ms 250; fan-out on the event loops 247; GPU memory builds 237; coalesced queue signals 239; AUX compositors look every 8 ms 234. Measured · mixer process, % of one vCPU, 192 sources at 1080p30 on develop on a branch Baseline, 13 October evening502 HEVC frames skip table clears471 Raw files resident in page-locked memory335 Compositor takes only signalled inputs322 Input hops: metadata, routing, one signal265 Event loop does not wake itself263 Program compositors look every 8 ms250 Fan-out on the pacing loops247 GPU memory builds237 Coalesced queue signals239 AUX compositors look every 8 ms234 each bar is one 20 s reading, three to four minutes after that build started
Measured. Bars are in the order the builds went onto the show. Readings of one build vary by a few points (236 to 243 for the same mixer later that morning), so steps of two or three points are within the noise; the large steps are not.

Every step

The running show: 192 sources at 1080p30 (105 HEVC files through NVDEC, 40 browser windows, 47 raw NV12 files), 256 scenes and 24 encoded outputs. Every figure is a 20-second reading of the mixer process from /proc, in % of one vCPU, three to four minutes after that build started. Every row: 0 missed deadlines and 0 output drops over its reading.

ChangeBranchMixerThreadsRunnable, no core
Baselinedevelop, 13 October5021,186108
HEVC frames skip five software-only table clearsdevelop (FFmpeg patch 0020)4711,18685
Raw files resident in page-locked memorymixer/raw-preload3351,18634
Compositor takes frames only from signalled inputsmixer/compositor-signalled-inputs3221,18643
Input hops: no metadata strings, no demux hop, one signalmixer/input-hops2651,08128
Event loop does not wake itself from its own threadmixer/event-loop-self-wake2631,081–
Program compositors look for frames every 8 msmixer/compositor-poll2501,081–
Each source's fan-out on its pacing loopmixer/fan-out-on-loop247889–
GPU memory builds (their own page)mixer/vram241–248, then 237889–
An edge's events write only when the last write was readmixer/coalesced-signals239––
Plus the fence-aware browser inputmixer/browser-acquire-fence236–238––
AUX compositors look for frames every 8 msmixer/aux-input-poll234––

“–”: not read for that build. Run-queue time was read for the evening's builds only. The zero-copy browser trial ran between the last two rows; the mixer read 238–243 with it.

Where it came from, by thread

The evening's builds, before the first and after the input hops (% of one vCPU, all threads of a group together):

Thread groupThreadsBeforeAfterWhat moved it
Reading threads, 47 raw and 105 compressed152139.727.3resident raw files: the 3.1 MB copy out of the page cache per picture is gone
Decoder threads10686.248.6the HEVC table clears (86 → 62), the metadata strings (57 → 49)
Program compositors A and B259.524.2signalled inputs (53 → 39), the input-hop build (39 → 24)
AUX compositors2033.119.6the input-hop build (29 → 20)
Source fan-outs15227.219.0the input-hop build (25 → 19)
Event loops532.526.0the input-hop build (29 → 26)
Demux threads1059.40the node is gone
Network senders2421.920.0unchanged
CUDA event handler110.39.9unchanged

With the polling builds that followed, each Program compositor fell again, from 12.1 to 7.3 %: the two together read 14.6 %, against 59.5 that evening and about 98 earlier on 13 October, before the wait used epoll. At the end of the evening the whole host read about 3.0 vCPUs for the mixer, 1.7 for the browser container and 0.2 for the WebRTC gateway.

Each change, in the order it went into the show:

HEVC frames skip the software decode tables · on develop

A profile of the decoder threads found a quarter of their time in five memset calls in FFmpeg's hevc_frame_start: about 650 KB of per-picture tables cleared for every frame, about 66 of the 260 µs a frame cost. Only FFmpeg's software decode and its loop filters read those tables, and with NVDEC doing the decode neither runs. FFmpeg patch 0020 skips the clears when a hardware decoder is in use; software decoding is unchanged. The decoder threads fell from 86 to 62 %, the mixer from 502 to 471.

Raw files resident in page-locked memory · mixer/raw-preload

With canvas.raw_upload: "direct" (raw frames read straight into page-locked memory) each of the 1,410 raw pictures a second was still copied once by the kernel, out of the page cache. The resident mode keeps a looped file's pictures in one block of page-locked memory the size of the file: each picture is read into its place the first time it is shown, and on every later pass only the transfer to the GPU remains. Where a pass starts the node compares the file's modification time, and reads the file again if it has changed. The reading threads fell from 139.7 to 27.3 %, the mixer from 471 to 335, and the time threads spent runnable without a core from 85 to 34.

The compositor takes frames only from inputs that signalled · mixer/compositor-signalled-inputs

A Program compositor woke about 2,700 times a second and on every wake looked at all 192 input queues for new frames: 26 % of its time was in the queue's peek. The engine's wait for several events, which already used epoll, now says which events fired, and a pass takes frames only from those sources. The two Program compositors together fell from 53 to 39 %.

Input hops · mixer/input-hops

Three changes in one build, measured together (322 → 265 %, 1,186 → 1,081 threads):

Compositors look for frames at an interval · mixer/compositor-poll, mixer/aux-input-poll

A compositor's playout only ever draws at a tick's deadline, so waking for every arriving frame only moves frames from one queue to another earlier than needed. With canvas.input_poll_ms a Program compositor sleeps to its next deadline and looks for frames at most that often in between, and at once for a cut. At 8 ms each Program compositor fell from 12.1 to 7.3 %, and its wakes from 3,400 to 230 a second. canvas.aux_input_poll_ms does the same for the AUX buses: at 8 ms the 20 AUX compositors fell from 20.6 to 17.6 % (264 to 184 wakes a second each) and the event loops from 31.4 to 28.6 %, about 4 points in all. A tick is drawn at its deadline either way; a source's queue of three frames must hold what arrives within one interval, and 8 ms suits rates up to 60 fps.

Fan-out on the pacing loops · mixer/fan-out-on-loop

Every source had a one_to_many node and thread that woke once per frame to hand the frame to its compositors and AUX buses, 47 % of it in the kernel. With canvas.source_fan_out: "event_loop" the fan-out (one_to_many_nb) runs on the event loop that already paces the source: 192 threads fewer (1,081 → 889). It saved no CPU: the 20 points moved onto the event loops (24 → 44 %), which then cost about 25 µs per handler because the loop rebuilt and polled its whole descriptor list on every pass. The first try failed to start because the setup's readiness check knew only the node type one_to_many; fixed.

The event loop · mixer/event-loop-self-wake, mixer/coalesced-signals

What did not help, and what is left

On 8 vCPUs

The stack's containers confined with a cpuset to fewer vCPUs (whole cores, both hyper-threads of each), after the evening's changes; the confinement reverts at the first missed deadline or drop.

WherevCPUsMinutesMissed deadlinesOutput dropsRunnable, no coreProgram compositor, waiting
Second L4 host, no viewers162.500170.6
Second L4 host122.500531.8
Second L4 host82.5001857.2
Second L4 host62.50049615.2
Demo host, live87002968.7
Demo host, live, final build with the zero-copy browser815001521.7

The settings the show runs

"canvas": {
  "raw_upload": "resident",
  "input_poll_ms": 8,
  "aux_input_poll_ms": 8,
  "source_fan_out": "event_loop"
}

input_routing is input by default on the branch, and the metadata and signal changes have no setting. Setup builds shows with raw_upload: "direct" and leaves the others unset, which is the earlier behaviour.

At a glance

Related pages

Where in the code

Source links are pinned to the branch commits that run in the show, or to develop where the change has landed.