どんなプログラミング言語でもメモリリークの追跡は難しいものです。特に、他人の作ったライブラリでメモリリークがある場合、原因を切り分けるのは非常に面倒な仕事になります。
Pythonにはtracemallocという組み込みモジュールがあり、これがメモリリークの調査に便利だったのでまとめてみます。
メモリ消費量トップ10を出す
tracemallocのマニュアルにも書いてあるのですが、下記のようにするとメモリ消費量のトップ10が出せます。
import tracemalloc
tracemalloc.start()
# ... メモリリークしてるっぽい処理 ...
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
print("[ Top 10 ]")
for stat in top_stats[:10]:
print(stat)
これを実行すると、下記のような結果が得られます。
[ Top 10 ]
/usr/lib/python3.7/encodings/utf_8.py:16: size=11.1 MiB, count=307696, average=38 B
/home/pi/.local/lib/python3.7/site-packages/bleak/backends/bluezdbus/utils.py:45: size=4466 KiB, count=15042, average=304 B
/home/pi/.local/lib/python3.7/site-packages/bleak/backends/bluezdbus/utils.py:38: size=3617 KiB, count=53719, average=69 B
/home/pi/.local/lib/python3.7/site-packages/bleak/backends/bluezdbus/scanner.py:276: size=2592 KiB, count=37355, average=71 B
/home/pi/.local/lib/python3.7/site-packages/bleak/backends/scanner.py:35: size=2318 KiB, count=35176, average=67 B
(以下略)
どうやらencodings/utf_8.py
の16行目で確保しているメモリが解放されていないようです。
バックトレースつきで表示する
上記の内容だけで原因がわかればいいのですが、ライブラリの最内側の処理だけ示されても問題の特定が難しいことがあります。バックトレースつきで表示すると原因がわかりやすくなります。
import tracemalloc
tracemalloc.start(15)
# ... メモリリークしてるっぽい処理 ...
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('traceback')
print("[ Top 10 ]")
for stat in top_stats[:10]:
print(stat)
for line in stat.traceback.format():
print(line)
print("=====")
実行結果は次のようになります。
[ Top 10 ]
/home/pi/.local/lib/python3.7/site-packages/dbus_next/aio/message_bus.py:365: size=5133 KiB, count=153575, average=34 B
File "/home/pi/.local/lib/python3.7/site-packages/dbus_next/aio/message_bus.py", line 365
if self._unmarshaller.unmarshall():
(中略)
File "/home/pi/.local/lib/python3.7/site-packages/dbus_next/_private/unmarshaller.py", line 172
return decode(str_mem_slice)
File "/usr/lib/python3.7/encodings/utf_8.py", line 16
return codecs.utf_8_decode(input, errors, True)
=====
(以下略)
呼び出し元が表示されるので、原因特定の助けになりますね。
ちなみにtracemalloc.start()
の第一引数がバックトレースの段数で、これを増やせばもっと上まで追いかけることができます。
スナップショット2回の差分を表示する
特定の期間でのメモリ消費量の差分を表示したい場合は次のように記述できます。
import tracemalloc
tracemalloc.start()
# ... アプリケーション開始 ...
snapshot1 = tracemalloc.take_snapshot()
# ... メモリリークしてそうな関数呼び出し ...
snapshot2 = tracemalloc.take_snapshot()
top_stats = snapshot2.compare_to(snapshot1, 'lineno')
print("[ Top 10 differences ]")
for stat in top_stats[:10]:
print(stat)
この場合も Snapshot.compare_to()
の第二引数を'lineno'
から 'traceback'
に変更することでバックトレース表示できます。