skip to content

Reading Profiles with pprof

Turning a profile into an answer: which function actually burns the time, and how the graph moved after your fix - the -diff_base workflow is how you prove an optimisation landed.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

4

In `go tool pprof`, what does the `top` command list, and what do its flat and cum columns mean?

level: juniorimportance: must knowfreq 76%

answer

  1. two columns, two different questions
  2. one is self time, one includes callees
  3. main is always near the top of one of them
  4. sum% is a running total, not a per-row share
  5. sort by flat to find the burning code

basics

~20 s

top ranks functions by their share of a profile's samples. flat counts samples taken inside that function's own code; cum counts those plus everything it called. A wrapper has a tiny flat and a huge cum.

solid answer

~50 s

`go tool pprof profile.pb.gz` drops you at an interactive prompt, and `top` prints the heaviest functions one per row with flat, flat%, sum%, cum and cum% columns. Flat is the weight of the samples in which that function was the one actually executing — its own instructions. Cum is flat plus everything reached through it, so any function merely on the path to the real work carries a large cum: `main` is typically near 100% cum and 0% flat. `top` sorts by flat by default and `top -cum` re-sorts by cumulative. sum% is the running total of flat down the list, so you can see how much of the profile the rows you are looking at explain. Read cum to find the call path, and flat to find the code that is actually burning the CPU.

code

text · 9 lines
text
(pprof) top
Showing nodes accounting for 1.72s, 86.00% of 2s total
Dropped 61 nodes (cum <= 0.01s)
      flat  flat%   sum%        cum   cum%
     0.82s 41.00% 41.00%      0.82s 41.00%  bytes.Equal
     0.40s 20.00% 61.00%      0.44s 22.00%  runtime.mallocgc
     0.30s 15.00% 76.00%      1.46s 73.00%  main.(*generator).renderField
     0.20s 10.00% 86.00%      1.90s 95.00%  main.(*generator).walkSchema
         0     0% 86.00%      1.98s 99.00%  main.main

go deeper

for a junior

Be ready to say, without hesitating, that flat is time in the function's own code and cum is that plus everything it calls, and to point at the row you would investigate first.

for a middle

Explain why a wrapper carries a huge cum and no flat, what sum% accumulates, and when you would run top -cum instead of plain top.

for a senior

Show the diagnostic habit: use cum to walk down the call graph to the fork that matters, flat to pick the target, and say out loud when a profile is too diffuse for a point fix to help.

for a principal

Frame it as where an optimisation budget goes: a profile that is 40% in one leaf justifies a targeted change, one spread over fifty rows argues for a design change or for spending the budget elsewhere.

## What a profile is, before you read one A profile is a bag of *samples*. Each sample is a stack trace plus a value — for a CPU profile, an amount of CPU time attributed to that stack. `go tool pprof <profile>` loads that bag and opens an interactive prompt where you slice it. Go's own profiles carry function names inside the file, so `go tool pprof cpu.pprof` works on its own; passing the matching binary as well (`go tool pprof ./gen cpu.pprof`) is what lets you disassemble. ## `top` `top` is the first command everyone runs. It prints one row per function, sorted by flat weight, with five columns: - **flat** — the summed value of the samples in which this function was the *currently executing* frame, the leaf of the stack. This is self time. - **flat%** — that flat value as a share of the profile total. - **sum%** — the running sum of flat% from the top row down to this one. When sum% hits 90% you have seen the functions that account for nine tenths of the CPU. - **cum** — the summed value of every sample in which this function appears *anywhere* on the stack, leaf or not. This is self time plus the time of everything it called. - **cum%** — that cumulative value as a share of the total. `top` shows ten rows by default; `top20` or `top -nodecount=20` shows more, and `top -cum` re-sorts the same data by cumulative weight. ## The distinction that decides what you optimise Flat answers *where is the machine spending instructions*. Cum answers *which call path leads there*. They are different questions and the columns are not competing versions of the same number. A long stack of thin wrappers each carries the same enormous cum: `main`, then your top-level driver, then the loop, all the way down to the one function doing string comparison. Only the last of those has meaningful flat. If you sort by cum, the top of the list is your own high-level entry points; if you rewrite them you will change nothing, because none of the time is *in* them. This is the classic misread and it is expensive. Picture joining a team and being handed a CPU profile someone captured last week from the code generator that runs over the repo's schema files as a build step. `top -cum` puts `walkSchema` at 95% and it looks like the villain. Its flat is 0.20s out of 2s. The 1.9s belongs to what it calls — and the leaf underneath it is a byte comparison in a loop. Days spent restructuring `walkSchema` buy nothing; a few lines at the leaf buy most of the profile. The inverse mistake is real too: a function with big flat and no interesting cum is genuinely hot, but if it is called from twenty places, the fix may be to call it less rather than to make it faster — and only cum, per caller, tells you that. ## Reading a row correctly - flat ≈ cum: a leaf. All its time is its own; optimise its body. - flat ≈ 0, cum large: a pass-through. Do not optimise it; follow it downward. - flat large, cum much larger: it does real work *and* calls something expensive. Both are candidates. ## Practical notes Units follow the profile type: a CPU profile is in seconds, so the header reads `Showing nodes accounting for 1.72s, 86.00% of 2s total`. `Dropped N nodes (cum <= …)` means pprof hid nodes below its cutoff — they are still counted in the total, just not printed. Percentages are always of the whole profile, not of the row above, so cum% down a single call chain does not add up to anything; the same seconds are counted once at every level of the stack. A function that never appears as a leaf can still be the problem, and a function with 40% flat can be irreducible (a `memmove` in the runtime often is). `top` narrows the field; it does not name the fix. The next steps are looking at the annotated source of the hot function and at who calls it, which is what pprof's `list` and `peek` commands are for.

  • Why does `main` almost always sit at the top when you run `top -cum`, and what should you do about it?
    Because every sample's stack contains `main`, its cumulative weight is essentially the whole profile. That is a property of the call graph, not a finding. Ignore the frames near the root and walk downward until cum starts splitting between children — the fork points are where a real decision lives, and the leaves with high flat are where the CPU actually goes.
  • If the flat percentages of the top ten rows only add up to 30%, what does that tell you?
    That the cost is flat and diffuse rather than concentrated: there is no single hot function, so a point fix will not move the number. sum% is exactly the column that tells you this. Diffuse profiles usually mean the win is structural — doing less work overall, allocating less, or removing a layer — rather than tuning any one function.
  • How do you widen the `top` listing beyond the default ten rows?
    Type `top20` (or any number) at the pprof prompt, or pass `-nodecount=20`. You can also narrow instead of widen: `focus=<regexp>` keeps only samples whose stack matches, and `ignore=<regexp>` drops those that do, which is how you isolate one subsystem's share of a profile before reading the ranking.

Flat is how long each person on a relay team was personally running; cum is the elapsed time from when they took the baton until their leg of the race was fully over, including everyone downstream.

saying these in an interview costs you the question

  • Says cum is the function's own time and flat includes callees
  • Optimises the function with the largest cum without checking flat
  • Thinks cum% down a call chain should add up to 100%
  • Reads sum% as the function's share of the profile
  • Assumes top's ranking already names the fix
open as a page

What does `go tool pprof -http=:8080 cpu.pprof` open, and how do you read its flame graph?

level: middleimportance: should knowfreq 47%

basics

~20 s

It starts a local web server and opens pprof's browser UI over that profile: Top, Graph, Flame Graph and Peek views. In the flame graph each box is a stack frame, and its width is that call path's share of samples.

open as a page

In `go tool pprof`, what do the `list` and `peek` commands show that `top` does not?

level: middleimportance: should knowfreq 54%

basics

~20 s

list <regexp> prints a matching function's source annotated with per-line flat and cum weight, so you see which statement costs. peek <regexp> prints that function's callers and callees with the share each edge carries. top only ranks whole functions.

open as a page

How do you use `go tool pprof -diff_base` on two saved CPU profiles to find what got slower between builds?

level: seniorimportance: nice to knowfreq 34%

basics

~20 s

Run go tool pprof -diff_base=old.pprof new.pprof. pprof subtracts the base sample by sample, so rows are deltas: positive means the new profile spends more there, negative means less. It is only meaningful if both profiles cover comparable work.

open as a page