tracemalloc – 記憶體配置除錯
The tracemalloc module is a debug tool to trace memory blocks allocated by Python.
Source code: Lib/tracemalloc.py
透過 tracemalloc 將可以記錄下 Python 的記憶體區塊配置狀況,同時透過這些記錄來輸出有用的記憶體配置資訊,例如說執行記憶體配置的檔案以及行數、記憶體區塊大小等。也可以透過比對兩個不同的記憶體區塊快照,來找出記憶體洩漏的問題。
01. Quickstart Tutorial
我們透過 start()
開始記憶體除錯的功能:
1 2 |
>>> import tracemalloc >>> tracemalloc.start() |
結束時使用 stop()
結束除錯:
1 |
>>> tracemalloc.stop() |
想要記錄下當前的記憶體配置就使用 snapshot()
:
1 |
>>> snap = tracemalloc.take_snapshot() |
認識這三個基本 API 後,我們就可以開始應用 tracemalloc。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
# tm_snap_stats.py import tracemalloc import sys # Simulate memory allocate by creating bytes EMPTY_BYTES_SIZE = sys.getsizeof(b”) def allocate_bytes(size): bytes_len = (size – EMPTY_BYTES_SIZE) return b‘x’ * bytes_len # Start tracing tracemalloc.start() # Your program run here d = allocate_bytes(1024 * 1024) # Take memory snapshot snap = tracemalloc.take_snapshot() # Evaluate result stats = snap.statistics(‘lineno’) for stat in stats: print(stat) |
輸出如下:
1 2 3 |
$ ./python.exe tm_snap_stats.py tm_snap_stats.py:10: size=1024 KiB, count=1, average=1024 KiB $ |
我們可以來分析輸出,在 tm_snap_stats.py 的第 10 行,我們配置了總計大小為 1024 KiB 的記憶體區,共配置了 1 塊記憶體,記憶體區塊的平均大小為 1024 KiB。符合我們剛剛在程式中配置的大小 (allocate_bytes(1024 * 1024)
)。
但是為什麼不是顯示第 17 行 d = allocate_bytes(1024 * 1024)
而是第 10 行 return b'x' * bytes_len
呢?因為實際上做出記憶體配置動作的行數,是在呼叫 allocate_byte
之後第 10 行,而不是呼叫函式的 17 行。那如果這時候我們需要更進一步了解是從哪個地方呼叫產生記憶體配置的話要怎麼辦呢?
這時候我們就會需要參看 traceback:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
# tm_snap_traceback.py import tracemalloc import sys # Simulate memory allocate by creating bytes EMPTY_BYTES_SIZE = sys.getsizeof(b”) def allocate_bytes(size): bytes_len = (size – EMPTY_BYTES_SIZE) return b‘x’ * bytes_len # Storing 10 frames tracemalloc.start(10) # Your program run here d = allocate_bytes(1024 * 1024) # Take memory snapshot snap = tracemalloc.take_snapshot() # Evaluate Traceback top_stats = snap.statistics(‘traceback’) stats = top_stats[0] print(stats) for line in stats.traceback.format(): print(line) |
輸出如下:
1 2 3 4 5 6 |
$ ./python.exe tm_snap_traceback.py tm_snap_traceback.py:10: size=1024 KiB, count=1, average=1024 KiB File “tm_snap_traceback.py”, line 10 return b’x’ * bytes_len File “tm_snap_traceback.py”, line 17 d = allocate_bytes(1024 * 1024) |
程式碼第 14 行設定 tracemalloc 最多可以紀錄 10 層 traceback,第 23 行設定以 traceback group 起結果。輸出的部份可以看到記憶體配置 traceback 的結果,清楚的顯示是從第 17 行呼叫後進入第 10 行進行記憶體配置。
透過 tracemalloc,我們就能夠輕鬆的針對 Python 記憶體配置進行除錯。
02. HOW-TO Guides
Traceback 的用途?
traceback 會紀錄下 function call 的狀態,traceback 預設只會紀錄 1 層,可以透過 tracemalloc.start(nframe)
來設定 tracemalloc 要紀錄多少層 traceback。我們可以改寫前面的範例,將 tracemalloc.start(10)
改寫為 tracemalloc.start(1)
,在輸出的部份就能看到,tracemalloc 只有紀錄最近一次的 traceback,也就是 return b'x' * bytes_len
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
# tm_snap_traceback.py import tracemalloc import sys # Simulate memory allocate by creating bytes EMPTY_BYTES_SIZE = sys.getsizeof(b”) def allocate_bytes(size): bytes_len = (size – EMPTY_BYTES_SIZE) return b‘x’ * bytes_len # Storing 1 frames tracemalloc.start(1) # Your program run here d = allocate_bytes(1024 * 1024) # Take memory snapshot snap = tracemalloc.take_snapshot() # Evaluate Traceback top_stats = snap.statistics(‘traceback’) stats = top_stats[0] print(stats) for line in stats.traceback.format(): print(line) |
輸出如下:
1 2 3 4 |
$ ./python.exe tm_snap_traceback.py tm_snap_traceback.py:10: size=1024 KiB, count=1, average=1024 KiB File “tm_snap_traceback.py”, line 10 return b’x’ * bytes_len |
比較兩個不同的 snapshot
透過不同時期擷取的 snapchat,我們可以相互比較其中的差異:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
# tm_snap_compare_to.py import tracemalloc import sys # Simulate memory allocate by creating bytes EMPTY_BYTES_SIZE = sys.getsizeof(b”) def allocate_bytes(size): bytes_len = (size – EMPTY_BYTES_SIZE) return b‘x’ * bytes_len # Storing 10 frames tracemalloc.start(10) # Your program run here prev = tracemalloc.take_snapshot() data = [] for i in range(30): data.append(allocate_bytes(1024 * 1024)) snap = tracemalloc.take_snapshot() if not i % 10: stats = snap.compare_to(prev, ‘lineno’) print(‘[ Top 5 differences ]’) for stat in stats[:5]: print(stat) prev = snap |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
$ ./python.exe tm_snap_compare_to.py [ Top 5 differences ] tm_snap_compare_to.py:10: size=1024 KiB (+1024 KiB), count=1 (+1), average=1024 KiB /home/louielu/Python/cpython/Lib/tracemalloc.py:387: size=112 B (+112 B), count=2 (+2), average=56 B /home/louielu/Python/cpython/Lib/tracemalloc.py:524: size=72 B (+72 B), count=1 (+1), average=72 B tm_snap_compare_to.py:19: size=64 B (+64 B), count=1 (+1), average=64 B /home/grd/Python/cpython/Lib/tracemalloc.py:281: size=40 B (+40 B), count=1 (+1), average=40 B [ Top 5 differences ] tm_snap_compare_to.py:10: size=11.0 MiB (+10.0 MiB), count=11 (+10), average=1024 KiB /home/louielu/Python/cpython/Lib/tracemalloc.py:127: size=1064 B (+1064 B), count=7 (+7), average=152 B /home/louielu/Python/cpython/Lib/tracemalloc.py:207: size=928 B (+928 B), count=2 (+2), average=464 B /home/louielu/Python/cpython/Lib/tracemalloc.py:165: size=896 B (+896 B), count=2 (+2), average=448 B /home/louielu/Python/cpython/Lib/tracemalloc.py:462: size=848 B (+848 B), count=7 (+7), average=121 B [ Top 5 differences ] tm_snap_compare_to.py:10: size=21.0 MiB (+10.0 MiB), count=21 (+10), average=1024 KiB /home/louielu/Python/cpython/Lib/tracemalloc.py:113: size=2704 B (+2184 B), count=26 (+21), average=104 B /home/louielu/Python/cpython/Lib/tracemalloc.py:180: size=1872 B (+1584 B), count=26 (+22), average=72 B /home/louielu/Python/cpython/Lib/tracemalloc.py:127: size=2504 B (+1440 B), count=22 (+15), average=114 B /home/louielu/Python/cpython/Lib/tracemalloc.py:462: size=2192 B (+1344 B), count=28 (+21), average=78 B |
在 compare_to 還可以加上 cumulative
argument,如果 cumulutave
為 True
,則會累積計算所有 traceback 的結果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
# tm_snap_compare_to_cum.py import tracemalloc import sys # Simulate memory allocate by creating bytes EMPTY_BYTES_SIZE = sys.getsizeof(b”) def allocate_bytes(size): bytes_len = (size – EMPTY_BYTES_SIZE) return b‘x’ * bytes_len # Storing 10 frames tracemalloc.start(10) # Your program run here prev = tracemalloc.take_snapshot() data = [] for i in range(30): data.append(allocate_bytes(1024 * 1024)) snap = tracemalloc.take_snapshot() if not i % 10: stats = snap.compare_to(prev, ‘lineno’, True) print(‘[ Top 5 differences ]’) for stat in stats[:5]: print(stat) prev = snap |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
[ Top 5 differences ] tm_snap_compare_to_cum.py:20: size=1024 KiB (+1024 KiB), count=2 (+2), average=512 KiB tm_snap_compare_to_cum.py:10: size=1024 KiB (+1024 KiB), count=1 (+1), average=1024 KiB tm_snap_compare_to_cum.py:17: size=224 B (+224 B), count=4 (+4), average=56 B /home/louielu/Python/cpython/Lib/tracemalloc.py:524: size=224 B (+224 B), count=4 (+4), average=56 B /home/louielu/Python/cpython/Lib/tracemalloc.py:387: size=152 B (+152 B), count=3 (+3), average=51 B [ Top 5 differences ] tm_snap_compare_to_cum.py:20: size=11.0 MiB (+10.0 MiB), count=12 (+10), average=939 KiB tm_snap_compare_to_cum.py:10: size=11.0 MiB (+10.0 MiB), count=11 (+10), average=1024 KiB tm_snap_compare_to_cum.py:24: size=7612 B (+7612 B), count=42 (+42), average=181 B tm_snap_compare_to_cum.py:27: size=3720 B (+3720 B), count=8 (+8), average=465 B /home/louielu/Python/cpython/Lib/tracemalloc.py:508: size=3164 B (+3164 B), count=20 (+20), average=158 B [ Top 5 differences ] tm_snap_compare_to_cum.py:20: size=21.0 MiB (+10.0 MiB), count=22 (+10), average=977 KiB tm_snap_compare_to_cum.py:10: size=21.0 MiB (+10.0 MiB), count=21 (+10), average=1024 KiB tm_snap_compare_to_cum.py:24: size=17.1 KiB (+9896 B), count=169 (+127), average=104 B /home/louielu/Python/cpython/Lib/tracemalloc.py:508: size=7256 B (+4092 B), count=86 (+66), average=84 B /home/louielu/Python/cpython/Lib/tracemalloc.py:482: size=5328 B (+3912 B), count=72 (+57), average=74 B |
透過 Filter 來篩選輸出結果
在 snapshot 上,我們可以透過 filter_traces
來過濾資訊,透過tracemalloc.Filter
來新增過濾規則:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
# tm_snap_filter.py import tracemalloc import sys # Simulate memory allocate by creating bytes EMPTY_BYTES_SIZE = sys.getsizeof(b”) def allocate_bytes(size): bytes_len = (size – EMPTY_BYTES_SIZE) return b‘x’ * bytes_len # Storing 10 frames tracemalloc.start(10) d = allocate_bytes(1024 * 1024) import abc import idlelib import enum snap = tracemalloc.take_snapshot() snap = snap.filter_traces(( tracemalloc.Filter(False, ‘<frozen*’), )) top_stats = snap.statistics(‘lineno’) for stat in top_stats[:5]: print(stat) |
1 2 |
$ ./python.exe tm_snap_filter.py tm_snap_filter.py:10: size=1024 KiB, count=1, average=1024 KiB |
規則可以使用 UNIX 在檔名的萬用字元來配對:
1 2 3 |
tracemalloc.Filter(True, ‘test_*’) # Only match filename start with “test_” tracemalloc.Filter(False, tracemalloc.__file__) # Excludes tracemalloc module tracemalloc.Filter(False, ‘<unknown>’) # Excludes empty tracebacks |
03. Discussions
tracemalloc 是在 2013 年由 Victor Stinner 提出 PEP 454 後納入 Python 的標準函式庫之中。使用的基礎來自於 PEP 445 提供的記憶體配置 API,透過這些 API,tracemalloc 可以精準的獲得 Python 的記憶體配置狀況。
之所以會提出 PEP45 以及 PEP445 的原因在於,如果透過舊有記憶體檢測的工具如, Valgrind,在 CPython 除錯上並沒有太大的用途,原因是 CPython 的記憶體配置通常都只使用特定的函式 PyMem_Malloc()
來配置,這讓 Valgrind 在輸出結果時通常只會輸出沒有意義的東西。同時 CPython 內還有針對小型物件進行記憶體配置的 pymalloc
,而 Valgrind 這種工具也沒有辦法針對這個部份提供資訊。
當時的所能使用的記憶體檢測套件如 Heapy
、Pympler
、Meliae
等透過 garbage collector 來追蹤物件,透過 sys.getsize
獲得記憶體使用量並且以物件類型來分類結果。這些工具提供了一點用處,但是在一些事情上還是顯得力不從心,例如對於記憶體洩漏的問題,這些工具只在當洩漏出現在同一個型態上時比較有用處,同時一些內建型態如 str 或是 int 在除錯時並沒有方便。同時也很難鎖定是哪個地方出了問題。
另外一個問題是找出 reference cycles。當時的工具僅能夠在小地方使用,遇到大型程式時,會出現無法接受的 overhead。
tracemalloc 的出現,就是為了解決這些問題。其主要提供這些功能:
- 提供物件被建立時的 traceback。
- 針對記憶體配置區塊提供統計功能,能夠統計:總大小、數量以及平均大小等。
- 計算兩個 snapshot 之間的差異來偵測記憶體洩漏的問題。
tracemalloc API 參考了 faulthandler 套件,提供類似 enable() / start(), disable() / stop() 以及 is_enabled() / is_tracing() 的函式來操作。
04. References
- 27.7. tracemalloc — Trace memory allocations
- PEP 445 – Add new APIs to customize Python memory allocators
- PEP 454 – Add a new tracemalloc module to trace Python memory allocations
- Diagnosing and Fixing Memory Leaks in Python
Leave a Reply