How Tracemalloc Traces Memory Blocks in Python
Python’s built-in tracemalloc module provides deep
visibility into memory usage by tracking individual memory blocks
allocated by the interpreter. It works by hooking directly into
CPython's internal memory management APIs to record allocation events
alongside their corresponding Python tracebacks. This article explores
how tracemalloc operates under the hood, detailing its
integration with CPython memory allocators, its traceback capture
mechanism, and how it manages tracking overhead.
Hooking CPython Allocators
CPython structures its memory management into three primary allocation domains defined by PEP 445:
PYMEM_DOMAIN_RAW: Low-level memory wrappers around system allocators likemallocandfree.PYMEM_DOMAIN_MEM: The standard Python memory allocator used for general memory buffers.PYMEM_DOMAIN_OBJ: The object allocator, managed largely bypymalloc, optimized for small Python objects.
When tracemalloc.start() is called, the module replaces
the active memory hooks in these domains using
PyMem_SetAllocator(). It installs custom wrapper functions
around the standard allocation (malloc), reallocation
(realloc), and deallocation (free)
routines.
To avoid infinite recursion and skewed metrics,
tracemalloc uses its own dedicated raw allocator that
operates outside these traced domains when storing its internal tracking
structures.
Capturing Tracebacks on Allocation
Whenever Python code allocates memory—such as instantiating an object or extending a list—the request passes through one of the hooked allocator functions. The hook executes the following steps:
- Underlying Allocation: It calls the original allocator function to retrieve the raw memory pointer.
- Stack Inspection: If the allocation succeeds,
tracemallocinspects the current execution state of the calling thread (PyThreadState). - Frame Extraction: It walks the execution frames up
to a user-defined limit (
nframe, configured viatracemalloc.start(nframe)), collecting the filename and line number for each active Python frame. - Trace Creation: It packages the pointer address, block size, and collected traceback into a compact internal structure called a trace.
Storage and Traceback Interning
Storing full stack traces for every memory allocation introduces
significant memory overhead. To mitigate this, tracemalloc
employs an internal hash table that interns tracebacks.
Each unique sequence of code locations (filename and line number pairs) is stored once in a global table and assigned a reference count. If multiple allocations occur from the exact same call stack, they store a pointer to the same interned traceback structure rather than duplicating the frame list.
The allocated memory addresses are stored in an internal hash table mapping the memory block's address to its size and interned traceback reference.
Intercepting Deallocations
When a block of memory is released via free(), the
installed deallocation hook intercepts the call. It extracts the pointer
address and looks it up in the internal tracking table:
- Lookup: The hook finds the recorded trace associated with the memory address.
- Counter Updates: It decrements the module's running totals for allocated memory and block counts.
- Cleanup: It releases the reference to the interned traceback (freeing the traceback if its reference count drops to zero) and deletes the address entry from the hash table.
- Original Free: Finally, it passes the pointer to
the underlying CPython or system
free()function.
Reallocations (realloc) follow a hybrid path: the old
pointer's trace is updated or replaced with the new address and size,
updating running totals accordingly.
Generating Snapshots
When a user calls tracemalloc.take_snapshot(),
tracemalloc iterates over its internal hash table of active
memory blocks. It converts the raw C tracking data into immutable Python
objects (Snapshot, Statistic, and
Trace instances).
Because the module continuously updates its tracking table during runtime, snapshots represent an exact, point-in-time state of all live memory blocks currently allocated by Python code.