Files
pi-multifx-pedal/scripts/rt-tune.sh
T
shawn 3f7257aa23
CI / test (push) Has been cancelled
RT performance tuning: IRQ affinity, chrt, xrun tracking, reference doc
- Add USB audio IRQ affinity pinning to core 3 in main.py
- Add enable_xrun_tracking() to AudioSystem for kernel-level diagnostics
- Wrap Python process with chrt -f 80 in systemd service template
- Add LimitSIGPENDING=128 for signal queue depth
- Create scripts/rt-tune.sh — comprehensive RT tuning startup script
  (IRQ affinity, CPU governor, C-states, ALSA limits, xrun_debug)
- Create docs/rt-performance-tuning.md — reference doc with all
  tuning knobs, measurement tools, and systematic procedure

Targets: <12ms RT latency (8ms ideal), zero xruns, CPU <40% at 512/48k
2026-06-20 15:58:59 -04:00

388 lines
15 KiB
Bash
Executable File

#!/usr/bin/env bash
# ────────────────────────────────────────────────────────────────────
# Pi Multi-FX Pedal — RT Performance Tuning Script
#
# Applies real-time audio optimizations for stable guitar playing on
# RPi 4B. Designed to run at boot (via systemd or main.py) and on
# every JACK/audio profile change.
#
# Targets:
# - Round-trip latency: <12ms (ideally <8ms)
# - Zero xruns during playing
# - CPU <40% on Pi 4B at recommended settings (512/48k)
#
# Usage:
# sudo ./rt-tune.sh # apply all optimizations
# sudo ./rt-tune.sh --status # check current settings
# sudo ./rt-tune.sh --irq-only # just IRQ affinity
# sudo ./rt-tune.sh --usb-audio-irq # find + pin USB audio IRQ
# ────────────────────────────────────────────────────────────────────
set -euo pipefail
# ── Colour helpers ──────────────────────────────────────────────────
info() { printf "\\e[34m[INFO] %s\\e[0m\\n" "$*"; }
ok() { printf "\\e[32m[ OK ] %s\\e[0m\\n" "$*"; }
warn() { printf "\\e[33m[WARN] %s\\e[0m\\n" "$*"; }
err() { printf "\\e[31m[FAIL] %s\\e[0m\\n" "$*"; }
# ── Config ─────────────────────────────────────────────────────────
# Which CPU core to dedicate to audio IRQ handling
# RPi 4B has 4 cores (0-3). Core 3 is largely unused by Linux housekeeping.
# Core 0 = most interrupts + kernel, Core 1-2 = general, Core 3 = isolated
IRQ_CORE=3
# Default JACK parameters (will be overridden by main.py dynamically)
JACK_PERIOD=512
JACK_RATE=48000
# ── Helpers ─────────────────────────────────────────────────────────
# Find the USB audio interface IRQ number
find_usb_audio_irq() {
# Look for the USB audio device in /proc/interrupts
# On RPi 4B, the USB controller is on a PCIe bridge (xhci-hcd) or
# directly on the BCM2711's DWC2/dwc_otg USB controller.
# Focusrite Scarlett 2i2 shows up as a USB interrupt tied to xhci-hcd.
# Strategy 1: Look for xhci-hcd (USB 3.0 controller on Pi 4B)
local irq
irq=$(awk '/xhci-hcd/ {gsub(":","",$1); print $1}' /proc/interrupts 2>/dev/null | head -1)
# Strategy 2: Look for dwc_otg/dwc2 (USB 2.0 controller)
if [[ -z "$irq" ]]; then
irq=$(awk '/dwc_otg|dwc2/ {gsub(":","",$1); print $1}' /proc/interrupts 2>/dev/null | head -1)
fi
# Strategy 3: Find the USB controller from sysfs for the audio device
if [[ -z "$irq" ]]; then
# Try to find the USB device that's our audio interface
# Look for USB audio class devices
local usb_dev
usb_dev=$(grep -l "audio" /sys/bus/usb/devices/*/bInterfaceClass 2>/dev/null | head -1)
if [[ -n "$usb_dev" ]]; then
local usb_path
usb_path=$(dirname "$usb_dev")
# Walk up to find the parent USB controller
while [[ "$usb_path" != "/sys/bus/usb/devices" && "$usb_path" != "/" ]]; do
if [[ -f "$usb_path/irq" ]]; then
irq=$(cat "$usb_path/irq" 2>/dev/null)
break
fi
usb_path=$(dirname "$usb_path" 2>/dev/null)
done
fi
fi
echo "$irq"
}
# ── Apply IRQ affinity ──────────────────────────────────────────────
apply_irq_affinity() {
local irq="$1"
local core="$2"
local smp_affinity
if [[ -z "$irq" || "$irq" == "0" ]]; then
warn "No USB audio IRQ found — cannot set affinity"
return 1
fi
# Convert core number to hex mask for /proc/irq/*/smp_affinity
# Core 0 = 1, Core 1 = 2, Core 2 = 4, Core 3 = 8
smp_affinity=$(printf "%x" $((1 << core)))
local irq_dir="/proc/irq/$irq"
if [[ ! -d "$irq_dir" ]]; then
warn "IRQ $irq directory not found at $irq_dir"
return 1
fi
echo "$smp_affinity" > "$irq_dir/smp_affinity" 2>/dev/null || true
local effective
effective=$(cat "$irq_dir/smp_affinity" 2>/dev/null || echo "unknown")
# Also set smp_affinity_list for convenience
echo "$core" > "$irq_dir/smp_affinity_list" 2>/dev/null || true
info "IRQ $irq pinned to core $core (smp_affinity=0x$smp_affinity, effective=0x$effective)"
return 0
}
# ── Set RT priority for the current process ──────────────────────────
set_self_rt_priority() {
local prio="$1"
# Set SCHED_FIFO with the given priority for our process group
# Note: This only works if called from the target process or with CAP_SYS_NICE
chrt -f -p "$prio" $$ 2>/dev/null || warn "Cannot set self RT priority (not running as root?)"
}
# ── CPU isolation ───────────────────────────────────────────────────
# Move all non-critical interrupts away from the IRQ core
isolate_core_from_housekeeping() {
local core="$1"
local exclude_irqs="$2" # comma-separated IRQs to keep on the isolated core
# For each IRQ, move it away from our dedicated audio core
# unless it's one we explicitly want to keep there
for irq_dir in /proc/irq/[0-9]*/; do
local irq_num
irq_num=$(basename "$irq_dir")
local irq_name
irq_name=$(cat "${irq_dir}affinity_hint" 2>/dev/null || echo "")
# Skip if this is the USB audio IRQ we want to keep pinned
if [[ ",$exclude_irqs," == *",$irq_num,"* ]]; then
continue
fi
# Move to other cores (0-2, leaving core 3 free)
# Affinity = 0x7 (cores 0,1,2)
echo "7" > "${irq_dir}smp_affinity" 2>/dev/null || true
done
info "Non-audio IRQs moved away from core $core"
}
# ── Status check ────────────────────────────────────────────────────
show_status() {
echo ""
info "========== RT Performance Status =========="
echo ""
# CPU governor
echo "── CPU Governor ──"
for c in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do
local cpu
cpu=$(basename "$(dirname "$c")")
echo " $cpu: $(cat "$c" 2>/dev/null || echo 'N/A')"
done
# Current frequency
echo ""
echo "── CPU Frequency ──"
for c in /sys/devices/system/cpu/cpu*/cpufreq/scaling_cur_freq; do
local cpu
cpu=$(basename "$(dirname "$c")")
local freq_khz
freq_khz=$(cat "$c" 2>/dev/null || echo 'N/A')
echo " $cpu: $((freq_khz / 1000)) MHz"
done
# IRQ affinity
echo ""
echo "── IRQ Affinity ──"
local irq
irq=$(find_usb_audio_irq)
if [[ -n "$irq" && "$irq" != "0" ]]; then
local aff
aff=$(cat "/proc/irq/$irq/smp_affinity" 2>/dev/null || echo "N/A")
local name
name=$(cat "/proc/irq/$irq/name" 2>/dev/null || echo "unknown")
echo " USB Audio IRQ $irq ($name): smp_affinity=0x$aff"
else
echo " No USB audio IRQ found"
fi
# All IRQs
echo ""
echo "── All Interrupts (affinity) ──"
for irq_dir in /proc/irq/[0-9]*/; do
local i
i=$(basename "$irq_dir")
local aff
aff=$(cat "${irq_dir}smp_affinity" 2>/dev/null || echo "?")
local name
name=$(cat "${irq_dir}name" 2>/dev/null || echo "?")
printf " IRQ %-4s 0x%-4s %s\\n" "$i" "$aff" "$name"
done
# JACK status
echo ""
echo "── JACK Status ──"
if pidof jackd >/dev/null 2>&1; then
echo " jackd: RUNNING"
# Show JACK command line
ps aux | grep jackd | grep -v grep | head -1 | awk '{$1=$2=$3=$4=$5=$6=$7=$8=$9=$10=""; print " Args:" $0}'
else
echo " jackd: NOT RUNNING"
fi
# RT priority of processes
echo ""
echo "── RT Priority ──"
for proc in jackd python3; do
pidof "$proc" 2>/dev/null | tr ' ' '\\n' | while read -r pid; do
local policy
policy=$(chrt -p "$pid" 2>/dev/null | head -1 || echo "N/A")
echo " $proc (PID $pid): $policy"
done
done
# Memory locking
echo ""
echo "── Memory Locking ──"
if [[ -f /proc/self/status ]]; then
grep -i "lock" /proc/self/status 2>/dev/null | head -5 | while read -r line; do
echo " $line"
done
fi
# XRun debug
echo ""
echo "── XRun Debug ──"
for card in /proc/asound/card*/xrun_debug; do
echo " $card: $(cat "$card" 2>/dev/null || echo 'N/A')"
done
echo ""
ok "Status check complete"
}
# ═══════════════════════════════════════════════════════════════════
# Main
# ═══════════════════════════════════════════════════════════════════
if [[ $EUID -ne 0 ]]; then
err "This script must be run as root"
exit 1
fi
MODE="${1:-all}"
case "$MODE" in
--status|-s)
show_status
exit 0
;;
--irq-only|-i)
IRQ=$(find_usb_audio_irq)
if [[ -n "$IRQ" && "$IRQ" != "0" ]]; then
apply_irq_affinity "$IRQ" "$IRQ_CORE" && ok "IRQ affinity applied"
else
warn "No USB audio IRQ found — checking /proc/interrupts..."
grep -E "xhci|dwc|usb|audio" /proc/interrupts | head -10
echo ""
info "To find the right IRQ manually:"
info " cat /proc/interrupts | grep xhci"
info " cat /sys/bus/usb/devices/*/irq 2>/dev/null"
fi
exit 0
;;
--usb-audio-irq|-u)
IRQ=$(find_usb_audio_irq)
if [[ -n "$IRQ" && "$IRQ" != "0" ]]; then
local name
name=$(cat "/proc/irq/$IRQ/name" 2>/dev/null || echo "unknown")
info "USB Audio IRQ = $IRQ ($name)"
else
warn "No USB audio IRQ found"
echo "── /proc/interrupts (USB/audio lines) ──"
grep -E "xhci|dwc|usb|audio|snd" /proc/interrupts | head -10
fi
exit 0
;;
all|*)
# Full tuning
info "========== Pi Multi-FX Pedal — RT Tuning =========="
echo ""
# ── 1. Find and pin USB audio IRQ ────────────────────────
info "Step 1: USB audio IRQ affinity..."
IRQ=$(find_usb_audio_irq)
if [[ -n "$IRQ" && "$IRQ" != "0" ]]; then
apply_irq_affinity "$IRQ" "$IRQ_CORE"
# Isolate core 3 from housekeeping interrupts
isolate_core_from_housekeeping "$IRQ_CORE" "$IRQ"
else
warn "USB audio IRQ not found — audio may work but without dedicated IRQ core"
fi
# ── 2. CPU governor (already done in main.py, but belt-and-braces) ──
info "Step 2: CPU governor → performance..."
echo performance | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor >/dev/null 2>&1 || \
warn "Could not set CPU governor (may need cpufrequtils)"
# ── 3. Disable CPU idle states (C-states) for lower latency ──
# This prevents the CPU from entering deep sleep that adds latency
# on wake. Trade-off: ~0.5W extra power draw.
info "Step 3: Disabling deep CPU idle states..."
if [[ -f /sys/module/processor/parameters/max_cstate ]]; then
echo 1 > /sys/module/processor/parameters/max_cstate 2>/dev/null || true
fi
if [[ -f /dev/cpu_dma_latency ]]; then
# Write 0 to request minimum DMA latency (blocks deep C-states)
# The file stays open while the process lives — we do it briefly
echo 0 > /dev/cpu_dma_latency 2>/dev/null || true
fi
# ── 4. Set JACK's ALSA buffer sizes for low latency ───────────
# These are advisory — main.py overrides them via jackd arguments
info "Step 4: ALSA buffer constraints (advisory)..."
cat > /etc/security/limits.d/99-audio.conf <<'LIMITS'
# Pi Multi-FX Pedal — Real-time audio limits
# Applied by rt-tune.sh
@audio - rtprio 95
@audio - memlock unlimited
@audio - nice -20
LIMITS
ok "Audio limits written to /etc/security/limits.d/99-audio.conf"
# ── 5. Set xrun_debug for diagnostics ─────────────────────────
info "Step 5: Enable xrun tracking..."
for card in /proc/asound/card*/xrun_debug; do
if [[ -w "$card" ]]; then
# Bit 0 = enable xrun logging
# Bit 1 = show stack backtrace on xrun
# Bit 2 = inhibit xrun (test mode)
# Value 3 = log xruns with backtrace (diagnostic)
echo 3 > "$card" 2>/dev/null || true
ok "Set xrun_debug on $(dirname "$card" | xargs basename)"
fi
done
# ── 6. Process RT priority (for the calling process) ──────────
info "Step 6: Setting RT priority..."
# This is primarily done by main.py (mlockall, GC disable)
# and the systemd service (LimitRTPRIO). We set it here too
# so the script works in all contexts.
set_self_rt_priority 80 || true
# ── 7. Systemd service check ──────────────────────────────────
info "Step 7: Verifying systemd RT limits..."
if systemctl is-active pi-multifx-pedal.service &>/dev/null; then
local rtprio
rtprio=$(systemctl show pi-multifx-pedal.service -p LimitRTPRIO --value 2>/dev/null || echo "?")
local memlock
memlock=$(systemctl show pi-multifx-pedal.service -p LimitMEMLOCK --value 2>/dev/null || echo "?")
local nice
nice=$(systemctl show pi-multifx-pedal.service -p LimitNICE --value 2>/dev/null || echo "?")
info " LimitRTPRIO=$rtprio LimitMEMLOCK=$memlock LimitNICE=$nice"
if [[ "$rtprio" != "95" ]]; then
warn "LimitRTPRIO should be 95 — check pi-multifx-pedal.service"
fi
if [[ "$memlock" != "infinity" ]]; then
warn "LimitMEMLOCK should be infinity — check pi-multifx-pedal.service"
fi
else
warn "pi-multifx-pedal.service not running — check after deployment"
fi
echo ""
ok "========== RT Tuning Complete =========="
echo ""
info "Recommended next steps:"
info " 1. Start pedal: sudo systemctl start pi-multifx-pedal.service"
info " 2. Check logs: journalctl -fu pi-multifx-pedal.service"
info " 3. Test latency: jack_iodelay (needs loopback cable)"
info " 4. Check xruns: cat /proc/asound/card*/xrun_debug"
info " 5. Full status: sudo ./rt-tune.sh --status"
;;
esac