1from collections import Sequence, Iterable 2from functools import total_ordering 3import fnmatch 4import linecache 5import 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): 43 return hash((self.traceback, self.size, self.count)) 44 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): 83 return hash((self.traceback, self.size, self.size_diff, 84 self.count, self.count_diff)) 85 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, 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 # frame is a tuple: (filename: str, lineno: int) 145 self._frame = frame 146 147 @property 148 def filename(self): 149 return self._frame[0] 150 151 @property 152 def lineno(self): 153 return self._frame[1] 154 155 def __eq__(self, other): 156 return (self._frame == other._frame) 157 158 def __lt__(self, other): 159 return (self._frame < other._frame) 160 161 def __hash__(self): 162 return hash(self._frame) 163 164 def __str__(self): 165 return "%s:%s" % (self.filename, self.lineno) 166 167 def __repr__(self): 168 return "<Frame filename=%r lineno=%r>" % (self.filename, self.lineno) 169 170 171@total_ordering 172class Traceback(Sequence): 173 """ 174 Sequence of Frame instances sorted from the most recent frame 175 to the oldest frame. 176 """ 177 __slots__ = ("_frames",) 178 179 def __init__(self, frames): 180 Sequence.__init__(self) 181 # frames is a tuple of frame tuples: see Frame constructor for the 182 # format of a frame tuple 183 self._frames = frames 184 185 def __len__(self): 186 return len(self._frames) 187 188 def __getitem__(self, index): 189 if isinstance(index, slice): 190 return tuple(Frame(trace) for trace in self._frames[index]) 191 else: 192 return Frame(self._frames[index]) 193 194 def __contains__(self, frame): 195 return frame._frame in self._frames 196 197 def __hash__(self): 198 return hash(self._frames) 199 200 def __eq__(self, other): 201 return (self._frames == other._frames) 202 203 def __lt__(self, other): 204 return (self._frames < other._frames) 205 206 def __str__(self): 207 return str(self[0]) 208 209 def __repr__(self): 210 return "<Traceback %r>" % (tuple(self),) 211 212 def format(self, limit=None): 213 lines = [] 214 if limit is not None and limit < 0: 215 return lines 216 for frame in self[:limit]: 217 lines.append(' File "%s", line %s' 218 % (frame.filename, frame.lineno)) 219 line = linecache.getline(frame.filename, frame.lineno).strip() 220 if line: 221 lines.append(' %s' % line) 222 return lines 223 224 225def get_object_traceback(obj): 226 """ 227 Get the traceback where the Python object *obj* was allocated. 228 Return a Traceback instance. 229 230 Return None if the tracemalloc module is not tracing memory allocations or 231 did not trace the allocation of the object. 232 """ 233 frames = _get_object_traceback(obj) 234 if frames is not None: 235 return Traceback(frames) 236 else: 237 return None 238 239 240class Trace: 241 """ 242 Trace of a memory block. 243 """ 244 __slots__ = ("_trace",) 245 246 def __init__(self, trace): 247 # trace is a tuple: (domain: int, size: int, traceback: tuple). 248 # See Traceback constructor for the format of the traceback tuple. 249 self._trace = trace 250 251 @property 252 def domain(self): 253 return self._trace[0] 254 255 @property 256 def size(self): 257 return self._trace[1] 258 259 @property 260 def traceback(self): 261 return Traceback(self._trace[2]) 262 263 def __eq__(self, other): 264 return (self._trace == other._trace) 265 266 def __hash__(self): 267 return hash(self._trace) 268 269 def __str__(self): 270 return "%s: %s" % (self.traceback, _format_size(self.size, False)) 271 272 def __repr__(self): 273 return ("<Trace domain=%s size=%s, traceback=%r>" 274 % (self.domain, _format_size(self.size, False), self.traceback)) 275 276 277class _Traces(Sequence): 278 def __init__(self, traces): 279 Sequence.__init__(self) 280 # traces is a tuple of trace tuples: see Trace constructor 281 self._traces = traces 282 283 def __len__(self): 284 return len(self._traces) 285 286 def __getitem__(self, index): 287 if isinstance(index, slice): 288 return tuple(Trace(trace) for trace in self._traces[index]) 289 else: 290 return Trace(self._traces[index]) 291 292 def __contains__(self, trace): 293 return trace._trace in self._traces 294 295 def __eq__(self, other): 296 return (self._traces == other._traces) 297 298 def __repr__(self): 299 return "<Traces len=%s>" % len(self) 300 301 302def _normalize_filename(filename): 303 filename = os.path.normcase(filename) 304 if filename.endswith('.pyc'): 305 filename = filename[:-1] 306 return filename 307 308 309class BaseFilter: 310 def __init__(self, inclusive): 311 self.inclusive = inclusive 312 313 def _match(self, trace): 314 raise NotImplementedError 315 316 317class Filter(BaseFilter): 318 def __init__(self, inclusive, filename_pattern, 319 lineno=None, all_frames=False, domain=None): 320 super().__init__(inclusive) 321 self.inclusive = inclusive 322 self._filename_pattern = _normalize_filename(filename_pattern) 323 self.lineno = lineno 324 self.all_frames = all_frames 325 self.domain = domain 326 327 @property 328 def filename_pattern(self): 329 return self._filename_pattern 330 331 def _match_frame_impl(self, filename, lineno): 332 filename = _normalize_filename(filename) 333 if not fnmatch.fnmatch(filename, self._filename_pattern): 334 return False 335 if self.lineno is None: 336 return True 337 else: 338 return (lineno == self.lineno) 339 340 def _match_frame(self, filename, lineno): 341 return self._match_frame_impl(filename, lineno) ^ (not self.inclusive) 342 343 def _match_traceback(self, traceback): 344 if self.all_frames: 345 if any(self._match_frame_impl(filename, lineno) 346 for filename, lineno in traceback): 347 return self.inclusive 348 else: 349 return (not self.inclusive) 350 else: 351 filename, lineno = traceback[0] 352 return self._match_frame(filename, lineno) 353 354 def _match(self, trace): 355 domain, size, traceback = trace 356 res = self._match_traceback(traceback) 357 if self.domain is not None: 358 if self.inclusive: 359 return res and (domain == self.domain) 360 else: 361 return res or (domain != self.domain) 362 return res 363 364 365class DomainFilter(BaseFilter): 366 def __init__(self, inclusive, domain): 367 super().__init__(inclusive) 368 self._domain = domain 369 370 @property 371 def domain(self): 372 return self._domain 373 374 def _match(self, trace): 375 domain, size, traceback = trace 376 return (domain == self.domain) ^ (not self.inclusive) 377 378 379class Snapshot: 380 """ 381 Snapshot of traces of memory blocks allocated by Python. 382 """ 383 384 def __init__(self, traces, traceback_limit): 385 # traces is a tuple of trace tuples: see _Traces constructor for 386 # the exact format 387 self.traces = _Traces(traces) 388 self.traceback_limit = traceback_limit 389 390 def dump(self, filename): 391 """ 392 Write the snapshot into a file. 393 """ 394 with open(filename, "wb") as fp: 395 pickle.dump(self, fp, pickle.HIGHEST_PROTOCOL) 396 397 @staticmethod 398 def load(filename): 399 """ 400 Load a snapshot from a file. 401 """ 402 with open(filename, "rb") as fp: 403 return pickle.load(fp) 404 405 def _filter_trace(self, include_filters, exclude_filters, trace): 406 if include_filters: 407 if not any(trace_filter._match(trace) 408 for trace_filter in include_filters): 409 return False 410 if exclude_filters: 411 if any(not trace_filter._match(trace) 412 for trace_filter in exclude_filters): 413 return False 414 return True 415 416 def filter_traces(self, filters): 417 """ 418 Create a new Snapshot instance with a filtered traces sequence, filters 419 is a list of Filter or DomainFilter instances. If filters is an empty 420 list, return a new Snapshot instance with a copy of the traces. 421 """ 422 if not isinstance(filters, Iterable): 423 raise TypeError("filters must be a list of filters, not %s" 424 % type(filters).__name__) 425 if filters: 426 include_filters = [] 427 exclude_filters = [] 428 for trace_filter in filters: 429 if trace_filter.inclusive: 430 include_filters.append(trace_filter) 431 else: 432 exclude_filters.append(trace_filter) 433 new_traces = [trace for trace in self.traces._traces 434 if self._filter_trace(include_filters, 435 exclude_filters, 436 trace)] 437 else: 438 new_traces = self.traces._traces.copy() 439 return Snapshot(new_traces, self.traceback_limit) 440 441 def _group_by(self, key_type, cumulative): 442 if key_type not in ('traceback', 'filename', 'lineno'): 443 raise ValueError("unknown key_type: %r" % (key_type,)) 444 if cumulative and key_type not in ('lineno', 'filename'): 445 raise ValueError("cumulative mode cannot by used " 446 "with key type %r" % key_type) 447 448 stats = {} 449 tracebacks = {} 450 if not cumulative: 451 for trace in self.traces._traces: 452 domain, size, trace_traceback = trace 453 try: 454 traceback = tracebacks[trace_traceback] 455 except KeyError: 456 if key_type == 'traceback': 457 frames = trace_traceback 458 elif key_type == 'lineno': 459 frames = trace_traceback[:1] 460 else: # key_type == 'filename': 461 frames = ((trace_traceback[0][0], 0),) 462 traceback = Traceback(frames) 463 tracebacks[trace_traceback] = traceback 464 try: 465 stat = stats[traceback] 466 stat.size += size 467 stat.count += 1 468 except KeyError: 469 stats[traceback] = Statistic(traceback, size, 1) 470 else: 471 # cumulative statistics 472 for trace in self.traces._traces: 473 domain, size, trace_traceback = trace 474 for frame in trace_traceback: 475 try: 476 traceback = tracebacks[frame] 477 except KeyError: 478 if key_type == 'lineno': 479 frames = (frame,) 480 else: # key_type == 'filename': 481 frames = ((frame[0], 0),) 482 traceback = Traceback(frames) 483 tracebacks[frame] = traceback 484 try: 485 stat = stats[traceback] 486 stat.size += size 487 stat.count += 1 488 except KeyError: 489 stats[traceback] = Statistic(traceback, size, 1) 490 return stats 491 492 def statistics(self, key_type, cumulative=False): 493 """ 494 Group statistics by key_type. Return a sorted list of Statistic 495 instances. 496 """ 497 grouped = self._group_by(key_type, cumulative) 498 statistics = list(grouped.values()) 499 statistics.sort(reverse=True, key=Statistic._sort_key) 500 return statistics 501 502 def compare_to(self, old_snapshot, key_type, cumulative=False): 503 """ 504 Compute the differences with an old snapshot old_snapshot. Get 505 statistics as a sorted list of StatisticDiff instances, grouped by 506 group_by. 507 """ 508 new_group = self._group_by(key_type, cumulative) 509 old_group = old_snapshot._group_by(key_type, cumulative) 510 statistics = _compare_grouped_stats(old_group, new_group) 511 statistics.sort(reverse=True, key=StatisticDiff._sort_key) 512 return statistics 513 514 515def take_snapshot(): 516 """ 517 Take a snapshot of traces of memory blocks allocated by Python. 518 """ 519 if not is_tracing(): 520 raise RuntimeError("the tracemalloc module must be tracing memory " 521 "allocations to take a snapshot") 522 traces = _get_traces() 523 traceback_limit = get_traceback_limit() 524 return Snapshot(traces, traceback_limit) 525