Jitter Measurement and RT Comparison
Time estimate: ~45 minutes Prerequisites: Level Display: SDL2, PREEMPT_RT Latency
Learning Objectives
By the end of this tutorial you will be able to:
- Quantify sensor jitter and frame jitter with statistical analysis
- Compare standard vs PREEMPT_RT kernel performance
- Understand why worst-case matters more than average for real-time
- Use CPU stress to reveal scheduling differences
Understanding Jitter Measurement
Jitter is the variation in timing between consecutive events that should be equally spaced. For example, a loop targeting 1 ms per iteration that actually varies between 0.8 ms and 3.2 ms has 2.4 ms of jitter. In embedded systems, jitter matters because a single late event can cause a visible stutter, a missed control deadline, or a sensor reading gap. The tool cyclictest measures worst-case scheduling latency by repeatedly sleeping for a fixed interval and recording how late the wakeup actually occurs. On a standard Linux kernel, worst-case latency under load can reach several milliseconds. With the PREEMPT_RT patch, nearly all kernel code becomes preemptible, reducing worst-case latency to tens of microseconds. Plotting a histogram of measured latencies reveals the distribution — a tight cluster near the target with a long tail indicates occasional scheduling delays. The length of that tail is what determines whether the system meets real-time requirements.
For the full theory on real-time scheduling, preemption models, and latency analysis, see the Real-Time Systems Reference.
1. Enable CSV Logging
Run the SDL2 level display application with CSV logging enabled:
Each row in the CSV contains:
| Column | Unit | Description |
|---|---|---|
timestamp_ns |
ns | Absolute monotonic timestamp |
sensor_dt_ns |
ns | Delta between consecutive sensor reads |
frame_dt_ns |
ns | Delta between consecutive frame presentations |
latency_ns |
ns | Sensor-to-display latency |
roll_deg |
degrees | Filtered roll angle |
pitch_deg |
degrees | Filtered pitch angle |
The sensor_dt_ns reveals how consistently the sensor thread runs. The frame_dt_ns shows how consistently the render loop meets VSync deadlines. The latency_ns measures the total pipeline delay from reading the sensor to presenting the frame.
Note
For the statistical theory behind jitter analysis — why worst-case matters, percentile definitions, how many samples you need, and outlier classification — see Real-Time Systems Reference § Jitter Statistics.
2. Baseline Test
First, establish a baseline on the standard kernel with no competing load.
Close any unnecessary applications and services:
Run the test for 2 minutes:
Verify the data:
You should have approximately 7,200 data points (60 fps x 120 seconds).
Checkpoint
baseline.csv exists and contains roughly 7,000+ rows of data.
3. Stress Test
Now repeat the measurement while generating CPU load. The stress-ng tool creates synthetic load on specific cores:
Run the stress test on 3 out of 4 CPU cores, leaving one core for the application:
stress-ng --cpu 3 &
STRESS_PID=$!
sudo SDL_VIDEODRIVER=kmsdrm ./level_sdl2 -l stress.csv &
APP_PID=$!
sleep 120
kill $APP_PID
kill $STRESS_PID
During the test, you can observe CPU load in another terminal:
You should see 3 cores at 100% and one core running the level display application.
Checkpoint
stress.csv exists with roughly 7,000+ rows. You may have noticed occasional visual stutters during the stress test.
Stuck?
- If
stress-ngis not available, usestressor runwhile true; do :; done &three times - If the app crashes under load, check available memory with
free -m
4. Install PREEMPT_RT
The PREEMPT_RT kernel patch transforms Linux from a general-purpose OS to a real-time capable system. It makes nearly all kernel code preemptible, reducing worst-case latency from milliseconds to microseconds.
Follow the existing PREEMPT_RT Latency tutorial to install and boot the RT kernel. Then return here.
Verify you are running the RT kernel:
The kernel version should contain rt in the name (e.g., 6.1.77-rt24-v8+).
This should output 1 on an RT kernel.
Checkpoint
uname -r shows an -rt kernel version and /sys/kernel/realtime reads 1.
5. RT Baseline
Repeat the baseline test on the RT kernel with no load:
Tip
Make sure you have rebuilt or copied the level_sdl2 binary after switching kernels if the kernel headers changed. SDL2 user-space binaries typically do not need rebuilding, but the BMI160 kernel module does.
6. RT Stress Test
Apply the same CPU stress on the RT kernel:
stress-ng --cpu 3 &
STRESS_PID=$!
sudo SDL_VIDEODRIVER=kmsdrm ./level_sdl2 -l rt_stress.csv &
APP_PID=$!
sleep 120
kill $APP_PID
kill $STRESS_PID
You now have four CSV files representing the complete test matrix:
| File | Kernel | Load |
|---|---|---|
baseline.csv |
Standard | None |
stress.csv |
Standard | 3-core stress |
rt_baseline.csv |
PREEMPT_RT | None |
rt_stress.csv |
PREEMPT_RT | 3-core stress |
7. Run Analysis Script
The analysis script computes statistics and generates comparison plots:
python3 src/embedded-linux/scripts/jitter-measurement/analyze_jitter.py \
baseline.csv stress.csv rt_baseline.csv rt_stress.csv \
--plot
This produces:
- A histogram of sensor dt and frame dt for each test condition
- A time series plot showing jitter spikes
- A summary table printed to the console
- Plots saved as
jitter_analysis.png
Stuck?
- Install dependencies if needed:
pip3 install numpy matplotlib - If
--plotfails on headless Pi, add--save-onlyto write plots to file without displaying - You can also analyze manually with simple shell commands:
8. Fill Comparison Table
Using the output from the analysis script, fill in this table with your measured values:
| Metric | Standard | Standard + Load | RT | RT + Load |
|---|---|---|---|---|
| Sensor dt avg (us) | ||||
| Sensor dt max (us) | ||||
| Frame dt avg (ms) | ||||
| Frame dt max (ms) | ||||
| Dropped frames (>25 ms) | ||||
| Latency 99th pct (ms) |
Tip
Typical results you might expect:
- Sensor dt avg should be similar across all conditions (~5000 us)
- Sensor dt max is where RT shines: standard kernel under load might show spikes of 10,000+ us, while RT stays below 6,000 us
- Frame dt avg should be ~16.6 ms everywhere (hardware-paced by VSync)
- Frame dt max shows dropped frames — standard + load might hit 33+ ms, RT + load stays closer to 17 ms
- Dropped frames count tells the practical story
Checkpoint
Your comparison table is filled with measured data. The RT kernel should show noticeably better worst-case numbers, especially under load.
What Just Happened?
RT kernel reduces worst-case sensor jitter but may not improve average frame timing (display is hardware-paced). Under load, the difference becomes dramatic — RT keeps the sensor loop deterministic while the standard kernel shows spikes.
Why does this matter? In real-time systems, worst-case matters more than average. A system that averages 5 ms sensor reads but occasionally spikes to 50 ms is worse than one that consistently delivers 6 ms reads. The spike causes a visible stutter or, in safety-critical systems, a missed control deadline.
The frame timing is less affected because SDL_RenderPresent() with VSync is fundamentally hardware-paced. The display controller generates VBlank interrupts at a fixed 60 Hz regardless of CPU load. However, if the render thread misses its deadline (takes more than 16.6 ms to prepare a frame), you get a dropped frame — and the standard kernel drops more frames under load than the RT kernel.
Challenges
Challenge: CPU Isolation
Add isolcpus=1,2,3 to the kernel command line (/boot/firmware/cmdline.txt), reboot, and pin the sensor thread to an isolated core:
Deliverable
- [ ] Filled comparison table with measured values from all four test conditions
- [ ] Histogram plots from the analysis script (
jitter_analysis.png) - [ ] Written explanation (2-3 sentences) of why worst-case jitter matters more than average in embedded systems