[{"data":1,"prerenderedAt":530},["ShallowReactive",2],{"page-\u002Fsystematic-debugging-performance-profiling\u002Fmemory-profiling-with-tracemalloc\u002Fcomparing-tracemalloc-snapshots-to-locate-growth\u002F":3},{"id":4,"title":5,"body":6,"description":496,"extension":497,"meta":498,"navigation":128,"path":526,"seo":527,"stem":528,"__hash__":529},"content\u002Fsystematic-debugging-performance-profiling\u002Fmemory-profiling-with-tracemalloc\u002Fcomparing-tracemalloc-snapshots-to-locate-growth\u002Findex.md","Comparing tracemalloc Snapshots to Locate Growth",{"type":7,"value":8,"toc":489},"minimark",[9,30,35,71,75,106,256,264,274,294,305,325,329,358,362,428,432,455,470,479,485],[10,11,12,13,17,18,21,22,25,26,29],"p",{},"You have two ",[14,15,16],"code",{},"tracemalloc"," snapshots taken at different points in a process's life and need to know precisely which lines retained more memory in the second. Printing each snapshot's top statistics side by side is unreadable. ",[14,19,20],{},"Snapshot.compare_to"," computes the per-line delta for you, returning ",[14,23,24],{},"StatisticDiff"," objects you can sort by ",[14,27,28],{},"size_diff"," to put the growing lines at the top.",[31,32,34],"h2",{"id":33},"prerequisites","Prerequisites",[36,37,38,58],"ul",{},[39,40,41,45,46,49,50,53,54,57],"li",{},[42,43,44],"strong",{},"Python 3.4+"," for ",[14,47,48],{},"compare_to","; the ",[14,51,52],{},"StatisticDiff.size_diff"," \u002F ",[14,55,56],{},"count_diff"," fields documented here are stable from 3.6.",[39,59,60,61,64,65,70],{},"Two snapshots taken with the same ",[14,62,63],{},"nframe",", captured per ",[66,67,69],"a",{"href":68},"\u002Fsystematic-debugging-performance-profiling\u002Fmemory-profiling-with-tracemalloc\u002F","memory profiling with tracemalloc",".",[31,72,74],{"id":73},"solution","Solution",[10,76,77,80,81,84,85,88,89,91,92,95,96,98,99,102,103,105],{},[14,78,79],{},"second.compare_to(first, key_type)"," diffs the two snapshots grouped by ",[14,82,83],{},"'lineno'"," or ",[14,86,87],{},"'traceback'"," and returns a list of ",[14,90,24],{},". Each entry exposes ",[14,93,94],{},"size"," (current bytes), ",[14,97,28],{}," (byte change), ",[14,100,101],{},"count"," (current blocks), and ",[14,104,56],{}," (block change).",[107,108,113],"pre",{"className":109,"code":110,"language":111,"meta":112,"style":112},"language-python shiki shiki-themes github-light github-dark","import tracemalloc\n\ntracemalloc.start(25)\n\nbefore = tracemalloc.take_snapshot()        # baseline\n\nbuckets = []\nfor i in range(20_000):\n    buckets.append(bytes(256))              # steady growth: 20k retained blocks\n\nafter = tracemalloc.take_snapshot()         # after the workload\n\n# Diff grouped by source line; default ordering is by ABSOLUTE size_diff.\ndiff = after.compare_to(before, \"lineno\")\n\n# Re-sort for pure growth so reclaimed lines do not surface at the top.\ndiff.sort(key=lambda stat: stat.size_diff, reverse=True)\n\nfor stat in diff[:5]:\n    print(\n        f\"{stat.size_diff\u002F1024:+9.1f} KiB  \"      # byte change (+ = growth)\n        f\"blocks {stat.count_diff:+7d}  \"          # block change\n        f\"{stat.traceback[0]}\"                     # the source line\n    )\n","python","",[14,114,115,123,130,136,141,147,152,158,164,170,175,181,186,192,198,203,209,215,220,226,232,238,244,250],{"__ignoreMap":112},[116,117,120],"span",{"class":118,"line":119},"line",1,[116,121,122],{},"import tracemalloc\n",[116,124,126],{"class":118,"line":125},2,[116,127,129],{"emptyLinePlaceholder":128},true,"\n",[116,131,133],{"class":118,"line":132},3,[116,134,135],{},"tracemalloc.start(25)\n",[116,137,139],{"class":118,"line":138},4,[116,140,129],{"emptyLinePlaceholder":128},[116,142,144],{"class":118,"line":143},5,[116,145,146],{},"before = tracemalloc.take_snapshot()        # baseline\n",[116,148,150],{"class":118,"line":149},6,[116,151,129],{"emptyLinePlaceholder":128},[116,153,155],{"class":118,"line":154},7,[116,156,157],{},"buckets = []\n",[116,159,161],{"class":118,"line":160},8,[116,162,163],{},"for i in range(20_000):\n",[116,165,167],{"class":118,"line":166},9,[116,168,169],{},"    buckets.append(bytes(256))              # steady growth: 20k retained blocks\n",[116,171,173],{"class":118,"line":172},10,[116,174,129],{"emptyLinePlaceholder":128},[116,176,178],{"class":118,"line":177},11,[116,179,180],{},"after = tracemalloc.take_snapshot()         # after the workload\n",[116,182,184],{"class":118,"line":183},12,[116,185,129],{"emptyLinePlaceholder":128},[116,187,189],{"class":118,"line":188},13,[116,190,191],{},"# Diff grouped by source line; default ordering is by ABSOLUTE size_diff.\n",[116,193,195],{"class":118,"line":194},14,[116,196,197],{},"diff = after.compare_to(before, \"lineno\")\n",[116,199,201],{"class":118,"line":200},15,[116,202,129],{"emptyLinePlaceholder":128},[116,204,206],{"class":118,"line":205},16,[116,207,208],{},"# Re-sort for pure growth so reclaimed lines do not surface at the top.\n",[116,210,212],{"class":118,"line":211},17,[116,213,214],{},"diff.sort(key=lambda stat: stat.size_diff, reverse=True)\n",[116,216,218],{"class":118,"line":217},18,[116,219,129],{"emptyLinePlaceholder":128},[116,221,223],{"class":118,"line":222},19,[116,224,225],{},"for stat in diff[:5]:\n",[116,227,229],{"class":118,"line":228},20,[116,230,231],{},"    print(\n",[116,233,235],{"class":118,"line":234},21,[116,236,237],{},"        f\"{stat.size_diff\u002F1024:+9.1f} KiB  \"      # byte change (+ = growth)\n",[116,239,241],{"class":118,"line":240},22,[116,242,243],{},"        f\"blocks {stat.count_diff:+7d}  \"          # block change\n",[116,245,247],{"class":118,"line":246},23,[116,248,249],{},"        f\"{stat.traceback[0]}\"                     # the source line\n",[116,251,253],{"class":118,"line":252},24,[116,254,255],{},"    )\n",[107,257,262],{"className":258,"code":260,"language":261,"meta":112},[259],"language-text"," +5000.0 KiB  blocks  +20000  compare.py:11\n    +1.4 KiB  blocks     +12  compare.py:8\n","text",[14,263,260],{"__ignoreMap":112},[10,265,266,267,270,271,273],{},"Line 11 — the ",[14,268,269],{},"buckets.append(bytes(256))"," call — grew by ~5 MiB and exactly 20,000 blocks. When the same line is reached from several places, switch the key to ",[14,272,87],{}," so each call path is a separate entry, then format the winner's full stack:",[107,275,277],{"className":109,"code":276,"language":111,"meta":112,"style":112},"diff_tb = after.compare_to(before, \"traceback\")\ndiff_tb.sort(key=lambda stat: stat.size_diff, reverse=True)\nprint(\"\\n\".join(diff_tb[0].traceback.format()))   # full path to the growing line\n",[14,278,279,284,289],{"__ignoreMap":112},[116,280,281],{"class":118,"line":119},[116,282,283],{},"diff_tb = after.compare_to(before, \"traceback\")\n",[116,285,286],{"class":118,"line":125},[116,287,288],{},"diff_tb.sort(key=lambda stat: stat.size_diff, reverse=True)\n",[116,290,291],{"class":118,"line":132},[116,292,293],{},"print(\"\\n\".join(diff_tb[0].traceback.format()))   # full path to the growing line\n",[10,295,296,297,301,302,304],{},"To see what was ",[298,299,300],"em",{},"reclaimed"," instead, sort ascending: the most negative ",[14,303,28],{}," entries are the lines that freed the most memory between snapshots — useful for confirming that a fix actually released the objects you expected.",[107,306,308],{"className":109,"code":307,"language":111,"meta":112,"style":112},"for stat in sorted(diff, key=lambda s: s.size_diff)[:3]:\n    if stat.size_diff \u003C 0:\n        print(f\"freed {(-stat.size_diff)\u002F1024:.1f} KiB at {stat.traceback[0]}\")\n",[14,309,310,315,320],{"__ignoreMap":112},[116,311,312],{"class":118,"line":119},[116,313,314],{},"for stat in sorted(diff, key=lambda s: s.size_diff)[:3]:\n",[116,316,317],{"class":118,"line":125},[116,318,319],{},"    if stat.size_diff \u003C 0:\n",[116,321,322],{"class":118,"line":132},[116,323,324],{},"        print(f\"freed {(-stat.size_diff)\u002F1024:.1f} KiB at {stat.traceback[0]}\")\n",[31,326,328],{"id":327},"why-this-works","Why this works",[10,330,331,333,334,337,338,341,342,344,345,348,349,351,352,354,355,357],{},[14,332,48],{}," keys every allocation group in both snapshots and computes ",[14,335,336],{},"size_diff = after.size - before.size"," per key, so a group present only in the later snapshot shows its full size as growth, and a freed group shows a negative diff. The default sort is by ",[298,339,340],{},"absolute"," ",[14,343,28],{},", which deliberately surfaces the biggest ",[298,346,347],{},"change"," in either direction; re-sorting by signed ",[14,350,28],{}," separates growth from reclamation. Matching ",[14,353,63],{}," between the two snapshots is required because the grouping key for ",[14,356,87],{}," is the frame tuple — mismatched depths produce keys that never line up.",[31,359,361],{"id":360},"edge-cases-and-failure-modes","Edge cases and failure modes",[36,363,364,375,387,408,418],{},[39,365,366,371,372,374],{},[42,367,368,369],{},"Mismatched ",[14,370,63],{},": comparing a 1-frame snapshot with a 25-frame snapshot under ",[14,373,87],{}," grouping yields meaningless diffs because the keys differ; capture both at the same depth.",[39,376,377,380,381,383,384,386],{},[42,378,379],{},"Absolute-sort surprise",": forgetting that the default sort is by absolute value lets a large ",[298,382,300],{}," line outrank a real leak; always re-sort by signed ",[14,385,28],{}," when hunting growth.",[39,388,389,397,398,400,401,403,404,407],{},[42,390,391,393,394,396],{},[14,392,56],{}," vs ",[14,395,28],{}," divergence",": growth in ",[14,399,28],{}," with flat ",[14,402,56],{}," means objects got ",[298,405,406],{},"bigger",", not more numerous — a different bug class than an unbounded collection.",[39,409,410,413,414,417],{},[42,411,412],{},"Cumulative parameter",": ",[14,415,416],{},"compare_to(old, key, cumulative=True)"," aggregates over every frame in the traceback rather than just the leaf, which can attribute growth to a high-level caller; use it deliberately, not by default.",[39,419,420,423,424,70],{},[42,421,422],{},"Snapshots taken too close together"," capture transient buffers that net to noise; bracket a meaningful unit of work, and for the full leak workflow follow ",[66,425,427],{"href":426},"\u002Fsystematic-debugging-performance-profiling\u002Fmemory-profiling-with-tracemalloc\u002Ffinding-memory-leaks-with-tracemalloc-snapshots\u002F","finding memory leaks with tracemalloc snapshots",[31,429,431],{"id":430},"frequently-asked-questions","Frequently Asked Questions",[10,433,434,437,438,440,441,443,444,443,446,448,449,451,452,454],{},[42,435,436],{},"What does Snapshot.compare_to return?","\nIt returns a list of ",[14,439,24],{}," objects, one per group, each carrying ",[14,442,94],{},", ",[14,445,28],{},[14,447,101],{},", and ",[14,450,56],{},". The list is sorted by absolute ",[14,453,28],{}," descending by default, so the lines with the largest memory change appear first.",[10,456,457,460,462,463,465,466,469],{},[42,458,459],{},"How do I sort the diff by growth instead of absolute change?",[14,461,48],{}," sorts by absolute ",[14,464,28],{},", which mixes growth and shrinkage at the top. To rank pure growth, re-sort the returned list with ",[14,467,468],{},"key=lambda s: s.size_diff, reverse=True"," so the largest positive deltas lead.",[10,471,472,475,476,478],{},[42,473,474],{},"Why are some size_diff values negative?","\nA negative ",[14,477,28],{}," means that line retained less memory in the second snapshot than the first, because objects were freed between the two captures. Positive values are growth; negatives are reclamation.",[10,480,481,482],{},"← Back to ",[66,483,484],{"href":68},"Memory Profiling with tracemalloc",[486,487,488],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":112,"searchDepth":125,"depth":125,"links":490},[491,492,493,494,495],{"id":33,"depth":125,"text":34},{"id":73,"depth":125,"text":74},{"id":327,"depth":125,"text":328},{"id":360,"depth":125,"text":361},{"id":430,"depth":125,"text":431},"Use Snapshot.compare_to to diff two tracemalloc snapshots, sort StatisticDiff entries by size_diff, and read which source lines or call paths are retaining new memory.","md",{"slug":499,"type":500,"breadcrumb":501,"datePublished":502,"dateModified":502,"faq":503,"howto":510},"comparing-tracemalloc-snapshots-to-locate-growth","long_tail","Comparing Snapshots","2026-06-18",[504,506,508],{"q":436,"a":505},"It returns a list of StatisticDiff objects, one per group, each carrying size, size_diff, count, and count_diff. The list is sorted by absolute size_diff descending by default, so the lines with the largest memory change appear first.",{"q":459,"a":507},"compare_to sorts by absolute size_diff, which mixes growth and shrinkage at the top. To rank pure growth, re-sort the returned list with key=lambda s: s.size_diff, reverse=True so the largest positive deltas lead.",{"q":474,"a":509},"A negative size_diff means that line retained less memory in the second snapshot than the first, because objects were freed between the two captures. Positive values are growth; negatives are reclamation.",{"name":511,"description":512,"steps":513},"How to compare two tracemalloc snapshots","Diff two snapshots and rank the source lines by retained-byte growth to locate where memory is accumulating.",[514,517,520,523],{"name":515,"text":516},"Capture two snapshots around the workload","Take a baseline snapshot, run the workload, then take a second snapshot.",{"name":518,"text":519},"Call compare_to with a grouping key","Run second.compare_to(first, 'lineno') or 'traceback' to get a list of StatisticDiff entries.",{"name":521,"text":522},"Sort by size_diff for pure growth","Re-sort the result by size_diff descending so positive growth leads instead of absolute change.",{"name":524,"text":525},"Read the top entries and their call paths","Inspect size_diff, count_diff, and traceback on the leading entries to attribute the growth.","\u002Fsystematic-debugging-performance-profiling\u002Fmemory-profiling-with-tracemalloc\u002Fcomparing-tracemalloc-snapshots-to-locate-growth",{"title":5,"description":496},"systematic-debugging-performance-profiling\u002Fmemory-profiling-with-tracemalloc\u002Fcomparing-tracemalloc-snapshots-to-locate-growth\u002Findex","rHekwuKT51W9HrbCcir9dnXC_IeeOtlvZa2kEWH-_Lo",1781793487406]