Game of Life Clock Optimisations

7 November 2013

I've been doing a lot of optimisation work recently and so I decided to take a quick look at the performance of the Game of Life Clock. It was written quickly with little thought to anything beyond getting a result on the screen so there should be plenty of room for improvements. With a small 500 x 200 canvas there won't be any noticeable improvements so a 5,000 x 2,000 canvas should make things a bit more interesting as that results in 1666 x 666 cells.

Firstly, a quick overview of the process:

  1. Create a 2-dimensional array to represent all of the cells,
  2. Print a set of characters into the array,
  3. Draw each cell into the canvas,
  4. Calculate next step,
  5. Draw each cell into the canvas, back to 4, and repeat.

With that in mind there are a few places that have the potential to be bottleneck:

  • Printing the characters into the array of cells. There are 35 cells per character to set, which isn't too many, but we're currently only printing 5 characters at a time.
  • Drawing the cells into the canvas. In a 1666 x 666 grid that makes 1,109,556 cells which could be slow depending on the canvas implementation.
  • Calculating the next step. This iterates over all of the cells and then reads the 8 surrounding cells, so accessing roughly 8,876,448 array indexes.

Profiling

Running the Chrome profiler for a few moments yields the following:

We can clearly see that the majority of time is spent in the fill method of the canvas context, so our first goal will be to reduce the number of calls to this method. Without calculating any statistics, observations of the visualisation indicate that relatively few cells actually change state in each step. With this in mind the code can be changed to not redraw every cell, but only those that need redrawing.

Our render method goes from:

// clear canvas

for (y = 0; y < nextCells.length; ++y) {
    for (x = 0; x < nextCells[y].length; ++x) {
        // fill cell
    }
}

To

for (y = 0; y < nextCells.length; ++y) {
    for (x = 0; x < nextCells[y].length; ++x) {
        if (oldCells && oldCells[y][x] === nextCells[y][x]) {
            continue;
        }
        // clear cell
        // fill cell
    }
}

A follow up profile shows a vast improvement in the step-by-step rendering:

Still to come

This optimisation was very simple but also effective. What this doesn't address, however, is the first render to a blank canvas, it's still filling 1,109,556 cells. I'm confident we can reduce the step-by-step rendering time even more, but next up we'll take a look at reducing that initial render time.