深入 CPython 的 sys.monitoring:PEP 669 执行事件监控 API 完全指南
【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython
本篇技术指南以 CPython 官方文档 Doc/library/sys.monitoring.rst 为核心骨架,系统讲解 PEP 669 落地到 CPython 3.12+ 的执行事件监控(Execution Event Monitoring)体系:工具标识符(Tool ID)、事件分类、全局与逐代码对象(per code object)的事件开关、回调注册与各事件签名,并结合仓库源码(Python/instrumentation.c、Include/internal/pycore_instruments.h等)剖析其字节码插桩与"停止整个世界"(Stop The World)再插桩的实现原理。读完本文,你将能基于sys.monitoring从零编写一套调试器、覆盖率统计或轻量级 Profiler,并理解它为何比旧的sys.settrace/sys.setprofile更适合高性能监控场景。
一、认识 sys.monitoring:它是"命名空间"而非模块
sys.monitoring是 CPython 3.12(PEP 669)起随解释器内置提供的执行事件监控接口,用于在程序运行过程中接收回调,从而观察函数调用、返回、行号推进、分支跳转与异常等执行细节。
首先必须澄清一个最容易踩的坑:sys.monitoring是sys模块内部的一个命名空间对象,而不是一个可独立导入的模块。直接执行:
import sys.monitoring # ModuleNotFoundError: No module named 'sys.monitoring'会抛出ModuleNotFoundError。正确用法是导入sys后访问其属性:
import sys sys.monitoring # 模块对象(m_name 为 "sys.monitoring")从实现上看,该对象由 Python/instrumentation.c 中的PyModuleDef monitoring_module定义,模块名被命名为"sys.monitoring",再通过_Py_CreateMonitoringObject()创建并挂载到sys模块的属性上。
整个监控 API 由三个核心部件构成:
- 工具标识符(Tool identifiers):整数编号 + 关联名称,用于让多个监控工具并行协作而不互相干扰;
- 事件(Events):
sys.monitoring.events命名空间下的一组事件常量; - 回调(Callbacks):为某个工具、某个事件注册的回调函数,事件发生时由虚拟机触发。
这三个部件对应文档原文的Tool identifiers、Events与Callbacks三节,下面逐一展开。
二、工具标识符(Tool ID):让多个监控工具和平共处
2.1 为什么需要 Tool ID
当调试器、覆盖率工具、Profiler、JIT 优化器等多个工具同时希望监听执行事件时,如果它们共用一套回调,就会相互踩踏。sys.monitoring用0~5 共 6 个整数 Tool ID把各工具隔离:每个工具申请一个 ID,各工具拥有自己独立的事件集合与回调表。
文档同时说明:目前工具之间是完全独立的,一个工具不能监控另一个工具的行为(即不能用监控 API 观察其他工具注册了什么),这一限制未来可能放开。
2.2 预定义的 ID 常量
虽然虚拟机对 0~5 的所有 ID 一视同仁,但 PEP 669 预定义了几个 ID 以便生态协作:
sys.monitoring.DEBUGGER_ID = 0 # 调试器 sys.monitoring.COVERAGE_ID = 1 # 覆盖率工具 sys.monitoring.PROFILER_ID = 2 # Profiler sys.monitoring.OPTIMIZER_ID = 5 # JIT 优化器这些常量在 Include/cpython/monitoring.h 之外另有对应内部宏:PY_MONITORING_DEBUGGER_ID 0、PY_MONITORING_COVERAGE_ID 1、PY_MONITORING_PROFILER_ID 2、PY_MONITORING_OPTIMIZER_ID 5,定义于 Include/internal/pycore_instruments.h。注意:这个内部头文件还预留了PY_MONITORING_SYS_PROFILE_ID 6与PY_MONITORING_SYS_TRACE_ID 7——它们是解释器内部用来支撑旧版sys.setprofile()/sys.settrace()的工具位(详见后文"与 sys.settrace/sys.setprofile 的关系"),面向用户的可用 ID 只有 0~5。
Tool ID 的合法性校验位于check_valid_tool()(Python/instrumentation.c),越界会抛出ValueError: invalid tool %d (must be between 0 and 5)。
2.3 工具生命周期管理 API
一个工具在使用前必须先注册 ID,使用结束后释放。官方提供 4 个函数:
| 函数 | 说明 | 异常/边界 |
|---|---|---|
use_tool_id(tool_id: int, name: str, /) -> None | 在tool_id使用前必须调用;name为工具名 | tool_id必须在 0~5;若该 ID 已被占用则抛ValueError |
clear_tool_id(tool_id: int, /) -> None | 注销与tool_id关联的全部事件与回调函数 | 需先注册过该 ID |
free_tool_id(tool_id: int, /) -> None | 工具不再需要该 ID 时调用;内部先调用clear_tool_id再释放 ID | 需先注册过该 ID |
get_tool(tool_id: int, /) -> str \| None | 返回占用该 ID 的工具名;若无人使用返回None | tool_id必须在 0~5 |
参数末尾的
/表示这些函数只支持位置参数,不支持关键字传参。
以use_tool_id为例,Python/instrumentation.c 的实现细节是:工具名必须是str(否则抛ValueError: tool name must be a str),且monitoring_tool_names[tool_id]已非空时抛ValueError: tool %d is already in use;名称被存储在解释器状态PyInterpreterState.monitoring_tool_names数组中。free_tool_id(L2237-L2254)则会先_PyMonitoring_ClearToolId清空事件与回调,再Py_CLEAR释放名称引用。
典型的工具启动/收尾模式:
import sys MY_TOOL = 4 sys.monitoring.use_tool_id(MY_TOOL, "MyTool.Tracer") assert sys.monitoring.get_tool(MY_TOOL) == "MyTool.Tracer" # ... 注册事件与回调,开始工作 ... sys.monitoring.free_tool_id(MY_TOOL) # 收尾,等价于 clear + 释放这也是仓库测试 Lib/test/test_monitoring.py 的规范用法——该测试在每个用例的tearDown中都会调用sys.monitoring.free_tool_id(TEST_TOOL)保证测试间互不污染(其test_tool用例就断言了get_tool返回注册时传入的名称)。
三、事件体系:events 命名空间与事件分类
3.1 事件的三种定位与事件常量
监控 API 支持 18 种执行事件(外加 1 个已弃用事件)。它们都是sys.monitoring.events命名空间的属性,每个事件是一个 2 的幂整数常量,因此"事件集合"可以直接用按位或组合,例如同时要PY_RETURN与PY_START就写PY_RETURN | PY_START。此外还提供:
sys.monitoring.events.NO_EVENTS # 0 的别名,便于显式比较NO_EVENTS的典型用法是判断当前没有任何事件在监听:
if sys.monitoring.get_events(sys.monitoring.DEBUGGER_ID) == sys.monitoring.events.NO_EVENTS: ... # 该工具当前未激活任何事件把NO_EVENTS(即 0)设为事件集合等价于关闭全部事件。事件常量的构造逻辑在_Py_CreateMonitoringObject()(Python/instrumentation.c):先创建一个types.SimpleNamespace类型的命名空间对象,再用add_power2_constant()按1 << i依次填入各事件,最后挂上NO_EVENTS = 0。
从 Include/cpython/monitoring.h 可以看到事件编号的完整布局(序号即1 << 序号的幂次):
| 事件编号 | 宏常量 | 对应 events 属性 | 分类 |
|---|---|---|---|
| 0~10 | PY_MONITORING_EVENT_PY_START...PY_MONITORING_EVENT_STOP_ITERATION | PY_START...STOP_ITERATION | 局部事件(Local,可逐位置关闭) |
| 11~15 | RAISE/EXCEPTION_HANDLED/PY_UNWIND/PY_THROW/RERAISE | 同左 | 其他事件(Other,主要面向异常) |
| 16~17 | C_RETURN/C_RAISE | 同左 | 附属事件(Ancillary,由 CALL 控制) |
| 18 | BRANCH | 同左 | 已弃用(3.14 起) |
3.2 局部事件(Local Events)
局部事件与程序的正常执行绑定,发生在明确的位置上,因而可以针对某个具体代码位置单独禁用。共 11 种:
PY_START:Python 函数开始(发生在调用之后立刻,被调方 frame 已在栈上)PY_RESUME:Python 函数被恢复执行——针对生成器与协程函数,不含throw()调用PY_RETURN:Python 函数返回(发生在 return 之前的一瞬间,被调方 frame 仍在栈上)PY_YIELD:Python 函数产出值(yield 之前触发,被调方 frame 仍在栈上)CALL:Python 代码中的一次调用(发生在调用之前)LINE:即将执行一条与前一条指令行号不同的指令INSTRUCTION:一条虚拟机指令即将被执行JUMP:控制流图中发生一次无条件跳转BRANCH_LEFT:条件分支走向"左"BRANCH_RIGHT:条件分支走向"右"STOP_ITERATION:人为抛出的StopIteration(见 3.5 专门说明)
关于"左/右"分支,文档强调:没有任何保证哪一边是"左"哪一边是"右",唯一保证是整个程序运行期间方向保持一致,具体如何向用户呈现左右完全由工具决定。
3.3 附属事件(Ancillary Events)与已弃用事件
C_RAISE(从任意可调用对象抛出的异常,Python 函数除外,在退出后发生)与C_RETURN(从任意可调用对象返回,Python 函数除外,在返回后发生)虽然可以被监听,但受控于CALL事件:只有对应位置的CALL事件处于被监听状态时,才会看到C_RETURN/C_RAISE。这一约束在源码层强制执行——set_events(Python/instrumentation.c)和set_local_events都会检查:若事件集合包含C_RETURN_EVENTS却不包含C_CALL_EVENTS,直接抛ValueError: cannot set C_RETURN or C_RAISE events independently,随后还会把C_RETURN_EVENTS从集合中剥离(仅作CALL的附随产物)。
已弃用事件BRANCH:该事件在 3.14 起被弃用。文档给出的理由是:改用BRANCH_LEFT/BRANCH_RIGHT会获得更好的性能,因为它们能被独立地按位置禁用。源码侧也保留了兼容处理:set_events/set_local_events收到BRANCH位时,会将其清除并自动展开成BRANCH_RIGHT | BRANCH_LEFT(L2367-L2370)。
3.4 其他事件(Other Events):与位置解耦
另有一类事件不与程序中的某个特定位置强绑定,无法针对单个代码位置单独禁用:
RAISE:异常被抛出(会导致STOP_ITERATION事件的除外)RERAISE:异常被重新抛出,例如finally块结束时的隐式 re-raiseEXCEPTION_HANDLED:某个异常被处理PY_THROW:Python 函数通过throw()调用恢复执行PY_UNWIND:Python 函数在异常展开(unwinding)期间退出,包括函数内部直接抛出并放任继续传播的异常
3.15 的行为变化:文档标注
versionchanged:: 3.15——"其他事件"现在也可以按整个 code object 粒度开关了:回调返回DISABLE会为整个 code object(针对当前工具)禁用该事件。仓库当前处于开发分支,Include/internal/pycore_instruments.h 头文件注释也印证了这一点:"Other events. These can now be turned on and disabled on a per code object basis."
3.5 STOP_ITERATION 事件的前因后果
PEP 380 规定:生成器或协程返回一个值时会抛出一个StopIteration异常。但用抛异常来传返回值非常低效,因此 CPython 3.12+ 的实现除非该异常会对其他代码可见,否则不再真的抛异常。
为了让工具能监控到真实的异常又不必拖慢生成器/协程,sys.monitoring提供了可局部禁用的STOP_ITERATION事件。需要特别强调的是:STOP_ITERATION事件与"针对StopIteration异常的RAISE事件"在语义上是等价的,生成事件时二者可以互换。实现出于性能考虑会优先产生STOP_ITERATION,但也可能对某个StopIteration产生RAISE事件——所以工具若要统计真正的StopIteration,应当同时对这两种事件都做好准备。
3.6 未来扩展
文档明确"未来可能增加更多事件",事件编号存在天然扩展空间(内部事件总数上限为_PY_MONITORING_EVENTS,共 19 个位,见 pycore_instruments.h)。
四、开关事件:全局、按 code object、DISABLE 与重启
一个事件要被触发,需要同时满足:① 事件已打开;② 已注册对应回调。事件开关分两层:全局(对整个解释器)与逐 code object(局部)。如果一个事件同时在全局和局部都打开,它仍然只会触发一次。
默认情况下没有任何事件处于激活状态。
4.1 全局事件开关
sys.monitoring.get_events(tool_id: int, /) -> int # 返回该工具所有已激活事件组成的 int sys.monitoring.set_events(tool_id: int, event_set: int, /) -> Noneset_events会激活event_set中置位的全部事件;- 若tool_id未注册使用(不在
monitoring_tool_names中),抛ValueError; event_set超出合法事件范围(>= 1 << 19)或非法组合也会抛ValueError。
实现上,set_events(Python/instrumentation.c)内部会先做合法性检查与BRANCH/附属事件归一化,然后调用_PyEval_StopTheWorld(interp)暂停所有线程、执行_PyMonitoring_SetEvents()完成真正的字节码插桩、再_PyEval_StartTheWorld(interp)恢复运行。全局活动工具集合存放在解释器状态的interp->monitors(_Py_GlobalMonitors结构,每个事件一个字节记录哪些工具在位)中。
4.2 按 code object 开关(局部事件)
sys.monitoring.get_local_events(tool_id: int, code: CodeType, /) -> int sys.monitoring.set_local_events(tool_id: int, code: CodeType, event_set: int, /) -> None这两个函数只针对局部事件生效,需要传入一个types.CodeType对象(一般从函数/方法的__code__属性取得)。文档特别提示:凡是接受CodeType的函数都应能接受来自非 Python 定义的函数的"外观相似对象"——这与 Doc/c-api/monitoring.rst 描述的 C API 约定一致(C 侧事件触发接口接受codelike对象)。源码中get_local_events/set_local_events都先做PyCode_Check(code)类型校验,不满足则抛TypeError: code must be a code object。set_local_events同样执行StopTheWorld+_PyMonitoring_SetLocalEvents()的重插桩流程(L2456-L2459)。
每个 code object 的局部监控状态存于其_co_monitoring字段指向的_PyCoMonitoringData结构(pycore_instruments.h)中,其中:
local_monitors/active_monitors:记录各工具请求监听与实际生效的局部事件;tools/line_tools/per_instruction_tools:按 code unit 记录"这个位置要通知哪些工具",是实现逐位置禁用的数据基础;tool_versions[PY_MONITORING_TOOL_IDS]:记录各工具插桩时的版本号,用于增量去插桩/重插桩;per_instruction_opcodes:指令级事件需要保存的原始操作码。
从源码结构可以推断:LINE、INSTRUCTION事件拥有独立的按 code unit 的数据通道,因此它们的"按位置禁用"开销可以被压缩到每 code unit 一个字节级别,这正是高性能监控的根基。
4.3 从回调中返回 DISABLE:按位置精准关闭
DISABLE是sys.monitoring模块层面的一个特殊单例值(源码中为_PyInstrumentation_DISABLE,随模块初始化挂载,见 L2574)。它只能在回调函数中作为返回值使用:
- 对局部事件:返回
DISABLE会禁用当前这个代码位置的该事件。它不会改动"设置了哪些事件",也不影响同事件的其他代码位置; - 对其他事件(3.15 起):返回
DISABLE会按整个 code object维度禁用该事件(针对当前工具)。
文档着重指出:按位置禁用对高性能监控至关重要。例如调试器可以让程序在"除了少数几个断点之外全部禁用监控"的状态下近乎零开销地运行,只有真正命中断点位置时才产生事件。
4.4 restart_events():全局重新启用
sys.monitoring.restart_events() -> Nonerestart_events会为所有工具重新启用所有曾被DISABLE关闭的事件。其实现(Python/instrumentation.c)比较精巧:利用"版本号"机制——每次设置/重启事件都会递增一个全局版本号,而每个 code object 记录着"上次按什么版本插的桩";restart_events会把last_restart_version提升到一个中间值、再推进全局版本,随后调用instrument_all_executing_code_objects()让所有正在执行的 code object 重新按新版本插桩,从而"复活"被禁用的位置。若版本号溢出则抛OverflowError: events set too many times。
五、注册回调:register_callback 与各类事件签名
5.1 注册与注销
sys.monitoring.register_callback(tool_id: int, event: int, func: Callable | None, /) -> Callable | None- 为tool_id与event注册回调func;
- 如果该工具/事件之前已有回调,旧回调会被注销并作为返回值返回;否则返回
None; - 注销回调只需传
func=None:sys.monitoring.register_callback(tool_id, event, None); - 回调可以在任意时刻注册或注销(甚至可以在另一个回调触发过程中进行);
- 若同一事件在全局与局部都打开了,回调只被调用一次,因此工具的代码需要能同时处理两种触发来源。
源码中的校验逻辑(Python/instrumentation.c):
check_valid_tool:tool_id 必须在 0~5;_Py_popcount32(event) != 1:event 必须是单个事件(2 的幂),一次注册多个事件会抛ValueError: The callback can only be set for one event at a time;- event 编号越界抛
ValueError: invalid event %d; - 触发审计钩子
PySys_Audit("sys.monitoring.register_callback", "O", func)——即注册回调会发出sys.monitoring.register_callback审计事件,便于安全工具审计; func is None时内部转为NULL完成注销。
5.2 MISSING:表达"调用没有参数"
MISSING是另一个特殊单例值(_PyInstrumentation_MISSING),专门用于CALL/C_RAISE/C_RETURN事件中表示"该调用没有参数"。CALL事件回调的第四个参数arg0仅在确有参数时才是真实值;若调用没有参数,arg0就是MISSING。
5.3 各类事件的回调签名总表
回调的返回值除了DISABLE外,返回任何其他对象都不会产生效果。不同事件传给回调的参数不同,官方文档的完整契约如下(CodeType即types.CodeType,instruction_offset是指令偏移量):
| 事件 | 回调签名 | 要点说明 |
|---|---|---|
PY_START,PY_RESUME | func(code: CodeType, instruction_offset: int) -> object | |
PY_RETURN,PY_YIELD | func(code: CodeType, instruction_offset: int, retval: object) -> object | 携带返回值 |
CALL,C_RAISE,C_RETURN | func(code: CodeType, instruction_offset: int, callable: object, arg0: object) -> object | arg0可为MISSING;code是发起调用处的 code object,callable是即将被调用的对象 |
RAISE,RERAISE,EXCEPTION_HANDLED,PY_UNWIND,PY_THROW,STOP_ITERATION | func(code: CodeType, instruction_offset: int, exception: BaseException) -> object | 携带异常对象 |
LINE | func(code: CodeType, line_number: int) -> object | 注意第二参是行号而非指令偏移 |
BRANCH_LEFT,BRANCH_RIGHT,JUMP | func(code: CodeType, instruction_offset: int, destination_offset: int) -> object | destination_offset是下一步将要执行的位置 |
INSTRUCTION | func(code: CodeType, instruction_offset: int) -> object |
关于CALL事件,文档给了两条关键语义:
CALL的code表示"正在发起调用的那个 code object",callable才是触发了该事件的、即将被调用的对象;- 对于实例方法,callable会是从类上找到的函数对象,而arg0被设为实例本身(即方法的
self参数)。
5.4 完整示例:一个最小可用的调用监控器
综合上面所有 API,一个监控"Python 函数调用并统计返回"的最小工具如下:
import sys TID = 3 # 自己选一个 0~5 之间未被占用的 ID if sys.monitoring.get_tool(TID) is None: sys.monitoring.use_tool_id(TID, "call.tracer") calls = {} def on_py_start(code, offset): calls[code] = calls.get(code, 0) + 1 def on_call(code, offset, callable, arg0): arg = "no-args" if arg0 is sys.monitoring.MISSING else repr(arg0) print(f"CALL {callable.__qualname__} at {code.co_filename}:{offset} arg0={arg}") sys.monitoring.register_callback(TID, sys.monitoring.events.PY_START, on_py_start) sys.monitoring.register_callback(TID, sys.monitoring.events.CALL, on_call) # 需要同时监听 CALL 才能收到 C_RETURN/C_RAISE(附属事件规则) def on_c_return(code, offset, callable, retval): print(f"C_RETURN {callable.__qualname__} -> {retval!r}") sys.monitoring.register_callback(TID, sys.monitoring.events.C_RETURN, on_c_return) # 打开事件:全局打开(对所有函数生效) sys.monitoring.set_events( TID, sys.monitoring.events.PY_START | sys.monitoring.events.CALL | sys.monitoring.events.C_RETURN, # CALL 已包含,合法 ) def demo(x): return x * 2 demo(21) print("PY_START count:", calls[demo.__code__]) # 收尾清理 sys.monitoring.free_tool_id(TID)5.5 只监控"某个函数":set_local_events 用法
如果只想精确监控某一个函数(例如只在函数foo上设事件),使用set_local_events:
sys.monitoring.set_local_events( TID, foo.__code__, sys.monitoring.events.LINE | sys.monitoring.events.PY_RETURN, ) def on_line(code, lineno): print(f"line {lineno}") def on_return(code, offset, retval): print(f"returned {retval!r}") return sys.monitoring.DISABLE # 该位置只触发一次,随后按位置关闭在回调中返回sys.monitoring.DISABLE,即可实现文档所说的"断点式"精准监控:定位到目标行后立刻关闭该位置的事件,把额外开销降到最低。
六、底层原理:从事件到字节码插桩
6.1 本地事件需要字节码插桩
sys.monitoring并不是简单地在解释器主循环里查一张"事件→回调"大表。对PY_START、LINE、CALL、JUMP、BRANCH_*这类局部事件,事件与具体指令绑定,因此需要在字节码层面插桩(instrumentation):VM 会改写 code object 中的指令(例如把一条指令替换为INSTRUMENTED_LINE之类的特殊指令并保存原始操作码),让指令执行到该位置时去查询"哪个工具监听、该调哪个回调"。
关键数据路径是 Include/internal/pycore_instruments.h 中定义的_PyCoMonitoringData(挂载在PyCodeObject._co_monitoring上)以及_Py_LocalMonitors/_Py_GlobalMonitors(每事件用一个字节的位图记录哪些工具在位,即"工具集")。由于每事件 8 个工具位可用 1 个uint8_t表达,判断"某个事件是否有工具监听"只需一次内存读与一次按位与,这是它能够低开销运行的结构基础。
由于插桩会改写正在执行的字节码,必须先停止所有线程再改。因此set_events/set_local_events/restart_events乃至启用sys.settrace时,源码都遵循同一范式:
_PyEval_StopTheWorld(interp) # 1. 停止世界,保证无线程正在执行被改代码 ... 修改插桩 / 更新版本号 ... _PyEval_StartTheWorld(interp) # 2. 恢复运行见 Python/instrumentation.c 的set_events实现。
6.2 事件触发与 C 层 Fire 接口
当某个被监控位置真正执行时,VM 通过_Py_call_instrumentation*系列内部函数(pycore_instruments.h)把事件分发给对应工具的回调。面向扩展模块作者,CPython 在 3.13 起提供了C 级监控 API,完整文档见 Doc/c-api/monitoring.rst,头文件声明位于 Include/cpython/monitoring.h:
- 每个事件对应一个
PyMonitoring_FireXxxEvent(...)接口(如PyMonitoring_FirePyStartEvent、PyMonitoring_FireLineEvent、PyMonitoring_FireCallEvent),用于扩展在模拟 Python 代码执行时主动触发监控事件; - 这些函数接收一个
PyMonitoringState结构(封装事件的激活状态)以及事件参数:codelike(CodeType或模拟它的对象)、指令偏移,以及部分事件特有的参数; - VM 在触发事件时会自动禁用 tracing,用户代码无需自己处理重入问题;
- 调用监控函数时不应处于异常已设置状态(文档明确列出的少数"与当前异常配合工作"的接口除外);
- 所有 Fire 函数成功返回 0、出错返回 -1 并设置异常;
- 注意:监控 API 目前没有受限 API(Limited API),
monitoring.h顶部注释写明#ifndef Py_LIMITED_API守护。
6.3 与 sys.settrace / sys.setprofile 的关系
sys.monitoring并未完全取代旧机制——从 pycore_instruments.h 可以看出,解释器内部把sys.settrace和sys.setprofile映射为两个保留工具 ID(6 与 7),通过 Python/legacy_tracing.c 把旧 API 转译到监控框架上。也就是说:新旧两套追踪体系底层共用同一套监控/插桩机制,且解释器利用 0~5 之外预留的这两个位,使旧 API 与sys.monitoring用户工具互不干扰。
6.4 与其他文档/测试的交叉印证
仓库中与sys.monitoring相关的第一手资料还包括:
- 官方 API 参考(本文主题):Doc/library/sys.monitoring.rst;
- C 级扩展 API:适用于任何想"生成"事件而非仅仅"消费"事件的扩展模块,见 Doc/c-api/monitoring.rst;
- 功能测试:Lib/test/test_monitoring.py 覆盖了 Tool 生命周期、各事件触发次数、
DISABLE/MISSING语义等(如MonitoringBasicTest、MonitoringCountTest);Lib/test/test_free_threading/test_monitoring.py 覆盖自由线程(free-threaded)构建下的监控行为; - C API 测试样例:Modules/_testcapi/monitoring.c。
七、实践要点与最佳实践小结
回顾文档与源码,落地一个sys.monitoring工具时应遵循以下要点:
- 命名空间,不可导入:统一
import sys后用sys.monitoring/sys.monitoring.events; - ID 即契约:0~5 任选,优先使用预定义常量(调试器用
DEBUGGER_ID、覆盖率用COVERAGE_ID、Profiler 用PROFILER_ID),use_tool_id注册、free_tool_id释放、get_tool探测占用;同一 ID 不能重复注册(会抛ValueError); - 事件集合用按位或:所有 events 常量都是 2 的幂;
NO_EVENTS(0) 用于显式关闭与空集比较; - 先开事件、再等回调:事件未打开则回调不会被触发;全局与局部都打开也只触发一次,回调需容忍两类来源;
- CALL 附属规则:想收
C_RETURN/C_RAISE,必须同时监听CALL,否则set_events会直接抛ValueError; - 按位置禁用是性能关键:局部事件在回调中返回
DISABLE即关掉当前位置,实现"除断点外零开销";restart_events()可全局复活所有被禁位置;3.15 起其他事件也可按整个 code object 禁用; - 不要用旧 BRANCH:3.14 起已弃用,用
BRANCH_LEFT/BRANCH_RIGHT替代(性能更好且可独立禁用); - 统计 StopIteration 需双保险:
STOP_ITERATION与携带StopIteration的RAISE语义等价、产生时互换,二者都应处理; - 区分 code 与 callable:
CALL系列事件里,第一参 code 是调用发生处的 code object,callable才是被调用对象;无参数调用时arg0 == MISSING,实例方法调用时arg0即self、callable是类上的函数对象; - 扩展模块请用 C API:需要主动产生事件时参考 Doc/c-api/monitoring.rst 的
PyMonitoring_Fire*系列,注意目前无 Limited API 版本。
sys.monitoring让 CPython 首次拥有了一套面向并行多工具、可按 code object 与代码位置精准裁剪的官方监控通道。理解其 Tool ID 隔离、事件分类与DISABLE/MISSING契约后,无论是实现断点调试器、轻量级覆盖率统计还是自研采样 Profiler,你都有了比sys.settrace更精确、更可控的底层接口。
【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考