PEP 454 – 添加一个新的 tracemalloc 模块以跟踪 Python 内存分配
- 作者:
- Victor Stinner <vstinner at python.org>
- BDFL 委托:
- Charles-François Natali <cf.natali at gmail.com>
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2013 年 9 月 3 日
- Python 版本:
- 3.4
- 决议:
- Python-Dev 消息
摘要
本 PEP 提议添加一个新的 tracemalloc 模块来跟踪 Python 分配的内存块。
基本原理
像 Valgrind 这样的经典通用工具可以获取内存块被分配时的 C 回溯。使用此类工具分析 Python 内存分配没有帮助,因为大多数内存块都在同一个 C 函数中分配,例如在 PyMem_Malloc() 中。此外,Python 有一个用于小对象的分配器,称为“pymalloc”,它为了效率而保留空闲块。这些工具无法很好地处理这种情况。
有一些专门用于 Python 语言的调试工具,例如 Heapy Pympler 和 Meliae,它们使用垃圾回收器模块(例如 gc.get_objects()、gc.get_referrers() 和 gc.get_referents() 函数)列出所有存活对象,计算它们的大小(例如使用 sys.getsizeof()),并按类型对对象进行分组。这些工具提供了应用程序内存使用情况的更好估计。当大多数内存泄漏是同一类型的实例,并且该类型仅在少数函数中实例化时,它们很有用。当对象类型非常常见(如 str 或 tuple)时,就会出现问题,并且很难识别这些对象是在哪里实例化的。
查找引用循环也是一个难题。有不同的工具可以绘制所有引用的图表。这些工具不能用于拥有数千个对象的大型应用程序,因为图表太大而无法手动分析。
提案
使用 PEP 445 中的自定义分配 API,可以很容易地在 Python 内存分配器上设置一个钩子。钩子可以检查 Python 内部以检索 Python 回溯。获取当前回溯的想法来自 faulthandler 模块。faulthandler 在崩溃时转储所有 Python 线程的回溯,这里的想法是在 Python 分配内存块时获取当前 Python 线程的回溯。
本 PEP 提议添加一个新的 tracemalloc 模块,这是一个用于跟踪 Python 分配的内存块的调试工具。该模块提供以下信息
- 对象被分配时的回溯
- 按文件名和行号划分的已分配内存块统计信息:已分配内存块的总大小、数量和平均大小
- 计算两个快照之间的差异以检测内存泄漏
tracemalloc 模块的 API 类似于 faulthandler 模块的 API:enable() / start()、disable() / stop() 和 is_enabled() / is_tracing() 函数、环境变量(PYTHONFAULTHANDLER 和 PYTHONTRACEMALLOC)以及 -X 命令行选项(-X faulthandler 和 -X tracemalloc)。请参阅 faulthandler 模块的文档。
跟踪内存分配的想法并不新鲜。它最初于 2005 年在 PySizer 项目中实现。PySizer 的实现方式不同:回溯存储在帧对象中,一些 Python 类型将跟踪与对象类型名称关联起来。PySizer 对 CPython 的补丁增加了性能和内存占用的开销,即使 PySizer 未使用。tracemalloc 将回溯附加到底层,即内存块,当模块未跟踪内存分配时没有开销。
tracemalloc 模块是为 CPython 编写的。其他 Python 实现可能无法提供它。
API
要跟踪 Python 分配的大多数内存块,应尽快启动该模块,方法是将 PYTHONTRACEMALLOC 环境变量设置为 1,或使用 -X tracemalloc 命令行选项。tracemalloc.start() 函数可以在运行时调用以开始跟踪 Python 内存分配。
默认情况下,已分配内存块的跟踪仅存储最近的帧(1 帧)。要在启动时存储 25 帧:将 PYTHONTRACEMALLOC 环境变量设置为 25,或使用 -X tracemalloc=25 命令行选项。set_traceback_limit() 函数可以在运行时用于设置限制。
函数
clear_traces() 函数
清除 Python 分配的内存块的跟踪。另请参阅
stop()。
get_object_traceback(obj) 函数
获取 Python 对象 obj 被分配时的回溯。返回Traceback实例,如果tracemalloc模块未跟踪内存分配或未跟踪对象的分配,则返回None。另请参阅
gc.get_referrers()和sys.getsizeof()函数。
get_traceback_limit() 函数
获取跟踪回溯中存储的最大帧数。获取限制必须跟踪内存分配的
tracemalloc模块,否则将引发异常。限制由
start()函数设置。
get_traced_memory() 函数
获取tracemalloc模块跟踪的内存块的当前大小和最大大小,作为元组:(size: int, max_size: int)。
get_tracemalloc_memory() 函数
获取tracemalloc模块用于存储内存块跟踪的内存使用量(以字节为单位)。返回一个int。
is_tracing() 函数
如果tracemalloc模块正在跟踪 Python 内存分配,则为True,否则为False。另请参阅
start()和stop()函数。
start(nframe: int=1) 函数
开始跟踪 Python 内存分配:在 Python 内存分配器上安装钩子。收集的跟踪回溯将被限制为 nframe 帧。默认情况下,内存块的跟踪仅存储最近的帧:限制为1。nframe 必须大于或等于1。存储超过
1帧仅在按'traceback'分组计算统计信息或计算累积统计信息时有用:请参阅Snapshot.compare_to()和Snapshot.statistics()方法。存储更多帧会增加
tracemalloc模块的内存和 CPU 开销。使用get_tracemalloc_memory()函数测量tracemalloc模块使用了多少内存。
PYTHONTRACEMALLOC环境变量(PYTHONTRACEMALLOC=NFRAME)和-Xtracemalloc=NFRAME命令行选项可用于在启动时开始跟踪。另请参阅
stop()、is_tracing()和get_traceback_limit()函数。
stop() 函数
停止跟踪 Python 内存分配:卸载 Python 内存分配器上的钩子。同时清除 Python 分配的内存块的跟踪。在清除跟踪之前,调用
take_snapshot()函数以获取跟踪的快照。另请参阅
start()和is_tracing()函数。
take_snapshot() 函数
获取 Python 分配的内存块的跟踪快照。返回一个新的Snapshot实例。快照不包括在
tracemalloc模块开始跟踪内存分配之前分配的内存块。跟踪的回溯被限制为
get_traceback_limit()帧。使用start()函数的 nframe 参数来存储更多帧。要获取快照,
tracemalloc模块必须正在跟踪内存分配,请参阅start()函数。另请参阅
get_object_traceback()函数。
过滤器
Filter(inclusive: bool, filename_pattern: str, lineno: int=None, all_frames: bool=False) 类
内存块跟踪过滤器。有关 filename_pattern 的语法,请参阅
fnmatch.fnmatch()函数。'.pyc'和'.pyo'文件扩展名替换为'.py'。示例
Filter(True, subprocess.__file__)仅包含subprocess模块的跟踪Filter(False, tracemalloc.__file__)排除tracemalloc模块的跟踪Filter(False, "<unknown>")排除空回溯
inclusive 属性
如果 inclusive 为True(包含),则只跟踪在文件名为filename_pattern且行号为lineno的文件中分配的内存块。如果 inclusive 为
False(排除),则忽略在文件名为filename_pattern且行号为lineno的文件中分配的内存块。
lineno 属性
过滤器的行号 (int)。如果 lineno 为None,则过滤器匹配任何行号。
filename_pattern 属性
过滤器的文件名模式 (str)。
all_frames 属性
如果 all_frames 为True,则检查回溯的所有帧。如果 all_frames 为False,则仅检查最近的帧。如果回溯限制小于
2,则忽略此属性。请参阅get_traceback_limit()函数和Snapshot.traceback_limit属性。
帧
Frame 类
回溯中的帧。
Traceback类是Frame实例的序列。
filename 属性
文件名 (str)。
lineno 属性
行号 (int)。
快照
Snapshot 类
Python 分配的内存块的跟踪快照。
take_snapshot()函数创建一个快照实例。
compare_to(old_snapshot: Snapshot, group_by: str, cumulative: bool=False) 方法
与旧快照计算差异。获取按 group_by 分组的StatisticDiff实例的排序列表作为统计信息。有关 group_by 和 cumulative 参数,请参阅
statistics()方法。结果按以下顺序从大到小排序:
StatisticDiff.size_diff的绝对值、StatisticDiff.size、StatisticDiff.count_diff的绝对值、Statistic.count,然后按StatisticDiff.traceback排序。
dump(filename) 方法
将快照写入文件。使用
load()重新加载快照。
filter_traces(filters) 方法
创建一个新的Snapshot实例,其中包含已过滤的traces序列,filters 是Filter实例的列表。如果 filters 是一个空列表,则返回一个包含跟踪副本的新Snapshot实例。所有包含过滤器同时应用,如果没有包含过滤器匹配则忽略跟踪。如果至少一个排除过滤器匹配则忽略跟踪。
load(filename) 类方法
从文件中加载快照。另请参阅
dump()。
statistics(group_by: str, cumulative: bool=False) 方法
获取按 group_by 分组的Statistic实例的排序列表作为统计信息。
group_by 描述 'filename'文件名 'lineno'文件名和行号 'traceback'回溯 如果 cumulative 为
True,则累积跟踪回溯的所有帧的内存块大小和计数,而不仅仅是最近的帧。累积模式只能与 group_by 等于'filename'和'lineno'且traceback_limit大于1一起使用。结果按以下顺序从大到小排序:
Statistic.size、Statistic.count,然后按Statistic.traceback排序。
traceback_limit 属性
traces回溯中存储的最大帧数:快照生成时get_traceback_limit()的结果。
traces 属性
Python 分配的所有内存块的跟踪:Trace实例序列。序列的顺序是不确定的。使用
Snapshot.statistics()方法获取已排序的统计信息列表。
统计
Statistic 类
内存分配统计。
Snapshot.statistics()返回一个Statistic实例列表。另请参阅
StatisticDiff类。
count 属性
内存块数 (int)。
size 属性
内存块总大小(以字节为单位)(int)。
traceback 属性
内存块分配时的回溯,Traceback实例。
统计差异
StatisticDiff 类
旧Snapshot实例与新Snapshot实例之间内存分配的统计差异。
Snapshot.compare_to()返回一个StatisticDiff实例列表。另请参阅Statistic类。
count 属性
新快照中的内存块数量 (int):如果内存块在新快照中已释放,则为0。
count_diff 属性
旧快照与新快照之间内存块数量的差异 (int):如果内存块在新快照中已分配,则为0。
size 属性
新快照中内存块的总大小(以字节为单位)(int):如果内存块在新快照中已释放,则为0。
size_diff 属性
旧快照与新快照之间内存块总大小(以字节为单位)的差异 (int):如果内存块在新快照中已分配,则为0。
traceback 属性
内存块分配时的回溯,Traceback实例。
跟踪
Trace 类
内存块的跟踪。
Snapshot.traces属性是Trace实例的序列。
size 属性
内存块大小(以字节为单位)(int)。
traceback 属性
内存块分配时的回溯,Traceback实例。
回溯
Traceback 类
Frame实例序列,按从最新帧到最旧帧的顺序排序。回溯至少包含
1帧。如果tracemalloc模块未能获取帧,则使用文件名"<unknown>"和行号0。生成快照时,跟踪的回溯限制为
get_traceback_limit()帧。请参阅take_snapshot()函数。
Trace.traceback属性是Traceback实例。
被拒绝的替代方案
记录对内存分配器的调用
另一种方法是记录对 malloc()、realloc() 和 free() 函数的调用。调用可以记录到文件或通过网络发送到另一台计算机。日志条目的示例:函数名称、内存块大小、内存块地址、分配发生时的 Python 回溯、时间戳。
日志不能直接使用,获取内存的当前状态需要解析以前的日志。例如,无法直接获取 Python 对象的跟踪,就像 get_object_traceback(obj) 使用跟踪所做的那样。
Python 使用生命周期很短的对象,因此大量使用内存分配器。它有一个针对生命周期短的小对象(小于 512 字节)优化的分配器。例如,Python 测试套件平均每秒调用 malloc()、realloc() 或 free() 270,000 次。如果日志条目大小为 32 字节,则每秒生成 8.2 MB 或每小时 29.0 GB。
该替代方案被拒绝,因为它效率较低且功能较少。在不同进程或不同计算机中解析日志比在同一进程中维护已分配内存块的跟踪要慢。
前期工作
- Python Memory Validator (2005-2013):由 Software Verification 开发的商业 Python 内存验证器。它使用 Python 反射 API。
- PySizer:Nick Smallbone 的 2005 年 Google Summer of Code 项目。
- Heapy (2006-2013):Sverker Nilsson 编写的 Guppy-PE 项目的一部分。
- PEP 草案:支持跟踪 CPython 中的低级内存使用 (Brett Canon, 2006)
- Muppy:Robert Schuppenies 于 2008 年开发的项目。
- asizeof:Jean Brouwers 编写的纯 Python 模块,用于估计对象大小 (2008)。
- Heapmonitor:它提供了测量单个对象的功能,并可以跟踪某些类的所有对象。由 Ludwig Haehne 于 2008 年开发。
- Pympler (2008-2011):基于 asizeof、muppy 和 HeapMonitor 的项目
- objgraph (2008-2012)
- Dozer:CherryPy 内存泄漏调试器的 WSGI 中间件版本,由 Marius Gedminas 编写 (2008-2013)
- Meliae:John A Meinel 自 2009 年以来开发的 Python 内存使用分析器
- gdb-heap:Dave Malcolm 编写的 Python GDB 脚本 (2010-2011),用于分析堆内存的使用
- memory_profiler:由 Fabian Pedregosa 编写 (2011-2013)
- caulk:由 Ben Timby 于 2012 年编写
另请参阅 Pympler 相关工作。
链接
tracemalloc
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0454.rst