WebGL in the real world – a short case study – Part 2

In a recent post I described a WebGL pilot project for a client.  After experimenting with a couple of WebGL frameworks I reverted to basic principles and wrote a purpose-built display app that was able to display 506K textured triangles at interactive rates.  The demo let the user navigate through a pseudo-architectural scene using first-person-shooter style keyboard navigation.

There were some caveats on performance.  One was that the scenes appeared to be fill-rate limited.  That meant that performance would vary inversely with the size of the canvas that I was using.  Another is that interactivity would periodically “jump” every second or so – in other words, you’d miss a frame or two every second or so as you moved through the scene.  Anecdotally, I noticed this more in Chrome than Safari or Firefox (this was on the Mac — things looked better on Windows).

I attributed the jumping to browser issues, and to the way, perhaps, that WebGL flow control was being implemented.  My experience with real time browser programming in the past had conditioned me to not expect rock solid performance.  I’d seen this talk, which details some of the issues that Google was having with implementing WebGL.  And finally, my clients were not complaining about the jumping.  So I let it be.

But some weeks later when I returned to the project for some cleanup and refactoring I idly ran some profiling in the Chrome Developer Tools.  To my surprise, the profiler showed that a lot of time was being spent in a call to setMatrixUniforms(), which I was calling in the main display loop for every one of 560 objects.  The definition of this function is

  function setMatrixUniforms() {
    gl.uniformMatrix4fv(shaderProgram.pMatrixUniform, false, new Float32Array(pMatrix.flatten()));
    gl.uniformMatrix4fv(shaderProgram.mvMatrixUniform, false, new Float32Array(mvMatrix.flatten()));

and is responsible for setting the perspective and model view matrices in the GLSL shader program.  It turns out that these matrices were not changing on a per-object basis, so I moved this call from the display loop to the beginning of the scene display function. After this simple change, the profiler was no longer showing excessive activity, and the jumpiness in scene navigation went away.

Astute readers may already have guessed what was going on, but I have to admit that not only did I not have any idea, but I didn’t really care at the time because I was busy with other matters.  But I mentioned what had happened to a colleague and fortunately he was a little more interested than I was.  There were two hypotheses:

  1. resetting the matrix uniforms was somehow bringing the whole WebGL pipeline to a halt and degrading performance.
  2. the memory allocation and/or matrix flattening in new Float32Array(pMatrix.flatten()) were taking more time than we would have thought.

It was easy enough to test the hypotheses.  It turned out not to be the pipeline, and it wasn’t just the allocation of Float32Array.  Another look at the profiler showed that a lot of time was being spent on garbage collection.  There had been two memory allocations per object, for 560 objects, for 30 frames every second.  In other words, over 30,000 allocations per second.  Which presumably was triggering a garbage collection pass every second or so.

As it turns out, moving the function call was all I had to do.  But if that had not been the case, it would have been straightforward to rewrite the function to use a pre-allocated FloatArray to avoid the overhead of allocation and garbage collection.

I chalk this up to my relative inexperience with garbage collected languages, and to the relative unimportance of this issue in my previous programming projects.  Many years ago I spent a week doing a mathematical visualization with Java – my one and only experience with Java – and there I encountered a massive performance hit when garbage collection kicked in.  So maybe I should have seen this coming. I didn’t, but lesson learned.  Don’t be so fast to blame inconsistent WebGL frame rates on general browser flakiness.  And have more awareness of what’s going on in the bowels of Javascript.

Acknowledgement: the setMatrixUniforms() snippet comes from the lessons at learningwebgl.com, although I of course take full responsibility for my careless use of it.

Update: learningwebgl.com has updated to a new matrix library.  Not only does it look faster and more appropriate to WebGL, but I don’t think the problem described in this post exists any more.