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