AngularJSアプリのパフォーマンスを計測してみよう -- angular-benchpressの使い方

[AngularJSリファレンス](http://www.amazon.co.jp/dp/4844336681)の落ち穂拾いその2ということで、今回はangular-benchpressを紹介したいと思います。

- https://github.com/angular/benchpress

angular-benchpressは、Angularチームが開発するパフォーマンス計測ツールです。
AngularJSに限らず、Webアプリケーションのクライアントサイドのベンチマークをとることができます。
AngularJSやAngularDartの本体でもパフォーマンステスト用にこのツールを利用しているようです。

## インストールと実行

Node.jsとnpmがインストールされていれば、次のコマンドでインストールできます。

~~~
$ npm install -g angular-benchpress
~~~

ただし、angular-benchpressのバージョン0.1.0や0.1.2にはちょっとしたタイポがあって、動かない状態でリリースされていたりするので注意が必要です。
(v0.1.2の場合は、cli.jsの88行目の`benchmarksPath`の前に`+`を追加すると直ります。githubのmasterブランチでは修正済みです)

それでは、試しにAngularJS本体のパフォーマンステストを実行してみましょう。

~~~
$ git clone https://github.com/angular/angular.js.git
$ cd angular.js
$ grunt build
$ benchpress build
$ benchpress run
~~~

AngularJSでは、現在のところ次の2つのパフォーマンステストが用意されています。

- http://localhost:3339/benchpress-build/event-delegation-bp/
- http://localhost:3339/benchpress-build/largetable-bp/

## 画面の説明

それではブラウザを開き、次のURLにアクセスしてみましょう。

- http://localhost:3339/benchpress-build/event-delegation-bp/

すると、次のような画面が開きます。

![benchpress1.png](https://qiita-image-store.s3.amazonaws.com/0/3489/5133e9ca-f23c-cc64-fc2a-a0969a7e871a.png)

画面は大きく3つに分かれていて、上部が操作用のパネル、中央部がパフォーマンスの計測結果、そして下部には計測対象のHTMLが埋め込まれる形で表示されます。

### タブ

画面上部には次の3つのタブがあります。ですが、通常はControlsタブしか利用しません。

* Controls: パフォーマンス計測の操作画面が表示されるタブ
    * Loop
        * Pauseを押すまで計測を繰り返す
    * Onece
        * 1回だけ計測を実施
    * Loop 25x
        * 25回計測を実施
    * Profile
        * ブラウザのプロファイラ機能を利用して処理時間を計測
* Scripts: 計測に利用するスクリプトを、URLのパラメータで切り替えられることを解説しているタブ
    * 例えば、AngularJS 1.2と1.3でどれくらいの性能差があるのか比較するときなどに利用すると便利です。
* Tips: パフォーマンス計測をするときの注意点が書いてあるタブ
    * プラグインなどの影響を避けるために、計測の際にはシークレットウインドウを開いてからテストを実行しましょう
    * テストの実行中はマウスやキーボードに触らないようにしましょう

### 計測結果

では、Controlsタブを開いて、ラジオボタンのngClickを選択し、Loop 25xというボタンをクリックしてみましょう。
するとResultsに以下のような結果が出力されます。

![benchpress2.png](https://qiita-image-store.s3.amazonaws.com/0/3489/99fb8070-5b07-0562-abf8-8bcef457f350.png)

このテストでは、ngClickを1000個並べた時に`$scope.$apply`にかかる時間を計測しています。
計測結果としては、次の4つの項目が表示されます。
パフォーマンス計測だけでなく、メモリリークしているかどうかも調べることができそうです。

* test time (ms): 計測対象の処理を実行するのにかかった時間
    * mean: 平均値
    * stddev: 標準偏差
    * min: 最小値
    * max: 最大値
* gc time (ms): ガベージコレクションの実行(window.gc)にかかった時間
    * mean: 平均値
    * combined: test timeとgc timeの合計
* garbage (KB): ガベージコレクションにより解放されたメモリサイズ
    * mean: 平均値
* retained memory (KB): ガベージコレクションで解放し忘れたメモリサイズ
    * mean: 平均値

## AngularJS 1.2と1.3のパフォーマンス比較

AngularJS 1.2.23と1.3.0-rc.0でパフォーマンステストを実行してみると、Chrome 37の環境では、次のような結果になりました。

![event-delegation-bp_apply.png](https://qiita-image-store.s3.amazonaws.com/0/3489/265a18b4-68d2-6832-0294-5b7add9d3a3b.png)

![largetable-bp_apply.png](https://qiita-image-store.s3.amazonaws.com/0/3489/fa9be72a-4ceb-722a-03e7-70f111eca1c5.png)

![largetable-bp_create.png](https://qiita-image-store.s3.amazonaws.com/0/3489/33c459ac-bb7d-e1f4-ca67-0789572f2228.png)

![largetable-bp_destroy.png](https://qiita-image-store.s3.amazonaws.com/0/3489/7a56e8c5-ba02-434d-8165-7c9f6af7ce11.png)

AngularJS 1.3では大幅な性能改善がおこなわれているようですね！


## 自作フィルター・ディレクティブのパフォーマンスを計測してみよう

Markdown形式の文字列をHTMLに変換するフィルターとディレクティブをつくって、パフォーマンスを比較してみましょう。
なお、marked.jsを利用するので以下のページからダウンロードしておきます。

- https://github.com/chjj/marked

まずMarkdownフィルターとディレクティブを次のように実装します。説明は省略します。

~~~javascript:app.js
angular.module('app', [])
  .filter('mdFilter', ['$sce', function ($sce) {
    return function (input) {
      if (angular.isDefined(input)) {
        return $sce.trustAsHtml(marked(input));
      } else {
        return "";
      }
    };
  }])
  .directive('mdDirective', function () {
    return {
      restrict: 'E',
      require: 'ngModel',
      link: function (scope, element, attrs, ctrl) {
        ctrl.$render = function () {
          if (angular.isDefined(ctrl.$viewValue)) {
            element.html(marked(ctrl.$viewValue));
          }
        }
      }
    };
  });
~~~

次にパフォーマンス計測用のコントローラを用意します。

~~~javascript:perf.js
angular.module('app')
  .controller('PerfController', ['$scope', '$rootScope', function ($scope, $rootScope) {
    $scope.testData = '# test\n' +
      '## hoge\n' +
      ' * list1\n' +
      ' * list2\n' +
      ' * list3\n' +
      ' * list3\n' +
      '## fuga';

    $scope.rows = [];
    for (var i = 0; i < 1000; i++) {
      $scope.rows.push(i);
    }

    benchmarkSteps.push({
      name: '$apply',
      fn: function () {
        $rootScope.$apply();
      }
    });
  }]);
~~~

`$scope.testData`は、テストに利用するMarkdown形式の文字列です。
`$scope.rows`は、フィルターやディレクティブを1000回表示するためのダミーデータです。

`benchmarkSteps`は、angular-benchpressが用意しているグローバル変数です。この配列にパフォーマンスを計測したい処理を追加していきます。
ここでは`$rootScope.$apply`の時間を計測するように指定します。

次に、Markdownフィルターやディレクティブを利用するHTMLを用意します。

~~~html:main.html
<div ng-app="app">
  <div ng-controller="PerfController">
    <label><input type="radio" ng-model="benchType" value="filter">filter</label>
    <label><input type="radio" ng-model="benchType" value="directive">directive</label>
    <div ng-repeat="row in rows">
      <ng-switch on="benchType">
        <div ng-switch-when="filter">
          <div ng-bind-html="testData | mdFilter"></div>
        </div>
        <div ng-switch-when="directive">
          <md-directive ng-model="testData"></md-directive>
        </div>
      </ng-switch>
    </div>
  </div>
</div>
~~~

このHTMLはbenchpressのテンプレートの中に組み込まれるので、headやbodyやscriptタグを書く必要はありません。
ラジオボタンでフィルターとディレクティブの表示を切り替えられるようにしておきます。
また、ng-repeatを利用して、フィルターとディレクティブを1000回繰り返し表示するようにしておきます。

最後に設定ファイルを用意します。

~~~javascript:bp.conf.js
module.exports = function (config) {
  config.set({
    scripts: [
      {
        id: 'angular',
        src: 'angular.js'
      },
      {
        id: 'marked',
        src: 'marked.js'
      },
      {
        src: 'app.js'
      },
      {
        src: 'perf.js'
      }
    ]
  });
};
~~~

フィルターとディレクティブを動かすのに必要なJavaScriptファイルを指定しておきます。なおidを指定しておくと、テスト実行時にURLでファイルを切り替えることが可能になります。

以上の5つのファイルとangular.jsを`benchmarks/markdown`というディレクトリに配置します。(`benchmarks`という名前は固定、`markdown`は任意の名前をつけることができます)

そして、次のコマンドを実行します。

~~~
$ benchpress build
~~~

エラーが出ず、benchpress-buildというディレクトリが生成されていれば成功です。
次のコマンドを実行してベンチマークを起動します。

~~~
$ benchpress run
~~~

ブラウザで次のページにアクセスしてみましょう。

- http://localhost:3339/benchpress-build/markdown/

フィルターとディレクティブを切り替えて、それぞれで Loop 25x を実行してみます。
ぼくの環境では、フィルターだと20msかかっているものが、ディレクティブだと5msになりました。
今回のようなケースでは、フィルターよりもディレクティブのほうがよさそうだということがわかりますね。

## まとめ

AngularJSは他のフレームワークと比較してレンダリングが遅いという話をよく聞きます。
しかし、AngularJS 1.3で大幅な改善もおこなわれていますし、アプリケーションによって必要になるパフォーマンスは様々です。
そこで、自分たちが想定しているアプリケーションではどの程度のパフォーマンスが出るのか、angular-benchpressを使って計測してみるのはいかがでしょうか。