2

我正在使用Fibers来解决有关如何让 node.js 中的事件循环控制的问题,暂停一些同步代码的执行。大多数情况下,这很好用,但是我遇到了一个奇怪的崩溃,但是我找不到它的原因。

设置

有三个过程:

  • 一个主服务器进程,它接收要检测和执行的代码。当它接收到执行它的新代码时,使用 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);
    }
});

总结一下:当收到新代码时,会创建一个新进程来执行它。这个过程首先检测然后执行它。在这样做之前,它会启动第三个过程来计算代码的源映射。检测代码调用名为的函数callback在上面的代码中,将消息传递给运行时报告执行代码的进度。有时必须调整这些,需要调整的一个示例是“控制台日志”消息。要进行这种调整,需要通过第三个过程计算的源图。当回调需要源映射时,它调用 getSourceMap() 等待 sourceMap 进程完成其计算并在该等待时间内将控制权交给事件循环以使其能够从 sourceMap 进程接收消息(否则事件循环将是被阻止并且无法接收到任何消息)。

传递给回调的消息首先存储在一个数组中,然后出于性能原因作为批处理发送到主进程。但是,我们不希望主进程等待消息的时间过长,因此除了在达到阈值时发送一批消息外,我们还安排了一个函数sendMessagesWithEventLoopTurnAround()在事件循环中运行并检查是否有消息要发送。这有两个优点:

  1. 当执行进程正在等待源映射进程时,它可以利用这段时间发送它已经得到的消息。因此,如果 sourceMap 进程需要几秒钟才能完成,则主进程不必为已创建并包含正确数据的消息等待相同的时间。
  2. 当执行代码在事件循环中只生成非常少的消息时(例如,通过调度的函数setTimeInterval(f, 2000)每次执行只创建一条消息),它不必等待很长时间直到消息缓冲区已满(在本例中为 200 秒)但每 10 毫秒接收一次有关进度的更新(如果有任何变化)。

问题

什么有效

此设置在以下情况下工作正常

  1. 我不使用纤维和单独的过程来计算源图。相反,我在执行代码之前计算源映射。在这种情况下,我尝试执行的所有代码都按预期工作。
  2. 我确实使用了光纤和一个单独的进程并执行我不需要源映射的代码。例如 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
  1. 我确实使用了纤维和一个单独的进程和代码,我需要源映射但不使用事件循环,例如 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
  1. 我确实使用了 Fiber 和一个单独的进程和代码,我需要源映射并使用事件循环,但只有在源映射计算已经完成时才需要源映射(因此无需等待)。

例如

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 是什么,甚至我还能做些什么来缩小原因。任何帮助将不胜感激。

4

1 回答 1

2

像往常一样,一旦完成对问题的完全描述,就会出现解决方案。

我认为问题在于 setTimeout 执行的代码没有包装在 Fiber 中。因此,在该代码中调用 Fiber.yield() 会崩溃,这是可以理解的。

因此,解决方案是在执行的代码中覆盖 setTimeout。由于我已经提供了一个带有一些特殊功能的沙箱(例如我自己的控制台对象),我还可以将 setTimeout 的实现替换为将执行的功能包装在纤程中的沙箱,如下所示:

// this being the sandbox object, which si the global object for the executing code
this.setTimeout = function (functionToExecute, delay) {
    return setTimeout(function () {
        fibers(functionToExecute).run();
    }, delay);
};

此实现不支持向 setTimeout 传递额外的参数,但它可以很容易地扩展为这样做。它也不支持传递代码字符串而不是函数的 setTimeout 版本,但是谁会使用它呢?

为了使其完全工作,我必须交换setTimeout, setInterval, setImmediate and process.nextTick. 还有什么通常用来完成这样的角色的吗?

这只留下了一个问题,是否有比重新实现这些功能更简单的方法?

于 2013-05-05T13:17:09.813 回答