123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560 |
- from collections.abc import Sequence, Iterable
- from functools import total_ordering
- import fnmatch
- import linecache
- import os.path
- import pickle
- # Import types and functions implemented in C
- from _tracemalloc import *
- from _tracemalloc import _get_object_traceback, _get_traces
- def _format_size(size, sign):
- for unit in ('B', 'KiB', 'MiB', 'GiB', 'TiB'):
- if abs(size) < 100 and unit != 'B':
- # 3 digits (xx.x UNIT)
- if sign:
- return "%+.1f %s" % (size, unit)
- else:
- return "%.1f %s" % (size, unit)
- if abs(size) < 10 * 1024 or unit == 'TiB':
- # 4 or 5 digits (xxxx UNIT)
- if sign:
- return "%+.0f %s" % (size, unit)
- else:
- return "%.0f %s" % (size, unit)
- size /= 1024
- class Statistic:
- """
- Statistic difference on memory allocations between two Snapshot instance.
- """
- __slots__ = ('traceback', 'size', 'count')
- def __init__(self, traceback, size, count):
- self.traceback = traceback
- self.size = size
- self.count = count
- def __hash__(self):
- return hash((self.traceback, self.size, self.count))
- def __eq__(self, other):
- if not isinstance(other, Statistic):
- return NotImplemented
- return (self.traceback == other.traceback
- and self.size == other.size
- and self.count == other.count)
- def __str__(self):
- text = ("%s: size=%s, count=%i"
- % (self.traceback,
- _format_size(self.size, False),
- self.count))
- if self.count:
- average = self.size / self.count
- text += ", average=%s" % _format_size(average, False)
- return text
- def __repr__(self):
- return ('<Statistic traceback=%r size=%i count=%i>'
- % (self.traceback, self.size, self.count))
- def _sort_key(self):
- return (self.size, self.count, self.traceback)
- class StatisticDiff:
- """
- Statistic difference on memory allocations between an old and a new
- Snapshot instance.
- """
- __slots__ = ('traceback', 'size', 'size_diff', 'count', 'count_diff')
- def __init__(self, traceback, size, size_diff, count, count_diff):
- self.traceback = traceback
- self.size = size
- self.size_diff = size_diff
- self.count = count
- self.count_diff = count_diff
- def __hash__(self):
- return hash((self.traceback, self.size, self.size_diff,
- self.count, self.count_diff))
- def __eq__(self, other):
- if not isinstance(other, StatisticDiff):
- return NotImplemented
- return (self.traceback == other.traceback
- and self.size == other.size
- and self.size_diff == other.size_diff
- and self.count == other.count
- and self.count_diff == other.count_diff)
- def __str__(self):
- text = ("%s: size=%s (%s), count=%i (%+i)"
- % (self.traceback,
- _format_size(self.size, False),
- _format_size(self.size_diff, True),
- self.count,
- self.count_diff))
- if self.count:
- average = self.size / self.count
- text += ", average=%s" % _format_size(average, False)
- return text
- def __repr__(self):
- return ('<StatisticDiff traceback=%r size=%i (%+i) count=%i (%+i)>'
- % (self.traceback, self.size, self.size_diff,
- self.count, self.count_diff))
- def _sort_key(self):
- return (abs(self.size_diff), self.size,
- abs(self.count_diff), self.count,
- self.traceback)
- def _compare_grouped_stats(old_group, new_group):
- statistics = []
- for traceback, stat in new_group.items():
- previous = old_group.pop(traceback, None)
- if previous is not None:
- stat = StatisticDiff(traceback,
- stat.size, stat.size - previous.size,
- stat.count, stat.count - previous.count)
- else:
- stat = StatisticDiff(traceback,
- stat.size, stat.size,
- stat.count, stat.count)
- statistics.append(stat)
- for traceback, stat in old_group.items():
- stat = StatisticDiff(traceback, 0, -stat.size, 0, -stat.count)
- statistics.append(stat)
- return statistics
- @total_ordering
- class Frame:
- """
- Frame of a traceback.
- """
- __slots__ = ("_frame",)
- def __init__(self, frame):
- # frame is a tuple: (filename: str, lineno: int)
- self._frame = frame
- @property
- def filename(self):
- return self._frame[0]
- @property
- def lineno(self):
- return self._frame[1]
- def __eq__(self, other):
- if not isinstance(other, Frame):
- return NotImplemented
- return (self._frame == other._frame)
- def __lt__(self, other):
- if not isinstance(other, Frame):
- return NotImplemented
- return (self._frame < other._frame)
- def __hash__(self):
- return hash(self._frame)
- def __str__(self):
- return "%s:%s" % (self.filename, self.lineno)
- def __repr__(self):
- return "<Frame filename=%r lineno=%r>" % (self.filename, self.lineno)
- @total_ordering
- class Traceback(Sequence):
- """
- Sequence of Frame instances sorted from the oldest frame
- to the most recent frame.
- """
- __slots__ = ("_frames", '_total_nframe')
- def __init__(self, frames, total_nframe=None):
- Sequence.__init__(self)
- # frames is a tuple of frame tuples: see Frame constructor for the
- # format of a frame tuple; it is reversed, because _tracemalloc
- # returns frames sorted from most recent to oldest, but the
- # Python API expects oldest to most recent
- self._frames = tuple(reversed(frames))
- self._total_nframe = total_nframe
- @property
- def total_nframe(self):
- return self._total_nframe
- def __len__(self):
- return len(self._frames)
- def __getitem__(self, index):
- if isinstance(index, slice):
- return tuple(Frame(trace) for trace in self._frames[index])
- else:
- return Frame(self._frames[index])
- def __contains__(self, frame):
- return frame._frame in self._frames
- def __hash__(self):
- return hash(self._frames)
- def __eq__(self, other):
- if not isinstance(other, Traceback):
- return NotImplemented
- return (self._frames == other._frames)
- def __lt__(self, other):
- if not isinstance(other, Traceback):
- return NotImplemented
- return (self._frames < other._frames)
- def __str__(self):
- return str(self[0])
- def __repr__(self):
- s = f"<Traceback {tuple(self)}"
- if self._total_nframe is None:
- s += ">"
- else:
- s += f" total_nframe={self.total_nframe}>"
- return s
- def format(self, limit=None, most_recent_first=False):
- lines = []
- if limit is not None:
- if limit > 0:
- frame_slice = self[-limit:]
- else:
- frame_slice = self[:limit]
- else:
- frame_slice = self
- if most_recent_first:
- frame_slice = reversed(frame_slice)
- for frame in frame_slice:
- lines.append(' File "%s", line %s'
- % (frame.filename, frame.lineno))
- line = linecache.getline(frame.filename, frame.lineno).strip()
- if line:
- lines.append(' %s' % line)
- return lines
- def get_object_traceback(obj):
- """
- Get the traceback where the Python object *obj* was allocated.
- Return a Traceback instance.
- Return None if the tracemalloc module is not tracing memory allocations or
- did not trace the allocation of the object.
- """
- frames = _get_object_traceback(obj)
- if frames is not None:
- return Traceback(frames)
- else:
- return None
- class Trace:
- """
- Trace of a memory block.
- """
- __slots__ = ("_trace",)
- def __init__(self, trace):
- # trace is a tuple: (domain: int, size: int, traceback: tuple).
- # See Traceback constructor for the format of the traceback tuple.
- self._trace = trace
- @property
- def domain(self):
- return self._trace[0]
- @property
- def size(self):
- return self._trace[1]
- @property
- def traceback(self):
- return Traceback(*self._trace[2:])
- def __eq__(self, other):
- if not isinstance(other, Trace):
- return NotImplemented
- return (self._trace == other._trace)
- def __hash__(self):
- return hash(self._trace)
- def __str__(self):
- return "%s: %s" % (self.traceback, _format_size(self.size, False))
- def __repr__(self):
- return ("<Trace domain=%s size=%s, traceback=%r>"
- % (self.domain, _format_size(self.size, False), self.traceback))
- class _Traces(Sequence):
- def __init__(self, traces):
- Sequence.__init__(self)
- # traces is a tuple of trace tuples: see Trace constructor
- self._traces = traces
- def __len__(self):
- return len(self._traces)
- def __getitem__(self, index):
- if isinstance(index, slice):
- return tuple(Trace(trace) for trace in self._traces[index])
- else:
- return Trace(self._traces[index])
- def __contains__(self, trace):
- return trace._trace in self._traces
- def __eq__(self, other):
- if not isinstance(other, _Traces):
- return NotImplemented
- return (self._traces == other._traces)
- def __repr__(self):
- return "<Traces len=%s>" % len(self)
- def _normalize_filename(filename):
- filename = os.path.normcase(filename)
- if filename.endswith('.pyc'):
- filename = filename[:-1]
- return filename
- class BaseFilter:
- def __init__(self, inclusive):
- self.inclusive = inclusive
- def _match(self, trace):
- raise NotImplementedError
- class Filter(BaseFilter):
- def __init__(self, inclusive, filename_pattern,
- lineno=None, all_frames=False, domain=None):
- super().__init__(inclusive)
- self.inclusive = inclusive
- self._filename_pattern = _normalize_filename(filename_pattern)
- self.lineno = lineno
- self.all_frames = all_frames
- self.domain = domain
- @property
- def filename_pattern(self):
- return self._filename_pattern
- def _match_frame_impl(self, filename, lineno):
- filename = _normalize_filename(filename)
- if not fnmatch.fnmatch(filename, self._filename_pattern):
- return False
- if self.lineno is None:
- return True
- else:
- return (lineno == self.lineno)
- def _match_frame(self, filename, lineno):
- return self._match_frame_impl(filename, lineno) ^ (not self.inclusive)
- def _match_traceback(self, traceback):
- if self.all_frames:
- if any(self._match_frame_impl(filename, lineno)
- for filename, lineno in traceback):
- return self.inclusive
- else:
- return (not self.inclusive)
- else:
- filename, lineno = traceback[0]
- return self._match_frame(filename, lineno)
- def _match(self, trace):
- domain, size, traceback, total_nframe = trace
- res = self._match_traceback(traceback)
- if self.domain is not None:
- if self.inclusive:
- return res and (domain == self.domain)
- else:
- return res or (domain != self.domain)
- return res
- class DomainFilter(BaseFilter):
- def __init__(self, inclusive, domain):
- super().__init__(inclusive)
- self._domain = domain
- @property
- def domain(self):
- return self._domain
- def _match(self, trace):
- domain, size, traceback, total_nframe = trace
- return (domain == self.domain) ^ (not self.inclusive)
- class Snapshot:
- """
- Snapshot of traces of memory blocks allocated by Python.
- """
- def __init__(self, traces, traceback_limit):
- # traces is a tuple of trace tuples: see _Traces constructor for
- # the exact format
- self.traces = _Traces(traces)
- self.traceback_limit = traceback_limit
- def dump(self, filename):
- """
- Write the snapshot into a file.
- """
- with open(filename, "wb") as fp:
- pickle.dump(self, fp, pickle.HIGHEST_PROTOCOL)
- @staticmethod
- def load(filename):
- """
- Load a snapshot from a file.
- """
- with open(filename, "rb") as fp:
- return pickle.load(fp)
- def _filter_trace(self, include_filters, exclude_filters, trace):
- if include_filters:
- if not any(trace_filter._match(trace)
- for trace_filter in include_filters):
- return False
- if exclude_filters:
- if any(not trace_filter._match(trace)
- for trace_filter in exclude_filters):
- return False
- return True
- def filter_traces(self, filters):
- """
- Create a new Snapshot instance with a filtered traces sequence, filters
- is a list of Filter or DomainFilter instances. If filters is an empty
- list, return a new Snapshot instance with a copy of the traces.
- """
- if not isinstance(filters, Iterable):
- raise TypeError("filters must be a list of filters, not %s"
- % type(filters).__name__)
- if filters:
- include_filters = []
- exclude_filters = []
- for trace_filter in filters:
- if trace_filter.inclusive:
- include_filters.append(trace_filter)
- else:
- exclude_filters.append(trace_filter)
- new_traces = [trace for trace in self.traces._traces
- if self._filter_trace(include_filters,
- exclude_filters,
- trace)]
- else:
- new_traces = self.traces._traces.copy()
- return Snapshot(new_traces, self.traceback_limit)
- def _group_by(self, key_type, cumulative):
- if key_type not in ('traceback', 'filename', 'lineno'):
- raise ValueError("unknown key_type: %r" % (key_type,))
- if cumulative and key_type not in ('lineno', 'filename'):
- raise ValueError("cumulative mode cannot by used "
- "with key type %r" % key_type)
- stats = {}
- tracebacks = {}
- if not cumulative:
- for trace in self.traces._traces:
- domain, size, trace_traceback, total_nframe = trace
- try:
- traceback = tracebacks[trace_traceback]
- except KeyError:
- if key_type == 'traceback':
- frames = trace_traceback
- elif key_type == 'lineno':
- frames = trace_traceback[:1]
- else: # key_type == 'filename':
- frames = ((trace_traceback[0][0], 0),)
- traceback = Traceback(frames)
- tracebacks[trace_traceback] = traceback
- try:
- stat = stats[traceback]
- stat.size += size
- stat.count += 1
- except KeyError:
- stats[traceback] = Statistic(traceback, size, 1)
- else:
- # cumulative statistics
- for trace in self.traces._traces:
- domain, size, trace_traceback, total_nframe = trace
- for frame in trace_traceback:
- try:
- traceback = tracebacks[frame]
- except KeyError:
- if key_type == 'lineno':
- frames = (frame,)
- else: # key_type == 'filename':
- frames = ((frame[0], 0),)
- traceback = Traceback(frames)
- tracebacks[frame] = traceback
- try:
- stat = stats[traceback]
- stat.size += size
- stat.count += 1
- except KeyError:
- stats[traceback] = Statistic(traceback, size, 1)
- return stats
- def statistics(self, key_type, cumulative=False):
- """
- Group statistics by key_type. Return a sorted list of Statistic
- instances.
- """
- grouped = self._group_by(key_type, cumulative)
- statistics = list(grouped.values())
- statistics.sort(reverse=True, key=Statistic._sort_key)
- return statistics
- def compare_to(self, old_snapshot, key_type, cumulative=False):
- """
- Compute the differences with an old snapshot old_snapshot. Get
- statistics as a sorted list of StatisticDiff instances, grouped by
- group_by.
- """
- new_group = self._group_by(key_type, cumulative)
- old_group = old_snapshot._group_by(key_type, cumulative)
- statistics = _compare_grouped_stats(old_group, new_group)
- statistics.sort(reverse=True, key=StatisticDiff._sort_key)
- return statistics
- def take_snapshot():
- """
- Take a snapshot of traces of memory blocks allocated by Python.
- """
- if not is_tracing():
- raise RuntimeError("the tracemalloc module must be tracing memory "
- "allocations to take a snapshot")
- traces = _get_traces()
- traceback_limit = get_traceback_limit()
- return Snapshot(traces, traceback_limit)
|