How to read a flamegraph
You’ve run a profiling session, say with async-profiler, and now you’re staring at a flamegraph. It’s one of the best ways to see where a program’s time goes, as long as you know the reading grid. It comes down to a few rules.
What each axis means
A flamegraph aggregates thousands of stack samples. Each rectangle is a frame, meaning a method.
- Width: the share of samples where this method was on the stack. The wider it is, the more time the program spent there (CPU or wall-clock, depending on what you measured). This is the only dimension you use to prioritise.
- Height: the depth of the call stack. A frame sitting on another is a call: the bottom one called the one above.
- Colour: usually meaningless. The hues are random, just to tell neighbouring blocks apart. Some modes do use colour to separate Java, native and kernel, so check the legend before reading anything into it.
The classic trap: the horizontal axis isn’t time
This is the mistake people make most. Left to right is not a timeline. Frames at the same level are simply sorted alphabetically to merge identical stacks. A method on the far right doesn’t run “after” one on the left. A flamegraph answers “where does the time go?”, not “in what order?”. For chronology you need a different tool, a trace or a timeline.
In practice: hunt for plateaus
The approach that works:
- Start at the top and find the wide frames. The top of a stack is the code that was actually running when the sample was taken (its “self time”). A wide plateau up there is time burnt right there. First suspect.
- Go back down to follow the path. Tracing a wide frame downward shows you who led to it. Often the culprit isn’t the leaf itself but the fact that it gets called far too often, from higher up.
- Ignore thin, isolated towers. A narrow, very tall stack costs little: lots of nested calls, little total time. Not a priority.
The rule to keep in mind: width tells you how much it costs, position at the top tells you where it’s actually spent.
A few patterns you learn to spot
- A wide flat plateau right at the top: a hot method, computation, serialisation, parsing. Optimisable directly.
- A very wide base branching upward: an entry point that scatters time across a crowd of sub-calls. Look for the common factor rather than one method.
- Layers of framework or proxy frames stacked under your code: usually AOP, interceptors, reflection. That’s normal, until it gets wide.
Match your reading to the profiled event
The same graph reads differently depending on what was measured. On a CPU flamegraph, a wide frame is computation to optimise. On a wall-clock flamegraph, a wide frame can be a wait (I/O, lock, park), and the goal there isn’t to speed the code up but to remove the blocking. On an allocation flamegraph, width is bytes allocated, not time. Always keep in mind what you asked async-profiler to measure.
In short
Look for the wide, start at the top, don’t read the horizontal as a clock, and remember what the profiled event means. With that, a flamegraph that looked unreadable turns into a fairly direct map of what to fix first.
If you haven’t generated yours yet: Profiling the JVM with async-profiler.