Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 8952

Use Async Hooks to monitor asynchronous operations

$
0
0

これが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のようなものがほしい

ということです。

性能の計測

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

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

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


Viewing all articles
Browse latest Browse all 8952