)
CPython perf jitdump 时间戳修复深度解析macOS 上 JIT 代码映射为何无法与采样样本对齐gh-150723【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython本文聚焦 CPython 的 perf jitdumpJIT dump支持在 macOS 上的时间戳缺陷修复gh-150723事件时间戳原先使用CLOCK_MONOTONIC而 macOS 采样分析器如 samply用mach_absolute_time()给样本打时间戳两者时钟域不一致导致 JIT 代码映射无法与采样样本对齐、所有 Python 帧都无法被解析。文章将结合 修复说明 与 Python/perf_jit_trampoline.c 的完整实现讲解 jitdump 二进制格式、时间来源的选取逻辑、配套修复thread_id位宽以及如何用 测试套件 端到端验证该功能。一、背景CPython 如何让采样分析器看到 Python 函数Linux 的perf与 samply 这类采样式 CPU 分析器只能感知原生C符号它们看到的调用栈里只有_PyEval_EvalFrameDefault这类解释器 C 函数无法区分当前执行的是哪个 Python 函数。CPython 3.12 起引入“trampoline蹦床”机制解释器为每个 Python 函数在代码区中插入一段现场编译的蹦跳代码并把“蹦跳代码 ↔ Python 函数名/文件名”的映射关系告知分析器。映射有两条通道详见 Doc/howto/perf_profiling.rst后端开关映射文件说明perf-map 文本后端-X perf/ 环境变量PYTHONPERFSUPPORT1/tmp/perf-$PID.map简单文本依赖 frame pointer 展开栈jitdump 二进制后端-X perf_jit/ 环境变量PYTHON_PERF_JIT_SUPPORT1/tmp/jit-$PID.dump二进制 jitdump 格式携带 DWARF.eh_frame展开信息用于无 frame pointer 的构建本次修复针对的正是第二条通道。-X perf_jit的启用路径在 Python/initconfig.c 中定义sys.activate_stack_trampoline(perf_jit)则在 Python/sysmodule.c 中切换到 jit 回调解释器启动时依据config-perf_profiling的取值在 Python/pylifecycle.c 选择_Py_perfmap_callbacksperf-map或_Py_perfmap_jit_callbacksjitdump。适用前提该支持仅在启用了PY_HAVE_PERF_TRAMPOLINE的平台构建生效覆盖部分架构的 Linux 与 macOSmacOS 上从 Python 3.15 起可用 samply。可用python -m sysconfig | grep HAVE_PERF_TRAMPOLINE确认本机是否支持。二、缺陷本身两个时钟域导致“样本对不上映射”修复说明原文只有一段但信息量非常完整Fix perf jitdump timestamps on macOS. Events were stamped usingCLOCK_MONOTONIC, but macOS profilers timestamp their samples withmach_absolute_time(). The mismatch prevented the JIT code mappings from lining up with the samples, so no Python frame could be resolved.它揭示了一个容易被忽视的跨平台事实jitdump 文件的作用方式分析器读取/tmp/jit-$PID.dump把其中每条JR_CODE_LOAD事件记录为“在事件时间戳 T 时某段机器代码加载到了虚拟地址 VMA”。之后的采样处理逻辑是某个样本 S 落在地址 VMA 区间内且样本时间 ≥ 该映射生效时间 T时样本才能解析到该 JIT 符号。缺陷映射事件 T 取自CLOCK_MONOTONICLinux 单调时钟自 boot 起的纳秒而 macOS 上 samply 等分析器的样本时间取自mach_absolute_time()Mach 时钟 tick自系统启动起的硬件 tick 数数值量级与单调时钟完全不同。两个数值处于完全不同的时间轴不同的纪元、不同的单位、不同的推进速率比较必然失败——结果就是没有任何 Python 帧能被解析整个 jitdump 文件形同虚设。修复原则映射事件时间戳必须与分析器样本处于同一时钟域。对 macOS 而言就是mach_absolute_time()的纳秒化数值。这不是一个“时间显示不准”的表层问题而是二进制协议双方时钟契约被破坏的深层问题jitdump 格式本身不规定时钟源而是隐含要求“与消费方样本的时钟一致”Linux 上这个消费方恰好是CLOCK_MONOTONICmacOS 上则是mach_absolute_time()。三、源码修复剖析get_current_monotonic_ticks() 的平台分派修复落在 Python/perf_jit_trampoline.c 的时间工具函数中这是所有 jitdump 事件时间戳的唯一来源static int64_t get_current_monotonic_ticks(void) { #if defined(__APPLE__) // On macOS the jitdump file is consumed by profilers (such as samply) that // timestamp their samples using mach_absolute_time(). The jitdump event // timestamps must use the same clock domain, otherwise the JIT code // mappings cannot be lined up with the samples. static mach_timebase_info_data_t timebase {0, 0}; if (timebase.denom 0) { (void)mach_timebase_info(timebase); } uint64_t ticks mach_absolute_time(); return (int64_t)(ticks * timebase.numer / timebase.denom); #else struct timespec ts; if (clock_gettime(CLOCK_MONOTONIC, ts) ! 0) { Py_UNREACHABLE(); // Should never fail on supported systems return 0; } /* Convert to nanoseconds for maximum precision */ int64_t result ts.tv_sec; result * nanoseconds_per_second; result ts.tv_nsec; return result; #endif }值得逐点拆解的实现细节mach_absolute_time()返回的是 tick 数而非纳秒必须乘以mach_timebase_info()提供的换算比率numer/denom才能得到纳秒。代码把这个比率缓存进static局部变量denom 0时惰性初始化避免每条事件都做一次 Mach 系统调用——jitdump 事件在 JIT 编译热点路径上可能被高频写入这里的性能考量是合理的。Linux 分支保持CLOCK_MONOTONIC不变与内核侧perf的样本时钟一致这也印证了“jitdump 事件时钟 消费方样本时钟”这一设计契约。单调时钟的选用理由源码注释明确写道单调时钟不受系统时钟调整NTP 校时、手动改钟影响能保证事件之间以及与样本之间的时序关系稳定可靠。注意区分两类时间戳文件头Header.time_stamp用的是get_current_time_microseconds()gettimeofday墙钟微秒见 perf_jit_trampoline.c仅作元数据而每条事件PerfUnwindingInfo与PerfLoad的base.time_stamp才是走上述修复后的单调时钟写入处 与 CodeLoad 事件处。四、同一 issue 的配套修复JR_CODE_LOAD 的 32 位 thread_idgh-150723 还包含另一条修复见 同 issue 的另一条 NEWSJR_CODE_LOAD记录的thread_id字段被写成了 64 位而不是 jitdump 格式要求的 32 位偏移了整个记录的后续字段布局同样导致分析器无法解析 Python 帧。对应实现perf_jit_trampoline.c#if defined(__APPLE__) // The jitdump format defines the thread id field as a 32-bit value, but // pthread_threadid_np() returns a 64-bit id. Truncate it to 32 bits to // keep the record layout identical to other platforms. uint64_t thread_id 0; pthread_threadid_np(NULL, thread_id); ev.thread_id (uint32_t)thread_id; #elif defined(HAVE_GETTID) ev.thread_id gettid(); #else ev.thread_id syscall(SYS_gettid); // Get thread ID via system call #endif两条缺陷叠加时钟域错位 记录布局偏移解释了为什么修复前 macOS 上的症状是“完全没有 Python 帧”而不是部分解析。这也提醒我们jitdump 是紧凑二进制协议任何一个字段宽度或顺序出错都会让整条记录乃至后续解析全部错位。五、纵深理解CPython 的 jitdump 写入流程理解上述修复为何致命需要知道 CPython 是如何完整实现 perf jitdump 协议的Python/perf_jit_trampoline.c仅当PY_HAVE_PERF_TRAMPOLINE定义时编译1. 文件格式结构与 perf 工具文档格式严格对应文件头Header魔数0x4A695444JiTD、版本 1、结构大小、ELF 机器类型EM_X86_64/EM_AARCH64/EM_RISCV等由GetElfMachineArchitecture()给出、进程 PID、墙钟时间戳、flags五种事件类型PerfLoad(0)、PerfMove(1)、PerfDebugInfo(2)、PerfClose(3)、PerfUnwindingInfo(4)每个事件以struct BaseEvent { uint32_t event; uint32_t size; uint64_t time_stamp; }开头——修复的正是这个time_stamp的时钟域CodeLoadEvent含process_id、32 位thread_id、vma、code_address、code_size、code_id后随 NUL 结尾的符号名与机器码字节CodeUnwindingInfoEvent携带.eh_frame展开数据含 20 字节、经_Static_assert校验布局的EhFrameHeader。2. 初始化perf_map_jit_init()源码创建/tmp/jit-pid.dump0666 权限fdopen后设置 2MB 用户态缓冲setvbuf因为 jitdump 写入发生在高频路径上Linux把文件首页以mmap(PROT_READ|PROT_EXEC)映射到进程地址空间——perf正是靠扫描/proc/pid/maps中匹配 jitdump 命名模式的映射来发现目标进程macOS 分支跳过该映射注释说明 samply 通过 preload 机制查找 jitdump且该 mmap 很慢并把映射区域用_PyAnnotateMemoryMap打上 cpython:perf_jit_trampoline 内存标注生成build_id_salt (pid 32) ^ monotonic_ticks用于让每个合成 DSO 的 build-id 唯一防止 perf 把不同蹦跳的相同字节合并到同一 DSO。3. 写入一个函数映射perf_map_jit_write_entry_with_name()源码符号名格式化为py::函数名:文件名py::前缀用于在混合语言剖析中区分 Python 帧通过_PyJitUnwind_BuildEhFrame()声明见 Include/internal/pycore_jit_unwind.h生成 DWARF.eh_framejitdump 模式下 FDE 的 PC 字段采用PC 相对编码使 perf 注入生成的合成 DSO 自包含整个记录序列在map_lock互斥锁内串行写入防止并发 JIT 编译交错记录或复用code_id先写PerfUnwindingInfo并 8 字节对齐填充再写PerfLoad机器码尾部会写入一个由build_id_salt ^ (code_id 32) ^ code_size派生的 8 字节 marker确保每个合成 DSO 的 build-id 唯一但不改动实际执行的代码内存布局上为每个合成 DSOELF 头 .text 展开信息保留不重叠的地址范围padding 在初始化时按实际.eh_frame大小向上取整到 16 字节计算代码区按 32 字节对齐。4. 收尾perf_map_jit_fini()持锁fclose刷盘、munmap解除对 perf 的“活跃 JIT”信号、重置回调状态。六、验证方式测试套件与真实剖析流程1. 端到端回归测试Lib/test/test_samply_profiler.py 中的TestSamplyProfilerWithJitDump正是针对 gh-150723 的回归测试它走的是二进制 jitdump 后端-Xperf_jit与走 perf-map 文本后端的TestSamplyProfiler区分开来class TestSamplyProfilerWithJitDump(unittest.TestCase, TestSamplyProfilerMixin): # Regression test for gh-150723: exercises the binary jitdump backend # (-Xperf_jit) end to end through samply, unlike TestSamplyProfiler which # uses the textual perf-map backend (-Xperf). def run_samply(self, script_dir, script, activate_trampolineTrue): if activate_trampoline: return run_samply(script_dir, sys.executable, -Xperf_jit, script) return run_samply(script_dir, script)其验证逻辑继承自 TestSamplyProfilerMixin运行samply record --save-only -o profile.json.gz python -Xperf_jit perftest断言剖析输出 JSON 中出现py::foo:script、py::bar:script、py::baz:script三个 Python 帧再验证关闭 trampoline 时这些帧不出现。测试在 tearDown 中清理/tmp/jit-*.dump与/tmp/jitted-*.so残留文件并要求PY_HAVE_PERF_TRAMPOLINE 1才执行否则跳过。这直接印证了修复效果的可判定标准samply 输出里能否解析出py::前缀的 Python 符号。2. 手工复现macOS samply / Linux perf以官方 howtoDoc/howto/perf_profiling.rst给出的流程为准。jitdump 后端在 Linux 上的完整命令序列$ perf record -F 9999 -g -k 1 --call-graph dwarf -o perf.data python -Xperf_jit my_script.py $ perf inject -i perf.data --jit --output perf.jit.data $ perf report -g -i perf.jit.dataperf inject --jit会读取perf.data、自动拾取 Python 生成的 jit dump 文件并合并 JIT 信息同时在当前目录生成一批jitted-XXXX-N.so每个蹦跳的 ELF 映像。要点与限制需 perf v6.8或含修复的 v6.7.2更早版本因 perf 自身 bug 无法处理 JIT 模式发行版自定义版本号如6.7-3不一定等于6.7.3--call-graph dwarf默认抓 8192 字节栈快照可通过--call-graph dwarf,16384调整-O0构建栈帧更大可加大到 65528上限帧指针构建-fno-omit-frame-pointer可用python -m sysconfig | grep no-omit-frame-pointer检查走文本 perf-map 后端即可jitdump 后端的价值在于为无帧指针构建提供 DWARF 展开macOS 上则直接samply record剖析要求 Python 3.15samply 无法剖析已签名的 Python 可执行文件自编译/未签名/Homebrew 安装可用attach 到运行中进程前需执行一次samply setup自签名。七、结论与边界本修复的核心价值跨平台剖析支持的正确性。CPython 把 JIT 代码映射协议jitdump从“仅与 Linux perf 对齐”扩展为“与消费方时钟域对齐”——macOS 用mach_absolute_time()纳秒化Linux 保持CLOCK_MONOTONIC配合 32 位thread_id的布局修正macOS 上的 samply -Xperf_jit才第一次真正能解析出 Python 帧。从源码结构看get_current_monotonic_ticks()被同时用于事件时间戳与build_id_salt生成因此时钟域修复顺带保证了 salt 在同平台内语义一致两个分支互斥、无共享状态风险面很小。边界说明该机制仅在PY_HAVE_PERF_TRAMPOLINE启用的平台可用macOS 上跳过 mmap 发现机制、依赖 samply 的 preload 发现文本 perf-map 后端-X perf与 jitdump 后端-X perf_jit解决的是不同场景有无 frame pointer二者回调在 Python/jit_publish.c 等处按write_state区分切换。【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考