技術探し

JavaScriptを中心に記事を書いていきます :;(∩´﹏`∩);:

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

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

Node v9.0.0 (Current) | Node.js

今回は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を選ぶと起動します。
f:id:about_hiroppy:20171105204518p:plain

インスペクターのクライアント一覧: Debugging - Getting Started | Node.js
Inspector Help | Node.js
個人的には、NiMを入れると楽かなーと思います。
abouthiroppy.github.io

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

github.com

--trace-events-enabled

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

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

f:id:about_hiroppy:20171105200300p:plain

Tracing | Node.js v9.0.0 Documentation github.com www.chromium.org

--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を起こすことも可能です。

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

postd.cc

実例

github.com

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.com

Performance Timing API

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

Performance Timing API | Node.js v8.9.0 Documentation

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

abouthiroppy.hatenablog.jp

優しいコードの書き方へ

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

abouthiroppy.hatenablog.jp

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

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

github.com

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

v8::SnapshotCreator

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

github.com

さいごに

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

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

また、今月Node学園祭があるので是非お越し下さい:)

nodefest.jp