blob: b07594693974b4064c5be790eb9577dd0080a8f9 [file] [log] [blame]
Victor Stinnered3b0bc2013-11-23 12:27:24 +01001from collections import Sequence
2from functools import total_ordering
3import fnmatch
4import os.path
5import pickle
6
7# Import types and functions implemented in C
8from _tracemalloc import *
9from _tracemalloc import _get_object_traceback, _get_traces
10
11
12def _format_size(size, sign):
13 for unit in ('B', 'KiB', 'MiB', 'GiB', 'TiB'):
14 if abs(size) < 100 and unit != 'B':
15 # 3 digits (xx.x UNIT)
16 if sign:
17 return "%+.1f %s" % (size, unit)
18 else:
19 return "%.1f %s" % (size, unit)
20 if abs(size) < 10 * 1024 or unit == 'TiB':
21 # 4 or 5 digits (xxxx UNIT)
22 if sign:
23 return "%+.0f %s" % (size, unit)
24 else:
25 return "%.0f %s" % (size, unit)
26 size /= 1024
27
28
29class Statistic:
30 """
31 Statistic difference on memory allocations between two Snapshot instance.
32 """
33
34 __slots__ = ('traceback', 'size', 'count')
35
36 def __init__(self, traceback, size, count):
37 self.traceback = traceback
38 self.size = size
39 self.count = count
40
41 def __hash__(self):
Victor Stinner802a4842013-11-26 10:16:25 +010042 return hash((self.traceback, self.size, self.count))
Victor Stinnered3b0bc2013-11-23 12:27:24 +010043
44 def __eq__(self, other):
45 return (self.traceback == other.traceback
46 and self.size == other.size
47 and self.count == other.count)
48
49 def __str__(self):
50 text = ("%s: size=%s, count=%i"
51 % (self.traceback,
52 _format_size(self.size, False),
53 self.count))
54 if self.count:
55 average = self.size / self.count
56 text += ", average=%s" % _format_size(average, False)
57 return text
58
59 def __repr__(self):
60 return ('<Statistic traceback=%r size=%i count=%i>'
61 % (self.traceback, self.size, self.count))
62
63 def _sort_key(self):
64 return (self.size, self.count, self.traceback)
65
66
67class StatisticDiff:
68 """
69 Statistic difference on memory allocations between an old and a new
70 Snapshot instance.
71 """
72 __slots__ = ('traceback', 'size', 'size_diff', 'count', 'count_diff')
73
74 def __init__(self, traceback, size, size_diff, count, count_diff):
75 self.traceback = traceback
76 self.size = size
77 self.size_diff = size_diff
78 self.count = count
79 self.count_diff = count_diff
80
81 def __hash__(self):
Victor Stinner802a4842013-11-26 10:16:25 +010082 return hash((self.traceback, self.size, self.size_diff,
83 self.count, self.count_diff))
Victor Stinnered3b0bc2013-11-23 12:27:24 +010084
85 def __eq__(self, other):
86 return (self.traceback == other.traceback
87 and self.size == other.size
88 and self.size_diff == other.size_diff
89 and self.count == other.count
90 and self.count_diff == other.count_diff)
91
92 def __str__(self):
93 text = ("%s: size=%s (%s), count=%i (%+i)"
94 % (self.traceback,
95 _format_size(self.size, False),
96 _format_size(self.size_diff, True),
97 self.count,
98 self.count_diff))
99 if self.count:
100 average = self.size / self.count
101 text += ", average=%s" % _format_size(average, False)
102 return text
103
104 def __repr__(self):
105 return ('<StatisticDiff traceback=%r size=%i (%+i) count=%i (%+i)>'
106 % (self.traceback, self.size, self.size_diff,
Victor Stinnered3b0bc2013-11-23 12:27:24 +0100107 self.count, self.count_diff))
108
109 def _sort_key(self):
110 return (abs(self.size_diff), self.size,
111 abs(self.count_diff), self.count,
112 self.traceback)
113
114
115def _compare_grouped_stats(old_group, new_group):
116 statistics = []
117 for traceback, stat in new_group.items():
118 previous = old_group.pop(traceback, None)
119 if previous is not None:
120 stat = StatisticDiff(traceback,
121 stat.size, stat.size - previous.size,
122 stat.count, stat.count - previous.count)
123 else:
124 stat = StatisticDiff(traceback,
125 stat.size, stat.size,
126 stat.count, stat.count)
127 statistics.append(stat)
128
129 for traceback, stat in old_group.items():
130 stat = StatisticDiff(traceback, 0, -stat.size, 0, -stat.count)
131 statistics.append(stat)
132 return statistics
133
134
135@total_ordering
136class Frame:
137 """
138 Frame of a traceback.
139 """
140 __slots__ = ("_frame",)
141
142 def __init__(self, frame):
143 self._frame = frame
144
145 @property
146 def filename(self):
147 return self._frame[0]
148
149 @property
150 def lineno(self):
151 return self._frame[1]
152
153 def __eq__(self, other):
154 return (self._frame == other._frame)
155
156 def __lt__(self, other):
157 return (self._frame < other._frame)
158
159 def __hash__(self):
160 return hash(self._frame)
161
162 def __str__(self):
163 return "%s:%s" % (self.filename, self.lineno)
164
165 def __repr__(self):
166 return "<Frame filename=%r lineno=%r>" % (self.filename, self.lineno)
167
168
169@total_ordering
170class Traceback(Sequence):
171 """
172 Sequence of Frame instances sorted from the most recent frame
173 to the oldest frame.
174 """
175 __slots__ = ("_frames",)
176
177 def __init__(self, frames):
178 Sequence.__init__(self)
179 self._frames = frames
180
181 def __len__(self):
182 return len(self._frames)
183
184 def __getitem__(self, index):
Victor Stinner524be302014-02-01 04:07:02 +0100185 if isinstance(index, slice):
186 return tuple(Frame(trace) for trace in self._frames[index])
187 else:
188 return Frame(self._frames[index])
Victor Stinnered3b0bc2013-11-23 12:27:24 +0100189
190 def __contains__(self, frame):
191 return frame._frame in self._frames
192
193 def __hash__(self):
194 return hash(self._frames)
195
196 def __eq__(self, other):
197 return (self._frames == other._frames)
198
199 def __lt__(self, other):
200 return (self._frames < other._frames)
201
202 def __str__(self):
203 return str(self[0])
204
205 def __repr__(self):
206 return "<Traceback %r>" % (tuple(self),)
207
208
209def get_object_traceback(obj):
210 """
211 Get the traceback where the Python object *obj* was allocated.
212 Return a Traceback instance.
213
214 Return None if the tracemalloc module is not tracing memory allocations or
215 did not trace the allocation of the object.
216 """
217 frames = _get_object_traceback(obj)
218 if frames is not None:
219 return Traceback(frames)
220 else:
221 return None
222
223
224class Trace:
225 """
226 Trace of a memory block.
227 """
228 __slots__ = ("_trace",)
229
230 def __init__(self, trace):
231 self._trace = trace
232
233 @property
234 def size(self):
235 return self._trace[0]
236
237 @property
238 def traceback(self):
239 return Traceback(self._trace[1])
240
241 def __eq__(self, other):
242 return (self._trace == other._trace)
243
244 def __hash__(self):
245 return hash(self._trace)
246
247 def __str__(self):
248 return "%s: %s" % (self.traceback, _format_size(self.size, False))
249
250 def __repr__(self):
251 return ("<Trace size=%s, traceback=%r>"
252 % (_format_size(self.size, False), self.traceback))
253
254
255class _Traces(Sequence):
256 def __init__(self, traces):
257 Sequence.__init__(self)
258 self._traces = traces
259
260 def __len__(self):
261 return len(self._traces)
262
263 def __getitem__(self, index):
Victor Stinner524be302014-02-01 04:07:02 +0100264 if isinstance(index, slice):
265 return tuple(Trace(trace) for trace in self._traces[index])
266 else:
267 return Trace(self._traces[index])
Victor Stinnered3b0bc2013-11-23 12:27:24 +0100268
269 def __contains__(self, trace):
270 return trace._trace in self._traces
271
272 def __eq__(self, other):
273 return (self._traces == other._traces)
274
275 def __repr__(self):
276 return "<Traces len=%s>" % len(self)
277
278
279def _normalize_filename(filename):
280 filename = os.path.normcase(filename)
281 if filename.endswith(('.pyc', '.pyo')):
282 filename = filename[:-1]
283 return filename
284
285
286class Filter:
287 def __init__(self, inclusive, filename_pattern,
288 lineno=None, all_frames=False):
289 self.inclusive = inclusive
290 self._filename_pattern = _normalize_filename(filename_pattern)
291 self.lineno = lineno
292 self.all_frames = all_frames
293
294 @property
295 def filename_pattern(self):
296 return self._filename_pattern
297
298 def __match_frame(self, filename, lineno):
299 filename = _normalize_filename(filename)
300 if not fnmatch.fnmatch(filename, self._filename_pattern):
301 return False
302 if self.lineno is None:
303 return True
304 else:
305 return (lineno == self.lineno)
306
307 def _match_frame(self, filename, lineno):
308 return self.__match_frame(filename, lineno) ^ (not self.inclusive)
309
310 def _match_traceback(self, traceback):
311 if self.all_frames:
312 if any(self.__match_frame(filename, lineno)
313 for filename, lineno in traceback):
314 return self.inclusive
315 else:
316 return (not self.inclusive)
317 else:
318 filename, lineno = traceback[0]
319 return self._match_frame(filename, lineno)
320
321
322class Snapshot:
323 """
324 Snapshot of traces of memory blocks allocated by Python.
325 """
326
327 def __init__(self, traces, traceback_limit):
328 self.traces = _Traces(traces)
329 self.traceback_limit = traceback_limit
330
331 def dump(self, filename):
332 """
333 Write the snapshot into a file.
334 """
335 with open(filename, "wb") as fp:
336 pickle.dump(self, fp, pickle.HIGHEST_PROTOCOL)
337
338 @staticmethod
339 def load(filename):
340 """
341 Load a snapshot from a file.
342 """
343 with open(filename, "rb") as fp:
344 return pickle.load(fp)
345
346 def _filter_trace(self, include_filters, exclude_filters, trace):
347 traceback = trace[1]
348 if include_filters:
349 if not any(trace_filter._match_traceback(traceback)
350 for trace_filter in include_filters):
351 return False
352 if exclude_filters:
353 if any(not trace_filter._match_traceback(traceback)
354 for trace_filter in exclude_filters):
355 return False
356 return True
357
358 def filter_traces(self, filters):
359 """
360 Create a new Snapshot instance with a filtered traces sequence, filters
361 is a list of Filter instances. If filters is an empty list, return a
362 new Snapshot instance with a copy of the traces.
363 """
364 if filters:
365 include_filters = []
366 exclude_filters = []
367 for trace_filter in filters:
368 if trace_filter.inclusive:
369 include_filters.append(trace_filter)
370 else:
371 exclude_filters.append(trace_filter)
372 new_traces = [trace for trace in self.traces._traces
373 if self._filter_trace(include_filters,
374 exclude_filters,
375 trace)]
376 else:
377 new_traces = self.traces._traces.copy()
378 return Snapshot(new_traces, self.traceback_limit)
379
380 def _group_by(self, key_type, cumulative):
381 if key_type not in ('traceback', 'filename', 'lineno'):
382 raise ValueError("unknown key_type: %r" % (key_type,))
383 if cumulative and key_type not in ('lineno', 'filename'):
384 raise ValueError("cumulative mode cannot by used "
385 "with key type %r" % key_type)
Victor Stinnered3b0bc2013-11-23 12:27:24 +0100386
387 stats = {}
388 tracebacks = {}
389 if not cumulative:
390 for trace in self.traces._traces:
391 size, trace_traceback = trace
392 try:
393 traceback = tracebacks[trace_traceback]
394 except KeyError:
395 if key_type == 'traceback':
396 frames = trace_traceback
397 elif key_type == 'lineno':
398 frames = trace_traceback[:1]
399 else: # key_type == 'filename':
400 frames = ((trace_traceback[0][0], 0),)
401 traceback = Traceback(frames)
402 tracebacks[trace_traceback] = traceback
403 try:
404 stat = stats[traceback]
405 stat.size += size
406 stat.count += 1
407 except KeyError:
408 stats[traceback] = Statistic(traceback, size, 1)
409 else:
410 # cumulative statistics
411 for trace in self.traces._traces:
412 size, trace_traceback = trace
413 for frame in trace_traceback:
414 try:
415 traceback = tracebacks[frame]
416 except KeyError:
417 if key_type == 'lineno':
418 frames = (frame,)
419 else: # key_type == 'filename':
420 frames = ((frame[0], 0),)
421 traceback = Traceback(frames)
422 tracebacks[frame] = traceback
423 try:
424 stat = stats[traceback]
425 stat.size += size
426 stat.count += 1
427 except KeyError:
428 stats[traceback] = Statistic(traceback, size, 1)
429 return stats
430
431 def statistics(self, key_type, cumulative=False):
432 """
433 Group statistics by key_type. Return a sorted list of Statistic
434 instances.
435 """
436 grouped = self._group_by(key_type, cumulative)
437 statistics = list(grouped.values())
438 statistics.sort(reverse=True, key=Statistic._sort_key)
439 return statistics
440
441 def compare_to(self, old_snapshot, key_type, cumulative=False):
442 """
443 Compute the differences with an old snapshot old_snapshot. Get
444 statistics as a sorted list of StatisticDiff instances, grouped by
445 group_by.
446 """
447 new_group = self._group_by(key_type, cumulative)
448 old_group = old_snapshot._group_by(key_type, cumulative)
449 statistics = _compare_grouped_stats(old_group, new_group)
450 statistics.sort(reverse=True, key=StatisticDiff._sort_key)
451 return statistics
452
453
454def take_snapshot():
455 """
456 Take a snapshot of traces of memory blocks allocated by Python.
457 """
458 if not is_tracing():
459 raise RuntimeError("the tracemalloc module must be tracing memory "
460 "allocations to take a snapshot")
461 traces = _get_traces()
462 traceback_limit = get_traceback_limit()
463 return Snapshot(traces, traceback_limit)