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