内部のしくみ: Test Drive デモ Bubbles

私たちは以前の記事 (英語) の中で、IE10 に実装された JavaScript のパフォーマンス強化点についてお伝えしました。本日私たちは Bubbles (英語) を公開しました。この Bubbles デモは、Alex Gavriolov 氏の BubbleMark (英語) シミュレーションにインスパイアされたことを発端に、いくつかの点についてさらなる高度化を試みたものとなっています。Web プラットフォームの新機能を活用する大幅な強化が行われた現在のバージョンでは、HTML5 ゲームに共通するさまざまな特徴を確認することができます。この記事では、この Bubbles デモのしくみを見ていくことによって、デモの構造を明らかにすると共に、パフォーマンスに関する主な影響要因を検証したいと思います。

Windows 8 Release Preview の Metro スタイル IE10 で実行されている Bubbles デモのスクリーンショット
Windows 8 Release Preview の Metro スタイル IE10 で実行されている Bubbles デモ (英語)

デモの構造

デモは、アニメーションによって空間に浮かぶたくさんのバブルで構成されています。この中心となっているのは比較的シンプルな JavaScript 物理エンジンです。毎秒およそ 60 枚のアニメーション フレームの 1 枚 1 枚で、すべてのバブルの位置の再計算、重力の適用によるバブルの速度調整、衝突の計算といった処理を、この物理エンジンが実行しています。これらすべての計算には高度な浮動小数点演算を伴います。画面上のバブルは、CSS 変換が適用された DOM イメージ要素によって表現されます。画像は、まず原点に基づいて直線で移動し、次に動的な拡大/縮小によってバブルの膨らみを生成します。JavaScript では、ECMAScript 5 で導入されたアクセサー プロパティを持つオブジェクトとして各バブルが表現されます。

浮いているバブルの後ろにある大きな画像は、最初は、<canvas> 要素内に描画された完全な不透明マスクによって見えなくなっている状態からスタートします。2 つのバブルが衝突すると、マスクの不透明コンポーネントにぼかし (ガウス) フィルターが適用され、マスクによって隠されている部分が削除されて、透明度が拡散 (デフューズ) された効果が生まれます。この処理にも数多くの浮動小数点乗算を伴います。この乗算は、型付き配列の要素で実行されます (ブラウザーがサポートしている場合)。

浮いているバブルの上にはタッチ サーフェスがあります。タッチ サーフェスは、実行中のブラウザーがサポートしている場合はタッチ入力に応答し、サポートしていない場合はマウス イベントに応答します。タッチすると、タッチの応答としてシミュレーションによる磁気反発が浮いているバブルに適用され、バブルが別の方向に飛ばされるのがわかります。

アニメーションの効率化

IE10 には requestAnimationFrame (英語) API のサポートが実装されています。これは、JavaScript アプリケーションにアニメーションを設定する場合に、私たちが常に (setTimeout や setInterval の代わりに) 使用をお勧めしている API です (ブラウザーがサポートしている場合)。以前の記事でもお伝えしたように、コンピューター ハードウェアをより効率的に使用するにあたって、この API を使えば、使用するディスプレイがサポートする最大のフレーム レートを実現しながら、ユーザーに表示されない作業の処理に余計な負荷がかかるのを避けることができます。オペレーションの数を最小化しながらユーザー エクスペリエンスを最大化することで、消費電力も節約されます。IE10 Release Preview ではベンダー プレフィックスなしでこの API をサポートしますが、これまでのいくつかの IE10 プレビュー リリース版との互換性を維持するため、プレフィックスの付いたバージョンにも対応します。Bubbles デモではこの API を使いますが、対応していない場合は setTimeout にフォールバックします。

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.cancelRequestAnimationFrame)

window.cancelRequestAnimationFrame(this.animationFrameTimer);

else

clearTimeout(this.animationFrameTimer);

}

DOM の値を文字列値から数値に変換

非常に柔軟な言語である JavaScript は、異なる型どうしの値の自動変換に幅広く対応しています。たとえば、文字列値を自動で数値に変換して算術演算に使うことができます。最新のブラウザーでは、このメリットと引き換えに非常に大きなパフォーマンス コストが発生する可能性があります。最新の JavaScript コンパイラが生成する特定の型専用のコードは、既知の型の値を使った演算の場合は非常に効率的ですが、予期しない型の値が出てきた場合のオーバーヘッドも非常に大きくなります。

Bubbles デモが読み込まれたときの numberOfBubbles プロパティの値は 100 です。アニメーション フレームごとに、各バブルの位置は以下のように調整されます。

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);

}

}

ユーザーが UI で別の値を選択した場合は、それに従って numberOfBubbles プロパティの値を調整する必要があります。シンプルなイベント ハンドラーの場合は、以下のような処理になるでしょう。

Demo.prototype.onNumberOfBubblesChange = function () {

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

//...

}

ユーザーの入力結果を読み取りながら、デモの JavaScript 部分に約 10% のオーバーヘッドが発生する、自然な方法のように感じられます。ドロップダウン リストから取得されて numberOfBubbles に割り当てられる値は (数値ではなく) 文字列なので、アニメーションの各フレームで発生する moveBubbles ループの繰り返しのたびに、この文字列を数値に変換する必要があります。

このことからわかるのは、DOM から抽出した値を、算術演算で使う前に明示的に変換した方が都合が良いということです。DOM プロパティの値は通常 JavaScript の文字列型で、文字列から数値への自動変換を反復実行すると負荷は非常に高くなります。ユーザーの選択に応じて numberOfBubbles を更新するより良い方法は、このデモにも採用されている以下のような処理となります。

Demo.prototype.onNumberOfBubblesChange = function () {

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

//...

}

ES5 のアクセサー プロパティの使用

ECMAScript 5 のアクセサー プロパティは、データのカプセル化、計算プロパティ、データの検証、変更通知に便利なメカニズムです。この Bubbles デモでは、バブルが膨らむたびにバブルの半径が調整され、計算後の radiusChanged プロパティによってバブル画像のサイズ変更が指定されます。

Object.defineProperties(Bubble.prototype, {

//...

radius: {

get: function () {

return this.mRadius;

},

set: function (value) {

if (this.mRadius != value) {

this.mRadius = value;

this.mRadiusChanged = true;

}

}

},

//...

});

正確な量はブラウザーによって異なりますが、すべてのブラウザーで、アクセサー プロパティのオーバーヘッドはデータ プロパティよりも大きくなります。

Canvas 画像データへのアクセスの最小化

基本的な姿勢として、クリティカルなパフォーマンス パスのループ内では、DOM への呼び出しをできる限り少なくする必要があります。Bubbles デモの例で言えば、すべてのバブルの位置を更新する場合に、ドキュメント内の対応する要素の参照を行うと (以下を参照)、パフォーマンス面で悪影響が発生します。

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();

}

こうした方法を使うのではなく、各バブルに対応する要素を JavaScript の bubble オブジェクトにキャッシュした後に、各アニメーション フレーム上でそれらに直接アクセスするようにします。

Bubble.prototype.render = function () {

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

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

this.updateScale();

}

ここではっきりしないのは、同様のオーバーヘッドは <canvas> を操作する際にも避けるべきかという点です。canvas.getContext("2D").getImageData() の呼び出しによって取得されるオブジェクトもまた DOM オブジェクトです。このデモでは、以下のようなコードを使ってバブルが衝突する効果を Canvas に描画できます。このバージョンでは、ループの反復のたびに imgData.data が読み取られますが、そのたびに DOM への呼び出しが必要になり、これが大きなオーバーヘッドとなります。

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);

}

<canvas> 画像データを更新するより良い方法は、以下のコード スニペットのように、data プロパティをキャッシュする方法です。data プロパティは型付き配列 (PixelArray) で、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);

}

型付き配列を使って浮動小数点数を格納

IE10 では、型付き配列がサポートされるようになりました。浮動小数点数を操作する場合、JavaScript 配列 (Array) よりも型付き配列 (Float32Array または Float64Array) を使用した方がメリットが大きくなります。JavaScript 配列はあらゆる型の要素を保持できますが、通常は、浮動小数点値を配列に追加する前に、これらをヒープに割り当てる (ボックス化する) ことが必要になり、ここでパフォーマンスが損なわれます。最新のブラウザーで一貫した高パフォーマンスを維持するためには、Float32Array または Float64Array を使って、浮動小数点値を格納するインテントを指定します。これによって JavaScript エンジンがヒープによるボックス化を回避して、特定の型専用の操作を生成するなど、その他のコンパイラ最適化処理を実行できるようになります。

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();

}

この例は、背景画像を隠すために Canvas に適用されている不透明マスクを、Float64Arrays を使って保持、更新する Bubbles デモの例です。ブラウザーが型付き配列をサポートしていない場合は、コードは通常の配列にフォールバックします。Bubbles デモで型付き配列を使うメリットは設定によって異なりますが、IE10 を中くらいのウィンドウ サイズで開いた場合では、型付き配列によって全体のフレーム レートが 10% ほど上昇します。

まとめ

このブログ記事では、新たに公開された Bubble デモについて、そのしくみを解説しながら、IE10 Release Preview で飛躍的に強化された JavaScript 実行関連機能の活用方法について説明しました。アニメーションを活用するアプリケーションで高パフォーマンスを実現するための重要なテクニックのいくつかはご紹介できたと思います。IE10 Release Preview の JavaScript ランタイム (Chakra) の変更に関する技術面の詳細情報が必要な場合は、以前の記事 (英語) をご覧ください。私たちは、IE10 Release Preview におけるパフォーマンスの飛躍的な強化と新機能をご利用いただけるようになったことを喜ぶのはもちろん、こうした機能や各種の Web 標準、テクノロジを通して、これまでにない魅力的なアプリケーションが続々と登場することを楽しみにしています。

— JavaScript 担当プログラム マネージャー Andrew Miadowicz