Node.jsのパフォーマンスチューニングのtips

2017 / 11 / 06

Edit
🚨 This article hasn't been updated in over a year
💁‍♀️ This post was copied from Hatena Blog

Node9 が 10/31 に出ました 🎉🎉🎉

Node v9.0.0 (Current) | Node.js Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine.

今回は Node 単体の話なので、Express、Nginx 等のチューニングに関してはココには書きません。 また、libuv 等のコード内部の話もしません。

—inspect, —inspect-brk

もともとあった、--debugから移行されました。(v8.0.0 ~) Chrome を使いデバッグ、プロファイリング等を使えるようになります。 ブラウザで使えるので、いつも使っている感じと同じです。 --inspect-brk--debug-brkと同様に最初の行にブレークポイントを設置し、起動します。

$ node --inspect test.js
Debugger listening on ws://127.0.0.1:9229/b565921e-23f2-4cee-b124-33e97fc3aa32
For help see https://nodejs.org/en/docs/inspector

chrome からchrome://inspect/#devicesを指定すると選択肢がでるので、そこから inspect を選ぶと起動します。

インスペクターのクライアント一覧:

個人的には、NiM を入れると楽かなーと思います。

NIM (Node Inspector Manager) Node.jsのデバッグのため、 V8 Inspectorを自動で起動する拡張機能です。

—trace-opt, —trace-deopt

コードの最適化の解析を行います。

$ node --trace-opt test.js
[marking 0x3ad0f4375d1 <JSFunction normalizeStringPosix (sfi = 0x3ad423d7d81)> for optimized recompilation, reason: hot and stable, ICs with typeinfo: 46/67 (68%), generic ICs: 0/67 (0%)]
[compiling method 0x3ad0f4375d1 <JSFunction normalizeStringPosix (sfi = 0x3ad423d7d81)> using TurboFan]
[optimizing 0x3ad0f4375d1 <JSFunction normalizeStringPosix (sfi = 0x3ad423d7d81)> - took 1.867, 1.776, 0.019 ms]
[completed optimizing 0x3ad0f4375d1 <JSFunction normalizeStringPosix (sfi = 0x3ad423d7d81)>]
[marking 0x3ad423fcee1 <JSFunction Module._nodeModulePaths (sfi = 0x3ad423ba5a1)> for optimized recompilation, reason: hot and stable, ICs with typeinfo: 23/23 (100%), generic ICs: 0/23 (0%)]
[compiling method 0x3ad423fcee1 <JSFunction Module._nodeModulePaths (sfi = 0x3ad423ba5a1)> using TurboFan]
[optimizing 0x3ad423fcee1 <JSFunction Module._nodeModulePaths (sfi = 0x3ad423ba5a1)> - took 0.659, 3.009, 0.049 ms]
[completed optimizing 0x3ad423fcee1 <JSFunction Module._nodeModulePaths (sfi = 0x3ad423ba5a1)>]

marking は再コンパイル用のマーキングで、それは再コンパイルされ最適化されます。 最適化が不可能な場合は、マーキングの代わりにdisabled optimizationというのが付きます。

上記をみればわかるように、その関数が最適化されたかどうかがわかります。

—prof

CPU プロファイリングです。 V8 内のプロファイラの実行をサンプリングします。

$ node --prof test.js
$ ls
isolate-0x103000000-v8.log test.js
$ node --prof-process isolate-0x103000000-v8.log # logは読みづらいので読めるようにする

各セクションごとに情報が分かれます。

 [Summary]:
   ticks  total  nonlib   name
      3    5.0%    5.0%  JavaScript
     50   83.3%   83.3%  C++
      1    1.7%    1.7%  GC
      0    0.0%          Shared libraries
      7   11.7%          Unaccounted

取得されたサンプルの比率(5.0%, 83.3%, etc…)が割合となり、その言語のコードで発生したことを示します。 そして、各セクションを見るといいと思います。

セクション例

   ticks parent  name
   6326   44.2%  /lib/x86_64-linux-gnu/libm-2.15.so
   6325  100.0%    LazyCompile: *exp native math.js:91
   6314   99.8%      LazyCompile: *calculateMandelbrot http://localhost:8080/Demo.js:215

各セクションは、ツリーになっており、この場合は親コールの合計時間における 44.2%がシステム内のmath.exp()を実行するのに使われています。 関数名の前の*はその時間が最適化された関数で費やされていることを示し、~の場合は最適化された関数ではないことを示します。

詳しくは公式が出している以下の記事を見ると、手順がわかりやすいと思います。

Easy profiling for Node.js Applications | Node.js Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine.

https://github.com/v8/v8/wiki/Using%20V8%E2%80%99s%20internal%20profiler

—trace-events-enabled

トレース情報を管理します。 --trace-events-enabledフラグを渡すと有効化されます。 カテゴリを指定したい場合は、--trace-event-categoriesを使い続けてカテゴリを指定します。 カテゴリデフォルトはnodev8になります。 chrome でchrome://tracing/を指定することにより、生成したのをロードすることが可能です。

$ node --trace-events-enabled test.js
$ node --trace-events-enabled --trace-event-categories v8,custom-category test.js

—trace-gc

Garbage Collection のトレースです。 メモリリークのデバッグに役立つでしょう。

$ node --trace-gc test.js
[43929:0x102801c00]       39 ms: Scavenge 3.4 (6.3) -> 3.1 (7.3) MB, 0.9 / 0.0 ms  allocation failure
[43929:0x102801c00]       50 ms: Scavenge 3.6 (7.3) -> 3.5 (8.3) MB, 1.2 / 0.0 ms  allocation failure

$ node --optimize_for_size --max_old_space_size=4096 --gc_interval=100 #このようにV8のGCを操作することも可能

--expose-gcを指定することにより、手動で GC を起こすことも可能です。

メモリリーク周りは以下の記事を参考にするとわかりやすくていいと思います。

Node.jsでのJavaScriptメモリリークを発見するための簡単ガイド | POSTD 目次 初めに 極小理論 ステップ1. 問題の再現と確認 ステップ2. 最低3回のヒートダンプ採取 ステップ3. 問題の発見 ステップ4. 問題解決の確認 他のリソースへのリンク まとめ Someth…

実例

Garbage collection changes in 8.7.0 · Issue #917 · nodejs/help Node.js Version: 8.7.0 OS: debian jessie armhf Scope (install, code, runtime, meta, other?): runtime...

node-report

公式が出しているモジュールです。 現在、node-report は Core とは別で切り分けられておりスタンドアローンですが、将来的には Core に入る予定です。 ネイティブのスタックトレース、ヒープ統計情報、プラットフォーム情報、リソース使用状況などが人間が読める形でレポート化されます。

$ npm i node-report
$ node -r node-report test.js
$ cat node-report.20171105.202142.9066.001.txt
================================================================================
==== Node Report ===============================================================
...
Node.js version: v9.0.0
...
================================================================================
==== JavaScript Stack Trace ====================================================
...
================================================================================
==== Native Stack Trace ========================================================
...
================================================================================
==== JavaScript Heap and Garbage Collector =====================================
...
================================================================================
==== Resource Usage ============================================================
...
================================================================================
==== Node.js libuv Handle Summary ==============================================
...
================================================================================
==== System Information ========================================================
...
================================================================================

GitHub - nodejs/node-report: Delivers a human-readable diagnostic summary, written to file. Delivers a human-readable diagnostic summary, written to file. - GitHub - nodejs/node-report: Delive...

Performance Timing API

v8.5.0 から入ったブラウザでも使われる API です。 現在は Stability: 1(実験的)です。

詳しくは以下の記事をどうぞ

NodeにPerformance Timing APIが追加される - 技術探し NodeへPerformance Timing APIの初期実装が入ります。 資料 W3C NodeへのPR Performance Timing API 実装 Nodeで使ってみる 手順 コード A...

優しいコードの書き方へ

v8.3.0 から V8 の Turbofan, Ignition がデフォルトで Crankshaft から移行され、昔のような最適化のためのコードの書き方をしなくても良くなりました。

Node8.3.0でデフォルトになるTF/Iに関わるベンチマークについて - 技術探し buildersconでのスライド 8.3.0 プロポーザル TurboFan + Ignition TurboFan Ignition Flow V8の5.9は・・・? パフォーマンス webpac...

先日の Chrome Dev Summit でも V8 チームが今後はそのようなアンチパターンをなくしていくと言っています。(つまりどの書き方をしても同じ結果になる) また、トランスパイルは気をつけるべきです。 Babel には babel-preset-env というターゲットバージョンによりトランスパイルをするツールがあります。 babel-preset-env では Node のバージョンを指定することにより、V8 に優しいコードに変換することが可能です。 すべてのコードをトランスパイルするべきではありません。 基本的にトランスパイルされるコードは無駄な処理が多いからです。(これはそのものがエンジン側で実装されてないため) なので、エンジン側で未実装なもの(e.g. stage-x)だけをトランスパイルするべきです。

{
  "presets": [
    [
      "@babel/env",
      {
        "targets": {
          "node": "current"
        }
      }
    ]
  ]
}

https://github.com/babel/babel/tree/master/experimental/babel-preset-env

先日、monorepo の babel へ移行され次のバージョンでは scoped packages になりました 🎉

v8::SnapshotCreator

将来的に入るかもしれませんが、今現在、ArrayBuffersに関して議論中です。

Discussion/Tracking SnapshotCreator support · Issue #13877 · nodejs/node Relevant earlier discussion on #9473 . v8::SnapshotCreator is a means to capture a heap snapshot of ...

さいごに

今回は、パフォーマンスチューニングをするのに手助けになる手法を数個列挙してみました。 しかし、Node, V8 の最適化周りのオプションの話をするとまだたくさんあるとおもいますが一旦このへんで。 詳しくはnode --v8-optionsへ。--print-code, --print-opt-code, --code-comments --track-heap-objects, etc… その他には、I/O(libuv)とイベントループの理解も大切だと思います。

もしチューニング等でお困りでしたら、Twitterかメールで聞いてくだされば答えれるかもしれません。