ROB-320 · Lesson 2 of 12 · 30 min

Timing, measurement and worst-case execution time

A timing requirement is only as good as the numbers used to check it. This lesson is about getting those numbers: how long code takes to run, how much that varies, and what the longest possible time is. The longest time is called the worst-case execution time, or WCET, and it is the single most important number in real-time analysis, because every schedulability test in lesson 5 starts from it. Getting it wrong in the optimistic direction means a system that passes every analysis and still misses deadlines in the field. Getting it badly wrong in the pessimistic direction means buying a processor three times larger than needed.

Execution time is not a fixed property of a piece of code. It depends on the input data, which branches are taken, the state of the caches and pipelines when the code starts, what other cores are doing to shared memory and buses, and whether interrupts arrive mid-way. A measurement taken on a quiet bench with warm caches is a best case, not a worst case. The skill is to measure under conditions that push execution time up and to understand which effects the measurement may have missed.

Three ways to bound execution time

There are three broad approaches, and serious projects often use more than one.

  • Measurement-based: run the code many times under stress and record the maximum. This is cheap and realistic, but it only finds the worst case that happened to occur. Engineers add a safety factor, often 20 to 50 %, to cover cases not exercised.
  • Static analysis: tools model the processor's pipeline and caches and analyze every path through the compiled code to compute a guaranteed upper bound. This gives a safe number, but it needs a detailed processor model, struggles with complex multicore chips, and can be pessimistic. It is common in aerospace and automotive certification.
  • Hybrid: measure execution times of small code segments on the target, then combine them along the worst path found by analyzing the program structure. This reduces pessimism while keeping some of the safety of analysis.

Whatever the method, the result should be written down with its conditions: compiler version and optimization level, clock frequency, cache configuration, and the load present during measurement. A WCET without conditions cannot be reused when anything changes.

Measuring with a GPIO pin and an oscilloscope

The simplest and most trustworthy measurement uses a spare output pin. Set the pin high at the start of the code under test and low at the end, then watch it on an oscilloscope or logic analyzer. The pulse width is the execution time, the spacing between pulses is the actual period, and the oscilloscope's persistence or histogram mode accumulates thousands of cycles so that the longest pulse and the widest spacing stand out. Toggling a pin costs only a few processor cycles, so it disturbs the timing very little, and the measurement is made by independent hardware rather than by the software being measured.

Raising and lowering a debug pin around the work being timed (STM32-style register names)
/* Bracket the control step with a GPIO pin for oscilloscope timing */
void control_isr(void)
{
    GPIOB->BSRR = PIN_DEBUG;          /* pin high: start of step  */
    read_encoder();
    compute_control();
    write_pwm();
    GPIOB->BSRR = PIN_DEBUG << 16;    /* pin low: end of step     */
}
An oscilloscope-style trace of a debug pin over several control periods. Each period starts with a pulse. Most pulses are short, but one pulse is noticeably wider and starts slightly late. Annotations mark the period T, the execution time C as the pulse width, release jitter as the spread in start times, and the longest pulse as the observed worst case.
A debug pin shows execution time as pulse width and release jitter as the spread in pulse start times

Two refinements help. Use separate pins for different tasks so their interaction is visible, for example a pin that goes high whenever an interrupt is running. And measure from the hardware event rather than from the start of the code: triggering the oscilloscope on the timer or sensor signal and measuring to the end of the output write captures the interrupt latency and wake-up time too, which is the latency the controller actually experiences.

Cycle counters and software timestamps

Most modern processors include a free-running cycle counter. On ARM Cortex-M3 and higher cores it is the DWT CYCCNT register in the debug unit; on x86 it is the time-stamp counter read by RDTSC; RISC-V defines a cycle CSR. Reading the counter before and after a block and subtracting gives its execution time in cycles, which divided by the clock frequency gives seconds. Software timestamps make it easy to collect statistics over millions of runs and to store a histogram on the target, but the measurement itself takes time and the code that records it can disturb caches. Keep the bookkeeping tiny: update a running maximum and a histogram bin, and send the results out slowly from a low-priority task.

Using the Cortex-M cycle counter to keep a running maximum and a histogram
/* Cortex-M: measure a function with the DWT cycle counter */
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;   /* enable trace unit   */
DWT->CYCCNT = 0;
DWT->CTRL  |= DWT_CTRL_CYCCNTENA_Msk;             /* start counting      */

uint32_t t0 = DWT->CYCCNT;
compute_control();
uint32_t cycles = DWT->CYCCNT - t0;               /* wrap-safe subtract  */
if (cycles > max_cycles) max_cycles = cycles;      /* running maximum     */
hist[cycles >> 6]++;                               /* 64-cycle bins       */

Worked problem

Converting cycles to time

A controller step on a 480 MHz processor takes between 31,200 and 58,700 cycles over a million runs. What are the best and worst observed execution times, and what WCET would you budget with a 30 % margin?

Show solution
  1. Best: \( 31{,}200 / 480 \times 10^{6} = 65 \) µs.
  2. Worst observed: \( 58{,}700 / 480 \times 10^{6} = 122.3 \) µs.
  3. Budgeted WCET: \( 122.3 \times 1.3 = 159 \) µs.

Answer: Observed times range from 65 to 122 µs; budget about 160 µs. The nearly two-to-one spread is typical of code that misses in cache or takes different branches on different cycles.

Why execution time varies

Understanding the sources of variation helps an engineer design measurements that find the true worst case.

Common sources of execution-time variation and how to expose them
SourceEffectHow to provoke it in testing
Input-dependent branchesDifferent paths take different timesDrive inputs to limits, saturation and error cases
CachesFirst access after eviction is much slowerRun cache-thrashing tasks or flush caches before each run
Shared buses and memory on multicore chipsOther cores delay memory accessRun memory-heavy load on all other cores
Interrupts and preemptionHigher-priority work stretches elapsed timeGenerate maximum interrupt and network traffic
Frequency scaling and thermal throttlingClock slows under heat or power limitsPin frequency, test in a warm enclosure
Floating-point special casesDenormal numbers can be very slow on some coresFeed tiny values; enable flush-to-zero
Memory allocation and system callsUnbounded or variable timeRemove from the control path entirely

A useful distinction is between execution time, the processor time a job needs if it runs alone, and response time, the elapsed time from release to completion including time spent waiting for or preempted by other work. WCET is about the first; lesson 5 builds response time from it. Measurements made with a debug pin around the whole task capture response time unless interrupts are disabled, so label measurements carefully.

Worked problem

Multicore interference

A trajectory task runs in 180 µs worst case when the other three cores are idle. With a memory-heavy perception workload on the other cores, its worst case rises to 290 µs. What interference factor should the budget use, and why does it matter?

Show solution
  1. Factor: \( 290 / 180 = 1.61 \).
  2. The task's utilization at 1 kHz rises from 0.18 to 0.29.

Answer: Budget with the loaded value, an interference factor of about 1.6. On multicore systems-on-chip, shared caches and memory controllers make one core's timing depend on the others, so WCET must be measured with realistic load on every core, or the cores must be partitioned with cache and memory bandwidth controls.

Statistics of execution time

Execution-time distributions are not normal. They usually have a sharp lower edge, a main hump and a long thin tail to the right, where the rare slow cases live. The mean and standard deviation say almost nothing about the tail, and the tail is what causes deadline misses. Report the maximum, high percentiles such as the 99.9th and 99.999th, and a histogram on a logarithmic count axis that makes the tail visible. The number of runs matters: a one-in-a-million slow path will not appear in a ten-thousand-run test. For a 1 kHz loop, one hour of operation is 3.6 million cycles, so a soak test of several hours is a minimum for a hard real-time task.

The mean hides the tail; a short test misses the rare slow path
import random
random.seed(7)
# Synthetic execution times (us): a main hump plus a rare slow path
samples = []
for i in range(2_000_000):
    t = 60 + random.gammavariate(4, 3)           # typical path
    if random.random() < 2e-5:                    # rare cache-cold case
        t += random.uniform(40, 70)
    samples.append(t)
first = max(samples[:10000])
samples.sort()
n = len(samples)
def pct(p):
    return samples[min(n - 1, int(p / 100 * n))]
mean = sum(samples) / n
print("mean     %.1f us" % mean)
for p in (50, 99, 99.9, 99.999):
    print("p%-7s %.1f us" % (p, pct(p)))
print("max      %.1f us" % samples[-1])
print("first 10,000 samples only, max %.1f us" % first)
Output
mean     72.0 us
p50      71.0 us
p99      90.2 us
p99.9    99.3 us
p99.999  126.1 us
max      146.4 us
first 10,000 samples only, max 110.7 us

The mean is 72 µs and the 99th percentile is 90 µs, but the maximum over two million runs is 146 µs, twice the mean. A ten-thousand-sample test tops out at about 111 µs: it sees the long tail of the normal path but none of the rare cache-cold cases. This is why a short bench test that 'never goes above 90 µs' is not evidence of a 90 µs WCET.

Tracing tools

Pins and counters time one thing at a time. Tracing tools record a timeline of everything: every task switch, interrupt entry and exit, lock acquisition and user-defined event, each with a timestamp. Hardware trace units such as ARM's Embedded Trace Macrocell stream instruction-level trace off the chip with almost no overhead. Software tracers built into operating systems record events into a memory buffer: FreeRTOS has trace hooks used by commercial and open-source viewers, Zephyr has a tracing subsystem, and Linux has ftrace, perf and the LTTng toolkit, with kernelshark and Trace Compass to view the results. ROS 2 has ros2_tracing, built on LTTng, which records callback start and end times so that end-to-end latency through a chain of nodes can be reconstructed.

A trace answers questions a single number cannot: which interrupt delayed the control task, which lower-priority task held a mutex, whether a periodic task drifted. The cost is data volume and some overhead, so traces are usually captured for short windows triggered by an anomaly, for example 'record continuously in a ring buffer and freeze it when the control task overruns'.

Worked problem

Sizing a trace buffer

An RTOS trace records 16-byte events. The system generates about 25,000 events per second. How much memory holds the last 2 seconds before a fault?

Show solution
  1. Events: \( 25{,}000 \times 2 = 50{,}000 \).
  2. Bytes: \( 50{,}000 \times 16 = 800{,}000 \) bytes, about 781 KiB.

Answer: About 800 kB, too much for many microcontrollers. Recording only scheduler and interrupt events, or storing 8-byte compressed events, brings it within reach; alternatively stream the trace off-chip through a debug probe.

Measuring periods and jitter

Execution time is half the story; the other half is whether tasks start when they should. Measure the actual period by timestamping each release and taking differences. Release jitter is the spread of start times relative to the ideal schedule, usually reported as the maximum deviation or as the difference between the largest and smallest observed periods. A periodic task should be released by a hardware timer, not by a software loop that sleeps for a fixed time after finishing its work, because sleeping a fixed time after variable work makes the period drift by the execution-time variation every cycle. Lesson 4 shows the correct RTOS call for absolute-time periodic waits, and lesson 9 shows the Linux equivalent.

On Linux, the standard tool for measuring how late the system wakes a high-priority thread is cyclictest from the rt-tests package. It runs threads that sleep until absolute times and records how late they actually wake. Lesson 9 uses its histograms to compare a standard kernel with a real-time one.

Common mistakes

  • MistakeTiming code on an idle bench with warm caches

    InsteadMeasure under maximum realistic load with caches and buses contended.

  • MistakeReporting mean and standard deviation of execution time

    InsteadReport maximum, high percentiles and a log-scale histogram.

  • MistakeRunning a short test and calling its maximum the WCET

    InsteadSoak for millions of cycles and add a documented margin.

  • MistakeReleasing a periodic task with a relative sleep after the work

    InsteadRelease it from a hardware timer or an absolute-time wait.

  • MistakeConfusing execution time with response time

    InsteadLabel measurements and use WCET as the input to response-time analysis.

Summary and key terms

  • Worst-case execution time is the key input to real-time analysis and depends on data, caches, buses and interrupts.
  • WCET can be bounded by measurement with a margin, by static analysis, or by hybrid methods.
  • A debug pin and an oscilloscope measure execution time, period and jitter with minimal disturbance.
  • Cycle counters give software timestamps for statistics over millions of runs.
  • Execution-time distributions have long right tails, so maxima and high percentiles matter, not means.
  • Multicore interference can stretch execution time and must be measured with realistic load on all cores.
  • Tracers record timelines of task switches and interrupts and explain why deadlines were missed.
WCET
Worst-case execution time: the longest time a piece of code can take to run in isolation.
BCET
Best-case execution time: the shortest time the code can take.
Response time
Elapsed time from a job's release to its completion, including waiting and preemption.
Static timing analysis
Computing a guaranteed WCET bound from the program and a processor model.
Cycle counter
A hardware register that counts processor clock cycles.
Debug pin
A spare output toggled by software so external instruments can time it.
Release jitter
Variation in when a periodic job actually starts.
Interference
Delay caused by other tasks, interrupts or cores sharing resources.
Trace
A timestamped record of system events such as task switches and interrupts.
cyclictest
A Linux tool that measures how late high-priority threads wake from timed sleeps.

Practice problems

Work each problem on paper first, then reveal the answer to check.

  1. A step takes 96,000 cycles at 400 MHz. How long is that?

    Show answer

    240 µs.

  2. Name the Cortex-M register used for cycle counting.

    Show answer

    DWT CYCCNT.

  3. Why does a debug pin barely disturb timing?

    Show answer

    Toggling it costs only a few cycles and an external instrument does the measuring.

  4. How many cycles does a 1 kHz loop run in 8 hours?

    Show answer

    28.8 million.

  5. What is the difference between WCET and response time?

    Show answer

    WCET is isolated execution; response time includes waiting and preemption.

  6. Give one way to provoke cache-related worst cases.

    Show answer

    Run cache-thrashing load or flush caches before each run.

  7. A measured maximum is 75 µs. What WCET would a 40 % margin give?

    Show answer

    105 µs.

  8. Why is the mean misleading for real-time work?

    Show answer

    It ignores the rare slow tail that causes misses.

  9. What Linux tool reconstructs ROS 2 callback latency?

    Show answer

    ros2_tracing, built on LTTng.

  10. Why should periodic tasks not use a relative sleep after their work?

    Show answer

    The period drifts by the variation in execution time.

Lesson quiz · 5 questions

Score 60% or more to complete this lesson.

Take the quiz