blob: 7780eca77d701c1c93391685e4a9c3ac383a40cf [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):
42 return (self.traceback, self.size, self.count)
43
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):
82 return (self.traceback, self.size, self.size_diff,
83 self.count, self.count_diff)
84
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,
107
108 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):
186 trace = self._frames[index]
187 return Frame(trace)
188
189 def __contains__(self, frame):
190 return frame._frame in self._frames
191
192 def __hash__(self):
193 return hash(self._frames)
194
195 def __eq__(self, other):
196 return (self._frames == other._frames)
197
198 def __lt__(self, other):
199 return (self._frames < other._frames)
200
201 def __str__(self):
202 return str(self[0])
203
204 def __repr__(self):
205 return "<Traceback %r>" % (tuple(self),)
206
207
208def get_object_traceback(obj):
209 """
210 Get the traceback where the Python object *obj* was allocated.
211 Return a Traceback instance.
212
213 Return None if the tracemalloc module is not tracing memory allocations or
214 did not trace the allocation of the object.
215 """
216 frames = _get_object_traceback(obj)
217 if frames is not None:
218 return Traceback(frames)
219 else:
220 return None
221
222
223class Trace:
224 """
225 Trace of a memory block.
226 """
227 __slots__ = ("_trace",)
228
229 def __init__(self, trace):
230 self._trace = trace
231
232 @property
233 def size(self):
234 return self._trace[0]
235
236 @property
237 def traceback(self):
238 return Traceback(self._trace[1])
239
240 def __eq__(self, other):
241 return (self._trace == other._trace)
242
243 def __hash__(self):
244 return hash(self._trace)
245
246 def __str__(self):
247 return "%s: %s" % (self.traceback, _format_size(self.size, False))
248
249 def __repr__(self):
250 return ("<Trace size=%s, traceback=%r>"
251 % (_format_size(self.size, False), self.traceback))
252
253
254class _Traces(Sequence):
255 def __init__(self, traces):
256 Sequence.__init__(self)
257 self._traces = traces
258
259 def __len__(self):
260 return len(self._traces)
261
262 def __getitem__(self, index):
263 trace = self._traces[index]
264 return Trace(trace)
265
266 def __contains__(self, trace):
267 return trace._trace in self._traces
268
269 def __eq__(self, other):
270 return (self._traces == other._traces)
271
272 def __repr__(self):
273 return "<Traces len=%s>" % len(self)
274
275
276def _normalize_filename(filename):
277 filename = os.path.normcase(filename)
278 if filename.endswith(('.pyc', '.pyo')):
279 filename = filename[:-1]
280 return filename
281
282
283class Filter:
284 def __init__(self, inclusive, filename_pattern,
285 lineno=None, all_frames=False):
286 self.inclusive = inclusive
287 self._filename_pattern = _normalize_filename(filename_pattern)
288 self.lineno = lineno
289 self.all_frames = all_frames
290
291 @property
292 def filename_pattern(self):
293 return self._filename_pattern
294
295 def __match_frame(self, filename, lineno):
296 filename = _normalize_filename(filename)
297 if not fnmatch.fnmatch(filename, self._filename_pattern):
298 return False
299 if self.lineno is None:
300 return True
301 else:
302 return (lineno == self.lineno)
303
304 def _match_frame(self, filename, lineno):
305 return self.__match_frame(filename, lineno) ^ (not self.inclusive)
306
307 def _match_traceback(self, traceback):
308 if self.all_frames:
309 if any(self.__match_frame(filename, lineno)
310 for filename, lineno in traceback):
311 return self.inclusive
312 else:
313 return (not self.inclusive)
314 else:
315 filename, lineno = traceback[0]
316 return self._match_frame(filename, lineno)
317
318
319class Snapshot:
320 """
321 Snapshot of traces of memory blocks allocated by Python.
322 """
323
324 def __init__(self, traces, traceback_limit):
325 self.traces = _Traces(traces)
326 self.traceback_limit = traceback_limit
327
328 def dump(self, filename):
329 """
330 Write the snapshot into a file.
331 """
332 with open(filename, "wb") as fp:
333 pickle.dump(self, fp, pickle.HIGHEST_PROTOCOL)
334
335 @staticmethod
336 def load(filename):
337 """
338 Load a snapshot from a file.
339 """
340 with open(filename, "rb") as fp:
341 return pickle.load(fp)
342
343 def _filter_trace(self, include_filters, exclude_filters, trace):
344 traceback = trace[1]
345 if include_filters:
346 if not any(trace_filter._match_traceback(traceback)
347 for trace_filter in include_filters):
348 return False
349 if exclude_filters:
350 if any(not trace_filter._match_traceback(traceback)
351 for trace_filter in exclude_filters):
352 return False
353 return True
354
355 def filter_traces(self, filters):
356 """
357 Create a new Snapshot instance with a filtered traces sequence, filters
358 is a list of Filter instances. If filters is an empty list, return a
359 new Snapshot instance with a copy of the traces.
360 """
361 if filters:
362 include_filters = []
363 exclude_filters = []
364 for trace_filter in filters:
365 if trace_filter.inclusive:
366 include_filters.append(trace_filter)
367 else:
368 exclude_filters.append(trace_filter)
369 new_traces = [trace for trace in self.traces._traces
370 if self._filter_trace(include_filters,
371 exclude_filters,
372 trace)]
373 else:
374 new_traces = self.traces._traces.copy()
375 return Snapshot(new_traces, self.traceback_limit)
376
377 def _group_by(self, key_type, cumulative):
378 if key_type not in ('traceback', 'filename', 'lineno'):
379 raise ValueError("unknown key_type: %r" % (key_type,))
380 if cumulative and key_type not in ('lineno', 'filename'):
381 raise ValueError("cumulative mode cannot by used "
382 "with key type %r" % key_type)
383 if cumulative and self.traceback_limit < 2:
384 raise ValueError("cumulative mode needs tracebacks with at least "
385 "2 frames, traceback limit is %s"
386 % self.traceback_limit)
387
388 stats = {}
389 tracebacks = {}
390 if not cumulative:
391 for trace in self.traces._traces:
392 size, trace_traceback = trace
393 try:
394 traceback = tracebacks[trace_traceback]
395 except KeyError:
396 if key_type == 'traceback':
397 frames = trace_traceback
398 elif key_type == 'lineno':
399 frames = trace_traceback[:1]
400 else: # key_type == 'filename':
401 frames = ((trace_traceback[0][0], 0),)
402 traceback = Traceback(frames)
403 tracebacks[trace_traceback] = traceback
404 try:
405 stat = stats[traceback]
406 stat.size += size
407 stat.count += 1
408 except KeyError:
409 stats[traceback] = Statistic(traceback, size, 1)
410 else:
411 # cumulative statistics
412 for trace in self.traces._traces:
413 size, trace_traceback = trace
414 for frame in trace_traceback:
415 try:
416 traceback = tracebacks[frame]
417 except KeyError:
418 if key_type == 'lineno':
419 frames = (frame,)
420 else: # key_type == 'filename':
421 frames = ((frame[0], 0),)
422 traceback = Traceback(frames)
423 tracebacks[frame] = traceback
424 try:
425 stat = stats[traceback]
426 stat.size += size
427 stat.count += 1
428 except KeyError:
429 stats[traceback] = Statistic(traceback, size, 1)
430 return stats
431
432 def statistics(self, key_type, cumulative=False):
433 """
434 Group statistics by key_type. Return a sorted list of Statistic
435 instances.
436 """
437 grouped = self._group_by(key_type, cumulative)
438 statistics = list(grouped.values())
439 statistics.sort(reverse=True, key=Statistic._sort_key)
440 return statistics
441
442 def compare_to(self, old_snapshot, key_type, cumulative=False):
443 """
444 Compute the differences with an old snapshot old_snapshot. Get
445 statistics as a sorted list of StatisticDiff instances, grouped by
446 group_by.
447 """
448 new_group = self._group_by(key_type, cumulative)
449 old_group = old_snapshot._group_by(key_type, cumulative)
450 statistics = _compare_grouped_stats(old_group, new_group)
451 statistics.sort(reverse=True, key=StatisticDiff._sort_key)
452 return statistics
453
454
455def take_snapshot():
456 """
457 Take a snapshot of traces of memory blocks allocated by Python.
458 """
459 if not is_tracing():
460 raise RuntimeError("the tracemalloc module must be tracing memory "
461 "allocations to take a snapshot")
462 traces = _get_traces()
463 traceback_limit = get_traceback_limit()
464 return Snapshot(traces, traceback_limit)