heapviz is a terminal heap profiler for Linux that renders a live, spatial map of your process's memory as it runs. Every character cell on screen is a fixed span of virtual address space; every colour is an allocation's current state. Allocate, and a block flashes green where it landed. Free it, and the space flashes red before fading out. Leave it alive, and it settles to blue.
Heap behaviour you normally infer from numbers (churn, fragmentation, allocator reuse patterns, leaks) becomes something you just look at.
Design target. heapviz is pre-alpha and this interface is not yet implemented.
See ROADMAP.md for what's actually built.
Most heap profilers make you choose between detail and speed. Valgrind's Massif gives you exquisite data at 20–100× slowdown. Sampling profilers stay fast but blur exactly the churn you're trying to see. heapviz is built around the position that you shouldn't have to pick.
Three design commitments drive everything.
1. The profiler is a guest in your process. The LD_PRELOAD interceptor
performs zero dynamic allocations while generating telemetry. It writes
fixed-size event packets straight into a lock-free shared-memory ring buffer
using atomic operations. If the ring is full it drops the event and increments
a counter. It never blocks your program, never takes a lock your threads
contend on, and never allocates from the allocator it's instrumenting.
Target: under 50 ns added per allocation.
2. The renderer earns its frame budget. The TUI keeps two terminal
framebuffers and diffs them, emitting escape codes only for cells that actually
changed, and only emitting a colour sequence when the colour actually changed
from the previous cell. One write(2) syscall per frame, no tearing.
Target: 60 FPS at under 1 ms CPU per frame.
3. A 64-bit address space fits on your screen. Rather than tracking millions of addresses individually, heapviz buckets them into a coarse-grained sparse grid whose granularity adapts to your terminal size and the heap's span. Live chunks live in an open-addressing Robin Hood hash table for O(1) lookup.
And one honesty commitment: if heapviz drops telemetry events, it says so, loudly, on screen. A profiler that quietly lies about what it missed is worse than no profiler.
- Spatial heap map: the whole heap as a colour-coded grid, each cell a fixed byte span that scales to your terminal
- Allocation heatmap: fresh
mallocpulses green, settles to blue as it ages;freeflashes red then fades, so you can see the allocator reusing space - Chunk overhead visualisation: yellow markers show glibc
ptmallocchunk headers, making the gap between what you asked for and what you got visible - Interactive inspector: move a cursor with
h/j/k/land read any chunk's address, requested size, real size, status, and lifetime - Fragmentation analysis: live percentage plus the largest contiguous free gap, because "can I still allocate 1 MB" is what people actually want to know
- Snapshot & leak diff: mark a point in time, then highlight everything allocated since that's still alive
- Telemetry health: ring buffer utilisation and dropped-event count, always visible
malloc · free · calloc · realloc · posix_memalign · aligned_alloc ·
memalign · valloc · pvalloc, plus everything that reaches them
transitively (strdup, asprintf, getline, C++ operator new).
| OS | Linux (kernel 3.2+ for process_vm_readv) |
| libc | glibc / ptmalloc; chunk-header decoding is allocator-specific |
| Terminal | UTF-8, 24-bit TrueColor recommended (256-colour and ASCII fallbacks planned) |
| Build | CMake 3.20+, a C++20 compiler (GCC 11+ / Clang 14+) |
Known limits, stated up front: LD_PRELOAD is ignored for setuid binaries
and has nothing to hook in statically linked ones — and it binds when a process
loads, so it can never be added to a process that is already running. Those
targets get snapshot mode instead (see Two modes),
which reads the heap from outside and therefore needs same-uid access or
CAP_SYS_PTRACE; the default yama/ptrace_scope=1 blocks it for anything
heapviz did not start itself. heapviz names the fix rather than failing quietly.
Not yet released. heapviz is pre-alpha; there is no build to install. Packaging, release binaries, and build-from-source instructions land with v0.1.0. Track progress in ROADMAP.md.
Build-tree usage:
# launch and instrument a program automatically
./build/debug/heapviz ./my_app --some-flag
# or attach to something already running libheapviz.so
./build/debug/heapviz 41820
# or point it at any process at all — see "Two modes" below
./build/debug/heapviz $(pgrep -n my_server)heapviz picks automatically. You do not choose, but you do need to know which one you are in, because the numbers mean different things — so the title row says.
Live mode happens when the target is running libheapviz.so: either heapviz
launched it, or you started it with the library preloaded. Every allocation and
free is recorded as it happens, which is what makes allocation ages, the
cumulative total, and the leak diff against a snapshot possible.
Snapshot mode happens for everything else, and it is what makes heapviz <pid> useful on a process you cannot restart. LD_PRELOAD binds when a process
loads, so a program that is already running has its malloc wired straight to
libc and nothing can retrofit the interceptor onto it. Instead heapviz reads the
target's heap directly — glibc writes a header before every allocation, and the
headers chain — and rebuilds the picture a few times a second.
| Live | Snapshot | |
|---|---|---|
| Needs the target restarted | yes | no |
| Live set, sizes, map, fragmentation | yes | yes |
| Allocation age / heat by age | yes | by when heapviz first saw it |
| Cumulative "Allocated", leak diff | yes | not observable |
Sees non-malloc heaps (Node, JVM…) |
no | no, but says how much it missed |
Two things to expect in snapshot mode:
- It needs permission to read the process. On most distributions
kernel.yama.ptrace_scopeis1, which permits it only for programs heapviz started itself. If the map is empty, heapviz names the fix: eithersudo sysctl -w kernel.yama.ptrace_scope=0, or grant the binary the capability once withsudo setcap cap_sys_ptrace+ep /path/to/heapviz. - It reads glibc's allocator, and not every program uses it for everything.
A runtime that reserves its heap in one big
mmapand sub-allocates inside it — Node, Bun, the JVM, some Python builds — has no chunk headers to follow, so most of its memory is invisible here. heapviz prints how many bytes it could not read beside the ones it could, so a small live figure against a large process is never mistaken for the whole story. - Small frees can read as still-live. A chunk in glibc's tcache or a fastbin keeps the bit that marks it in use, because glibc means to hand it straight back out. From outside the process there is no way to tell. It is bounded at 64 chunks per size class per thread, so it shows up on heaps doing heavy small allocation and not on ones doing large.
When heapviz launches a target, it owns the current terminal for its TUI.
Target stdin is therefore /dev/null, and target stdout/stderr go to the
private /tmp/heapviz-target-PID.log path printed at startup. This prevents a
target's output—including another full-screen TUI—from corrupting the heap map.
The log is left in place after the session ends so you can still read it;
--cleanup reaps abandoned shared-memory rings, not these, so delete them
yourself when you are done.
Interactive targets must therefore run in a separate terminal and be attached by PID. A program that needs a terminal will exit within a second or two of being launched this way; heapviz says so when it does, quoting the target's own last line of output.
For an interactive target, terminal 1 injects the library while leaving the target in control of that terminal:
./build/debug/heapviz --instrument claudeIt prints the target PID. In terminal 2, start the profiler with that number:
./build/debug/heapviz PIDThe full set is planned for v0.1.0. The last column says what works today, so that pressing a key and seeing nothing happen is answerable without reading the roadmap.
| Key | Action | |
|---|---|---|
h j k l |
Move the inspector cursor one cell | working |
H J K L |
Move ten cells or ten rows | working |
n N |
Jump to next / previous non-empty cell | working |
g G |
Jump to heap start / end | working |
Tab |
Cycle chunks within the selected cell | working |
Space |
Pause the display while continuing to drain telemetry | working |
s |
Take a snapshot | working |
d |
Toggle leak diff against the snapshot | working |
S |
Clear the snapshot | working |
r |
Reset statistics | working |
? |
Help | working |
q |
Quit | working |
Horizontal movement runs along the address space rather than stopping at the
edge of the terminal, so h in the first column steps onto the last cell of the
row above — that cell really is the previous one. Vertical movement keeps the
column. Resizing the terminal keeps the cursor on the same address, not the
same square.
heapviz restores termios, the cursor, and the alternate screen on every exit
path it can reach: quitting with q, SIGINT, SIGTERM, SIGHUP, a crash,
or an uncaught exception. SIGKILL cannot be caught by anything, so
kill -9 heapviz is the one case that can leave your shell without an echo or
a visible cursor. Recover with:
reset # or, if that is unavailable:
stty saneTyping it blind works even when the echo is off.
Pre-alpha, and under active construction, but it runs: heapviz ./your_app
automatically loads the interceptor and draws the process's heap. heapviz N
is the short attach form for a process that heapviz previously instrumented. The
LD_PRELOAD interceptor captures allocations at about 31 ns per call. The
terminal engine underneath is raw mode, a double-buffered grid, a differential
renderer that puts one write on the wire per frame, and a paced event loop that
handles resizing and idles at close to no CPU. Above that sit the spatial map,
a movable cursor, a chunk inspector, a telemetry metrics panel, live
fragmentation analysis with the largest free hole beside it, snapshot-based
leak hunting, responsive mockup-derived layout, semantic dark/light themes, and
double-resolution half-block map rendering. Use --theme light for a light
canvas or --no-animation for stable screenshots and CI captures.
heapviz --term-check exercises the terminal layer and the map against a
synthetic heap without needing a target.
ROADMAP.md tracks 212 tasks across 8 milestones, from the shared-memory ABI through the interceptor, sparse grid, ANSI engine, interactivity, and visual polish. CHANGELOG.md records what has actually shipped.
Early days, and the architecture is still settling. If you want to help, the roadmap's open decisions (§2) and the M1 bootstrap problem are where a second opinion would help most.
