Memray Memory Profiler

repository·main·Indexed 12 days ago

https://github.com/bloomberg/memray

A high-performance memory profiler for Python that tracks allocations in Python code, native C/C++ extensions, and the Python interpreter. It provides tools for finding memory leaks and allocation hotspots via flame graphs, live monitoring, and a pytest-memray plugin.

Tokens
26.8K
Snippets
101
Records
137
Agent score
97%

What's inside Memray

  1. Overview of Memray usage

    main

    Memray is a memory profiler that tracks memory allocations in Python code, native extension modules, and the Python interpreter itself. It can be used as a CLI tool or as a library for fine-grained profiling.

    Typical Workflow:

    1. Use the memray run subcommand to execute your program and create a capture file.
    2. Use a reporter subcommand (like memray flamegraph) to analyze the captured data file.
  2. What is Memray and what can it do?

    main

    Memray is a memory profiler for Python designed to track memory allocations across three layers: Python code, native extension modules (C/C++), and the Python interpreter itself.

    It is useful for:

    • Analyzing applications to discover the cause of high memory usage.
    • Finding memory leaks.
    • Identifying code hotspots that cause excessive allocations.

    Key capabilities include:

    • Full Call Stack Tracing: Unlike sampling profilers, Memray traces every function call for accurate representation.
    • Native Code Support: It captures C/C++ library calls to provide a complete call stack.
    • High Performance: Designed for minimal slowdown, with the option to enable/disable native code tracking.
    • Multi-threading Support: Works with both Python threads and native threads (e.g., C++ threads in extensions).
    • Visualization: Generates various reports, such as flame graphs, from collected data.

    Note: Memray only works on Linux and MacOS.

  3. Analyze individual threads using --split-threads

    main

    By default, Memray groups allocations occurring at the same source location across different threads together in the flame graph.

    If you use the --split-threads option, Memray enables thread-specific filtering. In the generated HTML report, a "Filter Thread" dropdown will appear, allowing you to select a specific thread to view its individual allocation pattern. To return to the merged view, use the "Reset" entry in the dropdown. Note that the root node (<root>) is always displayed as thread 0.

    memray flamegraph --split-threads <results>
  4. Interpret Memray Flame Graph reports

    main

    Memray flame graph reports consist of three main sections:

    1. Controls: UI elements to adjust the report appearance.
    2. Line Plot: Shows total memory usage (Y-axis) over time (X-axis).
    3. Flame Graph: A snapshot of memory usage at a specific moment (defaults to peak usage).

    How to read the graph:

    • Width: The width of a box represents the relative amount of memory used by that function/frame.
    • Stack Traces: Each row is a frame in the stack trace. You can click on a box to filter out less recent frames and focus on a specific function and its callees.
    • Allocation Source: In 'icicles' mode, the bottom-most rows represent the functions that actually performed the memory allocation.
  5. Interpret Icicle (default) flame graphs

    main

    When viewing the default icicle graph, use these rules to find memory issues:

    • Identify Allocators: Look for wide boxes at the bottom edge of the graph. These represent functions that directly responsible for large chunks of memory.
    • Ancestry (Bottom-Up): Reading upwards from a function shows its callers (parents).
    • Code Flow (Top-Down): Reading downwards shows the execution path and child functions.
    • Major Forks: Points where a node splits into several nodes indicate logical groupings or conditional execution paths.
    • Multi-threading: If the app is multi-threaded, stacks from all threads contributing to the peak memory will appear commingled in the graph.
  6. Understand Memray limitations with processes and Cython

    main

    Memray has specific limitations regarding process execution and certain extensions:

    • Process Tracking: Memray can track across forks, but it cannot track across an exec call. If a tracked child process calls an os.exec function (even to start a new Python interpreter), allocations in the new process will not be reported.
      • Note: On macOS, the default multiprocessing start method is spawn, which uses exec and will therefore not be tracked.
    • Cython: Cython functions are not included in Python stacks, even if the module was built with profiling support. To see inside Cython modules, you must use native tracking.
    • Greenlet: There is experimental support for the greenlet library. Using the Memray API to start tracking in one thread while another thread is using greenlet may result in incorrect stacks.
  7. Switch between Flame and Icicle graph orientations

    main
    Memray defaults to an Icicle graph (root at the top, functions below callers) because it is more efficient for browser scrolling. You can switch to a traditional Flame graph (root at the bottom, functions above callers) using the toggle button in the HTML report. The underlying data remains the same; only the vertical orientation changes.
  8. How temporary allocation thresholds work

    main

    The threshold determines the sensitivity of the 'temporary' classification:

    • Threshold 0: An allocation is only temporary if it is immediately deallocated (no other allocations occur in between).
    • Threshold 1: An allocation is temporary even if one other allocation occurs before it is deallocated. This is the recommended setting for detecting container growth patterns, as resizing a container typically involves one new allocation before the old one is freed.
    • Custom THRESHOLD: You can set any integer value to adjust how many intervening allocations are allowed before an allocation is no longer considered temporary.
  9. Use inverted flame graphs to aggregate memory usage

    main

    Standard flame graphs show the call hierarchy (children are functions called by the parent). In this view, if a function is called from multiple locations, it appears as multiple leaf nodes.

    Inverted flame graphs (generated with --inverted) reverse this: children are the functions that called the node. This aggregates all calls to a specific function into a single block, making it much easier to see the total memory spent in a specific function across the entire program execution.

    memray flamegraph --inverted <results>
  10. How memory fragmentation affects profiling

    main

    Memory fragmentation occurs when the system allocator's free memory is spread across the address space in small, non-contiguous fragments. This can cause the resident memory size to increase in unpredictable ways because the allocator might request more memory from the OS even if the total amount of free memory is sufficient to satisfy a new request.

    Because fragmentation depends on the specific system allocator (like GLIBC), a memory profiler cannot directly tell you why resident memory is growing due to fragmentation; it can only show that the resident size is increasing.

  11. Analyze Memray performance overhead configurations

    main

    Memray's performance impact varies depending on the tracing options used. The following configurations represent the main modes of operation:

    • Default options: Standard memory profiling.
    • --trace-python-allocators: Tracks allocations made via Python allocators. This provides more granular detail but incurs significantly higher overhead due to the high frequency of small object allocations in Python.
    • --native: Enables native (C/C++) stack trace tracking. This adds overhead related to capturing the native call stack.
    • --native --trace-python-allocators: Combines both native tracking and Python allocator tracking, representing the highest overhead configuration.
  12. Identify memory leaks caused by @functools.cache on methods

    main

    Using @functools.cache (which is equivalent to @functools.lru_cache(maxsize=None)) on an instance method can cause unexpected memory leaks.

    Because the cache stores the arguments used to call the function, and the first argument of an instance method is self, the cache retains a reference to the class instance (self) indefinitely. This prevents the Python Garbage Collector from deallocating the instance, even if it is no longer used elsewhere in your program.

    To resolve this, you can:

    1. Use a dedicated memoization method: Store the cache on the instance itself (e.g., self.my_cache = functools.cache(self._uncached_func)) so the cache is released when the instance is destroyed.
    2. Limit cache size: Use @functools.lru_cache(maxsize=N) instead of @cache to ensure old entries are evicted.
    3. Manual cleanup: Periodically call .cache_clear() on the decorated function.
    # Example of limiting cache size to prevent unbounded growth
    @functools.lru_cache(maxsize=10000)
    def factorial_plus(self, n: int) -> int:
        return n * self.factorial_plus(n - 1) + self.inc if n else 1 + self.inc