Unit 3 · OS & concurrency · Lesson 10 of 11
Profiling, bottlenecks and benchmarking pitfalls
Step 1: Learn the ideas
- Investigation method and the USE method
- Workload characterization and goals
- Sampling versus instrumentation versus tracing
- perf stat counters: cycles, instructions, IPC, cache and branch misses
- perf record, stack walking and symbols
- CPU and off-CPU flame graphs
- CPU, memory, I/O, lock and scheduler bottlenecks
- Latency histograms and percentiles
- Coordinated omission and open-loop load
- Benchmarking pitfalls: warmup, cache state, frequency scaling, noise
- Measuring the wrong thing: client limits, dead code, unrealistic data
- Continuous profiling in production
Why it matters
"The service got slow, what do you do?" is a common prompt for SRE, infrastructure and performance-sensitive roles, and the weak answer is a list of tools. The strong answer is a method that finds the limiting resource quickly and verifies the fix with a measurement you trust. The second half matters as much: a reported speedup, including one in a paper, can come from a benchmark that measured a warm cache, a throttled CPU, the load generator itself, or an average that hid the tail.
Core ideas
Method first. State the metric, such as p99 latency at a given load or throughput within an SLO, and reproduce the problem. For each resource (CPUs, memory, disks, network, locks, thread pools) check utilization, saturation and errors, the USE method, before reading code. Then change one thing at a time and re-measure under the same conditions.
Sampling, instrumentation, tracing.
- Sampling profilers such as
perf record, py-spy or async-profiler interrupt at a fixed rate and record stacks. Overhead is low enough for production, but results are statistical and depend on stack walking: missing frame pointers or symbols give broken stacks. - Instrumentation such as cProfile, manual timers or tracing spans records every call or region. Counts are exact, but overhead distorts short functions.
- Tracing with tracepoints or eBPF tools records specific events (system calls, disk I/O, scheduler switches) with timestamps, which is how you attribute latency rather than CPU time.
Counters. perf stat reports cycles, instructions, cache and branch misses, context switches and page faults. Low instructions per cycle with many cache misses points to memory stalls rather than "slow code". High CPU utilization can hide spinning or stalls, so utilization alone does not prove the work is useful.
Flame graphs. Each box is a function in the sampled stacks; its width is the share of samples containing it, and callees sit on top of callers. The x-axis is sorted alphabetically, not by time, so look for wide plateaus rather than a left-to-right sequence. A CPU flame graph shows only on-CPU time; latency spent waiting on locks, I/O or the run queue needs off-CPU analysis, which records where threads block and for how long.
Classifying bottlenecks.
- CPU-bound: busy cores and a wide hot path; improve the algorithm or parallelize.
- Memory-bound: low IPC and many cache misses; change layout or access pattern.
- I/O-bound: high device utilization or queue depth and threads blocked in I/O; batch, cache or change the access pattern.
- Lock-bound: low CPU and low throughput with threads waiting in futex calls; shrink critical sections or shard.
- Scheduler-bound: long run-queue latency or, in containers, quota throttling recorded in
cpu.stat.
Measuring latency. Record full distributions in histograms, and never average percentiles across machines; merge histograms instead. Many load generators are closed loops: each connection waits for a response before sending its next request, so when the server stalls they stop sending and never record the requests that would have waited. This coordinated omission hides exactly the bad periods. Use constant-rate, open-loop generators such as wrk2, or correct each latency to the request's intended send time.
Benchmarking pitfalls. Missing warmup (JIT compilation, lazy initialization, connection pools, page cache); unstated cache state; CPU frequency scaling, turbo and thermal throttling; noisy neighbors on shared hosts; a client, network or logging bottleneck masquerading as the system under test; compilers deleting code whose result is unused; data sets that fit in cache unlike production; too few runs and no variance. Even link order and environment size can shift results, as Mytkowicz et al. showed in "Producing Wrong Data Without Doing Anything Obviously Wrong!" (ASPLOS 2009). Active benchmarking means profiling the benchmark while it runs to confirm what limits it.
Common misconceptions
- "The flame graph x-axis is time." It is an alphabetical merge of stacks; flame charts show time order.
- "100 percent CPU means we need faster CPUs." It may be spinning, garbage collection or memory stalls; check IPC and what the cycles are doing.
- "Low CPU means the service has headroom." Threads may be blocked on locks or I/O, or throttled by a CPU quota.
- "The profiler shows why requests are slow." CPU profilers miss waiting; latency needs off-CPU analysis or tracing.
- "Averaging each host's p99 gives the fleet p99." Percentiles do not average; merge the distributions.
- "A microbenchmark speedup carries over to production." Cache state, data shape and contention differ.
How to approach it
Practice the method aloud on a scenario until it takes five minutes. Then do it for real: profile a deliberately slow program with perf (or py-spy for Python), produce a flame graph, fix the hot spot and verify. In interviews, name the metric, the resource checks, one profiling step and how you will confirm the fix.
Suggested order
- 1Read the USE method page and write its checklist for a single Linux server from memory.
- 2Read the Flame Graphs and CPU Flame Graphs pages, then generate a flame graph for a toy program with perf record and the FlameGraph scripts (or py-spy for Python).
- 3Read the Off-CPU Analysis page to learn how blocked time is measured.
- 4Keep Linux perf Examples open while running perf stat on a cache-friendly and a cache-hostile loop.
- 5Read Active Benchmarking, then watch How NOT to Measure Latency for percentiles and coordinated omission.
- 6Do the OSTEP system-call and TLB measurement homeworks as benchmarking-hygiene practice: pin CPUs, warm up, repeat and report variance.
Step 2: Check off what you can do
Tap each one you can genuinely do or explain. Each earns 10 XP.
Step 3: Go deeper
The USE MethodStart here
Brendan Gregg's websitePrimary method for checking every resource’s utilization, saturation and errors before profiling code.
Open(opens in a new tab)Link responded, 5 Oct 2026Flame GraphsStart here
Brendan Gregg's websiteSection: CPU Flame Graphs
How to generate and read CPU flame graphs; continue to the Off-CPU Analysis page for blocked time.
Open(opens in a new tab)Link responded, 5 Oct 2026Linux perf Examples
Brendan Gregg's websiteHands-on perf commands for counters, sampling and fixing broken stacks.
Open(opens in a new tab)Link responded, 5 Oct 2026Active Benchmarking
Brendan Gregg's websiteDiscipline for confirming what limits a benchmark while it runs.
Open(opens in a new tab)Link responded, 5 Oct 2026How NOT to Measure Latency (talk)
InfoQPercentiles, histograms and coordinated omission in latency measurement.
Open(opens in a new tab)Link responded, 5 Oct 2026
Step 4: Practice
Use a Linux machine or VM, since perf is Linux-only (on macOS, Instruments or py-spy can cover some steps). (1) Write a program with an obvious hot loop, a cache-hostile access pattern and a lock-contended section; profile it with perf stat and perf record, produce a flame graph and explain each finding. (2) Add a sleep or a blocking read and show that the CPU flame graph misses it while off-CPU analysis finds it. (3) Load-test a small HTTP service with a closed-loop tool and then with a constant-rate tool such as wrk2, and compare p99 near saturation. (4) Rerun one benchmark cold and warm, and with frequency scaling left on, and record how much each changes the result.
- OSTEP Ch. 6 measurement homework: system-call and context-switch costOpen(opens in a new tab)Not attemptedOSTEP homework
- OSTEP Ch. 19 measurement homework: TLB size and miss costOpen(opens in a new tab)Not attemptedOSTEP homework
Record attempts on each problem’s page (+15 XP, +10 more when solved on your own).
Step 5: Test yourself
Answer each one out loud or on paper before you finish the lesson.
- Question 1What can a sampling profiler miss that instrumentation catches, and vice versa?
- Question 2What does a low instructions-per-cycle value suggest?
- Question 3How would you tell a lock-bound service from an I/O-bound one?
- Question 4Why should you not average p99 latencies across hosts?
- Question 5Which warmup effects can make the first minutes of a benchmark unrepresentative?
- Question 6How can a container show low CPU usage and still suffer CPU-related latency?
You’re done when you can
- Walk through a slow-service investigation aloud in five minutes using the USE method and one profiling step.
- Produce a CPU flame graph for your own program and explain its widest frames correctly.
- Demonstrate a case where CPU profiling misses the cause and off-CPU analysis finds it.
- Explain coordinated omission with a numeric example and how open-loop load avoids it.
- List six benchmarking pitfalls with the control you would use for each.
Finish the lesson
Unlocks next: Database benchmark and workload design, Performance modeling, traces and bottleneck attribution


