Performance Tuning
This page covers the runtime knobs that determine how low you can push the
mixer’s buffer size before xruns appear. The end-to-end recipe (governor +
preempt + quantum) is at the bottom under Realtime tuning recipe.
JACK buffer size trade-off
The JACK/PipeWire quantum (buffer size in frames) is the primary latency/stability knob.
| Frames | Latency at 48 kHz | Stability |
|---|---|---|
| 32 | 0.67 ms | Needs PREEMPT_RT kernel + IRQ pinning |
| 64 | 1.3 ms | Very low — one IRQ spike = xrun |
| 128 | 2.7 ms | Sweet spot with governor=performance + preempt=full |
| 256 | 5.3 ms | Reliable on standard kernels |
| 512 | 10.7 ms | Stable on most hardware |
| 1024 | 21.3 ms | Very stable; PipeWire default |
Check current quantum:
pw-metadata -n settings 0 clock.quantum
Note:
clock.quantumreads/writes the default quantum that PipeWire advertises.
The tuning recipe below usesclock.force-quantuminstead, which additionally
prevents any application from requesting a different buffer size. Both keys
accept the same values;force-quantumis the stricter variant suitable for
a dedicated audio server.
Change quantum (temporary):
pw-metadata -n settings 0 clock.quantum 512
pw-metadata -n settings 0 clock.min-quantum 512
pw-metadata -n settings 0 clock.max-quantum 512
Change quantum (permanent):
Create ~/.config/pipewire/pipewire.conf.d/quantum.conf:
context.properties = {
default.clock.quantum = 512
default.clock.min-quantum = 512
default.clock.max-quantum = 512
}
Then systemctl --user restart pipewire pipewire-pulse wireplumber.
Relationship to ingest ring buffer:
The mixer’s ingest ring is sized at broadcast_frames * 64. At 512 frames this is 32768 frames (~683 ms at 48 kHz). If broadcasters send at 128-frame packets, the ring holds ~256 packets of headroom — enough to absorb network jitter without dropouts. Larger JACK buffers proportionally increase this headroom.
CPU realtime priority
For the lowest possible xrun rate, run the JACK server (PipeWire) and the mixer with realtime CPU priority.
Check that RT is working:
# Should show non-zero nice value / RT policy
chrt -p $(pidof shiloh-mixer-linux-x86_64)
The systemd unit already sets LimitRTPRIO=95. JACK will request SCHED_FIFO at priority 70 by default; the mixer’s audio callback inherits this.
If RT is not granted:
Check limits.conf or PAM limits. On Ubuntu the audio group gets RT permission via /etc/security/limits.d/audio.conf:
@audio - rtprio 95
@audio - memlock unlimited
Verify you are in the audio group:
groups | grep audio
Raise JACK server priority:
With PipeWire-JACK, PipeWire itself runs the RT graph. Set its priority in ~/.config/pipewire/pipewire.conf.d/rt.conf:
context.properties = {
mem.allow-mlock = true
default.clock.quantum = 512
}
context.modules = [
{ name = libpipewire-module-rt
args = {
nice.level = -11
rt.prio = 88
rt.time.soft = 200000
rt.time.hard = 200000
}
flags = [ ifexists nofail ]
}
]
CPU frequency governor
Power-saving governors introduce latency spikes that cause xruns. Lock all
CPUs to performance on the mixer server:
# Install cpupower (Ubuntu)
sudo apt install linux-tools-generic
# Set all cores to performance (runtime, not persistent)
sudo cpupower frequency-set -g performance
# Verify
for c in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do cat $c; done | sort -u
# → performance
Persistent (across reboots) — cpupower.service isn’t shipped on
modern Ubuntu, so drop a small systemd one-shot:
sudo tee /etc/systemd/system/cpu-performance.service >/dev/null <<'EOF'
[Unit]
Description=Lock all CPUs to performance governor
After=multi-user.target
[Service]
Type=oneshot
ExecStart=/usr/bin/cpupower frequency-set -g performance
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now cpu-performance.service
Effect: ewma DSP time on stg-srv001 dropped from ~360 µs to ~150 µs at
1024 frames after flipping powersave → performance, simply because the
JACK callback no longer waits for CPUs to ramp from the idle clock.
Kernel preempt mode
Ubuntu’s linux-generic ships PREEMPT_DYNAMIC, meaning any of four
modes can be selected at boot or runtime:
| Mode | Behaviour | Best for |
|---|---|---|
none |
Kernel only preempts at explicit schedule() |
Servers, batch |
voluntary |
Adds might_resched() hints — default for linux-generic |
Desktops |
full |
Preempts almost anywhere (except spinlocks) — equivalent to linux-lowlatency’s CONFIG_PREEMPT=y |
Audio |
lazy |
Preempts but defers TIF flag to next tick — compromise | Mixed |
For low-latency audio (≤ 256 frames), use full. You don’t need to
install linux-lowlatency-hwe.
Runtime change (resets on reboot):
sudo bash -c 'echo full > /sys/kernel/debug/sched/preempt'
sudo cat /sys/kernel/debug/sched/preempt
# → none voluntary (full) lazy
Persistent — append preempt=full to GRUB kernel cmdline:
# Edit /etc/default/grub, change e.g.
# GRUB_CMDLINE_LINUX_DEFAULT='quiet splash'
# to
# GRUB_CMDLINE_LINUX_DEFAULT='quiet splash preempt=full'
sudo sed -i -E "s/^(GRUB_CMDLINE_LINUX_DEFAULT='[^']*)'/\1 preempt=full'/" /etc/default/grub
sudo update-grub
# takes effect on next reboot
Beyond full: PREEMPT_RT — the real-time patch (mainlined in
kernel 6.12) turns spinlocks into mutexes so the kernel is fully
preemptible. Available on Ubuntu via linux-realtime (Ubuntu Pro)
or linux-lowlatency-rt (Ubuntu Studio). Required for stable
operation below 128 frames; usually unnecessary at 128+.
Network socket buffer sizes
At high packet rates (375 pps × multiple clients), the kernel UDP receive buffer can overflow if the mixer’s receive loop has any latency. Increase the default and maximum socket buffer sizes:
# Temporary
sysctl -w net.core.rmem_max=67108864
sysctl -w net.core.wmem_max=67108864
sysctl -w net.core.rmem_default=8388608
sysctl -w net.core.wmem_default=8388608
# Permanent (add to /etc/sysctl.d/99-audio.conf)
net.core.rmem_max = 67108864
net.core.wmem_max = 67108864
net.core.rmem_default = 8388608
net.core.wmem_default = 8388608
Apply: sudo sysctl --system
IRQ affinity
For USB audio devices, pin the USB host controller’s IRQ to a dedicated CPU core to reduce latency jitter:
# Find the IRQ for your USB controller
cat /proc/interrupts | grep xhci
# Pin to CPU 2 (example; adjust to a non-hyperthreaded core)
echo 4 | sudo tee /proc/irq/<IRQ_NUMBER>/smp_affinity
irqbalance can undo manual affinity settings — disable it on a dedicated audio server:
sudo systemctl disable --now irqbalance
PipeWire real-world profile
The following combination is stable on a Pi 4 (4-core A72) used as a relay endpoint:
quantum = 1024 frames (21 ms)
clock.rate = 48000
governor = performance
LimitRTPRIO = 95
On an x86_64 server (mixer role, Ubuntu 24.04, generic kernel 6.17):
quantum = 128 frames (2.67 ms)
clock.rate = 48000
governor = performance
preempt = full
LimitRTPRIO = 95
rmem_max = 67108864
Measured on stg-srv001 with the above:
- DSP ewma: 19 µs (of 2666 µs budget — 99 % headroom)
- DSP peak: 36 µs
- JACK xruns: 0 (sustained operation)
Realtime tuning recipe
End-to-end procedure to bring a mixer server from defaults (1024 frames,
powersave, voluntary) to 128 frames stable. Each step is reversible.
0. Audit prerequisites
# rtkit running
systemctl status rtkit-daemon | head -3
# user in audio group (with rtprio + memlock allowance)
groups | grep audio
grep -h "" /etc/security/limits.d/audio.conf /etc/security/limits.d/99-shiloh-rt.conf 2>/dev/null
# JACK callback thread (inside the mixer process) is SCHED_FIFO
PID=$(pgrep -f shiloh-mixer-linux | head -1)
ps -L -p $PID -o tid,cls,rtprio,comm | grep pw-data-loop
# → "FF 83 pw-data-loop" ← required
If pw-data-loop is not FF, rtkit isn’t granting RT. Check rtkit
status and that the mixer is launched via pw-jack.
Note: the
pw-data-loopthread name is PipeWire-specific. Under native
jackd, the RT callback thread may have a different name (e.g.
shiloh-mixeror a thread named after the JACK client). Grep for
FF(SCHED_FIFO) threads under the mixer process instead:
ps -L -p $PID -o cls,rtprio,comm | grep FF.
1. Flip CPU governor to performance
Eliminates DVFS jitter — single biggest improvement before touching the
buffer size. See CPU frequency governor.
2. Switch preempt to full
Required below 256 frames. See Kernel preempt mode.
3. Ramp the PipeWire quantum down in steps
Pin in halves, holding at each level long enough to measure xrun delta
from ~/mixer-state/diag.json. Restart the mixer between steps —
shiloh-mixer’s JACK client latches its buffer size at activation and
doesn’t renegotiate live.
for Q in 512 256 128; do
pw-metadata -n settings 0 clock.force-quantum $Q
systemctl --user restart shiloh-mixer
sleep 30
jq '{jack, process}' ~/mixer-state/diag.json
# check xrun_count and headroom_us; abort if either is bad
done
Persist 128 with a conf.d drop-in (see Change quantum (permanent)).
4. Verify after reboot
# All three should be in effect:
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor # → performance
cat /sys/kernel/debug/sched/preempt # → (full)
jq '.jack.buffer_size' ~/mixer-state/diag.json # → 128
Rollback
Each layer reverts independently:
# governor
sudo systemctl disable --now cpu-performance.service
sudo cpupower frequency-set -g powersave
# preempt (next reboot)
sudo sed -i "s/ preempt=full//" /etc/default/grub
sudo update-grub
# or immediately:
sudo bash -c 'echo voluntary > /sys/kernel/debug/sched/preempt'
# quantum
rm ~/.config/pipewire/pipewire.conf.d/quantum.conf
pw-metadata -n settings 0 clock.force-quantum 0 # unpin
systemctl --user restart pipewire wireplumber pipewire-pulse
systemctl --user restart shiloh-mixer
Profiling xruns
To capture which process caused an xrun:
# Enable JACK xrun callback logging (verbose mode)
RUST_LOG=debug ~/mixer/shiloh-mixer-linux-x86_64 2>&1 | grep -i xrun
# Check system-level scheduling misses
sudo perf sched record -a sleep 10
sudo perf sched latency | head -20
High latency from kernel threads (IRQ handlers, kworkers) usually indicates that irqbalance is competing with your RT audio thread. See IRQ affinity section above.