Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

49 - The worst case is the only case

A mouse with a budget sheet and a contingency line - planning for the worst case.

Concept node: see the DAG and glossary entry 49.

§4 gave the tick a budget: 33 ms at 30 Hz, and you spend it wisely. §39 gave the long computation an anytime contract: return the best answer you have when the deadline arrives. Both are soft real-time. A missed deadline costs quality - a dropped frame, a coarser answer - and the system keeps running. You have been doing soft real-time for the whole book, and for almost everything you will build, soft is the right and sufficient discipline.

Soft does not mean unmanaged. When the work outgrows the budget - more entities than you planned, a load spike, a heavier tick - you choose, in advance, how to miss. The rule is to shed fidelity, never integrity: the systems that keep the world valid run every tick, exact; the systems that keep it fresh and fine are the ones you cut. The buffered commit (§22) is what makes that safe - a tick that runs long still applies a whole world at the boundary, never a half-updated one, so the cost of an overrun is latency, not corruption. Within that, you degrade in a fixed priority order: drop the pure observers first, stretch the GC’s cadence, defer the slow-moving systems, and - the best lever - apply back-pressure to whatever creates the load, because deferring growth attacks the cause, not the symptom. Two disciplines keep it honest. The degradation is a logged decision, not a wall-clock branch, so a degraded run still replays (§37, §48); a shed that depends on how fast the machine happened to be running stops being a function of its inputs. And the staleness is bounded and self-healing: each shed defers work by a known number of ticks and catches up when the load drops. This is the budget read as a curve (§4): past the comfortable scale the rate slides, and graceful degradation is how you choose what slides.

This chapter marks the line where that stops. In hard real-time a missed deadline is not a dropped frame; it is a fault. The motor controller that computes the next current 200 microseconds late has already let the motor run away. The flight-control loop that skips a cycle has lost the aircraft for that cycle. The emergency stop that fires 10 ms late did not fire. When the deadline is a fault, the average case is irrelevant. A loop that meets its deadline 99.999 percent of the time has failed if the missing 0.001 percent is the brake.

That single sentence inverts the book. Every technique so far chased the mean: cache-friendly layout, SoA, parallelism, branch-predictable code - all of them make the common case fast and let the rare case be slow. Hard real-time chases the tail. It does not care that the average tick is 2 ms if one tick in a million is 40 ms, because the one is the only one that matters. The worst case is the only case.

What hard real-time demands is a different ledger.

Worst-case execution time (WCET), not average. You must know the longest the code can ever take, and prove it. That means every loop has a statically known maximum iteration count, every branch’s slower side is the one you count, there is no unbounded recursion, and there is no data-dependent loop without a bound. The §4 budget stops being an average you usually hit and becomes a ceiling you prove you never cross.

No allocation in the inner loop. malloc and free have unbounded worst-case time: the allocator may walk free lists, coalesce, or fall through to the OS for more pages. One hot-path allocation puts an unknowable spike in your WCET. Pre-allocate everything; the loop allocates nothing.

No blocking call in the inner loop. A syscall, a lock, an fsync, a page fault - each can stall for an unbounded time while the kernel does something else. The hot loop touches only memory that is already resident and locks that are already held or never contended.

Bounded jitter. Soft real-time asks “fast enough on average”. Hard real-time asks “does the tick fire every period, on time, with bounded variance”. The enemies of jitter are the very machinery that makes the average fast: the OS scheduler preempting you, interrupts, CPU frequency scaling, and the cache itself. You fight them with a real-time scheduler (SCHED_FIFO, SCHED_DEADLINE), CPU isolation and pinning (isolcpus), locking your pages in RAM so they never fault (mlockall), and pinning the clock so it does not slew.

Priority inheritance. A high-priority task waits on a lock held by a low-priority task that has been preempted by a medium-priority one. The high-priority task now misses its deadline because of a task it outranks - priority inversion, and it has crashed real spacecraft. The fix is a mechanism: a task holding a lock temporarily inherits the priority of the highest task waiting for it.

Here is the part that should reassure you: most of this discipline you already have. The book has avoided per-tick allocation since §7 - SoA columns are pre-sized, the §22 buffer is reused, §24 recycles slots instead of freeing. I/O lives off the hot path behind the §35 queue and the §37 revolver, exactly where a syscall must not be. A system is a bounded pass over a known-size table (§13); the order is fixed and deterministic (§16). You built these for performance, and they are hard-real-time preconditions for free. The data-oriented core is already most of the way to predictable.

And here is the part that should sober you: the rest is a genuine inversion, not just more of the same. The book measured averages; hard real-time needs proven worst cases, and the measured ns/element ladder of §27 is an average over a warm cache. The §1 cache staircase the book celebrates as a performance story is, here, a source of WCET uncertainty: a cache miss is the worst case, and code whose timing depends on cache state has a worst case far above its mean. The same is true of branch prediction, speculation, and frequency scaling - each speeds the common path and widens the gap between common and worst. Hard real-time often disables them, or budgets for their worst case, and runs slower on average to be predictable. That is the opposite of every instinct the book has trained. Hard real-time is not the book’s techniques turned up; it is a different objective function that sometimes overrules them.

The exclusion, stated as plainly as it can be, because here it is a safety matter: this book does not build hard-real-time systems, and a soft-real-time system must never be deployed where a hard one is required. A certified controller is a different craft - a real-time operating system, WCET analysis tools, and certification regimes (DO-178C for avionics, IEC 61508 for industrial safety) that this book does not touch. The simulator you built is a wonderful thing and it is not a brake. The value of this chapter is the line itself: knowing which side of it you are on, so you neither over-build a game loop into a control system nor, far worse, mistake your fast average for a guarantee you never made.

Measurements

This is the one chapter where the book’s measure-it-on-four-machines moat partly cannot reach: true WCET needs a real-time OS and formal timing analysis, not four Linux boxes, and the chapter says so rather than faking a number. Jitter, though, measures cleanly and is the right demonstration - record the actual period of every tick over a few million ticks on a stock scheduler and the histogram has a long, ugly tail driven by the OS, not your code. Pinning the core, raising the scheduling class, and locking pages tighten that tail visibly. The exercises build the histogram; the lesson is in the shape of the tail, and in which knobs flatten it.

Exercises

  1. Measure your jitter. Run an empty tick loop at a fixed period for a few million iterations and record the actual period each time (a monotonic clock). Plot the distribution. The mean is tight; find the tail - the p99.9 and the max - and note it is tens to hundreds of times the mean on a stock desktop.
  2. Find the scheduler. Re-run pinned to an isolated core (taskset/isolcpus) under SCHED_FIFO, with mlockall. Show the tail shrink. You did not make the code faster; you made the worst case smaller, which is the only thing hard real-time counts.
  3. Hunt an unbounded operation. Audit one system for anything without a static bound: a HashMap that can rehash, a Vec that can reallocate, a data-dependent loop, a format!. Each is a spike in your WCET. Replace one with a pre-sized, bounded equivalent and re-measure the tail.
  4. Allocation is a spike. Add one Box::new per tick to the hot loop and watch the tail grow; remove it and watch it shrink. Confirm the book’s no-per-tick-allocation discipline (§7, §24) was buying you tail latency all along.
  5. The cache is a worst case. Run a system over data small enough to stay in L1, then over data large enough to miss to RAM (§27). Compare not the means but the maxima. Argue why average-case layout tuning does not give a WCET, and what would.
  6. Soft, not hard - on purpose. Take your anytime system (§39) and write down, honestly, its worst-case time. Show it has none you can prove. Conclude what kind of deadline it may and may not be trusted with.
  7. (stretch) Priority inversion. Build three tasks - high, medium, low - sharing one lock, and reproduce the high task missing its deadline because the low task holds the lock while the medium task runs. Then enable priority inheritance and show the deadline met. Name the mechanism that fixed it.
  8. Degrade gracefully (the soft side). Overload the tick - more entities than the budget allows. Implement a priority-ordered shed: drop the inspection system first, stretch the GC cadence, then defer reproduction (back-pressure on the thing creating the load). Show two things hold: the world stays consistent every tick (the §22 buffered commit), and a degraded run still replays bit-for-bit because each shed was logged, not branched on the wall clock. Confirm the staleness is bounded - nothing is deferred more than a fixed number of ticks.

Reference notes in 49_worst_case_is_the_only_case_solutions.md.

What’s next

That answers the last of the four unattended questions: the system survives the stop (§46), reports what it is doing (§47), gives the same answer on every machine (§48), and you now know the line past which its soft deadlines cannot be trusted. §50 draws the operations group together - four chapters that were one move - and marks the turn from running the system to changing it, where the rest of the second act goes.