tracemalloc — 跟蹤記憶體分配

在 3.4 版本加入。

原始碼: Lib/tracemalloc.py


tracemalloc 模組是一個除錯工具,用於跟蹤 Python 分配的記憶體塊。它提供以下資訊:

  • 物件分配時的追溯

  • 按檔名和行號統計已分配記憶體塊:總大小、數量和已分配記憶體塊的平均大小

  • 計算兩個快照之間的差異以檢測記憶體洩漏

為了跟蹤 Python 分配的大多數記憶體塊,應該儘早啟動該模組,方法是將 PYTHONTRACEMALLOC 環境變數設定為 1,或者使用 -X tracemalloc 命令列選項。 tracemalloc.start() 函式可以在執行時呼叫以開始跟蹤 Python 記憶體分配。

預設情況下,已分配記憶體塊的跟蹤僅儲存最新幀(1 幀)。要在啟動時儲存 25 幀:將 PYTHONTRACEMALLOC 環境變數設定為 25,或使用 -X tracemalloc=25 命令列選項。

示例

顯示前 10 個

顯示分配最多記憶體的 10 個檔案

import tracemalloc

tracemalloc.start()

# ... run your application ...

snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')

print("[ Top 10 ]")
for stat in top_stats[:10]:
    print(stat)

Python 測試套件的輸出示例

[ Top 10 ]
<frozen importlib._bootstrap>:716: size=4855 KiB, count=39328, average=126 B
<frozen importlib._bootstrap>:284: size=521 KiB, count=3199, average=167 B
/usr/lib/python3.4/collections/__init__.py:368: size=244 KiB, count=2315, average=108 B
/usr/lib/python3.4/unittest/case.py:381: size=185 KiB, count=779, average=243 B
/usr/lib/python3.4/unittest/case.py:402: size=154 KiB, count=378, average=416 B
/usr/lib/python3.4/abc.py:133: size=88.7 KiB, count=347, average=262 B
<frozen importlib._bootstrap>:1446: size=70.4 KiB, count=911, average=79 B
<frozen importlib._bootstrap>:1454: size=52.0 KiB, count=25, average=2131 B
<string>:5: size=49.7 KiB, count=148, average=344 B
/usr/lib/python3.4/sysconfig.py:411: size=48.0 KiB, count=1, average=48.0 KiB

我們可以看到 Python 從模組載入了 4855 KiB 資料(位元組碼和常量),並且 collections 模組分配了 244 KiB 來構建 namedtuple 型別。

有關更多選項,請參閱 Snapshot.statistics()

計算差異

拍攝兩個快照並顯示差異

import tracemalloc
tracemalloc.start()
# ... start your application ...

snapshot1 = tracemalloc.take_snapshot()
# ... call the function leaking memory ...
snapshot2 = tracemalloc.take_snapshot()

top_stats = snapshot2.compare_to(snapshot1, 'lineno')

print("[ Top 10 differences ]")
for stat in top_stats[:10]:
    print(stat)

執行 Python 測試套件某些測試之前/之後的輸出示例

[ Top 10 differences ]
<frozen importlib._bootstrap>:716: size=8173 KiB (+4428 KiB), count=71332 (+39369), average=117 B
/usr/lib/python3.4/linecache.py:127: size=940 KiB (+940 KiB), count=8106 (+8106), average=119 B
/usr/lib/python3.4/unittest/case.py:571: size=298 KiB (+298 KiB), count=589 (+589), average=519 B
<frozen importlib._bootstrap>:284: size=1005 KiB (+166 KiB), count=7423 (+1526), average=139 B
/usr/lib/python3.4/mimetypes.py:217: size=112 KiB (+112 KiB), count=1334 (+1334), average=86 B
/usr/lib/python3.4/http/server.py:848: size=96.0 KiB (+96.0 KiB), count=1 (+1), average=96.0 KiB
/usr/lib/python3.4/inspect.py:1465: size=83.5 KiB (+83.5 KiB), count=109 (+109), average=784 B
/usr/lib/python3.4/unittest/mock.py:491: size=77.7 KiB (+77.7 KiB), count=143 (+143), average=557 B
/usr/lib/python3.4/urllib/parse.py:476: size=71.8 KiB (+71.8 KiB), count=969 (+969), average=76 B
/usr/lib/python3.4/contextlib.py:38: size=67.2 KiB (+67.2 KiB), count=126 (+126), average=546 B

我們可以看到 Python 已載入 8173 KiB 的模組資料(位元組碼和常量),這比上次拍攝快照時測試之前載入的 4428 KiB 要多。同樣,linecache 模組已快取 940 KiB 的 Python 原始碼以格式化追溯,所有這些都發生在上次快照之後。

如果系統記憶體不足,可以使用 Snapshot.dump() 方法將快照寫入磁碟,以便離線分析快照。然後使用 Snapshot.load() 方法重新載入快照。

獲取記憶體塊的追溯

顯示最大記憶體塊追溯的程式碼

import tracemalloc

# Store 25 frames
tracemalloc.start(25)

# ... run your application ...

snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('traceback')

# pick the biggest memory block
stat = top_stats[0]
print("%s memory blocks: %.1f KiB" % (stat.count, stat.size / 1024))
for line in stat.traceback.format():
    print(line)

Python 測試套件的輸出示例(追溯限制為 25 幀)

903 memory blocks: 870.1 KiB
  File "<frozen importlib._bootstrap>", line 716
  File "<frozen importlib._bootstrap>", line 1036
  File "<frozen importlib._bootstrap>", line 934
  File "<frozen importlib._bootstrap>", line 1068
  File "<frozen importlib._bootstrap>", line 619
  File "<frozen importlib._bootstrap>", line 1581
  File "<frozen importlib._bootstrap>", line 1614
  File "/usr/lib/python3.4/doctest.py", line 101
    import pdb
  File "<frozen importlib._bootstrap>", line 284
  File "<frozen importlib._bootstrap>", line 938
  File "<frozen importlib._bootstrap>", line 1068
  File "<frozen importlib._bootstrap>", line 619
  File "<frozen importlib._bootstrap>", line 1581
  File "<frozen importlib._bootstrap>", line 1614
  File "/usr/lib/python3.4/test/support/__init__.py", line 1728
    import doctest
  File "/usr/lib/python3.4/test/test_pickletools.py", line 21
    support.run_doctest(pickletools)
  File "/usr/lib/python3.4/test/regrtest.py", line 1276
    test_runner()
  File "/usr/lib/python3.4/test/regrtest.py", line 976
    display_failure=not verbose)
  File "/usr/lib/python3.4/test/regrtest.py", line 761
    match_tests=ns.match_tests)
  File "/usr/lib/python3.4/test/regrtest.py", line 1563
    main()
  File "/usr/lib/python3.4/test/__main__.py", line 3
    regrtest.main_in_temp_cwd()
  File "/usr/lib/python3.4/runpy.py", line 73
    exec(code, run_globals)
  File "/usr/lib/python3.4/runpy.py", line 160
    "__main__", fname, loader, pkg_name)

我們可以看到,大部分記憶體分配發生在 importlib 模組中,用於從模組載入資料(位元組碼和常量):870.1 KiB。追溯顯示了 importlib 最近載入資料的位置:doctest 模組的 import pdb 行。如果載入新模組,追溯可能會發生變化。

漂亮的前 10 個

顯示分配最多記憶體的 10 行程式碼,並進行漂亮輸出,忽略 <frozen importlib._bootstrap><unknown> 檔案

import linecache
import os
import tracemalloc

def display_top(snapshot, key_type='lineno', limit=10):
    snapshot = snapshot.filter_traces((
        tracemalloc.Filter(False, "<frozen importlib._bootstrap>"),
        tracemalloc.Filter(False, "<unknown>"),
    ))
    top_stats = snapshot.statistics(key_type)

    print("Top %s lines" % limit)
    for index, stat in enumerate(top_stats[:limit], 1):
        frame = stat.traceback[0]
        print("#%s: %s:%s: %.1f KiB"
              % (index, frame.filename, frame.lineno, stat.size / 1024))
        line = linecache.getline(frame.filename, frame.lineno).strip()
        if line:
            print('    %s' % line)

    other = top_stats[limit:]
    if other:
        size = sum(stat.size for stat in other)
        print("%s other: %.1f KiB" % (len(other), size / 1024))
    total = sum(stat.size for stat in top_stats)
    print("Total allocated size: %.1f KiB" % (total / 1024))

tracemalloc.start()

# ... run your application ...

snapshot = tracemalloc.take_snapshot()
display_top(snapshot)

Python 測試套件的輸出示例

Top 10 lines
#1: Lib/base64.py:414: 419.8 KiB
    _b85chars2 = [(a + b) for a in _b85chars for b in _b85chars]
#2: Lib/base64.py:306: 419.8 KiB
    _a85chars2 = [(a + b) for a in _a85chars for b in _a85chars]
#3: collections/__init__.py:368: 293.6 KiB
    exec(class_definition, namespace)
#4: Lib/abc.py:133: 115.2 KiB
    cls = super().__new__(mcls, name, bases, namespace)
#5: unittest/case.py:574: 103.1 KiB
    testMethod()
#6: Lib/linecache.py:127: 95.4 KiB
    lines = fp.readlines()
#7: urllib/parse.py:476: 71.8 KiB
    for a in _hexdig for b in _hexdig}
#8: <string>:5: 62.0 KiB
#9: Lib/_weakrefset.py:37: 60.0 KiB
    self.data = set()
#10: Lib/base64.py:142: 59.8 KiB
    _b32tab2 = [a + b for a in _b32tab for b in _b32tab]
6220 other: 3602.8 KiB
Total allocated size: 5303.1 KiB

有關更多選項,請參閱 Snapshot.statistics()

記錄所有跟蹤記憶體塊的當前大小和峰值大小

以下程式碼透過建立這些數字的列表,低效地計算兩個和,例如 0 + 1 + 2 + ...。此列表會暫時消耗大量記憶體。我們可以使用 get_traced_memory()reset_peak() 來觀察計算和之後的小記憶體使用量以及計算期間的峰值記憶體使用量

import tracemalloc

tracemalloc.start()

# Example code: compute a sum with a large temporary list
large_sum = sum(list(range(100000)))

first_size, first_peak = tracemalloc.get_traced_memory()

tracemalloc.reset_peak()

# Example code: compute a sum with a small temporary list
small_sum = sum(list(range(1000)))

second_size, second_peak = tracemalloc.get_traced_memory()

print(f"{first_size=}, {first_peak=}")
print(f"{second_size=}, {second_peak=}")

輸出

first_size=664, first_peak=3592984
second_size=804, second_peak=29704

使用 reset_peak() 確保我們能夠準確記錄 small_sum 計算期間的峰值,即使它遠小於自 start() 呼叫以來記憶體塊的總體峰值大小。如果沒有呼叫 reset_peak()second_peak 仍然是 large_sum 計算的峰值(即等於 first_peak)。在這種情況下,兩個峰值都遠高於最終的記憶體使用量,這表明我們可以進行最佳化(透過刪除對 list 的不必要呼叫,並編寫 sum(range(...)))。

API

函式

tracemalloc.clear_traces()

清除 Python 分配的記憶體塊的跟蹤。

另請參閱 stop()

tracemalloc.get_object_traceback(obj)

獲取 Python 物件 obj 被分配時的追溯。返回 Traceback 例項,如果 tracemalloc 模組未跟蹤記憶體分配或未跟蹤物件的分配,則返回 None

另請參閱 gc.get_referrers()sys.getsizeof() 函式。

tracemalloc.get_traceback_limit()

獲取跟蹤的追溯中儲存的最大幀數。

tracemalloc 模組必須正在跟蹤記憶體分配才能獲取限制,否則會引發異常。

限制由 start() 函式設定。

tracemalloc.get_traced_memory()

獲取 tracemalloc 模組跟蹤的記憶體塊的當前大小和峰值大小,以元組形式返回:(current: int, peak: int)

tracemalloc.reset_peak()

tracemalloc 模組跟蹤的記憶體塊的峰值大小設定為當前大小。

如果 tracemalloc 模組未跟蹤記憶體分配,則不執行任何操作。

此函式僅修改記錄的峰值大小,不像 clear_traces() 那樣修改或清除任何跟蹤。take_snapshot() 呼叫之前拍攝的快照可以與呼叫之後拍攝的快照進行有意義的比較。

另請參閱 get_traced_memory()

在 3.9 版本中新增。

tracemalloc.get_tracemalloc_memory()

獲取 tracemalloc 模組用於儲存記憶體塊跟蹤的記憶體使用量(以位元組為單位)。返回一個 int

tracemalloc.is_tracing()

如果 tracemalloc 模組正在跟蹤 Python 記憶體分配,則為 True,否則為 False

另請參閱 start()stop() 函式。

tracemalloc.start(nframe: int = 1)

開始跟蹤 Python 記憶體分配:在 Python 記憶體分配器上安裝鉤子。收集到的跟蹤的追溯將被限制為 nframe 幀。預設情況下,記憶體塊的跟蹤只儲存最近的幀:限制為 1nframe 必須大於或等於 1

您仍然可以透過檢視 Traceback.total_nframe 屬性來讀取組成追溯的原始總幀數。

儲存超過 1 幀僅在按 'traceback' 分組計算統計資訊或計算累積統計資訊時有用:請參閱 Snapshot.compare_to()Snapshot.statistics() 方法。

儲存更多幀會增加 tracemalloc 模組的記憶體和 CPU 開銷。使用 get_tracemalloc_memory() 函式來測量 tracemalloc 模組使用的記憶體量。

可以使用 PYTHONTRACEMALLOC 環境變數(PYTHONTRACEMALLOC=NFRAME)和 -X tracemalloc=NFRAME 命令列選項在啟動時開始跟蹤。

另請參閱 stop()is_tracing()get_traceback_limit() 函式。

tracemalloc.stop()

停止跟蹤 Python 記憶體分配:解除安裝 Python 記憶體分配器上的鉤子。還會清除所有以前收集的 Python 分配的記憶體塊的跟蹤。

在清除跟蹤之前,呼叫 take_snapshot() 函式來拍攝跟蹤快照。

另請參閱 start()is_tracing()clear_traces() 函式。

tracemalloc.take_snapshot()

拍攝 Python 分配的記憶體塊的跟蹤快照。返回一個新的 Snapshot 例項。

快照不包括在 tracemalloc 模組開始跟蹤記憶體分配之前分配的記憶體塊。

跟蹤的追溯限制為 get_traceback_limit() 幀。使用 start() 函式的 nframe 引數來儲存更多幀。

tracemalloc 模組必須正在跟蹤記憶體分配才能拍攝快照,請參閱 start() 函式。

另請參閱 get_object_traceback() 函式。

DomainFilter

class tracemalloc.DomainFilter(inclusive: bool, domain: int)

按記憶體塊的地址空間(域)過濾跟蹤。

在 3.6 版本加入。

inclusive

如果 inclusiveTrue (包含),則匹配在地址空間 domain 中分配的記憶體塊。

如果 inclusiveFalse (排除),則匹配未在地址空間 domain 中分配的記憶體塊。

domain

記憶體塊的地址空間(int)。只讀屬性。

Filter

class tracemalloc.Filter(inclusive: bool, filename_pattern: str, lineno: int = None, all_frames: bool = False, domain: int = None)

對記憶體塊的跟蹤進行過濾。

有關 filename_pattern 的語法,請參閱 fnmatch.fnmatch() 函式。'.pyc' 副檔名將替換為 '.py'

示例:

  • Filter(True, subprocess.__file__) 只包括 subprocess 模組的跟蹤

  • Filter(False, tracemalloc.__file__) 排除 tracemalloc 模組的跟蹤

  • Filter(False, "<unknown>") 排除空追溯

3.5 版本中更改: '.pyo' 副檔名不再替換為 '.py'

3.6 版本中更改: 添加了 domain 屬性。

domain

記憶體塊的地址空間(intNone)。

tracemalloc 使用域 0 來跟蹤 Python 進行的記憶體分配。C 擴充套件可以使用其他域來跟蹤其他資源。

inclusive

如果 inclusiveTrue (包含),則只匹配在檔名與 filename_pattern 匹配的檔案的 lineno 行號處分配的記憶體塊。

如果 inclusiveFalse (排除),則忽略在檔名與 filename_pattern 匹配的檔案的 lineno 行號處分配的記憶體塊。

lineno

過濾器的行號(int)。如果 linenoNone,則過濾器匹配任何行號。

filename_pattern

過濾器的檔名模式(str)。只讀屬性。

all_frames

如果 all_framesTrue,則檢查追溯的所有幀。如果 all_framesFalse,則只檢查最近的幀。

如果追溯限制為 1,則此屬性無效。請參閱 get_traceback_limit() 函式和 Snapshot.traceback_limit 屬性。

Frame

class tracemalloc.Frame

追溯中的幀。

Traceback 類是 Frame 例項的序列。

filename

檔名(str)。

lineno

行號(int)。

Snapshot

class tracemalloc.Snapshot

Python 分配的記憶體塊的跟蹤快照。

take_snapshot() 函式建立一個快照例項。

compare_to(old_snapshot: Snapshot, key_type: str, cumulative: bool = False)

與舊快照計算差異。以 StatisticDiff 例項的排序列表形式獲取統計資訊,按 key_type 分組。

有關 key_typecumulative 引數,請參閱 Snapshot.statistics() 方法。

結果按從大到小排序:StatisticDiff.size_diff 的絕對值、StatisticDiff.sizeStatisticDiff.count_diff 的絕對值、Statistic.count,然後按 StatisticDiff.traceback 排序。

dump(filename)

將快照寫入檔案。

使用 load() 重新載入快照。

filter_traces(filters)

建立一個新的 Snapshot 例項,其中包含過濾後的 traces 序列,filtersDomainFilterFilter 例項的列表。如果 filters 是一個空列表,則返回一個包含跟蹤副本的新 Snapshot 例項。

所有包含性過濾器同時應用,如果沒有包含性過濾器匹配跟蹤,則忽略該跟蹤。如果至少有一個排他性過濾器匹配跟蹤,則忽略該跟蹤。

3.6 版本中更改: 現在 filters 也接受 DomainFilter 例項。

classmethod load(filename)

從檔案中載入快照。

另請參閱 dump()

statistics(key_type: str, cumulative: bool = False)

獲取以 Statistic 例項的排序列表形式的統計資訊,按 key_type 分組

key_type

description

'filename'

filename

'lineno'

檔名和行號

'traceback'

traceback

如果 cumulativeTrue,則累加跟蹤的追溯中所有幀(而不僅僅是最近的幀)的記憶體塊大小和數量。累積模式只能與 key_type 等於 'filename''lineno' 一起使用。

結果按從大到小排序:Statistic.sizeStatistic.count,然後按 Statistic.traceback 排序。

traceback_limit

traces 的追溯中儲存的最大幀數:快照拍攝時 get_traceback_limit() 的結果。

traces

所有 Python 分配的記憶體塊的跟蹤:Trace 例項序列。

序列的順序未定義。使用 Snapshot.statistics() 方法獲取統計資訊的排序列表。

Statistic

class tracemalloc.Statistic

記憶體分配統計。

Snapshot.statistics() 返回一個 Statistic 例項列表。

另請參閱 StatisticDiff 類。

count

記憶體塊的數量(int)。

size

記憶體塊的總大小(以位元組為單位)(int)。

traceback

記憶體塊分配時的追溯,Traceback 例項。

StatisticDiff

class tracemalloc.StatisticDiff

舊快照和新 Snapshot 例項之間記憶體分配的統計差異。

Snapshot.compare_to() 返回一個 StatisticDiff 例項列表。另請參閱 Statistic 類。

count

新快照中的記憶體塊數量(int):如果新快照中記憶體塊已釋放,則為 0

count_diff

舊快照和新快照之間記憶體塊數量的差異(int):如果新快照中記憶體塊已分配,則為 0

size

新快照中記憶體塊的總大小(以位元組為單位)(int):如果新快照中記憶體塊已釋放,則為 0

size_diff

舊快照和新快照之間記憶體塊總大小(以位元組為單位)的差異(int):如果新快照中記憶體塊已分配,則為 0

traceback

記憶體塊分配時的追溯,Traceback 例項。

Trace

class tracemalloc.Trace

記憶體塊的跟蹤。

Snapshot.traces 屬性是 Trace 例項的序列。

3.6 版本中更改: 添加了 domain 屬性。

domain

記憶體塊的地址空間(int)。只讀屬性。

tracemalloc 使用域 0 來跟蹤 Python 進行的記憶體分配。C 擴充套件可以使用其他域來跟蹤其他資源。

size

記憶體塊的大小(以位元組為單位)(int)。

traceback

記憶體塊分配時的追溯,Traceback 例項。

Traceback

class tracemalloc.Traceback

Frame 例項的序列,按從最舊幀到最新幀的順序排序。

追溯至少包含 1 幀。如果 tracemalloc 模組未能獲取幀,則使用檔名 "<unknown>" 和行號 0

拍攝快照時,跟蹤的追溯限制為 get_traceback_limit() 幀。請參閱 take_snapshot() 函式。追溯的原始幀數儲存在 Traceback.total_nframe 屬性中。這允許判斷追溯是否因追溯限制而被截斷。

Trace.traceback 屬性是 Traceback 例項的一個例項。

3.7 版本中更改: 幀現在按從最舊到最新的順序排序,而不是從最新到最舊。

total_nframe

截斷前組成追溯的總幀數。如果資訊不可用,此屬性可以設定為 None

3.9 版本中更改: 添加了 Traceback.total_nframe 屬性。

format(limit=None, most_recent_first=False)

將追溯格式化為行列表。使用 linecache 模組從原始碼中檢索行。如果設定了 limit,如果 limit 為正,則格式化 limit 個最近的幀。否則,格式化 abs(limit) 個最舊的幀。如果 most_recent_firstTrue,則格式化幀的順序反轉,返回最新的幀而不是最後的幀。

類似於 traceback.format_tb() 函式,不同之處在於 format() 不包含換行符。

示例

print("Traceback (most recent call first):")
for line in traceback:
    print(line)

輸出

Traceback (most recent call first):
  File "test.py", line 9
    obj = Object()
  File "test.py", line 12
    tb = tracemalloc.get_object_traceback(f())