Using Reference Tracing to Detect Memory Leaks

Reference Tracing logs every allocation and de-allocation and this can be very useful in detecting memory leaks. In this example we will deliberately create a memory leak and see how this is detected in the log file.

Creating a Memory Leak

The pymemtrace.cMemLeak has a number of classes that can create a memory demand. The reference count of these objects can be manipulated directly and so we can cause a leak. For example using pymemtrace.cMemLeak.CMalloc:

from pymemtrace import cMemLeak

obj = cMemLeak.CMalloc(1024)
obj.inc_refcnt(1)

By incrementing the reference count we have prohibited the Python runtime from ever de-allocating the object. A memory leak.

Lets create a function that creates a number of these objects and, optionally, leaks them.

from pymemtrace import cMemLeak

def create_tmp_list_of_memory_objects(cause_leak: bool):
    l = []
    for i in range(4):
        obj = cMemLeak.CMalloc(1024)
        # Choose to leak or not.
        if cause_leak:
            obj.inc_refcnt(1)
        l.append(obj)
    while len(l):
        l.pop()

Using Reference Tracing

Firstly with no leak:

from pymemtrace import cPyMemTrace

with cPyMemTrace.ReferenceTracing(
        include_tp_names=['cMemLeak.CMalloc',],
) as profiler:
    create_tmp_list_of_memory_objects(cause_leak=False)

This creates a log file that we can analyse with pymemtrace.util.ref_trace_analyse:

File path: 20260419_120519_0_71199_O_0_PY3.13.2.log
2026-04-19 13:05:37,142 - ref_trace_analyse.py#338 - INFO     - Lines: 12 NEW: 4 DEL: 4 NEW - DEL: 0 MSG: 0
Initial Message:
test_reference_tracing_deliberate_leak_to_cwd(): Class: CMallocObject Leak: False
Untracked Objects [0]:
Type                                        Count
Live Objects [0]:
Previous Objects [4]:
    0x6000034c1f90 cMemLeak.CMallocObject                   NEW: test_cpymemtrace.py#818 DEL: test_cpymemtrace.py#823
    0x6000034c2550 cMemLeak.CMallocObject                   NEW: test_cpymemtrace.py#818 DEL: test_cpymemtrace.py#823
    0x6000034c25d0 cMemLeak.CMallocObject                   NEW: test_cpymemtrace.py#818 DEL: test_cpymemtrace.py#823
    0x6000034c2690 cMemLeak.CMallocObject                   NEW: test_cpymemtrace.py#818 DEL: test_cpymemtrace.py#885
Type count [1]:
Type                                          New      Del  New - Del
cMemLeak.CMallocObject                          4        4          0
Process time: 0.001 (s)

This shows that the four objects were allocated and then de-allocated correctly.

Now with a leak:

from pymemtrace import cPyMemTrace

with cPyMemTrace.ReferenceTracing(
        include_tp_names=['cMemLeak.CMalloc',],
) as profiler:
    create_tmp_list_of_memory_objects(cause_leak=True)

And this log file analysed with pymemtrace.util.ref_trace_analyse gives:

$ python pymemtrace/util/ref_trace_analyse.py 20260419_120519_1_71199_O_0_PY3.13.2.log
File path: 20260419_120519_1_71199_O_0_PY3.13.2.log
2026-04-19 13:05:55,774 - ref_trace_analyse.py#338 - INFO     - Lines: 8 NEW: 4 DEL: 0 NEW - DEL: 4 MSG: 0
Initial Message:
test_reference_tracing_deliberate_leak_to_cwd(): Class: CMallocObject Leak: True
Untracked Objects [0]:
Type                                        Count
Live Objects [4]:
    0x6000034c4090    4 cMemLeak.CMallocObject                   create_tmp_list_of_memory_objects test_cpymemtrace.py#818
    0x6000034c4110    4 cMemLeak.CMallocObject                   create_tmp_list_of_memory_objects test_cpymemtrace.py#818
    0x6000034c4290    4 cMemLeak.CMallocObject                   create_tmp_list_of_memory_objects test_cpymemtrace.py#818
    0x6000034c4390    4 cMemLeak.CMallocObject                   create_tmp_list_of_memory_objects test_cpymemtrace.py#818
Previous Objects [0]:
Type count [1]:
Type                                          New      Del  New - Del
cMemLeak.CMallocObject                          4        0          4
Process time: 0.001 (s)

And that shows that the four objects are still ‘alive’.

Whilst Reference Tracing can not pinpoint where a missing de-allocation should be it can certainly narrow down what types are not being de-allocated correctly.

Plotting Memory and Live Count

The script pymemtrace.util.ref_trace_analyse has an option to plot with gnuplot the RSS usage and the object count over time. This gives a visual view of memory usage and object allocation over time. This uses the --gnuplot-path for specifying the path for the gnuplot output and --gnuplot-types to provide a comma seperated list of types of interest.

Here is an example of a Python program reading ten geophysical data files and writing a HTML summary file for each. A message (MSG:) containing the file name is inserted into the log file for every read and write.

The Python code is instrumented with pymemtrace thus:

# Various imports here...

from pymemtrace import cPyMemTrace
from pymemtrace import cpymemtrace_decs

@cpymemtrace_decs.reference_tracing(message="LASToHTML",)
def las_file_to_html(las_file_path: str, html_file_path: str) -> None:
    """Reads and parses a LAS file and writes a summary to a HTML file."""
    # Mark the log file with a message to say we are starting the read.
    # This will be rendered as a label on the plot.
    cPyMemTrace.reference_tracing_write_message_to_log(
        f'Read LAS File "{os.path.basename(las_file_path)}'
    )
    # Read and parse the LAS file.
    las_file = LASRead.LASRead(las_file_path)
    # Mark the log file with a message to say we are starting the write.
    # This also will be rendered as a label on the plot.
    cPyMemTrace.reference_tracing_write_message_to_log(
        f'Write HTML "{os.path.basename(html_file_path)}'
    )
    # Write the HTML summary of las_file.
    write_html(las_file: LASRead.LASRead, html_file_path: str)

As well as using the pymemtrace.cpymemtrace_decs.reference_tracing() decorator for each file we write a message to the log when reading the input and another when writing the output. These messages will be converted to labels on the resulting plot of memory usage.

The decorator produces a 2GB log file with nearly 10m lines. The script pymemtrace.util.ref_trace_analyse can be used to analyse this log file. It is invoked by giving a gnuplot output directory and a list of types of interest. These types are LASRead which is the internal representation of the parsed file, this contains a list of LASSection objects and XhtmlStream is the means by which the HTML output is created:

$ python pymemtrace/util/ref_trace_analyse.py \
    20260606_105231_0_23826_O_0_PY3.13.13.log \
    --gnuplot-path=gnuplot_ref_trace \
    --gnuplot-types=LASRead,LASSection,XhtmlStream

pymemtrace.util.ref_trace_analyse takes around 70s to analyse this log file and write the following files to the gnuplot output directory:

20260606_105231_0_23826_O_0_PY3.13.13.log.dat
20260606_105231_0_23826_O_0_PY3.13.13.log.plt
20260606_105231_0_23826_O_0_PY3.13.13.log.png

The .dat file contains the time series data with one column for each type and the RSS. The .plt file contains a best effort of the gnuplot configuration file. This can be hand edited to improve the plot which can be rebuilt with gnuplot -p <.plt file>.

The plot looks like this:

RSS Usage and Live Counts.

This shows the behaviour of the code, it looks pretty healthy, the RSS is (mostly) reclaimed. Because of the way that Python’s small object memory allocator works it is quite usual to see the RSS slowly creep up during the lifetime of the process so this looks quite normal.

Importantly the live object count is moderate and as we would expect, ten LASRead objects have been created and all are de-allocated by the end of the log.

With a Memory Leak

Now we deliberately introduce a memory leak in the LASRead object. This is done by increasing the reference count so that the object, and all the objects it contains, are never de-allocated.

from pymemtrace import cMemLeak

# Code as above...
las_file = LASRead.LASRead(las_file_path)
# Increment the reference count so that las_file is never deallocated.
cMemLeak.py_incref(las_file)
# Continue the code as above...

Now the the plot looks distinctly different:

RSS Usage and Live Counts (with leak).

The RSS increases as usual so it is hard to see that there is a memory leak. However the live object count of LASRead and LASSection, which are ever increasing, makes it clear that those objects are not being de-allocated. This makes it far easier to track down that memory leak.

Instrumenting your code like this gives you a forensic view of its memory behaviour.