これが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を紹介したいです。
- 非同期性能を計測
- 非同期のDebug・Tracing
- 非同期ユーザ操作の追跡
- 非同期で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さんの記事を読んでください。
どうもありがとうございました、まだ宜しくお願いいたします。