Skip to content

Repository files navigation

heapviz

Watch your program's heap breathe, in real time, in your terminal.

C++20 Build Platform libc License Status


What is heapviz?

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.

heapviz interface
Design target. heapviz is pre-alpha and this interface is not yet implemented.
See ROADMAP.md for what's actually built.

Goals

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.


What it does

  • 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 malloc pulses green, settles to blue as it ages; free flashes red then fades, so you can see the allocator reusing space
  • Chunk overhead visualisation: yellow markers show glibc ptmalloc chunk headers, making the gap between what you asked for and what you got visible
  • Interactive inspector: move a cursor with h/j/k/l and 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

Interception coverage

malloc · free · calloc · realloc · posix_memalign · aligned_alloc · memalign · valloc · pvalloc, plus everything that reaches them transitively (strdup, asprintf, getline, C++ operator new).


How it works

Your program's malloc() calls pass through libheapviz.so, which writes 32-byte
          packets into a lock-free SPSC ring buffer in POSIX shared memory. heapviz runs as
          a separate process: it drains the ring into a sparse grid and hash table, then
          into a double-buffered renderer that emits one write() per frame to your terminal.
          It separately reads /proc/<pid>/maps and process_vm_readv for heap bounds and
          chunk headers. The two halves share nothing but a versioned 32-byte packet ABI and
          a ring header. The interceptor is a single producer; heapviz is a single consumer.
          Neither ever waits on the other.

Requirements

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.


Installation

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)

Two modes, and which one you get

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_scope is 1, which permits it only for programs heapviz started itself. If the map is empty, heapviz names the fix: either sudo sysctl -w kernel.yama.ptrace_scope=0, or grant the binary the capability once with sudo 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 mmap and 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 claude

It prints the target PID. In terminal 2, start the profiler with that number:

./build/debug/heapviz PID

Keybindings

The 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.

If the terminal is left in a strange state

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 sane

Typing it blind works even when the echo is off.


Project status

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.


Contributing

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.

License

GNU General Public License v3.0

About

heapviz is a terminal heap profiler for Linux that renders a live, spatial map of your process's memory as it runs.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages