Source code for pymemtrace.trace_malloc

"""
A wrapper around the tracemalloc standard library module.
"""
import functools
import logging
import sys
import tracemalloc

import typing


[docs]class TraceMalloc: """A wrapper around the tracemalloc module that can compensate for tracemalloc's memory usage.""" # Central flag to control all instances of TraceMalloc's TRACE_ON = True ALLOWED_GRANULARITY = ('filename', 'lineno', 'traceback')
[docs] def __init__(self, statistics_granularity: str = 'lineno'): """statistics_granularity can be 'filename', 'lineno' or 'traceback'.""" if statistics_granularity not in self.ALLOWED_GRANULARITY: raise ValueError( f'statistics_granularity must be in {self.ALLOWED_GRANULARITY} not {statistics_granularity}' ) self.statistics_granularity = statistics_granularity if self.TRACE_ON: if not tracemalloc.is_tracing(): tracemalloc.start() self.tracemalloc_snapshot_start: typing.Optional[tracemalloc.Snapshot] = None self.tracemalloc_snapshot_finish: typing.Optional[tracemalloc.Snapshot] = None self.memory_start: typing.Optional[int] = None self.memory_finish: typing.Optional[int] = None self.statistics: typing.List[tracemalloc.StatisticDiff] = [] self._diff: typing.Optional[int] = None
[docs] def __enter__(self): """Take a tracemalloc snapshot.""" if self.TRACE_ON: self.tracemalloc_snapshot_start = tracemalloc.take_snapshot() self.memory_start = tracemalloc.get_tracemalloc_memory() return self
[docs] def __exit__(self, exc_type, exc_val, exc_tb): """Take a tracemalloc snapshot and subtract the initial snapshot. Also note the tracemalloc memory usage.""" if self.TRACE_ON: self.tracemalloc_snapshot_finish = tracemalloc.take_snapshot() self.memory_finish = tracemalloc.get_tracemalloc_memory() self.statistics = self.tracemalloc_snapshot_finish.compare_to( self.tracemalloc_snapshot_start, self.statistics_granularity ) self._diff = None return False
@property def tracemalloc_memory_usage(self) -> typing.Optional[int]: """Returns the tracemalloc memory usage between snapshots of None of no tracing.""" if self.TRACE_ON: return self.memory_finish - self.memory_start @property def diff(self) -> int: """The net memory usage difference recorded by tracemalloc allowing for the memory usage of tracemalloc.""" if self.TRACE_ON: if self._diff is None: self._diff = sum(s.size_diff for s in self.statistics) - self.tracemalloc_memory_usage return self._diff return -sys.maxsize - 1
[docs] def net_statistics(self): """Returns the list of statistics ignoring those from the tracemalloc module itself.""" ret = [] for statistic in self.statistics: file_name = statistic.traceback[0].filename if file_name != tracemalloc.__file__: ret.append(statistic) return ret
[docs]def trace_malloc_log(log_level: int): """Decorator that logs the decorated function the use of Python memory in bytes at the desired log level. This can be switched to a NOP by setting TraceMalloc.TRACE_ON to False.""" def memory_inner(fn): @functools.wraps(fn) def wrapper(*args, **kwargs): with TraceMalloc() as tm: result = fn(*args, ** kwargs) logging.log(log_level, f'TraceMalloc memory delta: {tm.diff:,d} for "{fn.__name__}()"') return result return wrapper return memory_inner