ぼちぼち日記

おそらくプロトコルネタを書いていることが多いんじゃないかと思います。

Ninjaを使ってNode.jsをビルドする

今朝 Node.js でマージされたPull Request「 add configure option to build with ninja 」によって、Node.js が Ninja ビルドシステムに対応になりました。
Nodeの PaaSでは NodejitsuNode Ninjaなど忍者シリーズで命名されたものが思い当りますが、実は今回は全く関係ありません。
Ninjaビルドシステムは、GoogleChromiumプロジェクトの開発者、Evan Martin氏が開発した高速なビルドシステムです。このビルドシステムが作られた経緯や背景はこの記事「Chromiumの開発者、「Chrome」でも利用されているビルドシステム「Ninja」を公開」を参照してください。

1. NodeとNinjaの関係

Node.jsは、Googleから提供されている様々なオープンソーステクノロジーを活用しています。

このうちGYPは、Node.jsを各OSに適したビルドシステム(Unix系OS: make, Mac OS X: Xcode, Windows: Visual Studio)等を作るメタビルドシステムです。 Ninjaは、Chromeのビルド向けに開発されたこともあり、GYPがサポートしているビルドシステムです。(対象OSは Linux/Mac OS X/FreeBSD/Windows)
NodeのGYPについての解説は、以下のスライドで少し説明していますので、参考にどうぞ。

では、 Node が Ninja ビルドに対応したので早速試してみましょう。
ここでは さくらVPS 1G Linux(Ubuntu12.04)のサーバ上でNinjaを使ってNodeをビルドします。

2. Ninja のインストール手順

まず git でNinjaのソース一式を持ってきます。

$ git clone git://github.com/martine/ninja.git
Cloning into 'ninja'...
remote: Counting objects: 6844, done.
remote: Compressing objects: 100% (2272/2272), done.
remote: Total 6844 (delta 4876), reused 6367 (delta 4453)
Receiving objects: 100% (6844/6844), 3.18 MiB | 1012 KiB/s, done.
Resolving deltas: 100% (4876/4876), done.

そしてビルド用のスクリプト bootstrap.py を実行するだけです。(おそらくpythonC++コンパイラは必須でしょう)

$ cd ninja/
$ ./bootstrap.py
Building ninja manually...
Building ninja using itself...
warning: re2c not found; changes to src/*.in.cc will not affect your build.
wrote build.ninja.
[21/21] LINK ninja
Done!

そうしたらローカルディレクトリに ninja バイナリーができました。後はパスの通っているディレクトリに移すだけです。(私の場合 ~/bin にコピーしました)

$ ls -l ninja
-rwxrwxr-x 1 ohtsu ohtsu 2292163 Sep  5 15:22 ninja
$ ./ninja --version
git
$ cp ninja ~/bin/
$ which ninja
/home/ohtsu/bin/ninja

3. Ninja で Node をビルドする。

これも簡単。configure のオプションで --ninja を付け加えて make するだけです。

$ ./configure --ninja
{ 'target_defaults': { 'cflags': [],
                       'default_configuration': 'Release',
                       'defines': [],
                       'include_dirs': [],
                       'libraries': []},
  'variables': { 'clang': 0,
                 'gcc_version': 46,
                 'host_arch': 'x64',
                 'node_install_npm': 'true',
                 'node_prefix': '',
                 'node_shared_openssl': 'false',
                 'node_shared_v8': 'false',
                 'node_shared_zlib': 'false',
                 'node_use_dtrace': 'false',
                 'node_use_etw': 'false',
                 'node_use_openssl': 'true',
                 'target_arch': 'x64',
                 'v8_no_strict_aliasing': 1,
                 'v8_use_snapshot': 'true'}}
creating  ./config.gypi
creating  ./config.mk
$ make
touch out/Makefile
python tools/gyp_node -f ninja
ninja -C out/Release/
ninja: Entering directory `out/Release/'
[31/846] CC obj/deps/cares/src/cares.ares_options.o
(途中コンパイルWarning がいっぱい)
[846/846] LINK node
ln -fs out/Release/node node
$

はい、これで完了。無事 Node がビルドできました。パチパチ。

4. 本当に速いの? 性能評価

さぁ本当にビルドが速いんでしょうか? make とビルドの完了までの時間計測をしてみます。
まずは ninja から。

$ ./configure --ninja
(中略)
$ time make
touch out/Makefile
python tools/gyp_node -f ninja
ninja -C out/Release/
ninja: Entering directory `out/Release/'
(中略)
[846/846] LINK node
ln -fs out/Release/node node
real    3m36.642s
user    6m30.004s
sys     0m33.390s

次に make のビルド時間の計測。 make の場合は -j 2 をつけて2並列で走らせます。(Ninja は default で並列度 -r 2 になっています。)
今回さくらVPSは、仮想2CPUなので並列度2でビルドします。

$ ./configure
(中略)
$ time make -j 2
make -C out BUILDTYPE=Release V=1
(中略)
make[1]: Leaving directory `/home/ohtsu/tmp/github/node_build_make/out'
ln -fs out/Release/node node
real    3m44.649s
user    6m28.120s
sys     0m34.890s

あらっ、ほぼ一緒です。 Ninja 3分36秒、 Make 3分44秒。
それじゃ全部一気にビルドするより、普段やるようファイルが一部更新された場合のビルドが速いのかも?
ということで Ninja サポートされたコミットまで一度さかのぼって、それから差分更新でビルドした時間を比較します。といっても今朝なので2コミットしかありません。(でも最後のコミットが TLS のセッション対応なのでそれなりに大きい)
ではビルド時間を比較してみます。

$ git reset --hard HEAD~2
HEAD is now at 7b6d3ce build: add ninja support to Makefile
$ ./configure --ninja
(中略)
$ make
(中略)
$ git pull origin master
(中略)
$ time make
(中略)
real    0m5.359s
user    0m6.700s
sys     0m0.624s

次に make での差分更新ビルドの経過時間。

$ git reset --hard HEAD~2
(前までほぼ一緒)
$ time make -j 2
(中略)
real    0m5.835s
user    0m6.664s
sys     0m0.756s

あらっ、そんなに違いないのか。

5. 結果とまとめ

ということで、以下の通りまとめました。

real時間 Ninja make Ninja/make
全部ビルド 3m36.642s 3m44.649s 0.964
HEAD~2更新ビルド 0m5.359s 0m5.835s 0.918

Ninja Manualでは、Chromeは30,000以上のソースファイルがあって、1つのファイルを変更した時にビルドが開始するまで10秒かかっていたのが Ninja を使うと1秒以下になったと書いてあります。
Node だと、

$  git ls-files | grep \.cc$ | wc
    314     314    9769

ぐらいです。

まとめ:まだまだNodeは、Ninjaビルドの高速化の恩恵にあずからない程度のソースファイル数なんでしょうね。
(誰かわかる人、理由を教えてください。)