heaptrack
heaptrack (1.5.0 on the grounding host) intercepts malloc, free, realloc, and the C++ allocation operators through LD_PRELOAD or injection, records a backtrace per allocation, and compresses the trace to one file. heaptrack_print reads that file into four rankings: most calls, peak consumption, most temporary allocations, and leaks. It needs no rebuild and runs far faster than Valgrind's Massif.
Contract
| Field |
Bound contract |
| Trigger |
A program uses too much memory, allocates too often, or leaks, and the user wants the allocating call stacks; or two builds must be compared for allocation behavior. |
| Authority |
Reversible local: writes only heaptrack data files (heaptrack.<prog>.<pid>.zst or the -o target), heaptrack_print output files, and the test binary in the working directory; rollback is deleting them. No remote mutation. |
| Side effect |
Data files on disk. The traced program runs slower while every allocation is unwound and logged. |
| Done |
The report names the top call stacks by peak bytes, by call count, and by temporary allocations, lists the leak records with their stacks, and, for a comparison, the per-stack delta between the two runs. |
Inputs
- Target binary with debug info (
-g, or a RelWithDebInfo CMake build) so backtraces carry function names and source lines. Optimization level stays as in production.
- A representative workload; heaptrack records allocation counts, so the workload must exercise the suspected path.
- For Rust: the binary must use the system allocator (
#[global_allocator] static A: std::alloc::System = System;) so the interposed malloc sees every allocation.
- For a comparison: two recorded data files.
Procedure
Record. heaptrack ./myapp args writes heaptrack.myapp.<pid>.zst in the current directory and prints the analyze command on exit; heaptrack -o run1 ./myapp names the file run1.zst; heaptrack -p <pid> attaches to a running process by injection; --record-only skips the automatic analysis launch. Done when: the .zst file exists and heaptrack printed its summary lines (calls to allocation functions, temporary allocations).
Print the summary and rankings. heaptrack_print -f run1.zst prints, in order, MOST CALLS TO ALLOCATION FUNCTIONS, PEAK MEMORY CONSUMERS, MOST TEMPORARY ALLOCATIONS, and a summary block:
total runtime: 2.34s.
calls to allocation functions: 145234 (62064/s)
temporary allocations: 89123 (38087/s)
peak heap memory consumption: 45.23M
peak RSS (including heaptrack overhead): 78.45M
total memory leaked: 2.34M
Each ranking entry reads N calls to allocation functions with X peak consumption from followed by the stack, leaf first. Toggle sections with -p 0 (peaks), -a 0 (allocation counts), -T 0 (temporary), and -l 1 (leaks, off by default); -n 20 widens the number of reported peaks from the default 10, -s limits sub-peak backtraces. -t 1 shortens template identifiers and -m 1 merges backtraces (both on by default). Done when: the four rankings and the summary are captured.
Read each summary line against the code. calls to allocation functions high relative to work done: many small allocations, so pool or reserve. temporary allocations (freed before the next allocation) as a large share of calls: churn in a hot loop, so hoist the buffer. peak heap memory consumption: the live-heap maximum to compare with the memory budget. peak RSS includes heaptrack's own overhead and non-heap mappings, so compare peak heap between runs, not RSS. total memory leaked: bytes never freed at exit; the -l 1 ranking names the stacks. Done when: each non-trivial line is tied to a call stack.
Draw the allocation flamegraph. heaptrack_print -f run1.zst -F stacks.folded --flamegraph-cost-type peak writes folded stacks (cost types: allocations, temporary, leaked, peak); then flamegraph.pl --countname bytes --title "Peak heap" stacks.folded > heap.svg from flamegraphs. Done when: the SVG opens and the widest frame matches the top ranking entry.
Compare two runs with the built-in diff: heaptrack_print -f after.zst -d before.zst prints every ranking as the delta of after minus before, so a positive peak entry is a regression and a negative one an improvement. Done when: the top positive and top negative deltas are named with their stacks.
Export to Massif format when a Massif-reading tool is already in the workflow: heaptrack_print -f run1.zst -M run1.massif, then ms_print run1.massif. Done when: ms_print renders the timeline.
Suppress known leaks from third-party code: write a suppression file with one leak:<function pattern> line per pattern and pass --suppressions leaks.supp; --print-suppressions 1 lists the matches and the suppressed byte total; --disable-builtin-suppressions reveals what heaptrack hides by default. Done when: the leak ranking holds only owned code.
Failure and recovery
| Failure |
Cause |
Fix |
| Stacks show only addresses |
No debug info |
Rebuild with -g; for Rust add debug = true to the profile |
| Rust program records almost nothing |
Custom global allocator bypasses malloc |
Switch to std::alloc::System for the profiling build |
total memory leaked is large but the program frees on exit paths |
Exit through _exit or quick_exit skipped destructors |
Read the leak stacks; a still-referenced global at exit is not a bug, and a suppression records that decision |
| Data file is huge |
Allocation-heavy workload |
Shorten the workload; record only the phase under suspicion by attaching with -p after startup |
heaptrack -p fails to attach |
ptrace restricted |
Run as the process owner or propose CAP_SYS_PTRACE; otherwise start the program under heaptrack |
| Comparison shows every stack changed |
Different workloads or non-deterministic addresses in symbols |
Use identical inputs and -m 1; compare peak heap totals first |
Massif reaches the same heap questions on platforms without heaptrack and at a much higher slowdown; valgrind covers it. Leak detection without profiling cost is LeakSanitizer through address-sanitizer.
Output
An allocation report with the summary block, the top stacks by peak, by call count, and by temporary allocations, the leak records, the flamegraph path when drawn, the per-stack deltas when two runs were compared, and the data file paths.
1---2name: heaptrack3description: Use when tracking heap allocations on Linux with heaptrack: allocation hotspots, peak heap, temporary allocations, leaks, or a diff of two runs via heaptrack_print. Not for heap errors: use valgrind.4---56# heaptrack78heaptrack (1.5.0 on the grounding host) intercepts `malloc`, `free`, `realloc`, and the C++ allocation operators through `LD_PRELOAD` or injection, records a backtrace per allocation, and compresses the trace to one file. `heaptrack_print` reads that file into four rankings: most calls, peak consumption, most temporary allocations, and leaks. It needs no rebuild and runs far faster than Valgrind's Massif.910## Contract1112| Field | Bound contract |13|---|---|14| Trigger | A program uses too much memory, allocates too often, or leaks, and the user wants the allocating call stacks; or two builds must be compared for allocation behavior. |15| Authority | Reversible local: writes only heaptrack data files (`heaptrack.<prog>.<pid>.zst` or the `-o` target), `heaptrack_print` output files, and the test binary in the working directory; rollback is deleting them. No remote mutation. |16| Side effect | Data files on disk. The traced program runs slower while every allocation is unwound and logged. |17| Done | The report names the top call stacks by peak bytes, by call count, and by temporary allocations, lists the leak records with their stacks, and, for a comparison, the per-stack delta between the two runs. |1819## Inputs2021- Target binary with debug info (`-g`, or a `RelWithDebInfo` CMake build) so backtraces carry function names and source lines. Optimization level stays as in production.22- A representative workload; heaptrack records allocation counts, so the workload must exercise the suspected path.23- For Rust: the binary must use the system allocator (`#[global_allocator] static A: std::alloc::System = System;`) so the interposed `malloc` sees every allocation.24- For a comparison: two recorded data files.2526## Procedure27281. Record. `heaptrack ./myapp args` writes `heaptrack.myapp.<pid>.zst` in the current directory and prints the analyze command on exit; `heaptrack -o run1 ./myapp` names the file `run1.zst`; `heaptrack -p <pid>` attaches to a running process by injection; `--record-only` skips the automatic analysis launch. Done when: the `.zst` file exists and heaptrack printed its summary lines (`calls to allocation functions`, `temporary allocations`).292. Print the summary and rankings. `heaptrack_print -f run1.zst` prints, in order, `MOST CALLS TO ALLOCATION FUNCTIONS`, `PEAK MEMORY CONSUMERS`, `MOST TEMPORARY ALLOCATIONS`, and a summary block:3031 ```text32 total runtime: 2.34s.33 calls to allocation functions: 145234 (62064/s)34 temporary allocations: 89123 (38087/s)35 peak heap memory consumption: 45.23M36 peak RSS (including heaptrack overhead): 78.45M37 total memory leaked: 2.34M38 ```3940 Each ranking entry reads `N calls to allocation functions with X peak consumption from` followed by the stack, leaf first. Toggle sections with `-p 0` (peaks), `-a 0` (allocation counts), `-T 0` (temporary), and `-l 1` (leaks, off by default); `-n 20` widens the number of reported peaks from the default 10, `-s` limits sub-peak backtraces. `-t 1` shortens template identifiers and `-m 1` merges backtraces (both on by default). Done when: the four rankings and the summary are captured.413. Read each summary line against the code. `calls to allocation functions` high relative to work done: many small allocations, so pool or reserve. `temporary allocations` (freed before the next allocation) as a large share of calls: churn in a hot loop, so hoist the buffer. `peak heap memory consumption`: the live-heap maximum to compare with the memory budget. `peak RSS` includes heaptrack's own overhead and non-heap mappings, so compare peak heap between runs, not RSS. `total memory leaked`: bytes never freed at exit; the `-l 1` ranking names the stacks. Done when: each non-trivial line is tied to a call stack.424. Draw the allocation flamegraph. `heaptrack_print -f run1.zst -F stacks.folded --flamegraph-cost-type peak` writes folded stacks (cost types: `allocations`, `temporary`, `leaked`, `peak`); then `flamegraph.pl --countname bytes --title "Peak heap" stacks.folded > heap.svg` from `flamegraphs`. Done when: the SVG opens and the widest frame matches the top ranking entry.435. Compare two runs with the built-in diff: `heaptrack_print -f after.zst -d before.zst` prints every ranking as the delta of `after` minus `before`, so a positive peak entry is a regression and a negative one an improvement. Done when: the top positive and top negative deltas are named with their stacks.446. Export to Massif format when a Massif-reading tool is already in the workflow: `heaptrack_print -f run1.zst -M run1.massif`, then `ms_print run1.massif`. Done when: `ms_print` renders the timeline.457. Suppress known leaks from third-party code: write a suppression file with one `leak:<function pattern>` line per pattern and pass `--suppressions leaks.supp`; `--print-suppressions 1` lists the matches and the suppressed byte total; `--disable-builtin-suppressions` reveals what heaptrack hides by default. Done when: the leak ranking holds only owned code.4647## Failure and recovery4849| Failure | Cause | Fix |50|---|---|---|51| Stacks show only addresses | No debug info | Rebuild with `-g`; for Rust add `debug = true` to the profile |52| Rust program records almost nothing | Custom global allocator bypasses `malloc` | Switch to `std::alloc::System` for the profiling build |53| `total memory leaked` is large but the program frees on exit paths | Exit through `_exit` or `quick_exit` skipped destructors | Read the leak stacks; a still-referenced global at exit is not a bug, and a suppression records that decision |54| Data file is huge | Allocation-heavy workload | Shorten the workload; record only the phase under suspicion by attaching with `-p` after startup |55| `heaptrack -p` fails to attach | ptrace restricted | Run as the process owner or propose `CAP_SYS_PTRACE`; otherwise start the program under heaptrack |56| Comparison shows every stack changed | Different workloads or non-deterministic addresses in symbols | Use identical inputs and `-m 1`; compare peak heap totals first |5758Massif reaches the same heap questions on platforms without heaptrack and at a much higher slowdown; `valgrind` covers it. Leak detection without profiling cost is LeakSanitizer through `address-sanitizer`.5960## Output6162An allocation report with the summary block, the top stacks by peak, by call count, and by temporary allocations, the leak records, the flamegraph path when drawn, the per-stack deltas when two runs were compared, and the data file paths.