Skip to content
Work Practice Products Coding interviews Machine learning coding Quant developer HFT course Blog Method Why me About Enquire Book a call

Quant developer HFT course · Module 8: Measuring latency

Profiling the hot path

Lesson · 11 min

The question

The p50 of the book update is fine but p99.9 got worse after a release. How would you find out why? Would you run a profiler?

Two questions, two tools

"Where does the time go on average?" and "why was this one message slow?" are different questions.

Sampling profilers answer the first. perf record interrupts the program thousands of times a second and records the call stack each time. Functions on the stack in many samples are where the time goes. The overhead is low and no code changes are needed.

Tracing and instrumentation answer the second. You record timestamps at chosen points for every event, then look at the slow events individually.

The question is about the tail, so a sampling profile on its own is the wrong first tool: the p99.9 events are one in a thousand, so they are a tiny share of samples, drowned by the common case.

Sampling profiles and flame graphs

Still, the profile is where to start for a regression in the body, and its output is worth reading well.

perf record -F 9999 -g --call-graph dwarf -p <pid> -- sleep 30
perf report                       # or: perf script | stackcollapse-perf.pl | flamegraph.pl > out.svg
main_loopFeedHandler::on_packetStrategy::on_bookdecodeBook::applyunordered_map::findfair_value
A flame graph: each box is a function, its width the share of samples in which it was on the stack, its parents below. Wide boxes near the top are where time is spent. Here the hash lookup inside the book update stands out.

A flame graph draws each stack sample as a column of boxes from the outermost function at the bottom to the innermost at the top, merges identical stacks, and sorts alphabetically. Width is the fraction of samples; height is stack depth. Look for wide plateaus at the top: functions that are themselves busy. Compare before and after a release with a differential flame graph.

Practical points:

  • Build with frame pointers (-fno-omit-frame-pointer) or use DWARF unwinding, or the stacks will be broken.
  • Inlined functions disappear into their callers; perf report --inline and debug info recover some of them.
  • A spinning thread polling an empty queue shows its polling loop as hot. That is waiting, not work; profile under real load or filter it out.

Instrumentation and tracing

To explain individual slow events, record what happened to each one:

struct Trace { std::uint64_t seq; std::uint32_t t_rx, t_decoded, t_booked, t_decided, t_sent; };
// on the hot thread: t = rdtsc-based timestamps at each stage, pushed into a ring

Every message gets a fixed-size record of stage timestamps, pushed through a lock-free ring to a recorder (Module 4). Offline, select the slowest 0.1% and compare them with typical ones: which stage grew? Was it the same instrument, the same message type, the first message after an idle period, a burst? Did it coincide with a page fault, a context switch or an interrupt on that core, which kernel tracing (perf trace, ftrace, bpftrace) can record with timestamps on the same clock?

That correlation is how the tail is explained: "the slow book updates are all the first update to a price level not touched in the last few seconds, and they miss in L3" is a finding; "the profile looks the same" is not.

Other profiling tools

  • perf stat for counters over a run (next lesson).
  • Intel VTune and AMD uProf for vendor-specific detail, microarchitecture analysis and memory access views.
  • Off-CPU analysis (perf sched, offcputime from BCC) shows where threads wait rather than run: on locks, I/O, the scheduler.
  • Intel Processor Trace records every branch taken with very low overhead, so the exact path of one slow event can be reconstructed afterwards. It produces large amounts of data and is a specialist's tool, but it is the closest thing to replaying a single slow message.

Back to the question

I would not start with a sampling profile, because p99.9 events are one in a thousand samples and won't show. I would use per-message stage timestamps to see which stage grew in the slow events and what they have in common, and correlate them with kernel events on that core. If the slow stage is the book update, I'd then profile or microbenchmark that path on inputs like the slow ones, and diff against the previous release.

Check yourself

Question 1 of 3

Why is a sampling profiler a poor tool for explaining p99.9 latency?

Question 2 of 3

In a flame graph, what does the width of a box represent?

Question 3 of 3

A profile of a spinning trading thread shows 80% of samples in the queue-polling loop. What does that mean?

In the interview

Say your own answer out loud first, then open the one we would give.

How would you find the cause of a tail latency regression?

With per-message stage timestamps rather than a sampling profile, because tail events are too rare to show in samples. I would find which stage grew for the slowest events, look for what they have in common, and correlate with kernel events on that core. Then reproduce the slow case in a benchmark and compare releases.

How do you read a flame graph?

Each box is a function, stacked on its callers, with width proportional to the share of samples containing it. Wide boxes at the top of a stack are where time is spent. Differential flame graphs between versions show what grew.

What is the difference between sampling and instrumentation?

Sampling periodically records the stack with low overhead and no code changes, good for where time goes on average. Instrumentation records timestamps at chosen points for every event, at some overhead, good for explaining individual events and tails.