3f7257aa23
CI / test (push) Has been cancelled
- 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
388 lines
15 KiB
Bash
Executable File
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
|