node icon indicating copy to clipboard operation
node copied to clipboard

Move ESM loaders off-thread

Open JakobJingleheimer opened this issue 1 year ago • 1 comments

Resolves https://github.com/nodejs/node/issues/43658

JakobJingleheimer avatar Sep 18 '22 11:09 JakobJingleheimer

Review requested:

  • [ ] @nodejs/modules

nodejs-github-bot avatar Sep 18 '22 11:09 nodejs-github-bot

Does each worker thread get its own loader thread? E.g. if an app launches 2x worker threads, there will be 6x threads total: one main, 2x worker, 3x loader?

cspotcode avatar Sep 27 '22 18:09 cspotcode

The current design puts all loaders in the same worker thread (eg there are a total of 2 threads: the main and the worker). Internal things are handled via the main thread; userland things are handled by the worker thread.

JakobJingleheimer avatar Sep 27 '22 18:09 JakobJingleheimer

I think that's slightly different than what I'm asking about. If a node app uses the worker threads API to launch worker threads, does each get its own loader thread? (where each loader thread may have one or more loaders running within it)

Not does each loader get a thread, but does each user thread get a loader thread.

cspotcode avatar Sep 27 '22 18:09 cspotcode

If a node app uses the worker threads API to launch worker threads, does each get its own loader thread?

I think the current state (before this PR) is that loaders always execute in the main thread, even if they’re “for” user code that is in a worker thread. So I would think that after this PR, loaders would run inside their worker thread, regardless of whether they’re customizing main or worker thread user code.

So in other words, for an app using custom loaders where user code that spawns three workers, there are five threads total: main, loaders, and the three workers spawned by the user code.

This is just my guess, others can please correct me if I’m mistaken.

GeoffreyBooth avatar Sep 27 '22 18:09 GeoffreyBooth

I don't know how it's implemented but I would expect that each Node.js thread (main and workers) has its own separate loaders thread, at least for two reasons:

  • Worker threads are supposed to be as isolated as possible from the main thread and from each other
  • You can spawn a worker thread with a different set of --loader flags.

targos avatar Sep 27 '22 18:09 targos

First pass this seems fine but likely should investigate a thread per loader URL/some key. Having it per thread they instrument would be more costly if you spawn/tear down threads.

On Tue, Sep 27, 2022, 1:24 PM Michaël Zasso @.***> wrote:

I don't know how it's implemented but I would expect that each Node.js thread (main and workers) has its own separate loaders thread, at least for two reasons:

  • Worker threads are supposed to be as isolated as possible from the main thread and from each other
  • You can spawn a worker thread with a different set of --loader flags.

— Reply to this email directly, view it on GitHub https://github.com/nodejs/node/pull/44710#issuecomment-1259889616, or unsubscribe https://github.com/notifications/unsubscribe-auth/AABZJI7YII3ZMWWAGIMOC6LWAM3WJANCNFSM6AAAAAAQPNEM7E . You are receiving this because you are on a team that was mentioned.Message ID: @.***>

bmeck avatar Sep 27 '22 18:09 bmeck

What remains to be done?

targos avatar Oct 08 '22 11:10 targos

Something in this implementation is causing node to hang on startup. We suspect it's some kind of circular dependency, and the subsequent dependency smacks into the Atomics lock (preventing the rest of the flow to complete, which would release the initial lock). I think this circular dep is between ESMLoader and Worker (it's rather difficult to troubleshoot as output is swallowed).

There is a working PoC, so we know this works in principle (and that the Atomics otherwise behave appropriately, so the problem lies with integrating it into node).

This was on hold whilst I was on holiday. I'm working on it today and tomorrow (but will be away on a business trip next week).

JakobJingleheimer avatar Oct 08 '22 11:10 JakobJingleheimer

I found the circular dep:

process/esm_loader.js:12modules/esm/public_loader_proxy.js:3worker_thread.js:29/36process/pre_execution.js:562process/esm_loader.js

JakobJingleheimer avatar Oct 08 '22 19:10 JakobJingleheimer

Gah, it wasn't a circular dependency 🤦‍♂️ it was setting url to null here:

https://github.com/nodejs/node/blob/9836c67198b2d01350f283e10b872849e81abefa/lib/internal/worker.js#L141-L149

which caused the worker to spawn with nothing in it.

(I shouldn't have copied it over from the previous attempt at off-threading, which was later hard-coding the url).

I'm not sure if the internal worker should go through the classic path, or merely get require()ed (I tried both, and it doesn't seem to make a difference).

There is (now was) a circular dependency issue after the empty worker issue is addressed in

https://github.com/nodejs/node/blob/9836c67198b2d01350f283e10b872849e81abefa/lib/internal/main/worker_thread.js#L142

that's now fixed too.

NODE_DEBUG=esm,worker ./out/Release/node -p 1 still doesn't print 1, BUT it does now print debugging output, and we can see it getting fairly far along now, so progress:

ESM 13018: hooking up public ESMLoader
ESM 13018: creating worker for publicESMLoader
[…]
WORKER 15812: instantiating Worker. url: file:///…/nodejs/node/lib/internal/modules/esm/worker.js doEval: classic
WORKER 15812: [0] created Worker with ID 1

esm/worker.js's debug (worker for public ESM running) is not printing, so that's likely why things have stalled (as soon as the worker is running, it releases the lock).

JakobJingleheimer avatar Oct 09 '22 12:10 JakobJingleheimer

I'm currently investigating the V8 / C++ error thrown when the Worker is instantiated:

node:internal/worker:202
    this[kHandle] = new WorkerImpl(url,
                    ^
This Environment was initialized without a V8::Inspector
Thrown at:
    at Worker (node:internal/worker:202:21)
    at InternalWorker (node:internal/worker:446:5)
    at node:internal/modules/esm/public_loader_proxy:39:16
    at compileForInternalLoader (node:internal/bootstrap/loaders:331:7)
    at nativeModuleRequire (node:internal/bootstrap/loaders:362:14)
    at node:internal/process/esm_loader:49:45
    at compileForInternalLoader (node:internal/bootstrap/loaders:331:7)
    at nativeModuleRequire (node:internal/bootstrap/loaders:362:14)
    at node:internal/modules/cjs/loader:141:18
    at compileForInternalLoader (node:internal/bootstrap/loaders:331:7)

This error is also occurring in https://github.com/nodejs/node/pull/44732#issuecomment-1253914637.

JakobJingleheimer avatar Oct 23 '22 19:10 JakobJingleheimer

Adding a “request changes” here to see https://github.com/nodejs/node/issues/43658#issuecomment-1288170130 resolved.

It might be a little premature to drop a block: The loaders team (what're we called now?) also want the numbers you're concerned about, and I'm not gonna be the guy to tank node's performance 😉

Feel free to leave the block if you'd rather, but I fear leaving it might give passersby the wrong takeaway on the status of the PR.

JakobJingleheimer avatar Oct 23 '22 20:10 JakobJingleheimer

Now node --input-type=module -e 'console.log(1)' is creating the following error:

Error: [object Module] could not be cloned.
    at serialize (node:v8:371:7)
    at node:internal/modules/esm/worker:81:29
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)

We would need a way to pass those to one thread to another.

aduh95 avatar Nov 06 '22 17:11 aduh95