Async HooksとAsync Resourcesの導入
概要
この記事はNode.js Advent Calendar 2017 15日目の記事です。
カナダから非同期で失礼します。
Async hooksとはNode.jsの非同期イベントをトレースすることができるネイティブライブラリです。まだ試験段階なのでAPIが変わる可能性がありますが、Async Hooksに入門したので、Async HooksとAsync Resouresの使い方、実際の使用例の考察をまとめました。
Node.jsのバージョンはv9.3.0を使用しています。
Async Hooksの基本的な使い方
まずは下記のコードを見てみます。このコードはAsync Resourcesが1秒ごとに生成され、破棄される様子を見ることができます。
const fs = require('fs'); const util = require('util'); const asyncHooks = require('async_hooks'); const hooks = { // Resourceが生成されるときに呼ばれる init(asyncId, type, triggerAsyncId, resource) { const obj = { asyncId, type, triggerAsyncId, resource }; fs.writeSync(1, `init\n${util.format(obj)}\n`); }, // Resourceのcallbackが呼ばれる直前に呼ばれる before(asyncId) { fs.writeSync(1, `before\t${util.format({ asyncId })}\n`); }, // Resourceのcallbackが終了したときに呼ばれる after(asyncId) { fs.writeSync(1, `after\t${util.format({ asyncId })}\n`); }, // Resourceが破棄されるときに呼ばれる destroy(asyncId) { fs.writeSync(1, `destroy\t${util.format({ asyncId })}\n`); }, // Promiseのresolveが呼ばれるタイミングで呼ばれる promiseResolve(asyncId) { fs.writeSync(1, `promiseResolve\t${util.format({ asyncId })}\n`); } }; // Hookの生成 const asyncHook = asyncHooks.createHook(hooks); // Hookを有効にする asyncHook.enable(); // 1秒おきにResourceの生成 (function start() { // 実行中のasyncId const eid = asyncHooks.executionAsyncId(); // 呼び出し元のasyncId const tid = asyncHooks.triggerAsyncId(); console.log(`start\teid: ${eid} tid: ${tid}`); setTimeout(start, 1000); })(); // 10秒後にhookを無効にする setTimeout(() => asyncHook.disable(), 10000);
createHook
に任意の関数を渡すことで、非同期イベントの検知が簡単にできるようになります。特別難しい機能は無いので、それぞれの用語については下記の表にまとめました。
async_hooks
用語 | 型 | 説明 |
---|---|---|
createHook | Function | Hookの作成 |
executionAsyncId | Function | 実行中のasyncIdの取得 |
triggerAsyncId | Function | 呼び出し元のasyncIdの取得 |
createHook
用語 | 型 | 説明 |
---|---|---|
init | Function | Resoureが生成される時に呼ばれる |
before | Function | Resourceがcallbackを呼ぶ直前で呼ばれる |
after | Function | Resoureがcallbackを呼び終えた直後に呼ばれる |
destroy | Function | Resoureが破棄される時に呼ばれる |
promiseResolve | Function | PromiseのResourceでresolve が呼ばれる時に呼ばれる |
asyncId | number | Async Resourceに割り当てられたid |
triggetAsyncId | number | 呼び出し元のasyncId |
enable | Function | Hookの有効化 |
disable | Function | Hookの無効化 |
⚠使用上の注意⚠
createHook
の関数内でエラーが発生した際はuncaughtException
でキャッチすることはできず、スタックトレースを残してプロセスが終了されます。
またconsole.log
, process.stdout.write
は非同期オペレーションなためAsync Resourceが生成されます。Hook内の各関数ではfs.writeSync
を使用してください。
スタックトレースを遡る
何か面白いことできないかなぁと探っていたところ、興味深いコードを見つけたので、これをベースにAsync Resouresの誕生から終焉までの軌跡を表示するようにしてみました。 実際のログはこちらです。コードは以下のとおりです。
const delay = util.promisify(setTimeout); const DELAY = 1000; const map = new Map(); function init(asyncId, type, triggerAsyncId, resource) { const obj = { asyncId, type, triggerAsyncId, resource }; Error.captureStackTrace(obj, init); map.set(asyncId, [obj.stack, triggerAsyncId]); fs.writeSync(1, `init\n${util.format(obj)}\n`); } function destroy(asyncId) { fs.writeSync(1, `destroy\tasyncId: ${asyncId}\n`); showStackTrace(asyncId); map.delete(asyncId); } function showStackTrace(asyncId) { const array = map.get(asyncId); if (!array) { return; } const [stack, tid] = array; fs.writeSync(1, `stack: \tasyncId: ${asyncId} triggetAsyncId: ${tid}\n${stack.replace(/(.*)\n/, '')}\n`); showStackTrace(tid); } asyncHooks.createHook({ init, destroy }).enable(); let promise = delay(DELAY); for (let i = 0; i < 10; i++) { promise = promise.then(() => delay(DELAY)); }
スタックトレースを遡ることができるので、デバッグに使えるかもしれません。
メモリリークの検知
以下のコードはAsync Resousesが生成されたものの、リソースが破棄されない例です。map
のサイズが次第に大きくなっていくことが一目でわかるので、メモリリークの検知の早期発見に使えるかもしれません。
const map = new Map(); function init(asyncId) { map.set(asyncId, 1); fs.writeSync(1, `init\tasyncId: ${asyncId} mapSize: ${map.size}\n`); } function destroy(asyncId) { map.delete(asyncId); fs.writeSync(1, `destroy\tasyncId: ${asyncId} mapSize: ${map.size}\n`); } asyncHooks.createHook({ init, destroy }).enable(); const queue = []; (function start() { new Promise(resolve => queue.push(resolve)); setTimeout(start, 100); })();
Async Resourcesの基本的な使い方
AsyncResource
クラスを使用することで、任意の非同期イベントを作成することが可能です。emitBefore
とemitAfter
はセットなので発火する際には必ず両方を呼ぶ必要があります。
const { AsyncResource } = require('async_hooks'); // 自動的にinitが呼ばれる const asyncResource = new AsyncResource('Async'); // Callbackを呼ぶ直前に呼ぶ asyncResource.emitBefore(); // Callbackが終了した直後に呼ぶ asyncResource.emitAfter(); // AsyncResourceが破棄される時に呼ぶ asyncResource.emitDestroy(); // AsyncResourceのインスタンスに割り当てられたasyncIdを返す asyncResource.asyncId(); // 呼び出し元のasyncIdを返す asyncResource.triggerAsyncId();
Async Resourcesの導入
こちらのBluebird
のPRによると、メモリリークの問題も解決し安定してきたようなので、実際にAigle
のライブラリに適用してみようと思います。コミットはこちら。
まず、Nodeのバージョンがv9.3.0以上推奨とのことでv9.3.0以上のみを有効にしました。
const node = typeof process !== 'undefined' && process.toString() === '[object process]'; const supportAsyncHook = node && (() => { const support = '9.3.0'; const [smajor, sminor] = support.match(/\d+/g); const version = process.versions.node; const [vmajor, vminor] = version.match(/\d+/g); return vmajor > smajor || vmajor === smajor && vminor >= sminor; })();
次にconstructor
でAsyncResource
を生成します。
class Aigle { constructor(executor) { ... this._resource = supportAsyncHook && new AsyncResource('PROMISE'); ... } }
今回はPromiseライブラリなので、Native Promiseと同じtypeを使用しました。
これでAigle
インスタンスが生成されるときにinit
が呼ばれるようになります。
次に、onFulfilled
またはonRejected
が呼ばれる直前にemitBefore
を、直後にemitAfter
を追加します。
簡単な方法としては、
const original = onFulfilled; onFulfilled = value => { this._resource.emitBefore(); try { return original(value); } catch (e) { throw e; } finally { this._resource.emitBefore(); } };
以上のように追加するのが簡単ですが、パフォーマンスを考慮して別の関数にしました。callbackが呼ばれる直前・直後に追加すれば問題ありません。
AsyncResource
の問題点としてパフォーマンスが著しく低下するため、デフォルトではAsync Resourcesを無効にしています。有効にする方法は、
Aigle.config({ asyncResource: true });
で使用することができます。
パフォーマンスについては後日調査してまた記事を書こうと思います。
まとめ
Async Hooksを使って非同期イベントのトレースを簡単にすることができるようになりました、デバッグにとても便利そうなライブラリです。
またAsync Resourcesを使うことでカスタムな非同期イベントを生成できますが、パフォーマンスに難がありそうです。Immediate
イベントは自動的に発行されるので特別Async Resourcesを使う必要はないのかなぁとも思いましたが、あまり詳しくないのでわかりません。もし詳しい方いたらぜひシェアしていただけたらうれしいです。
今後も最新の情報収集とパフォーマンスの調査を行っていきたいと思います。