Browser Garbage Collection and Frame Rate
by Micah Catlin
27 October 2012
The good news is that modern browsers have vastly improved the efficiency and common-case runtime of their garbage collection phases. But are they good enough to make games with smooth animation today? How much garbage can your game safely create? Once your game loop fits into the CPU-time budget, can you know that it will be scheduled fast enough? Are you going to meet some target frame rate for the 99th percentile rendering time? The answers depend strongly on the client (hardware and browser), but we can make some measurements on typical computers and make some useful generalizations.
Let’s assume a typical game will have an animation loop which is gated by
window.requestAnimationFrame(), with the hope that the browser will schedule the loop at a stable 60fps. Also, assume that there is a single function which computes the next game state and “draws” it (to the DOM and Canvas). If we monitor the inter-arrival times of these
requestAnimationFrame() calls while watching the JS heap size we can see if garbage collection pauses are responsible for dropped frames.
The following example runs in Chrome, which supplies
window.performance.memory.usedJSHeapSize (when invoked with
--enable-memory-info). Similar affordances exist in Firefox and Safari.
We can also make some adjustments to the implementation of
computeNextGameStateAndPaint() to see the effect of different amounts of garbage generation.
After this runs for a while,
frameDataList will contain a list of samples which are easy to analyze:
> window.frameDataList [16.672734, 133128], [16.183574, 128228], [39.847293, -12158528], [16.974714, 119222], [16.672734, 140248]
Correlation between slow frames and GC events
Garbage collection events aren’t explicitly announced by the VM, but we can infer that one has just happened whenever the JS heap size decreases between two samples. We might undercount the number of garbage collection events by sampling too slowly, but we can’t overcount it this way.
Here are some plots showing the inter-arrival times of a series of animation frames, along with some decoration indicating changes to the size of the JS Heap. The bubble radius is proportional to the magnitude of the heap size change in the last frame, and color indicates the sign. This data was collected using Chrome 22 running on a high end laptop.
Here’s an example of an absurdly high amount of garbage generation with 200,000 anonymous objects becoming unreachable every frame. While this is an artificial and unrealistic memory-use profile, it still produces thought-provoking results. Notice how bi-modal the frame times are when there’s a lot of garbage being generated inside
computeNextGameStateAndPaint(). It’s clear that the slow frames which do occur almost always occur in conjunction with a garbage collection pass.
As we dial down the rate of garbage generation, the situation improves materially. GC pauses start to fit inside the gaps between frames, and predictable 30fps is within reach.
At 2,000 objects/tick, 60fps is looking rock-solid.
Too much garbage will certainly cause stalls that impact frame rate, but the critical amount of garbage is discoverable early in the development cycle of your application. It is possible to get stable fast frame rates provided you stay inside your per-frame CPU budget and keep the rate of garbage generation under control.
- Context around the window.performance.now() interface