Use Async Hooks to monitor asynchronous operations

これが[Node.js Advent Calendar 2019](https://qiita.com/advent-calendar/2019/nodejs) 19日目の記事です。宜しくお願いいたします。

# Use Async Hooks to monitor asynchronous operations

非同期がJavascriptの特徴で、そして難しいどころです。この記事がNodeJSのAsync Hooks機能で非同期操作を監視することを紹介したいです。

私がJia Liと申します。非同期について大好きで、[angular/zone.js](https://github.com/angular/angular/tree/master/packages/zone.js)という非同期管理のライブラリのCode Ownerです、一応NodeJSのAsyncHooksのCollaboratorとしてZone.jsとAsyncHooksの連携もやっています。

この記事がNodeJSのAsyncHooksの機能を紹介したいです。

## なんで非同期を監視したいですか？

機能を紹介する前に、まずUseCaseを紹介したいです。

1. 非同期性能を計測
2. 非同期のDebug・Tracing
3. 非同期ユーザ操作の追跡
4. 非同期でContext/Namespaceのようなものがほしい

ということです。

### 性能の計測

例えば、下記のコードでの非同期操作の性能を計測したい。

```javascript
function heavyWork() {
  for (let i = 0; i < 10000; i++) {
  }
}
function asyncOperation1() {
  setTimeout(heavyWork);
}
function asyncOperation2() {
  setTimeout(heavyWork);
}
function testAsync() {
  asyncOperation1();
  asyncOperation2();
}

const start = Date.now();
testAsync();
console.log('performance is', Date.now() - start);
```

非同期の関数の場合、この書き方で性能を正しく計測できないです。
でも、正しく計測したい場合、下記のような面倒なソースを書かないといけないです。
もちろん改善の余地があると思いますが、でもどうしてもいろいろな非同期のための処理を
入れる必要があります。

```javascript
function heavyWork() {
  for (let i = 0; i < 100000; i++) {
    let m = i * i;
  }
}

let total = 0;
let asyncOperation1Done = false;
let asyncOperation2Done = false;

function calculatePerformance(target) {
  const start = Date.now();
  target();
  return Date.now() - start;
}
function asyncOperation1() {
  setTimeout(() => {
    total += calculatePerformance(heavyWork);
    asyncOperation1Done = true;
    if (asyncOperation1Done && asyncOperation2Done) {
      doneFn();
    }
  });
}
function asyncOperation2(doneFn) {
  setTimeout(() => {
    total += calculatePerformance(heavyWork);
    asyncOperation2Done = true;
    if (asyncOperation1Done && asyncOperation2Done) {
      doneFn();
    }
  });
}

function testAsync(doneFn) {
  asyncOperation1(doneFn);
  asyncOperation2(doneFn);
}

testAsync(() => {
  console.log('total performance is', total);
});
```

このようなコードで拡張性もないし、非同期のCallbackにいじる必要もあるし、基本てきには現実ではないです。実際ほしいのはこのような感じのコードです。

```javascript
performanceWatcher.watch(() => {
  testAsync();
});
```

つまり、実際のアプリコードを触らなくて、非同期のLife CycleをInterceptできる方法がほしいです。
AsyncHooksが非同期のLife Cycleでいろいろ Callbackを提供しました、それを利用したら、非同期の監視などができます。
提供されたCallbackが
- init(asyncId, type, triggerAsyncId, resource): 非同期操作が初期化、Scheduleするとき呼び出されます。
- before(asyncId): 非同期のCallbackを実行する前に呼び出されます。
- after(asyncId): 非同期のCallbackを実行したあとで呼び出されます。
- destroy(asyncId): 非同期のリソースが開放するとき、呼び出されます。
- promiseResolve: Promiseのresolve関数を呼び出すときこのCallbackを呼び出されます。Promiseだけ有効です。

になります。
実際がこのようなイメージです。
```
setTimeout(() => { // init is called
  // before is called
  doSomething();
  // after is called
});
// destroyed will be called when VM decide to GC the resource
```

AsyncHooksを有効するため、下記のような設定が必要です。

```javascript
const async_hooks = require('async_hooks');
const asyncHook =
    async_hooks.createHook({ init, before, after, destroy, promiseResolve });
asyncHook.enable();
```

無効するには、

```javascript
asyncHook.disable();
```

そしたら、PerformanceWatcherをAsyncHooksで実装してみます。

```javascript
const async_hooks = require('async_hooks');

const asyncHook = async_hooks.createHook({init, before, after, destroy});
asyncHook.enable();
let total = 0;
let tasks = [];
let perfByAsyncId = {};

let doneCallback;

function init(asyncId, type, triggerAsyncId, resource) {
  tasks.push(asyncId);
}
function before(asyncId) {
  perfByAsyncId[asyncId] = {start: Date.now()};
}
function after(asyncId) {
  perfByAsyncId[asyncId] = {perf: Date.now() - perfByAsyncId[asyncId].start};
  for (let i = 0; i < tasks.length; i++) {
    if (tasks[i] === asyncId) {
      tasks.splice(i, 1);
      break;
    }
  }
  if (tasks.length === 0) {
    Object.keys(perfByAsyncId).forEach(id => {
      total += perfByAsyncId[id].perf;
    });
    doneCallback(total);
  }
}
function destroy(asyncId) {}

function start(targetFn, doneFn) {
  total = 0;
  tasks = [];
  doneCallback = doneFn;
  perfByAsyncId = {};
  targetFn();
}

module.exports.start = start;
```

計測するとき、使い方が下記のようになります。
```javascript
const p = require('./performance_watcher');
p.start(testAsync, (total) => {
  log('total performance is', total);
});
```

performanceWatcherについて、説明させていただきます。

```
// init
function init(asyncId, type, triggerAsyncId, resource) {
  tasks.push(asyncId);
}
```

initのとき、tasksという配列で非同期Idを記録します。この配列がEmptyではないと、なにか非同期の操作がまだ終わっていないという意味です。

```javascript
function before(asyncId) {
  perfByAsyncId[asyncId] = {start: Date.now()};
}
```
非同期のCallbackが実行した前に、開始時間を記録します。

```javascript
function after(asyncId) {
  perfByAsyncId[asyncId] = {perf: Date.now() - perfByAsyncId[asyncId].start};
  for (let i = 0; i < tasks.length; i++) {
    if (tasks[i] === asyncId) {
      tasks.splice(i, 1);
      break;
    }
  }
  if (tasks.length === 0) {
    Object.keys(perfByAsyncId).forEach(id => {
      total += perfByAsyncId[id].perf;
    });
    doneCallback(total);
  }
}
```
非同期のCallbackが実行したあとで、かかる時間を計測して、そして、Tasksの配列からこの非同期Idを削除します。もしTasksの配列がEmptyの場合、すべての非同期が完了ということになります。そして、すべての非同期Callbackかかる時間をプラスして、最後出力します。

このような感じで、実際のテストの対象を触らなくても、非同期の性能計測ができます。
性能計測だけではなく、いろいろな非同期の監視とかもできますので、とっても面白いツールです。

## Zone.js

私がメインでZone.jsをメンテしますが、Zone.jsがやっていることがAsyncHooksと似てます。非同期の監視と管理です、AsyncHooksと違って、Zone.jsがHooksだけではなく、Interceptorです。AsyncHooksが通知だけ受けられますが、Zone.jsが通知を受けるだけではなく、非同期のBehaviorを変わることもできます。皆さんが興味があったら、ぜひ@Quramyさんの[記事](https://qiita.com/Quramy/items/83f4fbc6755309f78ad2)を読んでください。

どうもありがとうございました、まだ宜しくお願いいたします。