Reading Chrome's trace output

Before you can speed up an HTML5 game, you have to know where the time is going. Frame rate numbers tell you that something is wrong, but not what. Chrome's about:tracing tool exposes the browser's internal instrumented function calls, giving you a frame-by-frame view of what the CPU and GPU are actually doing.

To open it, type about:tracing into Chrome's omnibox. You can then record a session, run your game for a few seconds, and inspect the resulting data.

Chrome omnibox
Type "about:tracing" into Chrome's omnibox

The tracing view is dense at first glance. Each row corresponds to a process being profiled, time runs left to right, and every colored box is one instrumented function call. For game profiling, the two rows that matter most are CrGpuMain, which shows GPU activity, and CrRendererMain, which shows the renderer process. Since every open tab gets its own CrRendererMain row during a trace, your first job is to identify which row belongs to your game.

Simple tracing result highlighted
Simple tracing result highlighted

There is no clean way to label rows by application, so look for the row with a regular, repeating pattern of activity — that is your main loop. Closing other tabs before recording helps narrow things down. Once you have found the right row, use the W, A, S, and D keys to navigate: A and D pan through time, and W and S zoom in and out. A game targeting 60Hz should show a pattern that repeats roughly every 16 milliseconds.

Looks like three execution frames
Looks like three execution frames

Zoom in far enough and you can read the individual function names in each box. The nesting shows the call stack: each function was invoked by the box above it. A typical frame trace might read MessageLoop::RunTask calling RenderWidget::OnSwapBuffersComplete, which in turn calls RenderWidget::DoDeferredUpdate, and so on.

The catch is that these are Chrome source-level function names, not your application's method names. You can infer a lot from the names themselves, but to connect the trace to your own code you need manual instrumentation.

Adding your own trace points

You can tag sections of your JavaScript with console.time and console.timeEnd:

console.time("update");
update();
console.timeEnd("update");
console.time("render");
update();
console.timeEnd("render");

Running the game again produces new boxes labeled "update" and "render" in the trace, showing exactly how long each tagged block took. That turns the raw Chrome function soup into readable markers for the hotspots in your game logic.

Tags added manually
Tags added manually

GPU-bound or CPU-bound?

With hardware-accelerated graphics, the critical profiling question is whether your frame is limited by the CPU or the GPU. Each frame spends time on both: game logic runs on the CPU and rendering runs on the GPU. The CrGPUMain row tells you when the GPU is busy.

GPU and CPU traces

In a simple balanced case, both rows show bursts of activity followed by idle periods within each 16ms frame. The trace becomes useful when performance is poor and you are unsure which resource is saturated. Consider a game that adds unnecessary work to its update loop:

console.time("update");
doExtraWork();
update(Math.min(50, now - time));
console.timeEnd("update");

console.time("render");
render();
console.timeEnd("render");

The resulting trace shows frames stretching from about 2270ms to 2320ms — roughly 50ms per frame, or 20Hz. The update box dominates the frame; render activity is squeezed into a sliver beside it. The GPU row, meanwhile, sits mostly idle. The optimization path here is to move some of that CPU work into shader code, making better use of the idle GPU.

GPU and CPU traces

The opposite problem shows up when the fragment shader itself is too expensive:

#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 and CPU traces when using slow GPU code
GPU and CPU traces when using slow GPU code

The repeating pattern here spans roughly 200ms, dropping the frame rate to about 5Hz. The CrRendererMain row is nearly empty — the CPU is waiting, not working — while the GPU row is continuously saturated. That is a clear sign of an over-heavy shader. Without this visibility you might be tempted to trim game logic to fix a 5Hz frame rate; the trace shows the CPU is mostly idle, so additional CPU work would be essentially free and would have no effect on frame time.

Profiling a real WebGL game

Because WebGL games run on open web technologies, you can profile third-party titles the same way you profile your own. A trace from the WebGL racer Skid Racer, for example, shows frames of about 20ms — roughly 50 FPS — with work spread across both the CPU and GPU, and the GPU as the busier resource.

Tracing a real game
Tracing a real game

Other WebGL titles worth experimenting with from the Chrome Web Store include Bouncy Mouse, Bejeweled, FieldRunners, Angry Birds, Bug Village, and Monster Dash.

Making the 16ms budget

Sustaining 60Hz means fitting every frame's work into 16ms of CPU time and 16ms of GPU time, with both resources running in parallel. Knowing which one is the constraint lets you shift work between the two rather than guessing at optimizations. The about:tracing view is the tool for making that call, and for confirming whether a given change actually moved the bottleneck.