これが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のようなものがほしい
ということです。
性能の計測
例えば、下記のコードでの非同期操作の性能を計測したい。
functionheavyWork(){for(leti=0;i<10000;i++){}}functionasyncOperation1(){setTimeout(heavyWork);}functionasyncOperation2(){setTimeout(heavyWork);}functiontestAsync(){asyncOperation1();asyncOperation2();}conststart=Date.now();testAsync();console.log('performance is',Date.now()-start);
非同期の関数の場合、この書き方で性能を正しく計測できないです。
でも、正しく計測したい場合、下記のような面倒なソースを書かないといけないです。
もちろん改善の余地があると思いますが、でもどうしてもいろいろな非同期のための処理を
入れる必要があります。
functionheavyWork(){for(leti=0;i<100000;i++){letm=i*i;}}lettotal=0;letasyncOperation1Done=false;letasyncOperation2Done=false;functioncalculatePerformance(target){conststart=Date.now();target();returnDate.now()-start;}functionasyncOperation1(){setTimeout(()=>{total+=calculatePerformance(heavyWork);asyncOperation1Done=true;if(asyncOperation1Done&&asyncOperation2Done){doneFn();}});}functionasyncOperation2(doneFn){setTimeout(()=>{total+=calculatePerformance(heavyWork);asyncOperation2Done=true;if(asyncOperation1Done&&asyncOperation2Done){doneFn();}});}functiontestAsync(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を有効するため、下記のような設定が必要です。
constasync_hooks=require('async_hooks');constasyncHook=async_hooks.createHook({init,before,after,destroy,promiseResolve});asyncHook.enable();
無効するには、
asyncHook.disable();
そしたら、PerformanceWatcherをAsyncHooksで実装してみます。
constasync_hooks=require('async_hooks');constasyncHook=async_hooks.createHook({init,before,after,destroy});asyncHook.enable();lettotal=0;lettasks=[];letperfByAsyncId={};letdoneCallback;functioninit(asyncId,type,triggerAsyncId,resource){tasks.push(asyncId);}functionbefore(asyncId){perfByAsyncId[asyncId]={start:Date.now()};}functionafter(asyncId){perfByAsyncId[asyncId]={perf:Date.now()-perfByAsyncId[asyncId].start};for(leti=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);}}functiondestroy(asyncId){}functionstart(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ではないと、なにか非同期の操作がまだ終わっていないという意味です。
functionbefore(asyncId){perfByAsyncId[asyncId]={start:Date.now()};}
非同期のCallbackが実行した前に、開始時間を記録します。
functionafter(asyncId){perfByAsyncId[asyncId]={perf:Date.now()-perfByAsyncId[asyncId].start};for(leti=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さんの記事を読んでください。
どうもありがとうございました、まだ宜しくお願いいたします。