Skip to content

Commit 8d59eb1

Browse files
jdvstinner
authored andcommitted
bpo-37961, tracemalloc: add Traceback.total_nframe (GH-15545)
Add a total_nframe field to the traces collected by the tracemalloc module. This field indicates the original number of frames before it was truncated.
1 parent f3ef06a commit 8d59eb1

File tree

5 files changed

+114
-64
lines changed

5 files changed

+114
-64
lines changed

Doc/library/tracemalloc.rst

+15
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,9 @@ Functions
313313
frames. By default, a trace of a memory block only stores the most recent
314314
frame: the limit is ``1``. *nframe* must be greater or equal to ``1``.
315315

316+
You can still read the original number of total frames that composed the
317+
traceback by looking at the :attr:`Traceback.total_nframe` attribute.
318+
316319
Storing more than ``1`` frame is only useful to compute statistics grouped
317320
by ``'traceback'`` or to compute cumulative statistics: see the
318321
:meth:`Snapshot.compare_to` and :meth:`Snapshot.statistics` methods.
@@ -659,13 +662,25 @@ Traceback
659662

660663
When a snapshot is taken, tracebacks of traces are limited to
661664
:func:`get_traceback_limit` frames. See the :func:`take_snapshot` function.
665+
The original number of frames of the traceback is stored in the
666+
:attr:`Traceback.total_nframe` attribute. That allows to know if a traceback
667+
has been truncated by the traceback limit.
662668

663669
The :attr:`Trace.traceback` attribute is an instance of :class:`Traceback`
664670
instance.
665671

666672
.. versionchanged:: 3.7
667673
Frames are now sorted from the oldest to the most recent, instead of most recent to oldest.
668674

675+
.. attribute:: total_nframe
676+
677+
Total number of frames that composed the traceback before truncation.
678+
This attribute can be set to ``None`` if the information is not
679+
available.
680+
681+
.. versionchanged:: 3.9
682+
The :attr:`Traceback.total_nframe` attribute was added.
683+
669684
.. method:: format(limit=None, most_recent_first=False)
670685

671686
Format the traceback as a list of lines with newlines. Use the

Lib/test/test_tracemalloc.py

+47-43
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def allocate_bytes(size):
3636
bytes_len = (size - EMPTY_STRING_SIZE)
3737
frames = get_frames(nframe, 1)
3838
data = b'x' * bytes_len
39-
return data, tracemalloc.Traceback(frames)
39+
return data, tracemalloc.Traceback(frames, min(len(frames), nframe))
4040

4141
def create_snapshots():
4242
traceback_limit = 2
@@ -45,27 +45,27 @@ def create_snapshots():
4545
# traceback_frames) tuples. traceback_frames is a tuple of (filename,
4646
# line_number) tuples.
4747
raw_traces = [
48-
(0, 10, (('a.py', 2), ('b.py', 4))),
49-
(0, 10, (('a.py', 2), ('b.py', 4))),
50-
(0, 10, (('a.py', 2), ('b.py', 4))),
48+
(0, 10, (('a.py', 2), ('b.py', 4)), 3),
49+
(0, 10, (('a.py', 2), ('b.py', 4)), 3),
50+
(0, 10, (('a.py', 2), ('b.py', 4)), 3),
5151

52-
(1, 2, (('a.py', 5), ('b.py', 4))),
52+
(1, 2, (('a.py', 5), ('b.py', 4)), 3),
5353

54-
(2, 66, (('b.py', 1),)),
54+
(2, 66, (('b.py', 1),), 1),
5555

56-
(3, 7, (('<unknown>', 0),)),
56+
(3, 7, (('<unknown>', 0),), 1),
5757
]
5858
snapshot = tracemalloc.Snapshot(raw_traces, traceback_limit)
5959

6060
raw_traces2 = [
61-
(0, 10, (('a.py', 2), ('b.py', 4))),
62-
(0, 10, (('a.py', 2), ('b.py', 4))),
63-
(0, 10, (('a.py', 2), ('b.py', 4))),
61+
(0, 10, (('a.py', 2), ('b.py', 4)), 3),
62+
(0, 10, (('a.py', 2), ('b.py', 4)), 3),
63+
(0, 10, (('a.py', 2), ('b.py', 4)), 3),
6464

65-
(2, 2, (('a.py', 5), ('b.py', 4))),
66-
(2, 5000, (('a.py', 5), ('b.py', 4))),
65+
(2, 2, (('a.py', 5), ('b.py', 4)), 3),
66+
(2, 5000, (('a.py', 5), ('b.py', 4)), 3),
6767

68-
(4, 400, (('c.py', 578),)),
68+
(4, 400, (('c.py', 578),), 1),
6969
]
7070
snapshot2 = tracemalloc.Snapshot(raw_traces2, traceback_limit)
7171

@@ -125,7 +125,7 @@ def test_new_reference(self):
125125

126126
nframe = tracemalloc.get_traceback_limit()
127127
frames = get_frames(nframe, -3)
128-
obj_traceback = tracemalloc.Traceback(frames)
128+
obj_traceback = tracemalloc.Traceback(frames, min(len(frames), nframe))
129129

130130
traceback = tracemalloc.get_object_traceback(obj)
131131
self.assertIsNotNone(traceback)
@@ -167,7 +167,7 @@ def test_get_traces(self):
167167
trace = self.find_trace(traces, obj_traceback)
168168

169169
self.assertIsInstance(trace, tuple)
170-
domain, size, traceback = trace
170+
domain, size, traceback, length = trace
171171
self.assertEqual(size, obj_size)
172172
self.assertEqual(traceback, obj_traceback._frames)
173173

@@ -197,8 +197,8 @@ def allocate_bytes4(size):
197197

198198
trace1 = self.find_trace(traces, obj1_traceback)
199199
trace2 = self.find_trace(traces, obj2_traceback)
200-
domain1, size1, traceback1 = trace1
201-
domain2, size2, traceback2 = trace2
200+
domain1, size1, traceback1, length1 = trace1
201+
domain2, size2, traceback2, length2 = trace2
202202
self.assertIs(traceback2, traceback1)
203203

204204
def test_get_traced_memory(self):
@@ -259,6 +259,9 @@ def test_snapshot(self):
259259
# take a snapshot
260260
snapshot = tracemalloc.take_snapshot()
261261

262+
# This can vary
263+
self.assertGreater(snapshot.traces[1].traceback.total_nframe, 10)
264+
262265
# write on disk
263266
snapshot.dump(support.TESTFN)
264267
self.addCleanup(support.unlink, support.TESTFN)
@@ -321,7 +324,7 @@ class TestSnapshot(unittest.TestCase):
321324
maxDiff = 4000
322325

323326
def test_create_snapshot(self):
324-
raw_traces = [(0, 5, (('a.py', 2),))]
327+
raw_traces = [(0, 5, (('a.py', 2),), 10)]
325328

326329
with contextlib.ExitStack() as stack:
327330
stack.enter_context(patch.object(tracemalloc, 'is_tracing',
@@ -336,6 +339,7 @@ def test_create_snapshot(self):
336339
self.assertEqual(len(snapshot.traces), 1)
337340
trace = snapshot.traces[0]
338341
self.assertEqual(trace.size, 5)
342+
self.assertEqual(trace.traceback.total_nframe, 10)
339343
self.assertEqual(len(trace.traceback), 1)
340344
self.assertEqual(trace.traceback[0].filename, 'a.py')
341345
self.assertEqual(trace.traceback[0].lineno, 2)
@@ -351,11 +355,11 @@ def test_filter_traces(self):
351355
# exclude b.py
352356
snapshot3 = snapshot.filter_traces((filter1,))
353357
self.assertEqual(snapshot3.traces._traces, [
354-
(0, 10, (('a.py', 2), ('b.py', 4))),
355-
(0, 10, (('a.py', 2), ('b.py', 4))),
356-
(0, 10, (('a.py', 2), ('b.py', 4))),
357-
(1, 2, (('a.py', 5), ('b.py', 4))),
358-
(3, 7, (('<unknown>', 0),)),
358+
(0, 10, (('a.py', 2), ('b.py', 4)), 3),
359+
(0, 10, (('a.py', 2), ('b.py', 4)), 3),
360+
(0, 10, (('a.py', 2), ('b.py', 4)), 3),
361+
(1, 2, (('a.py', 5), ('b.py', 4)), 3),
362+
(3, 7, (('<unknown>', 0),), 1),
359363
])
360364

361365
# filter_traces() must not touch the original snapshot
@@ -364,10 +368,10 @@ def test_filter_traces(self):
364368
# only include two lines of a.py
365369
snapshot4 = snapshot3.filter_traces((filter2, filter3))
366370
self.assertEqual(snapshot4.traces._traces, [
367-
(0, 10, (('a.py', 2), ('b.py', 4))),
368-
(0, 10, (('a.py', 2), ('b.py', 4))),
369-
(0, 10, (('a.py', 2), ('b.py', 4))),
370-
(1, 2, (('a.py', 5), ('b.py', 4))),
371+
(0, 10, (('a.py', 2), ('b.py', 4)), 3),
372+
(0, 10, (('a.py', 2), ('b.py', 4)), 3),
373+
(0, 10, (('a.py', 2), ('b.py', 4)), 3),
374+
(1, 2, (('a.py', 5), ('b.py', 4)), 3),
371375
])
372376

373377
# No filter: just duplicate the snapshot
@@ -388,21 +392,21 @@ def test_filter_traces_domain(self):
388392
# exclude a.py of domain 1
389393
snapshot3 = snapshot.filter_traces((filter1,))
390394
self.assertEqual(snapshot3.traces._traces, [
391-
(0, 10, (('a.py', 2), ('b.py', 4))),
392-
(0, 10, (('a.py', 2), ('b.py', 4))),
393-
(0, 10, (('a.py', 2), ('b.py', 4))),
394-
(2, 66, (('b.py', 1),)),
395-
(3, 7, (('<unknown>', 0),)),
395+
(0, 10, (('a.py', 2), ('b.py', 4)), 3),
396+
(0, 10, (('a.py', 2), ('b.py', 4)), 3),
397+
(0, 10, (('a.py', 2), ('b.py', 4)), 3),
398+
(2, 66, (('b.py', 1),), 1),
399+
(3, 7, (('<unknown>', 0),), 1),
396400
])
397401

398402
# include domain 1
399403
snapshot3 = snapshot.filter_traces((filter1,))
400404
self.assertEqual(snapshot3.traces._traces, [
401-
(0, 10, (('a.py', 2), ('b.py', 4))),
402-
(0, 10, (('a.py', 2), ('b.py', 4))),
403-
(0, 10, (('a.py', 2), ('b.py', 4))),
404-
(2, 66, (('b.py', 1),)),
405-
(3, 7, (('<unknown>', 0),)),
405+
(0, 10, (('a.py', 2), ('b.py', 4)), 3),
406+
(0, 10, (('a.py', 2), ('b.py', 4)), 3),
407+
(0, 10, (('a.py', 2), ('b.py', 4)), 3),
408+
(2, 66, (('b.py', 1),), 1),
409+
(3, 7, (('<unknown>', 0),), 1),
406410
])
407411

408412
def test_filter_traces_domain_filter(self):
@@ -413,17 +417,17 @@ def test_filter_traces_domain_filter(self):
413417
# exclude domain 2
414418
snapshot3 = snapshot.filter_traces((filter1,))
415419
self.assertEqual(snapshot3.traces._traces, [
416-
(0, 10, (('a.py', 2), ('b.py', 4))),
417-
(0, 10, (('a.py', 2), ('b.py', 4))),
418-
(0, 10, (('a.py', 2), ('b.py', 4))),
419-
(1, 2, (('a.py', 5), ('b.py', 4))),
420-
(2, 66, (('b.py', 1),)),
420+
(0, 10, (('a.py', 2), ('b.py', 4)), 3),
421+
(0, 10, (('a.py', 2), ('b.py', 4)), 3),
422+
(0, 10, (('a.py', 2), ('b.py', 4)), 3),
423+
(1, 2, (('a.py', 5), ('b.py', 4)), 3),
424+
(2, 66, (('b.py', 1),), 1),
421425
])
422426

423427
# include domain 2
424428
snapshot3 = snapshot.filter_traces((filter2,))
425429
self.assertEqual(snapshot3.traces._traces, [
426-
(3, 7, (('<unknown>', 0),)),
430+
(3, 7, (('<unknown>', 0),), 1),
427431
])
428432

429433
def test_snapshot_group_by_line(self):

Lib/tracemalloc.py

+18-8
Original file line numberDiff line numberDiff line change
@@ -182,15 +182,20 @@ class Traceback(Sequence):
182182
Sequence of Frame instances sorted from the oldest frame
183183
to the most recent frame.
184184
"""
185-
__slots__ = ("_frames",)
185+
__slots__ = ("_frames", '_total_nframe')
186186

187-
def __init__(self, frames):
187+
def __init__(self, frames, total_nframe=None):
188188
Sequence.__init__(self)
189189
# frames is a tuple of frame tuples: see Frame constructor for the
190190
# format of a frame tuple; it is reversed, because _tracemalloc
191191
# returns frames sorted from most recent to oldest, but the
192192
# Python API expects oldest to most recent
193193
self._frames = tuple(reversed(frames))
194+
self._total_nframe = total_nframe
195+
196+
@property
197+
def total_nframe(self):
198+
return self._total_nframe
194199

195200
def __len__(self):
196201
return len(self._frames)
@@ -221,7 +226,12 @@ def __str__(self):
221226
return str(self[0])
222227

223228
def __repr__(self):
224-
return "<Traceback %r>" % (tuple(self),)
229+
s = "<Traceback %r" % tuple(self)
230+
if self._total_nframe is None:
231+
s += ">"
232+
else:
233+
s += f" total_nframe={self.total_nframe}>"
234+
return s
225235

226236
def format(self, limit=None, most_recent_first=False):
227237
lines = []
@@ -280,7 +290,7 @@ def size(self):
280290

281291
@property
282292
def traceback(self):
283-
return Traceback(self._trace[2])
293+
return Traceback(*self._trace[2:])
284294

285295
def __eq__(self, other):
286296
if not isinstance(other, Trace):
@@ -378,7 +388,7 @@ def _match_traceback(self, traceback):
378388
return self._match_frame(filename, lineno)
379389

380390
def _match(self, trace):
381-
domain, size, traceback = trace
391+
domain, size, traceback, total_nframe = trace
382392
res = self._match_traceback(traceback)
383393
if self.domain is not None:
384394
if self.inclusive:
@@ -398,7 +408,7 @@ def domain(self):
398408
return self._domain
399409

400410
def _match(self, trace):
401-
domain, size, traceback = trace
411+
domain, size, traceback, total_nframe = trace
402412
return (domain == self.domain) ^ (not self.inclusive)
403413

404414

@@ -475,7 +485,7 @@ def _group_by(self, key_type, cumulative):
475485
tracebacks = {}
476486
if not cumulative:
477487
for trace in self.traces._traces:
478-
domain, size, trace_traceback = trace
488+
domain, size, trace_traceback, total_nframe = trace
479489
try:
480490
traceback = tracebacks[trace_traceback]
481491
except KeyError:
@@ -496,7 +506,7 @@ def _group_by(self, key_type, cumulative):
496506
else:
497507
# cumulative statistics
498508
for trace in self.traces._traces:
499-
domain, size, trace_traceback = trace
509+
domain, size, trace_traceback, total_nframe = trace
500510
for frame in trace_traceback:
501511
try:
502512
traceback = tracebacks[frame]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add a ``total_nframe`` field to the traces collected by the tracemalloc module.
2+
This field indicates the original number of frames before it was truncated.

0 commit comments

Comments
 (0)