3. 帧时间分析:用 PresentMon 定位卡顿帧
帧时间分析,说白了就是给每一帧“掐表”。
你想想看,双缓冲卡顿的本质是什么?是某一帧的耗时突然飙升,超过了 16.67ms(60fps)或 8.33ms(120fps)。但光知道“卡了”不够,我们得知道:这一帧到底卡在了哪里?
我个人习惯用 PresentMon 来做这件事。它轻量、免费,而且能直接告诉你每一帧的提交时间、显示时间,以及 CPU 和 GPU 各自干了多久。今天我就带你把它用透。
3.1 为什么非要用帧时间工具?
很多人排查卡顿,第一反应是看帧率(FPS)。但 FPS 是个平均值,它会把卡顿帧“稀释”掉。
举个例子:你跑了一分钟,平均 58fps,看起来还行。但实际上中间有 3 帧卡了 200ms,剩下的帧跑得飞快。平均帧率根本看不出来。
帧时间工具不一样。它记录的是每一帧的绝对耗时。卡顿帧会像一根根尖刺一样,直接戳在你眼前。
核心原则: 排查卡顿,永远看帧时间曲线,不要看平均帧率。平均帧率是骗人的。
3.2 PresentMon 基础用法
PresentMon 是微软开源的工具,原理是 hook 到 DXGI 的 Present 调用上。它不注入代码,只做旁观者,所以对性能影响极小。
3.2.1 快速上手
下载 PresentMon 后,我建议直接用命令行模式,最灵活。基本命令长这样:
PresentMon.exe -process_name YourGame.exe -output_file frametime.csv -captureall
参数说明:
-process_name:指定你要监控的进程名-output_file:输出到 CSV 文件,方便后续分析-captureall:捕获所有 Present 调用,包括垂直同步和窗口切换
跑起来之后,它会实时打印每一帧的数据。你让它跑个 30 秒到 1 分钟,模拟出卡顿场景,然后 Ctrl+C 停掉。
3.2.2 CSV 里到底有什么?
打开生成的 CSV 文件,你会看到一堆列。我挑几个最重要的说:
| 列名 | 含义 | 我的关注点 |
|---|---|---|
| PresentID | 帧序号 | 用来定位具体是哪一帧卡了 |
| TimeInSeconds | 该帧被提交的时间戳 | 看卡顿发生的时刻 |
| MsBetweenPresents | 两帧提交之间的间隔(毫秒) | 这就是“提交卡顿”的直接指标 |
| MsInPresentAPI | Present 调用本身的耗时 | 如果这个值很大,说明 GPU 在排队 |
| MsBetweenDisplayChange | 两帧实际显示到屏幕上的间隔 | 这是“显示卡顿”的直接指标 |
| Dropped | 是否被丢弃(1 表示丢弃) | 丢帧是卡顿的极端表现 |
我的小技巧: 我一般把 MsBetweenPresents 和 MsBetweenDisplayChange 这两列单独拉出来画折线图。前者看 CPU 提交节奏,后者看 GPU 实际显示节奏。两条线一对比,问题出在哪一目了然。
3.3 区分渲染卡顿与提交卡顿
这是本章最核心的内容。拿到帧时间曲线后,我们要回答一个问题:卡顿是渲染太慢导致的,还是提交太慢导致的?
3.3.1 渲染卡顿的特征
渲染卡顿,指的是CPU 或 GPU 在绘制这一帧的内容时花了太多时间。比如场景里突然刷出大量粒子、贴图流送卡住、或者 Shader 编译卡顿。
在 PresentMon 数据里,它的典型表现是:
MsBetweenPresents突然飙升(比如从 8ms 跳到 80ms)MsInPresentAPI保持正常(因为 Present 调用本身没阻塞)- 卡顿帧前后,帧时间曲线呈现“单根尖刺”或“连续几根尖刺”
我曾经在优化一个开放世界项目时,发现每次进入新区域都会卡 200ms。一看 PresentMon,MsBetweenPresents 直接拉满,但 MsInPresentAPI 只有 0.5ms。这就排除了提交问题,最后定位到是地形 LOD 切换时触发了同步加载。
3.3.2 提交卡顿的特征
提交卡顿,指的是CPU 把命令提交给 GPU 的过程被阻塞了。常见原因有:驱动缓冲区满了、垂直同步导致的 Back Pressure、或者双缓冲交换链本身的设计问题。
它的典型表现是:
MsBetweenPresents飙升MsInPresentAPI也跟着飙升(比如从 0.5ms 跳到 50ms)- 卡顿帧往往伴随着
Dropped标记为 1
嗯,这里要注意:提交卡顿经常被误认为是渲染卡顿。因为从外部看,都是帧率掉下去了。但修复手段完全不同——渲染卡顿你要优化 Shader 或 LOD,提交卡顿你要调整交换链参数或驱动设置。
避坑指南: 我曾经遇到过一个案例,帧时间曲线看起来像渲染卡顿,但 MsInPresentAPI 始终很高。排查了两天才发现,是显卡驱动把 Present 调用当成了同步点,导致 CPU 在等 GPU 完成上一帧。这不是渲染问题,是驱动层的提交阻塞。所以,永远不要只看一条曲线。
3.4 实战:用 PresentMon 数据画卡顿分类图
光说理论不够,我给你画一张流程图,帮你快速定位卡顿类型。
3.5 实操步骤:从 CSV 到结论
好了,理论讲完了,我给你一个可以直接上手的步骤清单:
- 采集数据:用 PresentMon 跑 30 秒,模拟出卡顿场景,保存 CSV。
- 画曲线:把
MsBetweenPresents和MsBetweenDisplayChange两列导入 Excel 或 Python,画折线图。 - 找尖刺:找到帧时间超过 33ms(30fps)的帧,标记出来。
- 查 MsInPresentAPI:看这些卡顿帧的
MsInPresentAPI是否也飙升。 - 下结论:
- 如果
MsInPresentAPI正常 → 渲染卡顿 - 如果
MsInPresentAPI飙升 → 提交卡顿
- 如果
- 验证:针对结论做一次小改动(比如关掉垂直同步),再跑一次 PresentMon,看卡顿尖刺是否消失。
我的习惯: 我一般会在 CSV 里加一列公式:=IF(MsBetweenPresents>33, "卡顿", "正常"),然后筛选出所有卡顿帧,批量看它们的 MsInPresentAPI 分布。这样比肉眼扫图快得多。
3.6 常见误区
最后,我列几个我踩过的坑,你遇到了可以少走弯路:
- 误区一:只看 CPU 帧时间。 很多人只盯着
MsBetweenPresents,忽略了MsBetweenDisplayChange。实际上,有时候 CPU 提交很流畅,但 GPU 显示卡了,那是驱动或交换链的问题。 - 误区二:把垂直同步的等待当成卡顿。 垂直同步开启时,如果帧率低于刷新率,
MsBetweenPresents会稳定在 16.67ms 的倍数上。这不是卡顿,是正常行为。卡顿指的是不规律的尖刺。 - 误区三:一次采集就下结论。 卡顿可能是偶发的。我建议至少采集 3 次,每次跑不同的场景,取交集。如果同一个位置每次都卡,那才是真问题。