Skip to content

Performance

A control loop at 1 kHz has 1000 µs per iteration. Whether OmniLoop costs 5 µs or 500 µs of that decides whether you can leave it armed on real hardware, so this page publishes measurements rather than adjectives.

Everything below is reproducible; the commands are at the bottom.

A live tick costs ~43 µs, measured through shared memory with the release core — a typical frame of 5 tunables and 8 metrics, published every iteration. That is about 4% of a 1 kHz budget, or 0.4% at 100 Hz.

Wall-clock cost of wrapping an iteration in with loop.tick():, minus the same loop body with no OmniLoop at all — so these are overhead, not total time. Each case runs in its own interpreter.

Case Tunables Metrics Overhead p95
minimal 1 0 3.6 µs 3.8 µs
typical 5 8 6.5 µs 6.6 µs
typical + limits armed 5 8 6.6 µs 6.7 µs
wide frame 5 64 18.6 µs 18.8 µs
typical + journal 5 8 11.5 µs 12.0 µs
live: typical 5 8 42.8 µs 47.9 µs
live: + 1 watchpoint 5 8 43.3 µs 48.3 µs
live: + 10 watchpoints 5 8 44.8 µs 45.6 µs
live: + journal 5 8 51.3 µs 55.4 µs

The live: rows go through shared memory and the Rust core. The rows above them stop at the SDK boundary, which is what isolates Python-side cost from IPC.

Three things worth reading off this table:

  • Limits are free per tick. Enforcement runs when an edit arrives, not every iteration, so arming a safety envelope costs nothing while you are not moving a slider.
  • Watchpoints are nearly free. Ten armed watchpoints add ~2 µs per tick. There is no reason to leave a tripwire disarmed to save time.
  • Journaling costs ~5–8 µs on the loop thread — a channel send and a buffer copy. The actual file I/O happens on a background thread and never blocks the loop.

Criterion benchmarks against the Rust core directly, for the paths that sit on the hot side of a mutation or a safety check.

Benchmark Time
sync_to_pointers, no limit 57 ns
sync_to_pointers, limit armed and in range 74 ns
sync_to_pointers, limit clamping 120 ns
evaluate_watchpoints, none armed 36 ns
evaluate_watchpoints, 1 armed 103 ns
evaluate_watchpoints, 10 armed 738 ns
write_frame (caller side, journal attached) ~306 ns
state_hash, 16 keys 1.13 µs

A limit costs 17 ns per write — one HashMap lookup, which is what limit enforcement claims.

Unarmed watchpoint evaluation is constant-time

Section titled “Unarmed watchpoint evaluation is constant-time”

Not merely cheap — constant, independent of how many variables the frame carries. If the unarmed path scanned every variable, a loop publishing 500 keys would pay for a feature it never switched on.

Variables registered evaluate_watchpoints, none armed
8 35.6 ns
64 35.7 ns
512 36.2 ns
CPU Intel Core Ultra 9 288V
OS Windows 11
Python 3.11.0
Core build release

Numbers from other machines will differ; the shape of the table should not.

Terminal window
# Rust core primitives
cargo bench -p omniloop-core
# Per-tick SDK overhead (release core required)
cd omniloop-sdk-python && maturin develop --release && cd ..
python omniloop-sdk-python/bench_tick.py
# Machine-readable, for tracking over time
python omniloop-sdk-python/bench_tick.py --json

pytest omniloop-sdk-python/tests/test_perf_budget.py runs the same paths as regression guards. Those assert against a deliberately loose multiple of the budget: they run on shared CI hardware where wall-clock timing is noisy, and they exist to catch a change in order of magnitude — an accidental O(n) scan, a lock taken per key, I/O moved onto the loop thread — not a few percent of drift. A flaky timing test gets muted, and a muted test guards nothing.

Per-tick latency and long-run memory are different failure modes, and a loop can be excellent at one while failing the other. An unbounded history buffer or a telemetry cache that only ever grows costs nothing per tick and everything four hours in.

soak.py runs a loop against a live server and watches RSS on both processes:

Terminal window
pip install psutil
python omniloop-sdk-python/soak.py --duration 3600 --rate 100

Over 5 minutes at 100 Hz (30,000 ticks) on the reference machine, both processes are flat:

Process RSS Growth
target loop ~22 MB ~0.0 MB/hour
telemetry server ~51 MB ~0.2 MB/hour

Those figures are from a 5-minute local run; the hour-long version is what CI runs nightly, and its artifacts are the authoritative record.

“Bounded” here does not mean RSS never rises — it does, during the first seconds, on any Python process, as allocator arenas fill. What matters is whether it is still climbing once steady state is reached, so an initial warm-up window is discarded and growth is measured as a least-squares slope across the remainder. A single GC pause cannot move that the way it moves a last-minus-first reading.

The budget is 25 MB/hour per process. For calibration: an injected 200 KB/tick leak measures ~13,700 MB/hour against it, and a clean run measures ~0.2 — the budget sits three orders of magnitude away from both.

The nightly workflow runs this for an hour on Linux and Windows, with and without journaling.

Per-tick cost is what a running loop pays; install footprint is what a container pays, and for a training job shipped to a cluster that is often the number people actually notice.

omniloop wheel 1.5 MB
Dependencies 6.0 MB
import omniloop 38 ms
from omniloop.server import create_app 123 ms

import omniloop in a training script pulls in no web-framework modules at allomniloop.server resolves its exports lazily, so a loop that never opens a dashboard never pays for one.

The server is built on Starlette rather than FastAPI. FastAPI’s value is request/response validation derived from type hints, and this server declares no models to validate: the payloads that matter travel over the websocket, and the HTTP surface is four routes taking at most one uploaded file between them. Its pydantic dependency was 11 MB — two thirds of the old footprint — paid on every install:

Dependencies Server import
FastAPI 16.9 MB 448 ms
Starlette 6.0 MB 123 ms

Every figure above is an x86 desktop, which is the wrong machine for a robot. bench_rt measures per-tick overhead and wake jitter separately on your hardware, at your rate, under your scheduler:

Terminal window
cargo run --release --example bench_rt -- --rate 1000 --seconds 60

See Real-Time Characterization for the kernel settings that decide whether the result is worth quoting, and for how to report one.

  • Dashboard rendering. The browser’s frame budget is its own problem.
  • Your training step. These figures are OmniLoop’s share of an iteration, not the iteration.
  • GPU or device memory. SnapshotReflector reduces device tensors to host scalars; what the tensors themselves cost is your framework’s concern.