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.

§4 gave the tick a budget: 33 ms at 30 Hz, and you spend it wisely. Most of the book has been soft real-time. A missed deadline costs quality - a dropped frame, a coarser answer - and the system keeps running. 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. 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. 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. It demands a different ledger: worst-case execution time you can prove (every loop a statically known maximum, no unbounded recursion, no data-dependent loop without a bound), no allocation in the inner loop (allocators have unbounded worst-case time), no blocking call in the inner loop, and bounded jitter (a real-time scheduler, CPU isolation, locked pages).

And here is the part this book must state more bluntly than the Rust edition would: CPython is not a hard-real-time runtime, and no discipline makes it one. Three reasons, each fatal on its own. The garbage collector can pause at any allocation to walk the object graph, for a time bounded by nothing you control. The GIL means another thread can hold the interpreter when your deadline arrives. And every ordinary Python operation allocates - a boxed int, a list resize, a temporary - so “no allocation in the inner loop” is a rule the language fights you on. You can soften each (pre-allocate numpy arrays so the hot loop boxes nothing, gc.disable() for a bounded window, pin the process), and that buys you better soft real-time - a tighter tail - but never a proven worst case. If a missed deadline is a fault, the controller is written in C or Rust on a real-time OS, with WCET analysis and a certification regime (DO-178C, IEC 61508) this book does not touch, and Python sits outside the loop, configuring and monitoring it. The simulator you built is a wonderful thing and it is not a brake.

The reassuring half still holds: the data-oriented core is already most of the way to predictable. No per-element Python allocation since §7 (numpy columns are pre-sized), the §22 buffer reused, §24 recycling slots, I/O off the hot path behind the §35 queue. You built these for performance, and they are what make the soft tail tight. They do not make CPython hard - nothing does - but they are the difference between a jitter histogram with a short tail and one with a long ugly one.

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, a CPython program must never be deployed where a hard one is required, and “I made the average fast” is not a guarantee you ever made. The value of this chapter is the line itself: knowing which side of it you are on.

Measurements

This is the one chapter where the measure-it moat partly cannot reach: true WCET needs a real-time OS and formal timing analysis, not a stock Python on a stock kernel, 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 and the histogram has a long, ugly tail. In Python the tail has named culprits the exercises can toggle: the OS scheduler, and on top of it the GC. gc.disable() for the measured window, pinning the core, and raising the scheduling class each visibly tighten the tail - and none of them flattens it to a guarantee, which is the lesson.

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 (time.perf_counter). Plot the distribution. The mean is tight; find the tail - p99.9 and the max - and note it is tens to hundreds of times the mean on a stock desktop.
  2. Watch the collector. Re-run with gc.disable() for the measured window, and again with a forced gc.collect() mid-run. Show the collection events in the tail. Conclude why a runtime that can pause for GC at any allocation cannot offer a worst-case bound.
  3. Allocation is a spike. Add one [0] * 1000 allocation per tick to the hot loop and watch the tail grow; remove it and watch it shrink. Confirm the numpy-columns, no-per-element-allocation discipline (§7) was buying you tail latency all along.
  4. Hunt an unbounded operation. Audit one system for anything without a static bound: a dict that can rehash, a list that reallocates, a data-dependent loop, an f"{...}". Each is a spike. Replace one with a pre-sized numpy equivalent and re-measure the tail.
  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.
  6. Soft, not hard - on purpose. Take your anytime system and write down, honestly, its worst-case time. Show CPython gives you none you can prove (name the GC, the GIL, the allocator). Conclude what kind of deadline it may and may not be trusted with.
  7. 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 the world stays consistent every tick (the §22 buffered commit), and that 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.