いくつかの同期コードの実行を一時停止して、node.js のイベント ループに制御を渡す方法に関する問題を解決するためにファイバーを使用しています。これはほとんどうまくいきますが、奇妙なクラッシュに遭遇しましたが、その理由を見つけることができません。
設定
次の 3 つのプロセスがあります。
- メインのサーバー プロセスであり、計測および実行するコードを受け取ります。実行する新しいコードを受け取ったら、child_process.fork() を使用して生成します
- 実行プロセス。これは、実行されたコードで何が起こったかを報告するために、特定のコールバックを時々呼び出すように受信コードをインストルメント化します。次に、 Contextifyを使用して作成されたサンドボックスでコードを実行します。これらのレポートには、コード内の行と列に関する誤った位置情報が含まれていることがあります。その場合、インストルメント化されたコード内の場所を元のコード内の場所にマップするためのソース マップが必要です。ただし、このソース マップの計算にはかなりの時間がかかります。したがって、実行を開始する前に、実行プロセスが生成されます
- ソース マップの計算プロセス。これは、元のコードとインストルメント化されたコードを取り、ソース マップを計算するだけです。完了すると、完成したソース マップを実行プロセスに送信して終了します。
実行プロセスが、実行が完了する前にコールバックでソース マップを必要とする場合、Fiber.yield() を使用して制御をイベント ループに渡し、実行を一時停止します。実行プロセスがデータを受け取ると、pausedFiber.run() を使用して実行を続行します。
これは次のように実装されます。
// server.js / main process
function executeCode(codeToExecute) {
var runtime = fork("./runtime");
runtime.on("uncaught exception", function (exception) {
console.log("An uncaught exception occured in process with id " + id + ": ", exception);
console.log(exception.stack);
});
runtime.on("exit", function (code, signal) {
console.log("Child process exited with code: " + code + " after receiving signal: " + signal);
});
runtime.send({ type: "code", code: code});
}
と
// runtime.js / execution process
var pausedExecution, sourceMap, messagesToSend = [];
function getSourceMap() {
if (sourceMap === undefined) {
console.log("Waiting for source map.");
pausedExecution = Fiber.current;
Fiber.yield();
pausedExecution = undefined;
console.log("Wait is over.")
}
if (sourceMap === null) {
throw new Error("Source map could not be generated.");
} else {
// we should have a proper source map now
return sourceMap;
}
}
function callback(message) {
console.log("Message:", message.type;)
if (message.type === "console log") {
// the location of the console log message will be the location in the instrumented code
/// we have to adjust it to get the position in the original code
message.loc = getSourceMap().originalPositionFor(message.loc);
}
messagesToSend.push(message); // gather messages in a buffer
// do not forward messages every time, instead gather a bunch and send them all at once
if (messagesToSend.length > 100) {
console.log("Sending messages.");
process.send({type: "message batch", messages: messagesToSend});
messagesToSend.splice(0); // empty the array
}
}
// function to send messages when we get a chance to prevent the client from waiting too long
function sendMessagesWithEventLoopTurnaround() {
if (messagesToSend.length > 0) {
process.send({type: "message batch", messages: messagesToSend});
messagesToSend.splice(0); // empty the array
}
setTimeout(sendMessagesWithEventLoopTurnAround, 10);
}
function executeCode(code) {
// setup child process to calculate the source map
importantDataCalculator = fork("./runtime");
importantDataCalculator.on("message", function (msg) {
if (msg.type === "result") {
importantData = msg.data;
console.log("Finished source map generation!")
} else if (msg.type === "error") {
importantData = null;
} else {
throw new Error("Unknown message from dataGenerator!");
}
if (pausedExecution) {
// execution is waiting for the data
pausedExecution.run();
}
});
// setup automatic messages sending in the event loop
sendMessagesWithEventLoopTurnaround();
// instrument the code to call a function called "callback", which will be defined in the sandbox
instrumentCode(code);
// prepare the sandbox
var sandbox = Contextify(new utils.Sandbox(callback)); // the callback to be called from the instrumented code is defined in the sandbox
// wrap the execution of the code in a Fiber, so it can be paused
Fiber(function () {
sandbox.run(code);
// send messages because the execution finished
console.log("Sending messages.");
process.send({type: "message batch", messages: messagesToSend});
messagesToSend.splice(0); // empty the array
}).run();
}
process.on("message", function (msg) {
if (msg.type === "code") {
executeCode(msg.code, msg.options);
}
});
要約すると、新しいコードを受け取ると、それを実行するための新しいプロセスが作成されます。このプロセスは、最初に計測してから実行します。その前に、コードのソース マップを計算する 3 番目のプロセスを開始します。インストルメント化されたコードは、次の名前の関数を呼び出しますcallback
上記のコードで、実行中のコードの進行状況を報告するメッセージをランタイムに渡します。これらは時々調整する必要があります。調整が必要な例の 1 つは、「コンソール ログ」メッセージです。この調整を行うには、3 番目のプロセスで計算されたソース マップが必要です。コールバックがソース マップを必要とする場合、getSourceMap() を呼び出します。これは、sourceMap プロセスが計算を終了するのを待機し、その待機時間中にイベント ループに制御を渡し、sourceMap プロセスからメッセージを受信できるようにします (それ以外の場合、イベント ループはブロックされ、メッセージを受信できませんでした)。
コールバックに渡されたメッセージは、最初に配列に格納されてから、パフォーマンス上の理由からメイン プロセスにバッチとして送信されます。ただし、メイン プロセスがメッセージを長時間待機することは望ましくないため、しきい値に達したときにメッセージのバッチを送信することに加えてsendMessagesWithEventLoopTurnAround()
、イベント ループで実行する関数をスケジュールし、送信するメッセージがあるかどうかを確認します。これには 2 つの利点があります。
- 実行プロセスがソース マップ プロセスを待機している場合、その時間を使用して、既に取得したメッセージを送信できます。そのため、sourceMap プロセスが終了するまでに数秒かかる場合、メイン プロセスは、作成済みで正しいデータを含むメッセージを同じ時間待機する必要はありません。
- 実行中のコードがイベント ループでごくわずかなメッセージしか生成しない場合 (たとえば、
setTimeInterval(f, 2000)
実行ごとに 1 つのメッセージのみを作成するようにスケジュールされた関数によって)、メッセージ バッファーがいっぱいになるまで長時間 (この例では 200 秒) 待機する必要はありません。ただし、10ms ごとに進行状況に関する更新を受信します (何かが変更された場合)。
問題
機能するもの
このセットアップは、次の場合にうまく機能します
- ソース マップを計算するために、ファイバーや別のプロセスは使用しません。代わりに、コードが実行される前にソース マップを計算します。その場合、実行しようとしたすべてのコードが期待どおりに機能します。
- 私はファイバーと別のプロセスを使用し、ソース マップを必要としないコードを実行します。たとえば
var a = 2;
、または
setTimeout(function () { var a = 2;}, 10)
最初のケースでは、出力は次のようになります。
Starting source map generation.
Message: 'variables init'
Message: 'program finished'
Sending messages.
Finished source map generation.
Source map generator process exited with code: 0 after receiving signal: null
- ファイバーと、ソースマップが必要な別のプロセスとコードを使用しますが、イベントループは使用しません。
console.log("foo");
その場合、出力は次のようになります。
Starting source map generation.
Message: 'console log'
Waiting for source map generation.
Finished source map generation.
Wait is over.
Message: 'program finished'
Sending messages.
Source map generator process exited with code: 0 after receiving signal: null
- 私はファイバーと、ソース マップが必要でイベント ループを使用する別のプロセスとコードを使用しますが、ソース マップは、ソース マップの計算が既に完了している場合にのみ必要です (つまり、待機はありません)。
例えば
setTimeout(function () {
console.log("foo!");
}, 100); // the source map generation takes around 100ms
その場合、出力は次のようになります。
Starting source map generation.
Message: 'function declaration'
Message: 'program finished'
Sending messages.
Finished source map generation.
Source map generator process exited with code: 0 after receiving signal: null
Message: 'function enter'
Message: 'console log'
Message: 'function exit'
Sending messages in event loop.
うまくいかないこと
ファイバーを使用し、イベントループを使用するプロセスとコードを分離した場合にのみ壊れますが、終了する前にソースマップが必要です。
setTimeout(function () {
console.log("foo!");
}, 10); // the source map generation takes around 100ms
出力は次のようになります。
Starting source map generation.
Message: 'function declaration'
Message: 'program finished'
Sending messages.
Message: 'function enter'
Message: 'console log'
Waiting for source map generation.
/path/to/code/runtime.js:113
Fiber.yield();
^
getSourceMap (/path/to/code/runtime.js:113:28),callback (/path/to/code/runtime.js:183:9),/path/to/code/utils.js:102:9,Object.console.log (/path/to/code/utils.js:190:13),null._onTimeout (<anonymous>:56:21),Timer.listOnTimeout [as ontimeout] (timers.js:110:15)
Child process exited with code: 8 after receiving signal: null
ここでクラッシュするプロセスが実行プロセスです。しかし、なぜそれが起こるのか、問題を追跡する方法がわかりません。上記でわかるように、何が起こっているのかを調べるために、いくつかのログ ステートメントを既に追加しています。実行プロセスで「キャッチされていない例外」イベントも聞いていますが、それは発生していないようです。
また、最後に表示されるログ メッセージは私のものではありません。ログ メッセージの前に何らかの説明文字列を付けているため、node.js 自体によって作成されたものです。なぜこれが発生するのか、どの終了コード 8 なのか、あるいは原因を絞り込むために他に何ができるのかさえわかりません。どんな助けでも大歓迎です。