Debugging with leaks & Instruments
leaks, vmmap, sample, spindump, and Instruments.app — when to reach for which.
Debugging with leaks & Instruments
When a macOS process misbehaves — leaks memory, spins CPU, hangs, mysteriously grows — Apple ships a small but excellent set of CLI diagnostic tools, plus the GUI Instruments.app for deeper exploration. Knowing which to reach for collapses "vague performance complaint" to "the bug is at line 47" much faster than guessing.
Analogy
This toolset is like a hospital's diagnostic kit. leaks is the blood test (one-shot, finds specific abnormal values). sample is the EEG (records short brain activity, shows what's happening cognitively). spindump is the MRI (full snapshot of the patient when they collapse). vmmap is the body map (here's everything the patient is carrying). Instruments is the multi-day workup with continuous monitoring — the right tool when you need to watch trends over time, not catch a single moment.
The four CLIs
All four are built-in. Plain text output. Scriptable. Each answers a specific diagnostic question.
| Tool | Question it answers |
|---|---|
leaks |
What memory is allocated and unreferenced? |
sample |
What is the process spending CPU time on? (last 10s) |
spindump |
What is every thread of this hung process doing right now? |
vmmap |
What does the virtual memory layout of this process look like? |
leaks — finding leaks
A "leak" by leaks's definition is an allocation that's still live in the process but no live pointer references it — i.e. it's unreachable, can never be freed, but hasn't been collected. (Pure garbage in any GC'd language; classic leak in C/Obj-C/C++/Swift with manual or ARC memory management.)
# Run against a running process by name or pid
leaks Safari
# Or against a specific pid
leaks 12345
# Even better — get full stack traces for the allocations
MallocStackLogging=1 ./myapp &
leaks myapp
The stack traces require MallocStackLogging=1 to be set when the process started. Without it, you get the bare addresses but not the call paths — much less useful.
Output looks like:
Process: myapp [12345]
Path: /usr/local/bin/myapp
Load Address: 0x10000
Process 12345: 234 nodes malloced for 4567 KB
Process 12345: 12 leaks for 1024 total leaked bytes.
Leak: 0x600001234 size=512 zone=DefaultMallocZone
Call stack: malloc | calloc | new_buffer | parse_response | http_get
The "Leak: ..." section names the function chain that allocated the memory that's now leaked. You walk back from the deepest function — that's where the allocation happened — and figure out which path through your code didn't free it.
For automated runs:
# Run a process, report leaks at exit:
MallocStackLogging=1 leaks --atExit -- ./myapp
sample — short CPU profile
sample records the call stack of every thread of a process every few milliseconds for some duration, then aggregates: "thread X spent 30% of its time inside parse_response."
sample Safari 10 # 10-second sample of Safari
sample Safari 10 -file /tmp/safari.txt # write to a file
sample 12345 5 -mayDie # sample even if the process exits
Output is a stack-tree report:
Sampling process 12345 for 10 seconds...
Sample analysis of process 12345 written to /tmp/safari.txt
Total number in stack (recursive counted multiple, when >=5):
45 _pthread_start (in libsystem_pthread.dylib)
45 thread_main (myapp)
32 parse_response (myapp)
32 json_parse (libfoo.dylib)
28 json_lex (libfoo.dylib)
4 json_alloc (libfoo.dylib)
13 http_send (myapp)
Read top-down: 45/45 samples were in _pthread_start → thread_main. 32 of those were in parse_response. The hot spot is json_lex (28 samples).
sample is the right tool when:
- You suspect CPU-bound work but don't know where it lives.
- You want to share a quick perf snapshot in a bug report.
- You don't have time to set up Instruments.
spindump — hang and spin reports
When a process is hung or unresponsive, spindump captures a much richer report than sample — including kernel-level info about lock waits, IO blocks, and what the system was doing.
sudo spindump 12345 10 -file /tmp/spin.txt
(spindump usually needs sudo for full access.) The output is structured per-thread, with annotations like:
Thread 0x12345 "main" priority 47 cpu time 4.5s
*4321 ??? (kernel) [thread waiting]
1234 __psynch_cvwait (libsystem_kernel)
4321 pthread_cond_wait_relative_np (libsystem_pthread)
4321 AppKit
4321 -[NSDocument runModalSavePanelForSaveOperation:delegate:didSaveSelector:contextInfo:]
4321 __CFRunLoopRun
[waiting on file IO from ~/Documents/large.bin]
You see "thread is waiting for a file IO that never returned" — diagnosis is much more direct than sample for hangs.
The system also runs spindump automatically when an app becomes unresponsive, writing the report to /Library/Logs/DiagnosticReports/. That's where the OS gathers the data behind those "force-quit?" dialogs.
vmmap — virtual memory layout
vmmap 12345
# or:
vmmap Safari --summary
Output describes every region the process has mapped:
==== Non-writable regions for process 12345
__TEXT 100000000-100001000 [ 4K 4K 0K 0K] r-x/rwx SM=COW myapp
__LINKEDIT 100100000-100110000 [ 64K 64K 0K 0K] r--/rwx SM=COW myapp
==== Writable regions for process 12345
__DATA 100040000-100050000 [ 64K 64K 32K 0K] rw-/rwx SM=PRV
MALLOC_TINY 7fa000000000-7fa000800000 [ 8M 1M 0K 0K] rw-/rwx SM=PRV ...
MALLOC_LARGE 7fa800000000-7fab00000000 [ 3.0G 1.5G 0K 0K] rw-/rwx SM=PRV ...
==== Summary for process 12345
ReadOnly portion of Libraries: Total=234M resident=68M
Writable regions: Total=4G written=2G resident=2G
Use vmmap when:
- The process's memory footprint is much larger than its allocation count suggests (huge mmap'd regions, big libraries, huge anonymous mappings).
- You want to know "what TYPE of memory is this consuming?" — code, data, malloc, file-mapped.
- You're debugging a OOM-killed process to understand where it spent the memory.
Instruments.app — the GUI
Instruments ships with Xcode (the full Xcode IDE; not the standalone CommandLineTools install). It's a GUI built around the same kernel facilities sample, leaks, vmmap, and friends use, but with a continuous timeline.
Standard templates:
| Template | What it shows |
|---|---|
| Time Profiler | Like sample, but continuous, with a timeline you can scrub |
| Allocations | Live allocation events, with stack traces — find leaks AND lifetime patterns |
| Leaks | Same as leaks CLI but live, with on-the-fly stack traces |
| System Trace | Threads, scheduler, system calls — kernel-level view |
| File Activity | What files are being read/written and by whom |
| Network | Network calls, sizes, timings |
| Energy | Power draw — battery impact analysis |
| Metal / Core Animation | GPU and animation profiling |
The killer feature: timelines. You can see CPU spike at 14:23:05, click into that moment, and Instruments shows the call stacks responsible. You can compare two runs side-by-side.
The trade-off: Instruments has UI overhead (RAM, CPU). For low-overhead snapshots in production, the CLIs are still the right answer. For "I want to understand this app's behaviour over a 5-minute scenario," Instruments wins.
Picking the right tool
A decision flow for a misbehaving process:
Is it leaking memory? (RSS climbs forever)
→ leaks (with MallocStackLogging) → fix the unreleased allocation
Is it CPU-bound? (one core pegged)
→ sample <pid> 10 → see the hot stack → optimise
Is it hung? (UI not responding)
→ spindump <pid> 10 → see what thread is waiting on → fix the wait
Is the memory footprint huge but no leaks?
→ vmmap --summary <pid> → identify large mappings → reduce them
Do I need a 5-minute timeline view of allocations and CPU?
→ Instruments.app, Allocations + Time Profiler templates
Ergonomics — the recipes that pay off
# Quick memory snapshot (no MallocStackLogging needed for this)
leaks <pid>
# Short CPU profile, no setup
sample <pid> 10
# A combined "what's wrong with this process" report
{
echo "=== top ==="
top -pid <pid> -l 1
echo "=== sample ==="
sample <pid> 5
echo "=== leaks ==="
leaks <pid>
echo "=== vmmap ==="
vmmap --summary <pid>
} > /tmp/process-report.txt
# Attach that file to a bug — most macOS engineers can read it cold
A note on production
leaks, sample, and spindump are safe to run against running production processes. They take a brief sample (sample for ~10s, leaks scans the heap, vmmap reads the kernel maps). They don't pause the process meaningfully.
leaks does pause the process briefly while it walks the heap, which can be a problem on processes with multi-GB heaps. Use it carefully on large servers; prefer Instruments' Allocations template if interruption is unacceptable.
What to internalise
leaksfor unreferenced allocations; setMallocStackLogging=1for stack traces.samplefor short CPU profile of a running process.spindumpfor hang reports — what every thread is waiting on.vmmapfor the memory-layout view;--summaryis the quick read.- Instruments.app for time-series profiling and exploring; the CLIs for one-shot snapshots and bug reports.
Tools in the wild
5 tools- clileaksfree tier
Built-in. Find unreferenced live allocations and (with MSL) their stack traces.
- clisamplefree tier
Built-in. Sample-based CPU profiler — short snapshot of a running process.
- clispindumpfree tier
Built-in. Hang/spin reports — what every thread is doing during a stall.
- clivmmapfree tier
Built-in. Virtual memory map of a process — regions, mappings, permissions.
- serviceInstrumentsfree tier
Xcode-bundled GUI profiler — Time Profiler, Allocations, System Trace, network, IO, energy.