4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Use Async Hooks to monitor asynchronous operations

Posted at

これがNode.js Advent Calendar 2019 19日目の記事です。宜しくお願いいたします。

Use Async Hooks to monitor asynchronous operations

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

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

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

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

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

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

ということです。

性能の計測

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

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

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

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にいじる必要もあるし、基本てきには現実ではないです。実際ほしいのはこのような感じのコードです。

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を有効するため、下記のような設定が必要です。

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

無効するには、

asyncHook.disable();

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

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;

計測するとき、使い方が下記のようになります。

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ではないと、なにか非同期の操作がまだ終わっていないという意味です。

function before(asyncId) {
  perfByAsyncId[asyncId] = {start: Date.now()};
}

非同期のCallbackが実行した前に、開始時間を記録します。

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さんの記事を読んでください。

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

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?