Real-Time Characterization
The Performance page publishes per-tick overhead on an x86 desktop. That is the wrong machine for a robot, and everyone reading it knows so. This page is the harness for measuring your own, and the settings that decide whether the result is worth quoting.
The two numbers, and why they are separate
Section titled “The two numbers, and why they are separate”Conflating them is what makes most published real-time figures useless.
| What it is | Whose problem | |
|---|---|---|
| Overhead | Wall-clock inside tick_begin + halt_poll + tick_end. |
OmniLoop’s. The only thing this project can be held responsible for. |
| Wake jitter | How late the OS started the iteration against its intended period. | Your kernel, scheduler and neighbours. OmniLoop cannot improve it. |
A run with 2 µs of overhead and 400 µs of wake jitter has an OS problem, not an instrumentation problem. The reverse is our bug. The harness reports both, side by side, and never charges one to the other — the jitter sample is taken before any OmniLoop call.
Running it
Section titled “Running it”cargo run --release --example bench_rt -- --rate 1000 --seconds 60microseconds mean p50 p90 p99 p99.9 max------------------------------------------------------------------------overhead 13.25 8 23 87 228 465wake jitter 13.07 0 0 371 2787 6791That sample is a Windows desktop with no real-time configuration at all, and it shows exactly what this page is about: the overhead p50 is 8 µs, but the tail is 228 µs at p99.9 — because a general-purpose scheduler preempts the process mid-tick. The wake-jitter tail is worse still at 2.8 ms. Neither number is a reason to distrust the loop; both are a reason not to run a 1 kHz controller on an unconfigured desktop.
Flags: --rate, --seconds, --channels, --publish-every, --journal, and
--json for machine-readable output.
Making the number mean something
Section titled “Making the number mean something”None of this requires a code change.
Pin the loop and give it a real-time priority
Section titled “Pin the loop and give it a real-time priority”sudo chrt -f 80 taskset -c 3 \ ./target/release/examples/bench_rt --rate 1000 --seconds 60 --jsonchrt -f 80 runs it SCHED_FIFO; taskset -c 3 pins it to one core. Do the
same to your real controller — OmniLoop deliberately does neither on your
behalf, because a library that quietly changes your scheduling class is a
library that will surprise you at the worst moment.
Isolate that core from the general scheduler
Section titled “Isolate that core from the general scheduler”In the kernel command line:
isolcpus=3 nohz_full=3 rcu_nocbs=3Then keep everything else off it. This is usually the single largest improvement to the jitter tail, and it is invisible to any code.
Lock memory so a page fault cannot land mid-tick
Section titled “Lock memory so a page fault cannot land mid-tick”The harness calls mlockall(MCL_CURRENT | MCL_FUTURE) itself on Linux and
reports whether it succeeded — it needs CAP_IPC_LOCK or root. Do the same in
your controller:
#include <sys/mman.h>mlockall(MCL_CURRENT | MCL_FUTURE);A page fault inside a tick is a multi-hundred-microsecond event that shows up only in the tail, which is exactly where it is hardest to explain.
Use a PREEMPT_RT kernel
Section titled “Use a PREEMPT_RT kernel”On stock Linux the tail is bounded by whatever the longest non-preemptible
section happens to be. PREEMPT_RT is what makes the p99.9 a design parameter
rather than a discovery.
Build in release
Section titled “Build in release”cargo run --release ... # Rustcmake -B build -DCMAKE_BUILD_TYPE=Release # C++A debug build of the engine is roughly 100x slower. Loop::release_build(),
ol_is_release_build() and the harness’s own header all report the profile, so
a controller can refuse to arm against a debug core.
Warm up before you raise priority
Section titled “Warm up before you raise priority”Both SDKs render frames into a buffer reserved at setup. It settles during the
first few hundred iterations and then never grows again — the harness discards a
warm-up window for this reason, and your controller should spin before it
switches to SCHED_FIFO.
Reporting a result
Section titled “Reporting a result”If you measure on hardware this project does not have — a Jetson, a Pi 5, an
x86 industrial box with PREEMPT_RT — the numbers are welcome. What makes them
comparable:
sudo chrt -f 80 taskset -c 3 ./bench_rt --rate 1000 --seconds 300 --jsonInclude alongside the JSON:
- CPU and board
uname -a, and whether the kernel isPREEMPT_RT- kernel command line (
isolcpus,nohz_full) - whether
memory_lockedcame back true - what else was running
Five minutes minimum. A 10-second run cannot see a p99.9 worth quoting: at 1 kHz that is ten samples in the tail bucket.
What the harness does not measure
Section titled “What the harness does not measure”- Your controller. The loop body is a token computation. The figures are OmniLoop’s share of an iteration, not the iteration.
- Dashboard or relay cost. Those run in another process;
publish_everydecimates what the loop hands them, and the relay’s own poll rate is its business. - GPU or device memory. Out of scope entirely.
- Determinism. Different question, different tool — see replay.
Related
Section titled “Related”- Performance — the published x86 figures and the per-tick accounting behind them.
- C++ SDK — what is and is not allocated on the tick path.
- Tethered Deployment — the checklist before any of this goes near hardware.