内部実装から読み解くNode.js(v11.0.0) Eventloop

# Goal

- Node.js Eventloopとは何かを知る
- JSとNode.jsの非同期APIを区別する
- NodejsではEventloopを止めてはいけないことを学ぶ
  - 他の同期処理をベースとした言語と同じ書き方をしてはいけない

---

# Q. 次の挙動は？

```js
setTimeout(() => {
  console.log('ok');
}, 0);
// 時間のかかる同期処理 => その極端な例として無限ループ
while (1) {}
```

---

# A. 何秒待っても出力されない

同期処理の言語に慣れていると、すぐにokの出力を期待してしまうが...

---

## Node.jsでのsetTimeout()

- Node.jsはJavaScriptエンジン(v8)を持っている
  - 基本的にはJSのAPIを利用できる
- ただし一部はNode.jsのランタイムでは[Node core API](https://github.com/nodejs/node/tree/master/lib)が実行される
- `setTimeout()`,`setInterval()`,`setImmediate()`は[JSのTimer](https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timers)ではなく[Node core APIのTimer](https://nodejs.org/api/timers.html)が実行される
  - これらはNode.js Eventloop内で処理される

---

## JS/Node.jsの主な非同期処理

- JS
  - Promise(ここでは`Object.observe()`として扱う[^1])
- Node.js
  - I/O
  - setImmediate
  - setTimeout/setInterval
  - process.nextTick
  - EventEmitter ※`emit()`のタイミング次第で同期にもなる

---

## Node.js Eventloop

- Node.jsの非同期処理は全てEventloop内で評価される
- [Node.jsコミッターの大津さんが作成したv0.11の図](https://www.slideshare.net/shigeki_ohtsu/node-v012tng12/14)が今でもわかりやすい
- Eventloopはユーザーランドの実行後に回るので、長い同期処理があるといつまでも非同期コールバックが評価されない

ということを内部実装を見て確認

---

# Node.js: 始まりの場所

`node ***.js`を実行すると...

1. [src/node_main.cc: main(argc, argv)](https://github.com/nodejs/node/blob/v11.0.0/src/node_main.cc#L94-L125)
  - stdout,stderrのバッファリングを0に
  - `node::Start(argc, argv)`
1. [src/node.cc: Start(argc, argv)](https://github.com/nodejs/node/blob/v11.0.0/src/node.cc#L2964-L3022)
  - v8の初期化など
1. [src/node.cc: Start(event_loop, args, exec_args)](https://github.com/nodejs/node/blob/v11.0.0/src/node.cc#L2916-L2962)
  - isolate作成: chromeのタブと同じ
1. [src/node.cc: Start(isolate, data, args, exec_args)](https://github.com/nodejs/node/blob/v11.0.0/src/node.cc#L2815-L2882)
  - context作成
  - `LoadEnvironment()`でC++からユーザーランドのJSを実行（globalを生やすとかbootstrap処理の後）
  -　[do-while](https://github.com/nodejs/node/blob/v11.0.0/src/node.cc#L2844-L2858)

---

# Node.js: libuv

- node.ccのdo-while内でuv_run()
  - MacやLinux: [deps/uv/src/unix/core.c](https://github.com/nodejs/node/blob/v11.0.0/deps/uv/src/unix/core.c#L350-L399)
  - Windows: [deps/uv/src/win/core.c](https://github.com/nodejs/node/blob/v11.0.0/deps/uv/src/win/core.c#L506-L560)
- depsディレクトリはNode.jsを支えるサードパーティの置き場（v8など）
  - uv_run()は[libuv](https://libuv.org/)というマルチプラットフォームなライブラリのAPI
- **libuvのuv_run()の[whileループ](https://github.com/nodejs/node/blob/v11.0.0/deps/uv/src/unix/core.c#L359-L390)をEventloop**と呼ぶ

---

## Eventloop ≠ node.cc do-while?

- [初期のnode.ccはdo-whileループなどなくuv_run()を実行しているだけだった](https://www.slideshare.net/shigeki_ohtsu/processnext-tick-nodejs/6)
- 今はnode.cc do-while内でlibuvのuv_run()を回す二重ループ構造
- これによりuv_run()終了後にemitされる`beforeExit`イベントのリスナーの中でEventloopを復活させることが可能

---

# Eventloopの終了条件

- uv_run()のwhileループの終了条件を見れば良い
  - [`uv__loop_alive()`](https://github.com/nodejs/node/blob/v11.0.0/deps/uv/src/unix/core.c#L338-L342)の戻り値が0になること
  - すなわち`uv__has_active_handles`と`uv__has_active_reqs`が0
  - activeなhandleまたはreqが無くなったとき

## handle/req

||特徴|module例|
| --- | --- | --- |
|handle|I/Oが発生していないときでもイベントループを維持|Timers, server.listen()|
|req|I/O発生中のみイベントループを維持|fs, http|

---

## Nodejsが終了する条件は?

- ~~コードの上から下まで実行された時~~
- ~~登録した非同期処理が全て実行された時~~
  - 実行待ちの非同期処理をdeactivateすることもできるので偽
- Eventloopが終了、つまり**activeなhandle/reqが無くなった時**
  - 但し`beforeExit`イベントリスナー内でhandle/reqが追加されると復活
  - もちろんuncaughtExceptionなどの例外終了やprocess.exit()などによる強制終了もある

---

# [libuv](http://libuv.org/)とは

- クロスプラットフォームで**非同期イベントの監視**を担う
  - `src/`以下でディレクトリ名でunix/winで分かれ、ゴリゴリのマクロで実装
- イベント例
  - 期間
  - timer
  - idle状態
  - 複数のfd(ファイルディスクリプタ)のstatus
      - file(disk I/O)やsocket(network I/O)など
  - SIGNAL
  - 子プロセスの状態
  - 別スレッドからの通知

---

## イベントの監視順

- 記述したコードの上から下の順に非同期コールバックはキューに積まれる
- [uv_run()](https://github.com/nodejs/node/blob/v8.2.0/deps/uv/src/unix/core.c#L351-L377)内の各箇所で評価
  - `uv__run_timers()`でsetTimeout/setIntervalを評価
  - `uv__io_poll()`でI/Oを評価
  - `uv__run_check()`でsetImmediate()を評価

---

## イベントの監視順(コードで確認)

```js:order.js
setImmediate(()=>{
  console.log('setImmediate');
});

var fs = require('fs');
fs.open('./README.md', 'r', (err, fd)=>{
  console.log('fs.open');
  fs.read(fd, Buffer.alloc(1000), 0, len = 1000, null, (err, bytesRead, buffer) => {
    console.log('fs.read');
  });
});
setTimeout(()=>{
  console.log('setTimeout');
}, 1);

console.log('userland');
// 実行結果
// userland
// setTimeout
// fs.open
// setImmediate
// fs.read
```

---

## process.nextTick/Promiseのタイミング

- `process.nextTick`はEventloop内ではなくユーザーランドの実行後
- `Promise`はJSレイヤーなので、Eventloop内ではなくユーザーランドの実行後
- どちらが先？
  - `Promise`は`Object.observe()`と同じ発火タイミングで、`process.nextTick()`の方が先になる

---

## process.nextTick/Promiseのタイミング（コードで確認）

```js:nextTick_vs_promise.js
Promise.resolve().then(() => console.log('promise resolved'));
process.nextTick(() => console.log('next tick'));
// 実行結果
// next tick1
// promise1 resolved
```

---

## （重要）非同期APIの順番は保証されない

- 各イベントの順番は保証されていない
- API互換の保証外の挙動のため、今後の修正で変わりうる
- **順番に依存しないような設計をすべき**

---

# Eventloopを止めるな！(NG)

```js:stop_eventloop.js
const max = process.argv[2] - 0; // 十万以上でやばい
for(let i = 0; i < max; i++)
  console.log('test');
```

- `process.memoryUsage()`などでメモリ利用量の変化を監視
  - rss(アプリケーションのメモリ使用量)がloop回数に比例して増大
- Eventloopが回らず、console.logがイベントキューに溜まり続けてメモリが圧迫されるため[^2]

---

# EventLoopを止めるな!(OK)

- while/forでユーザーランドで同期的に処理させるのではなく、非同期処理でEventloop内で処理をする

```js:alive_eventloop.js
const max = process.argv[2] - 0;
let counter = 1;
const timeout = setInterval(() => {
  if(counter++ > max) clearInterval(timeout);
  console.log('test');
}, 1);
```

---

# まとめ

- Eventloopとは、libuvによるイベント（主にfdやtimer）を監視し続ける仕組み
  - 監視に順番はあるが保証外なので依存しないよう設計する
- NodejsではEventloopが回るよう意識して実装する必要がある
  - 同期実行が長いとtimerが指定時に動かなかったりmemory leakぽい挙動をすることがある

---

[^1]: [Object.observe()とNode.jsのイベントループの関係](http://d.hatena.ne.jp/jovi0608/20140320/1395294325)
[^2]: 但しfd=1(tty)への書き込みは同期的に行われる。console.logは内部的にprocess.stdoutで実装されているが、それが同期か非同期かは出力先によって異なる: https://nodejs.org/api/process.html#process_a_note_on_process_i_o