tracemalloc.py 18 KB

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