如果无法衡量,就无法改进。
开尔文勋爵
若要让 HTML5 游戏运行得更快,您必须先找出性能瓶颈,但这可能很难。评估每秒帧数 (FPS) 数据只是第一步,但要想全面了解情况,您必须掌握 Chrome 活动中的细微差别。
about:tracing
工具提供的数据分析有助于您避免采取旨在提高性能但本质上是出于好意而进行的仓促权宜解决方法。这样,您可以节省大量时间和精力,更清楚地了解 Chrome 在每个帧中执行的操作,并利用这些信息优化游戏。
about:tracing
,您好!
借助 Chrome 的 about:tracing
工具,您可以了解 Chrome 在一段时间内的所有活动,而且细节非常丰富,因此一开始可能会感到信息量过大。Chrome 中的许多函数都已预先插桩以进行跟踪,因此您无需执行任何手动插桩,仍然可以使用 about:tracing
跟踪性能。(请参阅后面介绍如何手动插桩 JS 的部分)
如需查看轨迹视图,只需在 Chrome 的万能搜索框(地址栏)中输入“about:tracing”即可。
在跟踪工具中,您可以开始录制、运行游戏几秒钟,然后查看轨迹数据。以下示例展示了数据可能的样子:
是的,这确实令人困惑。我们来聊聊如何阅读它。
每行代表一个要分析的进程,左右轴表示时间,每个彩色方框都是插桩函数调用。其中包含许多不同类型资源对应的行。对游戏性能分析最有帮助的是 CrGpuMain(显示图形处理单元 [GPU] 正在执行的操作)和 CrRendererMain。每个轨迹都包含跟踪期间每个打开标签页的 CrRendererMain 行(包括 about:tracing
标签页本身)。
读取轨迹数据时,您的第一项任务是确定哪一行 CrRendererMain 与您的游戏相对应。
在此示例中,两个候选值分别为 2216 和 6516。遗憾的是,目前没有完善的方法来挑出您的应用,除非您查找执行大量定期更新的行(或者,如果您已使用轨迹点手动插桩代码,则查找包含轨迹数据的行)。在此示例中,从更新频率来看,6516 似乎正在运行主循环。如果您在开始轨迹记录之前关闭所有其他标签页,则更容易找到正确的 CrRendererMain。但仍可能会有针对游戏以外进程的 CrRendererMain 行。
查找您的镜架
在跟踪工具中找到游戏的正确行后,下一步是找到主循环。主循环在轨迹数据中看起来像重复的模式。您可以使用 W、A、S、D 键浏览轨迹数据:A 和 D 键用于向左或向右移动(在时间上前后移动),W 和 S 键用于放大和缩小数据。如果游戏的运行频率为 60Hz,您希望主循环是一个每 16 毫秒重复一次的模式。
找到游戏的心跳后,您可以深入了解代码在每一帧中具体执行了哪些操作。使用 W、A、S、D 键放大,直到您能看清函数框中的文本。
这些框显示了一系列函数调用,其中每个调用都由一个彩色框表示。每个函数都是由其上方的框调用的,因此在本例中,您可以看到 MessageLoop::RunTask 调用了 RenderWidget::OnSwapBuffersComplete,后者又调用了 RenderWidget::DoDeferredUpdate,依此类推。通过读取这些数据,您可以全面了解哪个函数调用了哪个函数以及每次执行所花的时间。
但问题就在这里。about:tracing
公开的信息是 Chrome 源代码中的原始函数调用。您可以根据名称推测每个函数的用途,但这些信息并不完全易于用户理解。查看帧的整体流程很有用,但您需要更人性化的信息来真正了解发生了什么。
添加轨迹标记
幸运的是,您可以通过 console.time
和 console.timeEnd
轻松地向代码添加手动插桩,以创建轨迹数据。
console.time("update");
update();
console.timeEnd("update");
console.time("render");
update();
console.timeEnd("render");
上述代码会在跟踪视图名称中使用指定的标记创建新的框,因此,如果您重新运行应用,则会看到“更新”和“渲染”框,其中显示了每个标记的开始调用和结束调用之间的间隔时间。
借助此功能,您可以创建人类可读的跟踪数据,以跟踪代码中的热点。
GPU 还是 CPU?
对于硬件加速图形,您在性能分析期间可以问的重要问题之一是:此代码是受 GPU 限制还是受 CPU 限制?在每个帧中,您都需要在 GPU 上执行一些渲染工作,并在 CPU 上执行一些逻辑工作;为了了解导致游戏运行缓慢的原因,您需要了解这两种资源之间的工作如何平衡。
首先,在轨迹视图中找到名为 CrGPUMain 的行,该行用于指示 GPU 在特定时间是否处于忙碌状态。
您可以看到,游戏的每一帧都会在 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");
现在,您会看到如下所示的轨迹:
此轨迹告诉我们什么?我们可以看到,所示帧从大约 2270 毫秒变为 2320 毫秒,这意味着每帧大约需要 50 毫秒(帧速率为 20Hz)。您可以在 update 框旁边看到代表渲染函数的彩色方块,但帧完全由 update 本身占据。
与 CPU 上的活动相比,您可以看到 GPU 在每个帧的大部分时间里仍然处于空闲状态。如需优化此代码,您可以查找可在着色器代码中执行的操作,并将其移至 GPU 以充分利用资源。
如果着色器代码本身运行缓慢且 GPU 过载,该怎么办?如果我们从 CPU 中移除不必要的工作,改为在片段着色器代码中添加一些工作,会怎么样?下面是一个不必要的开销很大的片段着色器:
#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);
}
}
使用该着色器的代码轨迹是什么样的?
再次提醒,请注意帧的时长。在这里,重复模式的持续时间为 200 毫秒,从大约 2750 毫秒到 2950 毫秒(帧速率约为 5Hz)。CrRendererMain 行几乎完全空白,这意味着 CPU 在大多数时间处于空闲状态,而 GPU 处于过载状态。这绝对是着色器过重的一个迹象。
如果您无法准确了解导致帧速率较低的原因,可以观察 5 Hz 更新,并可能会想要进入游戏代码,开始尝试优化或移除游戏逻辑。在本例中,这样做完全没有用,因为游戏循环中的逻辑并不是耗时的主要原因。事实上,此轨迹表明,在每个帧中执行更多 CPU 工作实际上是“免费”的,因为 CPU 处于空闲状态,因此向其分配更多工作不会影响帧的耗时。
真实示例
现在,我们来看看真实游戏中的轨迹数据。使用开放 Web 技术构建的游戏的一个很酷的功能是,您可以查看您喜爱的产品的最新动态。如果您想测试性能分析工具,可以从 Chrome 网上应用店中选择喜爱的 WebGL 游戏,然后使用 about:tracing
对其进行性能分析。这是从出色的 WebGL 游戏 Skid Racer 中提取的轨迹示例。
每帧大约需要 20 毫秒,这意味着帧速率约为 50 FPS。您可以看到,工作在 CPU 和 GPU 之间平衡分配,但 GPU 是需求最多的资源。如果您想了解如何对 WebGL 游戏的真实示例进行性能分析,不妨试玩一些使用 WebGL 构建的 Chrome 应用商店游戏,包括:
总结
如果您希望游戏以 60Hz 的速率运行,则对于每个帧,所有操作都必须在 16 毫秒的 CPU 时间和 16 毫秒的 GPU 时间内完成。您有两个可并行使用的资源,并且可以在这两个资源之间切换工作,以最大限度地提升性能。Chrome 的 about:tracing
视图是一款非常有用的工具,可帮助您深入了解代码的实际操作,并通过解决正确的问题最大限度地缩短开发时间。
后续操作
除了 GPU 之外,您还可以跟踪 Chrome 运行时的其他部分。Chrome Canary 是 Chrome 的早期版本,经过插桩以跟踪 IO、IndexedDB 和其他几项活动。您应阅读这篇 Chromium 文章,以便更深入地了解跟踪事件的当前状态。
如果您是 Web 游戏开发者,请务必观看以下视频。这是 Google 游戏开发者倡导者团队在 2012 年游戏开发者大会 (GDC) 上就 Chrome 游戏的性能优化所做的演示: