如果不能衡量,就无从改进。
开尔文勋爵
要提高 HTML5 游戏的运行速度,您必须先找出性能瓶颈,但这样做可能比较困难。评估每秒帧数 (FPS) 数据是一个开始,但要全面了解情况,您必须掌握 Chrome 活动中的细微差别。
about:tracing 工具提供的数据洞见可帮助您避免为了提高性能而采取的草率解决方法,但这种解决方法本质上是出于善意的猜测。您可以节省大量时间和精力,更清楚地了解 Chrome 在每一帧帧中的运行情况,并利用这些信息优化您的游戏。
Hello about:tracing
Chrome 的 about:tracing 工具可让您了解 Chrome 在一段时间内的所有活动,而且非常精细,以至于一开始您可能会觉得无从下手。Chrome 中的许多功能都针对开箱即用型跟踪进行了插桩测试,因此无需进行任何手动插桩,您仍然可以使用 about:tracing 来跟踪性能。(请参阅稍后有关手动检测 JS 的部分)
要查看跟踪视图,只需输入“about:tracing”Chrome 的多功能框(地址栏)中。
![Chrome 多功能框](https://web.developers.google.cn/static/articles/abouttracing/image/chrome-omnibox-cd7c2b0952da3.png?authuser=5&hl=sr)
在跟踪工具中,您可以开始录制,运行游戏几秒钟,然后查看轨迹数据。数据可能如下例所示:
![简单的跟踪结果](https://web.developers.google.cn/static/articles/abouttracing/image/simple-tracing-result-1d9ca669e5fb8.png?authuser=5&hl=sr)
没错,我感到很困惑。我们来谈谈如何解读这些数据。
每一行表示要分析的一个进程,左-右轴表示时间,每个彩色框表示一个插桩函数调用。屏幕上有多个不同类型的资源。最有趣的游戏分析是 CrGpuMain,它展示了图形处理器 (GPU) 正在执行的操作,以及 CrRendererMain。每个轨迹都包含轨迹期间每个打开的标签页(包括 about:tracing 标签页本身)的 CrRendererMain 行。
读取轨迹数据时,您的第一个任务是确定哪个 CrRendererMain 行对应于您的游戏。
![突出显示简单跟踪结果](https://web.developers.google.cn/static/articles/abouttracing/image/simple-tracing-result-hig-27a40f191d328.png?authuser=5&hl=sr)
在本示例中,两个候选字词分别为:2216 和 6516。遗憾的是,目前还没有一种更好的方法可以挑选您的应用,只能寻找执行大量定期更新的代码行(或者,如果您使用跟踪点手动对代码进行插桩,则查找包含跟踪数据的代码行)。在此示例中,6516 似乎正在根据更新频率运行主循环。如果您在开始跟踪之前关闭了所有其他标签页,将更容易找到正确的 CrRendererMain。但是,非游戏进程可能仍有 CrRendererMain 行。
查找您的取景框
在游戏的跟踪工具中找到正确的行后,下一步就是查找主循环。主循环看起来像是跟踪数据中的重复模式。您可以使用 W、A、S、D 键浏览跟踪数据:使用 A 和 D 键向左或向右移动(时间前后),使用 W 和 S 键放大和缩小数据。如果您的游戏以 60Hz 的频率运行,则主循环应该是一种每 16 毫秒重复一次的模式。
![看起来像是三个执行框架](https://web.developers.google.cn/static/articles/abouttracing/image/looks-three-execution-fr-f47b55db5f581.png?authuser=5&hl=sr)
找到游戏的检测信号后,您就可以深入了解代码在每一帧上执行的操作。使用 W、A、S、D 键可放大地图,直到您能够看清功能框中的文字。
![执行框架的深层](https://web.developers.google.cn/static/articles/abouttracing/image/deep-an-execution-frame-c862ec9cb9343.png?authuser=5&hl=sr)
这组方框显示了一系列函数调用,其中每个调用都用一个彩色方框表示。每个函数都由其上方的框调用,因此在本例中,您可以看到名为 RenderWidget::OnSwapBuffersComplete 的 MessageLoop::RunTask,它又称为 RenderWidget::DoDeferredUpdate,依此类推。通过读取这些数据,您可以全面了解每次执行的具体内容以及所花的时间。
但在这里,它有点粘人。about:tracing 显示的信息是来自 Chrome 源代码的原始函数调用。您可以根据名称对每个函数的作用进行有根据的推测,但这些信息并非非常人性化。了解帧的总体流程很有用,但您需要掌握一些更直观易懂的信息,才能真正弄清楚到底是怎么回事。
添加跟踪标记
幸运的是,console.time
和 console.timeEnd
有一种友好的方法可以在代码中添加手动插桩来创建轨迹数据。
console.time("update");
update();
console.timeEnd("update");
console.time("render");
update();
console.timeEnd("render");
上面的代码会使用指定的标记在跟踪视图名称中创建新的框,因此如果您重新运行应用,会看到“update”和“render”方框,其中显示每个代码的开始和结束调用之间的间隔时间。
![已手动添加标记](https://web.developers.google.cn/static/articles/abouttracing/image/tags-added-manually-97406d4b80c0e.png?authuser=5&hl=sr)
借助此功能,您可以创建人类可读的跟踪数据以跟踪代码中的热点。
GPU 还是 CPU?
使用硬件加速图形时,您可以在分析期间询问的最重要问题之一是:此代码受限于 GPU 还是 CPU 受限?对于每一帧,您需要在 GPU 上执行一些渲染工作,在 CPU 上执行一些逻辑;为了理解导致游戏运行缓慢的原因,您需要了解这两项资源的工作是如何平衡的。
首先,在名为 CrGPUMain 的跟踪视图上找到一行,该行用于指示 GPU 是否在特定时间处于忙碌状态。
![GPU 和 CPU 跟踪记录](https://web.developers.google.cn/static/articles/abouttracing/image/gpu-cpu-traces-4f4eb3986da83.png?authuser=5&hl=sr)
您可以看到,游戏的每一帧都会导致 CrRendererMain 和 GPU 上的 CPU 工作。上面的轨迹显示了一个简单的用例,其中 CPU 和 GPU 在每 16 毫秒的大部分时间里都处于空闲状态。
如果您的游戏运行缓慢,并且您不确定自己要将哪项资源消耗掉,跟踪视图会非常有用。了解 GPU 线和 CPU 线之间的关系是调试的关键。以与之前相同的示例为例,但在更新循环中添加一些额外的工作。
console.time("update");
doExtraWork();
update(Math.min(50, now - time));
console.timeEnd("update");
console.time("render");
render();
console.timeEnd("render");
现在,您将看到如下所示的跟踪记录:
![GPU 和 CPU 跟踪记录](https://web.developers.google.cn/static/articles/abouttracing/image/gpu-cpu-traces-803b8ee665596.png?authuser=5&hl=sr)
此跟踪记录说明了什么?我们可以看到,图中的帧从 2270 毫秒缩短到 2320 毫秒,即每一帧用时约 50 毫秒(帧速率为 20Hz)。您可以在更新框旁边看到表示渲染函数的彩色框,但帧完全由更新本身主导。
与 CPU 上的情况相反,您会发现 GPU 在每一帧的大部分时间内都处于空闲状态。如需优化此代码,您可以查找可以在着色器代码中完成的操作,并将其移至 GPU 以充分利用资源。
当着色器代码本身运行缓慢且 GPU 过度工作时,会发生什么情况?如果我们从 CPU 中移除不必要的工作,改为在 fragment 着色器代码中添加一些工作,结果会怎样?这是一个不必要的开销的 fragment 着色器:
#ifdef GL_ES
precision highp float;
#endif
void main(void) {
for(int i=0; i<9999; i++) {
gl_FragColor = vec4(1.0, 0, 0, 1.0);
}
}
使用该着色器的代码轨迹是什么样的?
![使用运行缓慢的 GPU 代码时的 GPU 和 CPU 跟踪记录](https://web.developers.google.cn/static/articles/abouttracing/image/gpu-cpu-traces-using-sl-ffb0094996429.png?authuser=5&hl=sr)
同样,请注意帧的时长。在此示例中,重复模式从大约 2750 毫秒变为 2950 毫秒,持续时间为 200 毫秒(帧速率约为 5Hz)。CrRendererMain 行几乎完全为空,这意味着,当 GPU 过载时,CPU 在大部分时间里都处于空闲状态。这肯定意味着您的着色器太重了。
如果您不了解导致帧速率偏低的确切原因,则可能会观察到 5 Hz 更新,并倾向于进入游戏代码,开始尝试优化或移除游戏逻辑。在这种情况下,这绝对没有用处,因为游戏循环中的逻辑并不是在占用时间。事实上,此轨迹表明,每帧执行更多的 CPU 工作本质上是“空闲”的因为 CPU 处于空闲状态,所以加大其工作量不会影响帧所花费的时间。
真实示例
现在,我们来看看从真实游戏中跟踪数据是什么样的。使用开放网络技术构建的游戏有一个很棒的优点,即您可以看到您喜爱的产品的最新动态。如果您想测试性能分析工具,可以从 Chrome 应用商店中选择喜爱的 WebGL 标题,并使用 about:tracing 进行分析。此示例轨迹来自出色的 WebGL 游戏 Skid Racer。
![追踪真实游戏](https://web.developers.google.cn/static/articles/abouttracing/image/tracing-real-game-adc53901fdb79.png?authuser=5&hl=sr)
每一帧似乎需要大约 20 毫秒,即帧速率大约为 50 FPS。您可以看到,CPU 和 GPU 之间的工作是均衡的,但 GPU 是需求最大的资源。如果你想了解分析真实的 WebGL 游戏示例是怎样的,请尝试玩玩 Chrome 应用商店中的一些使用 WebGL 构建的游戏,包括:
总结
如果您希望游戏以 60Hz 运行,那么对于每一帧,所有操作都必须占用 16 毫秒的 CPU 和 16 毫秒的 GPU 时间。您可以并行使用两种资源,并且可以在两种资源之间转移工作,从而最大限度地提升效果。Chrome 的 about:tracing 视图是一种非常有用的工具,它可以帮助您深入了解代码的实际运行情况,并且可以解决相应的问题,从而最大限度地缩短开发时间。
后续操作
除了 GPU,您还可以跟踪 Chrome 运行时的其他部分。Chrome Canary 版是 Chrome 的早期版本,通过设置插桩,可跟踪 IO、IndexedDB 和其他几项活动。建议您阅读这篇 Chromium 文章,更深入地了解跟踪事件的当前状态。
如果您是网页游戏开发者,请务必观看下面的视频。这是 Google 游戏开发技术推广团队在 2012 年游戏开发者大会上关于 Chrome 游戏性能优化的演示文稿: