Under the Hood: Bubbles

IEBlog

Internet Explorer Team Blog

Under the Hood: Bubbles

  • Comments 30

In an earlier post we discussed JavaScript performance improvements introduced in IE10. Today we published Bubbles, originally inspired by Alex Gavriolov’s simulation BubbleMark, to explore some of these advances. The current version has been greatly expanded to take advantage of the new features of the Web platform and includes characteristics common to HTML5 games. In this post we peek under the hood to see how the demo is constructed and examine the main factors that affect its performance.

Screen shot of Bubbles demo running in Metro style IE10 on the Windows 8 Release Preview
Bubbles demo running in Metro style IE10 on the Windows 8 Release Preview

The Structure of the Demo

The demo consists of a number of animated bubbles floating through space. At the core is a relatively simple JavaScript physics engine. On every animation frame, about 60 times per second, the physics engine recalculates the positions of all bubbles, adjusts the speed of each bubble by applying gravity, and computes collisions. All these computations involve intensive floating point math. Each bubble is represented on screen by a DOM image element to which a CSS transform is applied. The image is first translated around its origin, and then scaled dynamically to produce the effect of the bubble inflating. In JavaScript, every bubble is represented as an object with accessor properties, as introduced in ECMAScript 5.

Behind the floating bubbles is a large image, which starts off occluded by a fully opaque mask rendered in a <canvas> element. Whenever two bubbles collide, a portion of the occluding mask is removed by applying a Gaussian filter to the mask’s opacity component to produce a diffuse transparency effect. This process also involves many floating point multiplications, which are performed on elements of Typed Arrays, if supported by the browser.

Over the floating bubbles rests a touch surface, which responds to touch input if supported by the running browser or mouse events if not. In response to touch simulated magnetic repulsion is applied to the floating bubbles scattering them around.

Efficient Animation

In IE10 we introduced support for the requestAnimationFrame API. When building JavaScript applications with animation we continue to recommend using this API (instead of setTimeout or setInterval) on browsers that support it. As discussed in a previous post exploring how to make the most of your hardware, this API allows you to achieve maximum frame rate supported by the available display while avoiding excessive work that would remain invisible to the user. By doing the minimal number of operations to deliver the best user experience, you can save battery power. IE10 Release Preview supports this API without vendor prefix, but the prefixed version is also maintained for compatibility with earlier IE10 preview releases. The Bubbles demo uses this API and falls back to setTimeout when it is not available.

Demo.prototype.requestAnimationFrame = function () {

var that = this;

if (window.requestAnimationFrame)

this.animationFrameTimer =

window.requestAnimationFrame(function () { that.onAnimationFrame(); });

else

this.animationFrameTimer =

setTimeout(function () { that.onAnimationFrame(); }, this.animationFrameDuration);

}

 

Demo.prototype.cancelAnimationFrame = function () {

if (window.cancelAnimationFrame)

window.cancelAnimationFrame(this.animationFrameTimer);

else

clearTimeout(this.animationFrameTimer);

}

Converting DOM Values from Strings to Numbers

JavaScript is very flexible and includes a range of automatic conversions between values of different types. For example, string values are automatically converted to number values when used in arithmetic operations. In modern browsers, this convenience may come at a surprisingly high performance cost. Type specialized code emitted by state-of-the-art JavaScript compilers is very efficient in arithmetic on values of known types, but incurs very high overhead when it encounters values of unexpected types.

When the Bubbles demo is loaded, the numberOfBubbles property starts off with a value of 100. On every animation frame, the position of each bubble is adjusted:

function Demo() {

this.numberOfBubbles = 100;

//...

}

 

Demo.prototype.moveBubbles = function(elapsedTime) {

for (var i = 0; i < this.numberOfBubbles; i++) {

this.bubbles[i].move(elapsedTime, this.gravity);

}

}

When the user selects a different value in the UI, the value of the numberOfBubbles property must be adjusted accordingly. A straightforward event handler may do it like this:

Demo.prototype.onNumberOfBubblesChange = function () {

this.numberOfBubbles = document.getElementById("numberOfBubblesSelector").value;

//...

}

This seemingly natural way of reading the user’s input results in about 10% overhead in the JavaScript portion of the demo. Because the value obtained from the drop down and assigned to numberOfBubbles is a string (not a number), it has to be converted to a number on every iteration of the moveBubbles loop for every frame of the animation.

This demonstrates that it’s a good practice to explicitly convert values extracted from the DOM to numbers before using them in arithmetic operations. Values of DOM properties are typically of type string in JavaScript and the automatic string to number conversions can be quite expensive when done repeatedly. A better way to update numberOfBubbles based on user selection, as found in the demo, is as follows:

Demo.prototype.onNumberOfBubblesChange = function () {

this.numberOfBubbles = parseInt(document.getElementById("numberOfBubblesSelector").value);

//...

}

Working with ES5 Accessor Properties

ECMAScript 5 accessor properties are a convenient mechanism for data encapsulation, computed properties, data validation, or change notification. In the Bubbles demo, whenever a bubble is inflated the radius is adjusted, which sets the computed radiusChanged property to indicate that the bubble’s image needs to be resized.

Object.defineProperties(Bubble.prototype, {

//...

radius: {

get: function () {

return this.mRadius;

},

set: function (value) {

if (this.mRadius != value) {

this.mRadius = value;

this.mRadiusChanged = true;

}

}

},

//...

});

In all browsers, accessor properties add overhead as compared to data properties. The precise amount of overhead varies between browsers.

Minimizing Access of the Canvas ImageData

It’s a well-established practice to minimize the number of calls to DOM in loops on the critical performance path. For example, if the Bubbles demo updated the location of every bubble by looking up the corresponding element in the document (as below), performance would be adversely affected.

Bubble.prototype.render = function () {

document.getElementById("bubble" + this.id).style.left = Math.round(this.x) + "px";

document.getElementById("bubble" + this.id).style.top = Math.round(this.y) + "px";

this.updateScale();

}

Instead, the element corresponding to each bubble is cached in the bubble object in JavaScript once, and then accessed directly on every animation frame.

Bubble.prototype.render = function () {

this.element.style.left = Math.round(this.x) + "px";

this.element.style.top = Math.round(this.y) + "px";

this.updateScale();

}

What may be less clear is that care must be taken to avoid similar overhead when working with <canvas>. The object obtained by calling canvas.getContext("2D").getImageData() is also a DOM object. The code below could be used in the demo to draw the bubble collision effect to the canvas. In this version imgData.data is read on every iteration of the loop, which requires a call to the DOM and adds great overhead.

BubbleTank.prototype.renderCollisionEffectToCanvas = function(px, py) {

var imgData = this.canvasContext.getImageData(/*...*/)

//...

for (var my = myMin; my <= myMax; my++) {

for (var mx = mxMin; mx <= mxMax; mx++) {

var i = (mx + gaussianMaskRadius) + (my + gaussianMaskRadius) * gaussianMaskSize;

imgData.data[4 * i + 3] = 255 * occlusionMask[(px + mx) + (py + my) * canvasWidth];

}

}

this.canvasContext.putImageData(imgData, px - gaussianMaskRadius, py - gaussianMaskRadius);

}

A better way to update the <canvas> image data is by caching the data property as in the following code snippet. The data property is a typed array (PixelArray) and can be accessed very efficiently from JavaScript.

BubbleTank.prototype.renderCollisionEffectToCanvas = function(px, py) {

var imgData = this.canvasContext.getImageData(/*...*/)

var imgColorComponents = imgData.data;

//...

for (var my = myMin; my <= myMax; my++) {

for (var mx = mxMin; mx <= mxMax; mx++) {

var i = (mx + gaussianMaskRadius) + (my + gaussianMaskRadius) * gaussianMaskSize;

imgColorComponents[4 * i + 3] =

255 * occlusionMask[(px + mx) + (py + my) * canvasWidth];

}

}

this.canvasContext.putImageData(imgData, px - gaussianMaskRadius, py - gaussianMaskRadius);

}

Using Typed Arrays to Store Floating Point Numbers

In IE10 we added support for Typed Arrays. When working with floating point numbers it is advantageous to use Typed Arrays (Float32Array or Float64Array) instead of JavaScript arrays (Array). JavaScript arrays can hold elements of any type, but typically require that floating point values be allocated on the heap (boxing) before being added to the array. This affects performance. To achieve consistently high performance on modern browsers use Float32Array or Float64Array to indicate the intent of storing floating point values. Doing so you help the JavaScript engine avoid heap boxing and turn on other compiler optimizations, like generating type specialized operations.

BubbleTank.prototype.generateOcclusionMask = function() {

if (typeof Float64Array != "undefined") {

this.occlusionMask = new Float64Array(this.canvasWidth * this.canvasHeight);

} else {

this.occlusionMask = new Array(this.canvasWidth * this.canvasHeight);

}

this.resetOcclusionMask();

}

The example above shows how the Bubbles demo uses Float64Arrays to hold and update the occlusion mask applied to the canvas hiding the background image. If the browser doesn’t support Typed Arrays, the code falls back on regular arrays. The gains from using Type Arrays in the Bubbles demo vary with settings. In a medium-sized window in IE10, Typed Arrays improve the overall frame rate by around 10%.

Summary

In this blog post we explored the newly published Bubbles demo and examined how it takes advantage of dramatic gains in JavaScript execution the IE10 Release Preview. We shared a few important techniques to achieve good performance in animation-based applications. For more technical details about the changes in the JavaScript runtime (Chakra) in IE10 Release Preview, please, refer to the earlier post. We are excited about the greatly improved performance and new capabilities available in IE10 Release Preview and we hope they will allow for ever more fascinating applications to be build using Web standards and technologies.

—Andrew Miadowicz, Program Manager, JavaScript

  • Loading...