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 行上。 如果載入新模組,則回溯可能會更改。

漂亮的 top

用於顯示分配最多記憶體的 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() 那樣修改或清除任何跟蹤資訊。在呼叫 reset_peak() 之前使用 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* 幀。預設情況下,記憶體塊的跟蹤僅儲存最近的幀:限制為 1。 *nframe* 必須大於或等於 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)。只讀屬性。

過濾器

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 屬性。

class tracemalloc.Frame

回溯的幀。

Traceback 類是 Frame 例項的序列。

filename

檔名 (str)。

lineno

行號 (int)。

快照

class tracemalloc.Snapshot

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

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

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

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

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

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

dump(filename)

將快照寫入檔案。

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

filter_traces(filters)

建立一個新的 Snapshot 例項,其中包含經過篩選的 traces 序列。filters 是一個 DomainFilterFilter 例項的列表。如果 filters 是一個空列表,則返回一個新的 Snapshot 例項,其中包含 traces 的副本。

所有包含性過濾器會一次性應用,如果某個 trace 不符合任何包含性過濾器,則會被忽略。如果至少有一個排除性過濾器匹配某個 trace,則該 trace 會被忽略。

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

classmethod load(filename)

從檔案中載入快照。

另請參閱 dump()

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

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

key_type

描述

'filename'

filename

'lineno'

檔名和行號

'traceback'

traceback

如果 cumulativeTrue,則累加 trace 的回溯中所有幀的記憶體塊大小和計數,而不僅僅是最新的幀。累積模式只能與 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.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 例項。

回溯

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())