使用 tracemalloc 来跟踪分析 Python 程序的内存分配

tracemalloc 是Python3.4以后,新增加的功能。

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 最近加载的数据:在 import pdb 直线 doctest 模块。如果加载了新模块,则回溯可能会更改。

漂亮的陀螺

显示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 已分配。返回A Traceback 实例,或 None 如果 tracemalloc 模块未跟踪内存分配或未跟踪对象的分配。

也见 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() 在呼叫之前 reset_peak() 可以有意义地与通话后拍摄的快照进行比较。

也见 get_traced_memory() .

3.9 新版功能.tracemalloc.get_tracemalloc_memory()

获取 tracemalloc 用于存储内存块痕迹的模块。返回一 int .tracemalloc.is_tracing()

True 如果 tracemalloc 模块正在跟踪python内存分配, False 否则。

也见 start() 和 stop() 功能。tracemalloc.start(nframe: int = 1)

开始跟踪python内存分配:在python内存分配器上安装钩子。收集的痕迹追踪将限于 nFrice 框架。默认情况下,内存块的跟踪只存储最新的帧:限制为 1 . nFrice 必须大于或等于 1 .

通过查看 Traceback.total_nframe 属性。

存储超过 1 框架仅用于计算按分组的统计信息 'traceback' 或者计算累积统计:请参见 Snapshot.compare_to() 和 Snapshot.statistics() 方法。

存储更多的帧会增加 tracemalloc 模块。使用 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() 框架。使用 nFrice 的参数 start() 函数来存储更多帧。

这个 tracemalloc 模块必须跟踪内存分配才能获取快照,请参见 start() 功能。

也见 get_object_traceback() 功能。

DomainFilter

class tracemalloc.DomainFilter(inclusive: booldomain: int)

按地址空间(域)筛选内存块的跟踪。

3.6 新版功能.inclusive

如果 包容的 是 True (include),匹配地址空间中分配的内存块 domain .

如果 包容的 是 False (排除),匹配地址空间中未分配的内存块 domain .domain

内存块的地址空间 (int )只读属性。

滤波器

class tracemalloc.Filter(inclusive: boolfilename_pattern: strlineno: int = Noneall_frames: bool = Falsedomain: int = None)

过滤内存块的痕迹。

见 fnmatch.fnmatch() 函数的语法 filename_pattern . 这个 '.pyc' 文件扩展名替换为 '.py' .

实例:

  • Filter(True, subprocess.__file__) 只包括 subprocess 模块
  • Filter(False, tracemalloc.__file__) 排除 tracemalloc 模块
  • Filter(False, "<unknown>") 排除空的回溯

在 3.5 版更改: 这个 '.pyo' 文件扩展名不再替换为 '.py' .

在 3.6 版更改: 增加了 domain 属性。domain

内存块的地址空间 (int 或 None )

tracemalloc使用域 0 跟踪python进行的内存分配。C扩展可以使用其他域来跟踪其他资源。inclusive

如果 包容的 是 True (include),仅匹配在名称匹配的文件中分配的内存块 filename_pattern 在行号处 lineno .

如果 包容的 是 False (排除),忽略在名称匹配的文件中分配的内存块 filename_pattern 在行号处 lineno .lineno

行号 (int )过滤器的。如果 林诺 是 None ,筛选器匹配任何行号。filename_pattern

筛选器的文件名模式 (str )只读属性。all_frames

如果 all_frames 是 True ,将检查回溯的所有帧。如果 all_frames 是 False ,仅选中最近的帧。

如果回溯限制为 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: Snapshotkey_type: strcumulative: bool = False)

使用旧快照计算差异。将统计信息作为 StatisticDiff 实例分组依据 key_type .

见 Snapshot.statistics() 方法 key_type 和 累积的 参数。

结果按以下顺序从最大到最小排序:绝对值 StatisticDiff.size_diff , StatisticDiff.size ,绝对值 StatisticDiff.count_diff , Statistic.count 然后由 StatisticDiff.traceback .dump(filename)

将快照写入文件。

使用 load() 重新加载快照。filter_traces(filters)

创建新的 Snapshot 已筛选的实例 traces 序列, 过滤器 是一个列表 DomainFilter 和 Filter 实例。如果 过滤器 是空列表,返回新的 Snapshot 带有跟踪副本的实例。

同时应用所有包含筛选器,如果没有匹配的包含筛选器,则忽略跟踪。如果至少有一个独占筛选器与跟踪匹配,则忽略该跟踪。

在 3.6 版更改: DomainFilter 实例现在也被接受 过滤器 .classmethod load(filename)

从文件加载快照。

也见 dump() .statistics(key_type: strcumulative: bool = False)

将统计信息作为 Statistic 实例分组依据 key_type :

key_type描述
'filename'文件名
'lineno'文件名和行号
'traceback'追溯

如果 累积的 是 True ,累积跟踪的所有帧的内存块大小和计数,而不仅仅是最新帧。累积模式只能用于 key_type 等于 'filename' 和 'lineno' .

结果按以下顺序从大到小排序: Statistic.size , Statistic.count 然后由 Statistic.traceback .traceback_limit

在跟踪中存储的最大帧数 traces 结果: get_traceback_limit() 拍摄快照时。traces

python分配的所有内存块的跟踪:序列 Trace 实例。

序列的顺序未定义。使用 Snapshot.statistics() 方法获取统计信息的排序列表。

统计的

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 实例。

跟踪

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=Nonemost_recent_first=False)

将回溯格式设置为换行的行列表。使用 linecache 用于从源代码中检索行的模块。如果 limit 设置,格式化 limit 最近的帧如果 limit 是肯定的。否则,格式化 abs(limit) 最古老的框架。如果 most_recent_first 是 True ,格式化帧的顺序颠倒,首先返回最新帧,而不是最后一帧。

类似于 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())

本文为原创文章,未经授权禁止转载本站文章。
原文出处:兰玉磊的个人博客
原文链接:https://www.fdevops.com/2021/03/17/tracemalloc-28085
版权:本文采用「署名-非商业性使用-相同方式共享 4.0 国际」知识共享许可协议进行许可。

(2)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
兰玉磊的头像兰玉磊
上一篇 2021年3月17日
下一篇 2021年3月17日

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注