简介:Tracy 性能分析中文文档是一份面向 C/C++ 等语言开发者的跨平台性能剖析工具手册,覆盖 CPU、GPU 分析以及实时分析、帧分析、采样分析、远程或嵌入式遥测等核心机制,强调对被测程序影响最小化。文档在官方用户手册基础上清晰组织:先介绍快速概览与初始步骤,再分章讲解客户端标记、数据捕获、GUI 分析、CSV 导出及外部数据导入,并包含配置文件说明;其中客户端标记部分涉及区域、帧、锁、内存、GPU、纤程、调用栈等丰富插桩类型。集成方面,详细描述了添加 Tracy 仓库、包含源文件、编译 TracyClient.cpp,以及定义 TRACY_ENABLE、使用 FrameMark 和 ZoneScoped 标记关键代码的完整流程;同时针对 Visual Studio、Linux、Android、Docker 等平台给出环境配置、故障排除、更改网络端口、命名线程和崩溃处理等高级指导。资源包为单个 docx 文档,约 1.26MB,目前已有 229 人浏览学习。通读后可快速上手连接服务端、捕获存储性能数据并完成多维度分析,适合需要低开销定位 CPU/GPU 瓶颈并系统优化项目的开发者参考。
1. 实时分析为什么绕不开 Tracy:从一次半随机掉帧说起
排查游戏掉帧时,统计型分析器只能告诉你热点在哪个函数,却说不清为什么这一帧慢了 8ms、下一帧又恢复了。Tracy Profiler 是另一条路线:用插桩把程序执行切成帧和区域,再靠 x86/ARM 硬件计时器拿到纳秒级分辨率,逐帧还原每个函数的起止时刻、线程交互和锁等待,这正是定位“半随机卡顿”最缺的信息。它是开源的跨平台性能分析工具,覆盖 CPU 分析、GPU 分析、内存分配、上下文切换,也适用于压缩工具这类没有帧概念的程序。这篇笔记基于官方中文用户手册整理,从集成、插桩、踩坑到分析一条线走完。
2. 把 Tracy 接进项目:submodule、全局宏与首次 Connect
2.1 先定两件事:版本和构建配置
Tracy 支持 MSVC、GCC、Clang,需要 C++11 或更新编译器。确认支持的平台包括 Windows x86/x64、Linux x86/x64/ARM/ARM64、Android、FreeBSD、WSL、macOS、iOS、QNX,MinGW 可能可用但无主动维护。第一个决策是用稳定版 tag 还是 master 分支:稳定版没有破坏性更改、没有重大 UI 调整和协议变更,但修复只进 master;新项目我一般直接跟 master,已经在线上跑的项目锁最新 tag 更稳妥。
第二个决策是构建配置。Tracy 客户端默认是禁用状态,代码只有在定义TRACY_ENABLE宏时才会编译进插桩逻辑。这里有两个坑要提前说:第一是拼写,必须是TRACY_ENABLE而不是TRACY_ENABLED;第二是不能赋值,TRACY_ENABLE=0是无效的,Tracy 只检查宏是否“被定义”。这个宏必须全局定义,通过 CFLAGS 或 CMake 的target_compile_definitions传,而不能在某一个源文件里#define。原因是每个编译单元都会检查这个宏,局部定义意味着其他文件里所有插桩都被编译成空操作,服务端自然什么都收不到。编译时务必开常规优化选项,用发布构建做分析,分析 Debug 版没有意义——未优化代码和额外检查会改变程序行为,最后拿到的是“调试版本的行为”而非线上行为。
2.2 最小集成三步:两个源文件、两个宏、一次 Connect
第一步,在项目里添加 Tracy 仓库。官方推荐方式是 git 子模块,这样后续升级只要git submodule update一下:
git submodule add <tracy仓库地址> 3rdparty/tracy如果整个仓库已经推送到远端,克隆时加--recurse-submodules就能一起拉下来。不方便用 submodule 也没关系,Tracy 的全部源码都在public目录下,直接拷进项目同样可以。
第二步,把public/TracyClient.cpp加入构建目标,头文件路径指向public/。这里给一个最小可编译的 C++ 例子:
#include <tracy/Tracy.hpp> void HeavyTask() { ZoneScoped; // 区域标记:进入函数时自动记录开始时间戳 // 模拟一段耗时工作 for (volatile int i = 0; i < 10000; i++); } int main() { for (int frame = 0; frame < 600; ++frame) { HeavyTask(); FrameMark; // 告诉 Tracy 这一帧结束了 } return 0; }ZoneScoped必须写在函数作用域的第一行,离开作用域时自动发出结束事件,不需要手动配对;FrameMark放在每帧循环末尾,Tracy 以帧为基本单位切分时间线,掉帧在时间线上非常直观。当TRACY_ENABLE未定义时,这些宏会被编译成空操作,几乎没有成本,所以不需要维护两套插桩代码。
第三步,编译并同时运行应用和 Tracy 服务端。服务端是一个独立的 GUI 程序,既负责接收数据也负责展示。这里有个概念容易绕晕:被分析的应用叫“客户端”,分析器反而叫“服务端”,因为插桩端只负责采集和发送,真正的数据处理、存储都在分析器这边。启动服务端后点击 Connect,应用在运行且网络通畅时几秒内就会出现数据。
2.3 CMake 与 Meson 接入:两种可直接抄的配置
CMake 项目在add_subdirectory之前设置选项,然后链接别名目标:
option(TRACY_ENABLE "" ON) option(TRACY_ON_DEMAND "" ON) add_subdirectory(3rdparty/tracy) target_link_libraries(app PRIVATE Tracy::TracyClient)TRACY_ENABLE控制插桩是否编译进代码,TRACY_ON_DEMAND是按需分析模式,只有服务端连接后客户端才开始记录,适合长时间运行的服务或只关心某一段逻辑的程序。如果项目全局开了 LTO(CMAKE_INTERPROCEDURAL_OPTIMIZATION)后遇到编译时间过长或链接问题,Tracy 提供TRACY_LTO选项,可以在 CMake 配置阶段单独为自己的目标启用 LTO,不影响项目其他部分。
Meson 走 Wrap 流程,在subprojects/tracy.wrap写:
[wrap-git] url = <tracy仓库地址> revision = head depth = 1revision = head表示跟踪 master 分支,要锁定某个版本就把这个字段改成对应的 git tag。然后在meson.build里声明依赖:
tracy = dependency('tracy', static: true)这个依赖会自动把TRACY_ENABLE等宏定义透传到最终目标,不需要手动加。官方还建议分析构建用debugoptimized类型,因为既要保留调试注释又要优化代码:
meson setup build --buildtype=debugoptimized -Dtracy_enable=true如果不这么做,源码行号和变量信息可能在插桩端丢失,后面分析调用栈时缺关键一跳。
2.4 服务端构建、端口与环境稳定性
服务端构建依赖几个库:Unix 下需要libpthread和libdl,FreeBSD 还要libexecinfo。可以直接编译 profiler 工程,也可以把服务端嵌入到自己的应用里做成自包含分析工具。默认通信端口是 8086,TCP 协议,客户端另有 UDP 广播宣告存在;改端口有几种方式:TRACY_PORT宏同时改数据口和广播口,TRACY_DATA_PORT和TRACY_BROADCAST_PORT分别改,运行时还能设环境变量TRACY_PORT只改数据连接口,好处是不用重编客户端。
首次连接建议按这个清单验证数据是否正常:连接信息弹窗里能看到客户端名称、机器名和跟踪参数;时间线上每帧之间有FrameMark对应的分隔;展开任意线程,ZoneScoped区域有起止时间和次数。能看到这三样,集成就算通了。
环境稳定性对结果可信度影响很大。先关掉浏览器、音乐播放器这类非必要程序,确保调试器没有挂接被分析进程——调试器本身就会改变计时结果。CPU 的超线程、睿频和节能策略也会让数据波动,最理想的做法是把核心资源专注给被测程序,至少保证分析期间机器上没有其他重负载在跑。虚拟机和云主机会干扰时间戳与硬件计数器,Tracy 设计目标是裸机分析,云环境里拿到的数字只能看趋势,别当绝对值。
3. 客户端标记:ZoneScoped 开销 2.25ns 的底气与四类常用插桩
3.1 ZoneScoped 区域标记与开销真相
先说手册里的实测数据:官方在纹理压缩测试中,12 线程并行压缩一张 16384×16384 测试图,捕获了超过 2 亿个区域,每个区域“开始+结束”事件平均只带来 2.25ns 开销。这个数字比大部分分析器低一个数量级,原因是区域记录不是“创建一个对象再发消息”,而是往线程本地的一块无锁环形缓冲区里写固定格式的记录。手册给的 x64 汇编印证了这一点:读取 TLS 指针、检查缓冲区余量、写事件类型、执行 RDTSC 拿时间戳、写入静态源码位置结构指针,然后递增计数器。缓冲区以 128 项为一个批次,用满才触发一次分配换新缓冲区,运行期没有堆分配。
日常用的最多的是ZoneScoped,它自动用函数名作为区域名。需要改名字或附加信息时,用这些变体:
| 宏 | 效果 |
|---|---|
ZoneScopedN("name") | 自定义区域名 |
ZoneScopedC(0xFF0000) | 指定颜色 |
ZoneScopedNC("name", 0xFF0000) | 名字加颜色 |
ZoneValue(v) | 关联一个数值,显示成图表 |
递归场景有个硬限制:同一个源码位置在一个区域栈里的出现次数不能超过 255 次,做 UV 展开、树的递归遍历这类逻辑时要想办法拆源码位置,否则插桩数据会异常。
3.2 帧、锁、内存与消息
FrameMark负责基本帧标记。无帧概念的程序也可以定义逻辑帧:用FrameMarkNamed创建次级帧集合,比如把“读数据、压缩、写文件”各算一个阶段;非连续帧用FrameMarkStart/FrameMarkEnd手动控制。帧图像功能可以把每帧屏幕截图关联到时间线,查渲染卡顿特别直观,但带宽占用大,建议按需开:
FrameMark; FrameImage(ptr, w, h, stride, scale, offset);锁标记是把普通锁换成 Tracy 认识的类型:
static TracyLockable(std::mutex, g_lock); void Render() { std::lock_guard<TracyLockable> lock(g_lock); LockMark(g_lock); // 临界区逻辑 }TracyLockable在构造时自动注册,LockMark告诉 Tracy 当前线程持有了这把锁,等待时间会显示在时间线上,持锁过长的区域会在统计里标出来。内存跟踪用另一组宏:
void* p = malloc(256); TracyAlloc(p, 256); // 释放前 TracyFree(p);这组宏统计每次分配的大小和调用栈,配合第 5 章的内存窗口能查持有者。消息日志用TracyMessage把文本发到分析器,适合在关键路径上打点;TracyAppInfo可以上报版本号、构建号这类信息,跟踪文件里直接可见。
3.3 GPU 区域与纤程:渲染线程的正确姿势
GPU 插桩是 Tracy 区别于传统 CPU 分析器的地方。OpenGL 用TracyGpuZone,Vulkan 是TracyVulkanZone,D3D11 是TracyD3D11Zone,D3D12 是TracyD3D12Zone,Metal、OpenCL、CUDA 都有对应宏。GPU 区域的数据主要来自 GPU 时间戳,能看出 CPU 提交指令和 GPU 实际执行之间的错位——瓶颈在提交侧还是执行侧一目了然。每个 GPU 上下文有自己的时间线,跨上下文对比时注意时间基准是否对齐。macOS 上无法分析 OpenGL,这是手册里明确写的平台限制,Apple 平台整体开销也偏高,并且程序退出时分析会被中断,尾部区域和内存事件可能丢失。
纤程在用户态切换执行流,不经过操作系统调度,Tracy 默认感知不到。切换时必须手动调用TracyFiberEnter(fiber)和TracyFiberLeave,否则区域会渲染在线程上,调用栈和时序全部串掉。项目里用了协程库的话,这个问题要提前排查,不然数据出来会觉得玄学问题一堆。
3.4 按需模式与短生命周期程序:两个控制宏
默认情况下 Tracy 在程序进入 main 之前就开始记录事件,事件先存在本地内存里,等服务端连上来再上传。一个长时间运行的应用如果一直没人连接,内存占用可能涨到数 GB。TRACY_ON_DEMAND宏改成按需模式后,只有服务端建立连接才开始记录;代价是每个事件多一点簿记开销,客户端要维护一致状态给分析器“补历史”。
另一个实用宏是TRACY_NO_EXIT。短生命周期程序(比如 1 秒内跑完的压缩工具)在退出后客户端也跟着没了,服务端根本来不及连接。设置这个环境变量后,程序即使跑完也会挂在那里等待连接,分析完再手动结束:
TRACY_NO_EXIT=1 ./my-compressor input.dat配合按需模式,这是命令行工具做性能分析的推荐姿势。客户端发现默认是开启的,它会向本地网络广播自己的存在,局域网里多台设备都能看到;不需要广播时定义TRACY_NO_BROADCAST关掉。只想让本机连接就定义TRACY_ONLY_LOCALHOST,多网卡机器上这个很有用。
4. 集成避坑指南:五个翻车点与平台限制清单
这一章全部来自我照着手册复现时实际踩过的坑,以及社区里问得最多的问题,每条按现象、原因、解决写。
4.1 TRACY_ENABLE 只在单个源文件里定义
现象:编译链接全部成功,服务端连接正常,但时间线一片空白,插桩区域一个都没有。
原因:TRACY_ENABLE只在一个 .cpp 文件里写了#define,其他编译单元编译插桩头文件时宏不存在,ZoneScoped被展开成空操作。Tracy 的宏是按编译单元检查的,不跨文件生效。
解决:全局传宏。CMake 里用target_compile_definitions(app PUBLIC TRACY_ENABLE),Makefile 在 CFLAGS 里加-DTRACY_ENABLE。拼写必须是TRACY_ENABLE,不能赋值成 0。
4.2 MSVC 的“编辑并继续”导致编译失败
现象:MSVC 编译报 error C2131,提示表达式未计算为常量,后面跟着__LINE__Var相关的说明。
原因:项目开了“编辑并继续”(/ZI),这个功能让编译器在某些 C++ 标准行为上不合规。Tracy 用__LINE__构造静态源码位置对象时触发了这个冲突。
解决:项目属性里把调试信息格式从 /ZI 改成 /Zi,关闭编辑并继续。团队项目最好把这条写进规范,不然谁新建了调试配置谁就翻车。
4.3 虚拟机里 RDTSC 不可用与采样失真
现象:在 VM 或云主机里分析,服务端提示“CPU 不支持 RDTSC”或“CPU 不支持不变 TSC”;即使能连上,调用栈采样频率明显变低,部分采样缺少时间戳。
原因:虚拟化干扰了时间戳计数器和硬件性能计数器,问题因虚拟机配置而异,云环境尤其明显。
解决:Tracy 设计目标是裸机分析,不要在虚拟化环境里做性能判断。如果只能在 Windows VM 里跑,可以定义TRACY_TIMER_QPC强制走 QueryPerformanceCounter 重建应用,但时间分辨率会明显下降,采样数据只能做统计,没法精确对齐时间线。虚拟机里对比“改了代码前后的趋势”可以,别拿绝对数值当结论。
4.4 多 DLL 项目:多个 TracyClient.cpp 实例互相踩踏
现象:可执行文件加好几个插件 DLL 都接了 Tracy,运行时崩溃、冻结或数据乱码,偶尔还没有任何报错。
原因:每个模块各自编译了一份TracyClient.cpp,进程里出现多个 Tracy 实例,各自的缓冲区独立运作又共享同一端口,互相干扰且不会有编译警告。
解决:把TracyClient.cpp单独编成一个 Tracy DLL,可执行文件和所有需要插桩的 DLL 都链接到这个 DLL;Windows 上要定义TRACY_IMPORTS。如果模块是运行期动态加载卸载的,再同时定义TRACY_DELAYED_INIT(延迟到首次请求再初始化)和TRACY_MANUAL_LIFETIME(手动调用 StartupProfiler / ShutdownProfiler)。这里最容易犯的错是多库之间宏不一致,Tracy 对此没有任何错误提示,只能靠构建系统强制统一。
4.5 Android 的 /proc 限制与上下文切换无数据
现象:Android 8.0 以上设备,系统 CPU 使用率窗口一直是空的,上下文切换捕获也拿不到数据。
原因:从 Android 8.0 起系统禁止应用访问 /proc,这是安全增强,官方没有提供绕过开关。
解决:root 测试设备后,在 shell 里执行setenforce 0关闭 SELinux 限制,mount -o remount,hidepid=0 /proc启用外部进程检查,再降低性能计数器权限。这几条命令会降低设备安全性,只能在专用测试机上操作,别在正式设备上试。
4.6 需要知道的硬性限制
| 限制项 | 上限值 |
|---|---|
| 每把锁上使用的唯一线程数 | 64 |
| 唯一源码位置数 | 65534 |
| 同一区域栈内同源码位置递归次数 | 255 |
| 分析会话时长 | 1.6 天 |
| 内存释放事件数 | 2^32 |
| 唯一调用栈数 | 2^24 |
还有几个通常不用担心的限制:只支持小端 CPU,虚拟地址空间必须限制在 48 位,服务端 CPU 需要支持非对齐内存访问。遇到这些数字大多意味着代码或架构设计有问题,先想能不能降低插桩密度,而不是加大机器配置。Windows 通用应用(UWP)没有调用栈采样和系统分析,Apple 平台不支持TRACY_NO_EXIT且退出时数据会中断,接这些平台前先查功能支持矩阵。中文手册把这些散落的问题聚在了一章里,排查时直接翻本地副本比在线查快得多,这也是我保留这套文档的原因之一。
5. 分析捕获数据:从帧时间图到 CSV 导出的完整操作路径
5.1 主窗口:先读帧时间图,再进时间线
连接成功或加载跟踪文件后,主窗口顶部是帧时间图表,每个竖条代表一帧的耗时,颜色按是否超过目标帧时间区分。第一步就是找尖峰:超过分配时间的帧就是卡顿点。鼠标点击尖峰,下方时间线会自动跳到对应位置。时间线视图按线程分道,横向是时间轴,每个ZoneScoped区域显示为带名字的块,锁等待显示为另一层颜色条,图表数据单独一行。时间刻度支持缩放,锁一段范围后还可以加文字注释,事后重新打开跟踪文件时,注释会标记出当时关注的位置,这个习惯能省很多回看时间。
主窗口控制菜单里最重要的是通知区域,连接状态、带宽、丢包都会显示在这里。如果客户端数据量超过网络带宽,通知区会提示丢帧,这时降低插桩密度或开按需模式比加大缓冲更实际。
5.2 三个统计维度:插桩、采样与 GPU 模式
统计窗口有三种模式,对应三类数据来源。插桩模式统计所有ZoneScoped区域的总耗时、自身耗时、调用次数和平均耗时,适合看函数级时间分配。采样模式不依赖插桩,Tracy 周期性采样调用栈,得到程序热点的概率分布;它的局限是半随机卡顿这种低频高影响事件很难被采到,所以要先用采样模式找到大概方向,再用插桩模式精准定位。GPU 模式只看 GPU 区域,CPU 和 GPU 各自统计,对比提交耗时和执行耗时可以确认瓶颈位置。三种模式的切换在统计窗口顶部,数据互不污染,可以来回切着看。
5.3 查找区域、火焰图与调用栈
区域多的时候,在时间线上肉眼找是不现实的,用查找区域窗口。它列出所有源码位置,按名字过滤,支持正则;点击任意区域跳到它在时间线上的位置。火焰图是另一个高频入口,从时间线或帧时间图生成,把区域按调用关系叠成栈,宽度代表累计耗时,一眼看出占比最大的路径。火焰图和统计窗口的粒度不同,前者是时间线视角,后者是聚合视角,排性能问题时配合使用。
调用栈窗口展示某个区域的完整调用路径。要让调用栈可用,需要给插桩代码提供调试符号:Windows 下用 dbghelp,Linux 依赖标准符号解析,外部库要单独配置符号路径。内联帧默认参与解析,如果内联导致栈太深或符号错乱,可以关掉内联帧解析,用离线符号解析兜底。
5.4 内存窗口:分配追踪与栈树
内存窗口有四块:分配列表是全部分配释放的记录,活动分配是当前仍存活的内存,内存映射按时间线展示分配与释放的堆叠,自底向上和自顶向下两棵调用栈树分别回答“谁持有内存”和“内存从哪分配”。排查泄漏我一般这么走:先看活动分配列表里增长最快的分配大小,再右键打开自顶向下栈树找到分配源,回代码里查引用计数。对比两份跟踪文件可以确认泄漏是某次修改引入的。内存池(TracyPool)可以单独跟踪 GPU 或引擎层的内存域,避免和业务分配混在一起。
5.5 导出 CSV 与导入外部数据
统计窗口里选中区域集导出 CSV,字段包括区域名、调用次数、总耗时、平均耗时和最大耗时,适合在表格软件里做排序和制图。做回归对比时,我习惯把两份构建的 CSV 拉进表格按总耗时降序排,重点看变化超过 10% 的项,比肉眼看时间线快得多。导入外部数据的场景是某些受限平台必须用别的工具抓数据,再拿回 Tracy 里看;支持路径是把外部格式转换成 Tracy 可读格式后导入。需要注意的是,外部采样数据可能没有时间戳,导入后只能做统计,没法在时间线上精确对齐——接之前先看手册第 7 章确认你的分析器输出能不能转。
6. 进阶技巧:用 C API 做跨语言插桩,用汇编模式看指令级开销
Tracy 的 C++ 宏在能直接改源码的项目里很顺手,但只要代码嵌在脚本引擎或解释器里就失灵了。C API 是手册里明确写给“其他语言绑定”用的入口,Lua、Python、Fortran 的官方支持都建立在它上面。它的思路是手动管理区域生命周期:
#include <tracy/TracyC.h> ___tracy_c_zone_context ctx; char name[64] = "ScriptedTask"; ctx = ___tracy_emit_zone_begin(___tracy_source_location(name), 0); // 被监控的调用 ___tracy_emit_zone_end(ctx);C API 没有作用域自动回收,开始和结束必须配对,写绑定层时最容易漏的是异常路径——脚本抛错跳出去,结束事件没发,时间线上会出现一个没有收口的区域。我给某个图像处理 Demo 做脚本语言绑定时,第一版把结束调用放在了正常路径上,异常分支漏了,查了半天才发现。从那以后,每次写跨语言绑定,我都会先做异常注入测试,在区域中间故意抛错,确认结束事件一定会在被 unwinding 包住的地方发出。
如果项目不需要跨语言,另一个值得优先练的技巧是源码视图的汇编模式。选中一个区域后切到汇编模式,Tracy 会显示该函数的反汇编,每条指令旁边有对应的采样统计和时间开销。这不是看热闹用的:它能直观验证缓存 miss、分支预测失败或者 SIMD 指令的吞吐瓶颈。指令级统计依赖硬件采样,虚拟机里不可靠,所以这个技巧的前提仍然是裸机。
我自己的使用顺序固定三条:先用采样模式找到热点区域,再用插桩模式确认调用关系,最后用汇编模式看局部指令开销。从那次异常路径翻车之后,我每次接一套新语言或新平台,都强制先跑一遍最小插桩加 C API 的冒烟测试,确认开始结束事件在正常和异常两条路径上都配对,再上完整业务代码。这套流程省下的排查时间远超预期,希望帮到你。
本文还有配套的精品资源,点击获取