mocha-parallel / mocha-parallel-tests

Parallel test runner for mocha tests. Looking for maintainer.
MIT License
200 stars 45 forks source link

Investigate: using node async hooks to detect that process is about to exit #169

Open 1999 opened 6 years ago

1999 commented 6 years ago

The parent issue is #155 - we need to re-use forked processes because processes spawning is pretty expensive in terms of timing and RAM.

  1. if mocha is executed with a --exit flag, this is a bit easier: when the "end" event is fired in the subprocess it's about time to launch the next test (i.e. test file). A small problem here is that there can still be a case where some async action happens after some time and crashes the process.

  2. But if mocha is executed without this flag, which is a default behaviour, things become trickier. It means that the subprocess should listen for process "exit" event and only then it should send the data to the main process. But the issue is that the process.on('exit', handler) handler should be synchronous, i.e. we can't stop the process from exiting inside the handler. That's the reason why re-using processes is currently not implemented.

My guess is related to node.js async_hooks API which appeared in node@8 version. It allows you to see which "handles" prevent the process from exiting etc. It also has "destroy" and other hooks that we can use to detect that there's no more "handles" in the process which prevent the process from exiting. And ideally it should happen before "exit" event is fired. So if we could use this event (no more "handles") to detect that there's nothing left, we could've started re-using the process for other tests even if --exit flag is not specified.

As a side task here: it would be great to investigate how jest does that: does it re-use forked processes or spawns a new independent process for each file. Investigation result should be added to project's wiki.

1999 commented 6 years ago

cc @k03mad, this is a pretty advanced task and it's using a relatively new node API async_hooks. An example of its usage is this package - it's pretty straightforward. If you want to dive deep into JavaScript event loop, there's nothing better than this task.

1999 commented 6 years ago

Side note: chrome inspector is also using async hooks:

(function (exports, require, module, process) {'use strict';

const { createHook } = require('async_hooks');
const inspector = process.binding('inspector');
const config = process.binding('config');

if (!inspector || !inspector.asyncTaskScheduled) {
  exports.setup = function() {};
  return;
}

const hook = createHook({
  init(asyncId, type, triggerAsyncId, resource) {
    // It's difficult to tell which tasks will be recurring and which won't,
    // therefore we mark all tasks as recurring. Based on the discussion
    // in https://github.com/nodejs/node/pull/13870#discussion_r124515293,
    // this should be fine as long as we call asyncTaskCanceled() too.
    const recurring = true;
    if (type === 'PROMISE')
      this.promiseIds.add(asyncId);
    else
      inspector.asyncTaskScheduled(type, asyncId, recurring);
  },

  before(asyncId) {
    if (this.promiseIds.has(asyncId))
      return;
    inspector.asyncTaskStarted(asyncId);
  },

  after(asyncId) {
    if (this.promiseIds.has(asyncId))
      return;
    inspector.asyncTaskFinished(asyncId);
  },

  destroy(asyncId) {
    if (this.promiseIds.has(asyncId))
      return this.promiseIds.delete(asyncId);
    inspector.asyncTaskCanceled(asyncId);
  },
});

hook.promiseIds = new Set();

function enable() {
  if (config.bits < 64) {
    // V8 Inspector stores task ids as (void*) pointers.
    // async_hooks store ids as 64bit numbers.
    // As a result, we cannot reliably translate async_hook ids to V8 async_task
    // ids on 32bit platforms.
    process.emitWarning(
      'Warning: Async stack traces in debugger are not available ' +
      `on ${config.bits}bit platforms. The feature is disabled.`,
      {
        code: 'INSPECTOR_ASYNC_STACK_TRACES_NOT_AVAILABLE',
      });
  } else {
    hook.enable();
  }
}

function disable() {
  hook.disable();
}

exports.setup = function() {
  inspector.registerAsyncHook(enable, disable);
};

});