WEEK 9

Hard Real-Time & Safety

A system is not real-time because it is fast. It is real-time because it is on time, every time, and because you can prove what happens on the run where it is not. This week you take the loop you built and ask the only questions that matter in production: what is the deadline, what happens when it is missed, and can you replay the miss frame for frame.

Homework 7 · a deterministic replay bundle + a one-page safety case Integration and demo rehearsal Same car as week 8 — now with a clock
Part 1

1What is a deadline?

Four different numbers, one word. Getting them apart is most of this week, because every mitigation you will build depends on which one is hurting you.

Latency is how old your information is when you act on it. Period is how often you get to act. Jitter is how much the period and the latency vary from one cycle to the next. The deadline is the instant after which the answer is worthless — and it is a property of the world, not of your code.

The distinction that surprises people: jitter is worse than latency. A constant 140 ms delay is a known quantity, and you can compensate for it — predict the state forward, lower the gain, design for it. A delay that is 20 ms on most cycles and 180 ms on one in fifty cannot be compensated for, because on any given cycle you do not know which one you are in. You end up designing for the worst case anyway, and paying the average case for nothing.

Hence worst-case thinking. A hard real-time system is characterised by its worst execution time, not its typical one: WCET, p99.9, the maximum you have ever observed plus a margin. The question is never “how fast is it?” but “what is the slowest it can be, and what happens then?”

# three kinds of real-time, and they are engineering choices
hard   → a missed deadline is a failure          # steering, braking, airbags
firm   → a late answer is worthless, not fatal   # a video frame, a quote
soft   → a late answer is degraded but useful    # a dashboard, a log

# the honest way to model a slow loop on a fast world (rtsim)
sim.run(controller, control_every=sim.steps_for(0.05))
# -> the controller is polled every 50 ms and its command is HELD
#    in between: a zero-order hold, not a faster world
The world does not wait. A fixed-step simulator makes that explicit, and it is why rtsim refuses to let you use the wall clock as dt. Lateness is modelled, not absorbed.

Four numbers, one loop

namewhat it measuresthe fix
latencyage of your inputpredict forward; move the compute closer
periodhow often you decidecheaper stages; a faster loop
jittervariance of bothremove allocation, GC, locks, I/O from the path
deadlinewhen the answer diesyou cannot: the world sets it
Where jitter comes from, in Python. Allocation inside the loop, the garbage collector, a lock held by another task, a log write that touches the disk, a DNS lookup, an await on something unbounded. Every one of them is invisible in a mean and obvious in a p99.9.
50 Hz
20 ms
20 Hz
50 ms
5 Hz
200 ms
A deadline needs a stated consequence. “We aim for 50 Hz” is a wish. “The loop runs at 50 Hz; on a miss we hold the last command, and after three consecutive misses we enter safe-stop” is a specification — and it is testable.

Widget 1 · the control-loop deadline

The same car, the same gains, the same track as week 8. The only thing that changes is when the controller gets to think — and how reliably. Between decisions the last command is held, which is exactly what a real zero-order-hold loop does.

cross-track error deadline miss lane edge ±1.75 m
RMS after 3 s
—
peak |cte|
—
deadline misses
—
worst dead time
—
outcome
—
Run this experiment, in this order. Start clean: at a 20 ms period the car tracks to about 5 cm and misses nothing. Stretch the period to 60 ms — slightly worse, entirely predictable, and you could re-tune for it. Stretch it to 100 ms and the loop destabilises. Now put the period back to 20 ms and add 160 ms of jitter instead: the average latency is far lower than the 60 ms case, and the car is an order of magnitude worse. That single comparison is the entire argument for bounded worst-case behaviour over good average behaviour.
Part 2

2How does it work? Degrade on purpose, and be able to replay it

Two mechanisms carry almost all of the safety engineering in this course: a monitor that notices you have stopped being real-time, and a log that lets you prove afterwards exactly what happened.

A watchdog is a countdown that something healthy has to keep resetting. If the heartbeat stops — the perception process died, the model call hung, the queue starved — the countdown expires and the system moves to a defined state. The word that matters is defined: the failure mode has to be something you chose in advance and tested, not whatever the code happens to do when its inputs go stale.

Widget 2 · watchdog and safe-stop

Kill the perception stage and watch four different answers to the same failure. Move the failure onto the straight and then into the corner — the ranking of the policies changes, which is the point: a safe state is a property of the situation, not of the code.

perception healthy

Monitor log

detected after
—
distance blind
—
peak |cte| after
—
final speed
—
outcome
—
A watchdog that only logs is not a watchdog. It has to act, and the action has to be reachable without the thing that just died: no model call, no network, no allocation. Two more things this widget will not let you pretend. The timeout is a real trade — too long and you drive blind, too short and one slow frame triggers a spurious safe-stop, which is its own hazard. And the ranking of the policies depends on where the failure happens: at t = 3 s the car is on the straight and a defined policy contains it completely; move the failure to t = 7 s and it is mid-bend, where every policy leaves the lane and the only thing degradation buys you is the speed you leave it at. That is not a failure of the design — it is the reason a real system caps its speed when it is one sensor away from blind.

Now the part that makes a post-mortem possible at all. A run is deterministic if replaying the same inputs, in the same order, with the same seed, produces the same outputs bit for bit. Determinism is not a nice property; it is the difference between “something went wrong around 14:32” and “at step 743 the lane offset read −0.4 m and here is why”.

Widget 3 · deterministic replay, and a one-input diff

Record a run to a log, replay it step by step, then change exactly one input at one step and diff the two trajectories. The first divergent step is the thing your post-mortem is about.

log digest
—
replay digest
—
first divergence
—
max divergence
—
verdict
—

run.log — the replay bundle

# what has to be in the bundle for a replay to prove anything
{"schema": "rtsim.run/1",
 "seed": 0,                      # every random draw came from here
 "dt": 0.02, "control_every": 1,   # the clock, not the wall clock
 "track": "oval:60,25",          # the world, by value not by reference
 "gains": {"kp": 0.8, "kd": 1.1, "ki": 0.0},
 "noise": {"cross_track": 0.0}, "latency": {"sensor": 0},
 "git": "9f2c1ab",                # the code is an input too
 "rows": [[step, t, cte, he, v, steer], ...]}   # Obs.as_row()

# and the test that makes it a proof rather than a story
assert digest(replay(bundle)) == bundle["digest"]
Part 3

3Why does it matter? Because every fix costs latency

Filtering, fusing, retrying, and offloading to something bigger are all real improvements to accuracy — and every one of them is paid for in milliseconds, out of the same budget the deadline lives in.

Take the most common one. Your lane reading is noisy, so you filter it. A complementary filter blends the slow, absolute measurement with a fast prediction from the model you already have: x̂ = α·measured + (1−α)·(x̂prev + predicted change). It is a Kalman filter with the gain frozen instead of computed, it costs three multiplications, and it works. But α is a dial between two failures: turn it down and the estimate is smooth and late; turn it up and it is prompt and noisy. Late is a phase lag, and you saw last week what a phase lag does.

Widget 4 · the filter that buys accuracy with lag

One noisy lane reading, one complementary filter. Read both numbers: the RMSE tells you how much noise you removed, the lag tells you what it cost — and the lag is charged to the same budget as everything else.

RMSE measured
—
RMSE filtered
—
noise removed
—
added lag
—
at 10 m/s
—
Where the fusion comes from. The fast channel is not free information — it is your own model: wheel odometry plus the bicycle equations predict where you should be, and the camera corrects the drift. That is why the drift slider matters: with a biased predictor, a small α stops filtering noise and starts believing a lie.

The same arithmetic settles the argument everyone has about model size. A bigger model somewhere else is not an improvement if the round trip does not fit inside the deadline — and a network round trip is not a number you control, it is a distribution with a long tail, which is week 5's lesson arriving with a steering wheel attached.

Widget 5 · edge or cloud, against the deadline

The cloud model is more accurate and cheaper per FLOP. Put both against the loop deadline and the choice stops being a preference.

edge p99
—
cloud p99
—
deadline
20 ms
edge verdict
—
cloud verdict
—
at 10 m/s, cloud
—
The answer is almost always both. Edge inside the loop, because the loop has a deadline; cloud outside it, for the work that has no deadline — retraining, aggregation across the fleet, the map, the long-horizon plan. The design mistake is not choosing the cloud; it is putting the cloud on the critical path and then discovering the tail.
Part 4

4Five ways a safety story falls apart

Each of these is a real post-mortem that could not be written. Open them.

Reporting the mean, then being surprised by the tail

“Average loop time 8 ms, well inside 20.” Meanwhile p99.9 is 240 ms because of a garbage collection that happens once a minute, and once a minute the car drives two metres with its eyes shut. The mean was never the wrong number to compute — it was the wrong number to promise.

A hard real-time claim is a claim about the maximum. If you cannot bound it, you do not have a hard real-time system; you have a fast one, which is a different product.

Fix: report p50 / p99 / p99.9 / max and the deadline-miss count, alarm on the miss count rather than on the mean, and state the consequence of a miss in the specification.

A log you cannot replay

The log has outputs but not inputs. Or it has inputs but no seed, so every replay draws different noise. Or the loop calls time.time(), so the timing is different every run. Or the order of a dict or a set decides which detection wins, and that order changed between runs. The result is a log that documents that something happened without letting you reproduce it — which is a diary, not telemetry.

Fix: log every input, the seed, the config, the code version, and a digest of the output. Then write the test that replays the bundle and asserts the digest. If that test does not exist, your determinism is a belief.

A watchdog that logs and does nothing

logger.error("perception stale!") and then the loop carries on using the stale value. The monitor is technically working — it noticed — and the system is exactly as unsafe as it would have been without it, plus a log line that will be very embarrassing in the post-mortem.

Its cousin: a watchdog whose recovery path needs the thing that died. Reconnecting requires the network that is down, the safe state needs a model call, the alarm allocates memory during a memory failure.

Fix: the monitor must change what the actuator does, along a path that has no dependency on the failed component — and you must test it by killing the component, on purpose, in CI.

Fail-fast in a system that should fail-safe

An exception propagates out of the control task and the process exits. In a web service that is correct and admirable: crash, restart, serve again. In a moving vehicle it means the last command is held by hardware forever, or the actuator drops to zero at whatever angle it happened to be at. “Restart the process” is not a safe state; it is an absence of one.

Fix: decide, in writing, what the safe state is for each subsystem — and make the degraded mode reachable, tested and boring. Fail fast in the tooling, fail safe in the loop.

Allocation, logging and locks in the hot path

Every one of these is a jitter source that will not show up on your laptop with one client and will show up in the demo. A fresh numpy array per frame, an f-string built for a log line that is thrown away, a blocking disk write, a lock another task holds while it does I/O, an unbounded queue that grows until the allocator has to work for it.

Fix: pre-allocate and reuse buffers; log to a bounded ring buffer and drain it off the hot path; never hold a lock across an await or an I/O call; bound every queue. Then measure the p99.9 again and see what you actually bought.

Part 5

5Check yourself

Instant feedback, no grading. Pick the answer you would defend in a design review.

Q1Loop A has a constant 100 ms latency. Loop B averages 20 ms but ranges from 10 to 150 ms. Which is harder to control, and why?

turns a story into a proof.">

Q2You want a crash to be replayable frame for frame. What is the minimum the bundle has to contain?

Q3The lane reading is noisy, so you set the complementary filter's α to 0.05. RMSE drops nicely and the car starts weaving. What did you do?

Part 6

6Do the work

In class

  • Make the week-8 loop deterministic: fixed dt, one seed, every input logged, a digest of the output.
  • Replay the run that went worst, frame for frame, and assert the digest matches.
  • Profile the loop and produce p50 / p99 / p99.9 and a deadline-miss count — then find and remove one jitter source.
  • Add a monitor that degrades instead of failing, and kill the perception stage on purpose to prove it.
  • Stretch the control period to 50, 100 and 200 ms and record where the controller stops holding the lane.

Before next class

  • HW7 — a deterministic replay bundle (anyone can re-run it and get your numbers) plus a one-page safety case: the hazards, the deadline, the monitor, the safe state, and the evidence.
  • Rehearse the demo end to end, from a cold start, with the network unplugged.
  • Pick the failure you will present a post-mortem on. Bring the telemetry that explains it.
Code
Get the code
The starter repo is public. Use it as a template, clone it, make install, and put the replay harness beside your loop.
Map
Course home
The ten-week map, and how weeks 7 to 9 form one block.
Next
Week 10 · Season Finale →
Demos, the failure-mode drill, the risk-adjusted scoreboard, and a blameless post-mortem.
← Week 8 · The Self-Driving Car Week 10 · Season Finale →