-
-
Notifications
You must be signed in to change notification settings - Fork 5.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[babel 8] Move @babel/register
transform to a separate worker
#14025
[babel 8] Move @babel/register
transform to a separate worker
#14025
Conversation
@bmeck I know that you'd like to move ESM loaders to a separate thread so that they can also work for CJS require hooks; you might be interested in this as a real-world use case. |
Build successful! You can test your changes in the REPL here: https://babeljs.io/repl/build/50224/ |
5f7e40d
to
940f4b4
Compare
This comment has been minimized.
This comment has been minimized.
940f4b4
to
d35ef58
Compare
d35ef58
to
33bdff4
Compare
The remaining failures are probably because workers don't flush their console output synchronously 🤔 |
d401cfa
to
96e9d5c
Compare
To make sure that the This is because, by default Node.js first logs messages from the main thread, then yields to the event loop and logs messages from the worker: ExampleIn this code the
const {
isMainThread,
parentPort,
Worker,
MessageChannel,
receiveMessageOnPort,
} = require("worker_threads");
if (isMainThread) {
const worker = new Worker(__filename);
const mc = new MessageChannel();
const signal = new Int32Array(new SharedArrayBuffer(4));
signal[0] = 0;
worker.postMessage({ port: mc.port1, signal }, [mc.port1]);
Atomics.wait(signal, 0, 0);
const { message } = receiveMessageOnPort(mc.port2);
console.log("Received", message);
worker.unref();
} else {
parentPort.on("message", ({ port, signal }) => {
console.log("In worker!");
port.postMessage("Hi :)");
Atomics.store(signal, 0, 1);
Atomics.notify(signal, 0);
});
} The reason for this behavior is that For now I'll accept this behavior and modify our tests not to rely on a specific ordering, and I'll look for a proper fix* if it ever ends up being a real-world problem (I don't think usually plugins * The proper fix is to intercept |
This reverts commit 96e9d5c.
dffc56d
to
7c41309
Compare
static #worker_threads_cache; | ||
/** @type {typeof import("worker_threads")} */ | ||
static get #worker_threads() { | ||
return (WorkerClient.#worker_threads_cache ??= require("worker_threads")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not just use return require("worker_threads")
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Uh right, require
caches internally.
const output = await spawnNodeAsync( | ||
[testFileLog], | ||
path.dirname(testFileLog), | ||
{ NODE_OPTIONS: `-r ${registerFile}` }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{ NODE_OPTIONS: `-r ${registerFile}` }, | |
{ NODE_OPTIONS: `--require ${registerFile}` }, |
}); | ||
it("works with the --require flag", async () => { | ||
const output = await spawnNodeAsync( | ||
["-r", registerFile, testFileLog], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
["-r", registerFile, testFileLog], | |
["--require", registerFile, testFileLog], |
@nicolo-ribaudo it would be good to know exactly what moving to a Worker enables. Implementation to be ESM vs CJS is likely not compelling to various Node.js core contributors so any other expectations would be good. I doubt any performance benefits would come from this since it doesn't look like there will be a shared cache so likely it is some other driving force? |
The driving force is that currently some parts of Babel are optionally async (for example config loading, but we have already the infrastructure to allow using async plugin/preset factories and parsers), and even if it's written in CJS Babel already supports ESM plugins which can only be loaded asynchronously.
|
test.js
Outdated
@@ -0,0 +1,7 @@ | |||
const { isMainThread, Worker } = require("worker_threads"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file should be removed.
babel.config.js
Outdated
{ | ||
test: ["packages/babel-register/**/*.js"].map(normalize), | ||
sourceType: "script", | ||
parserOpts: { allowReturnOutsideFunction: true }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this option only needed by packages/babel-register/src/experimental-worker.js
? If so can we modify the source so that we don't need to universally apply allowReturnOutsideFunction
?
* If @babel/register is imported using the -r/--require flag, we | ||
* must the worker will have the same flag and we must avoid registering |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* If @babel/register is imported using the -r/--require flag, we | |
* must the worker will have the same flag and we must avoid registering | |
* If @babel/register is imported using the -r/--require flag, | |
* the worker will have the same flag and we must avoid registering |
6d523f7
to
e124143
Compare
I re-reviewed this (usually I don't review my own PRs, but it has been almost a month since I wrote this code), and I'm now merging it with a single ✔️ since it shouldn't affect the Babel 7 behavior anyway. |
@babel/register
transforms to a separate worker@babel/register
transform to a separate worker
Sorry I totally missed this PR before the commit landed. @nicolo-ribaudo A few questions: Performance?Do you have any figures on how much slower it performs in a worker? (I assume it will be at least somewhat slower due to added overhead of communication back and forth with worker) Reduce worker communication volumeCould performance be improved by keeping the source map cache (
i.e. Source map object is only passed from worker to main thread if/when it's needed, rather than upfront for every file which is transformed. Unless the application throws an error, this will not happen at all. Internal module cacheIn Babel 8, you intend to remove the code to maintain a separate module cache for modules required by Babel internally. While the internal module cache logic can safely be removed from compile.js, I think nodeWrapper.js is still not completely safe to remove. In main thread, hook.js requires 'source-map-support' package. source-map-support in turn requires a dependency 'from-buffer'. This happens before babel-register is active, so from-buffer is not transpiled, and the untranspiled version will be left in the module cache. If application code calls Same applies to 'source-map' package, and it's dependency 'whatwg-url'. |
Actually, would it be more performant to deal with the code cache in main thread instead of in the worker? Usually, the majority of files will be unchanged since babel-register was last run, so transformed code can just be retrieved from the cache. If cache was handled in the main thread, this would avoid a large number of round trips to the worker. If overhead of calling into the worker is significant, I imagine this could avoid much of the penalty. |
We investigated this when we moved parsing in
We can lazy-require |
Moving Babel to a worker allows us to run it asynchronously, which means that:
.mjs
config files@babel/core
to ESM while still supporting this CJS synchronous require hook@babel/register
with themselves (this check will be unnecessary in the future)This PR is heavily inspired by #13199: it uses the same strategy to communicate synchronously (with
Atomics.wait
/Atomics.notify
+receiveMessageOnPort
), and it has the same architecture withLocalClient
/WorkerClient
to temporarily (until Babel 8) support both running in the same thread and in a separate thread.This PR doesn't expose
@babel/register/experimental-worker
yet because it's.npmignore
d; I will open a semver-minor PR to expose it after that this one is merged (similarly to how #13199 was then exposed by #13398).Unfortunately this diff won't be easy to review, so here is a commit-by-commit description:
@babel/register
to CommonJS.cts
files (or.ts
files without exports) when also using the--isolatedModules
options. These files were almost untyped, so it's not a big loss.packages/babel-register/src/browser.ts
as deleted, but it has just been renamed topackages/babel-register/src/browser.js
.@babel/register
asynchronously in a workerpackages/babel-register/src/cache.js
was moved topackages/babel-register/src/worker/cache.js
without any change. The diff doesn't show the rename because I left a "proxy" CJS package in the old location just in case soneone (for example, our tests) relies on it.packages/babel-register/src/node.js
have mostly been moved topackages/babel-register/src/hook.js
and the non-worker code paths (with all theModule._cache
protection logic) should be the same; the differences are that it now calls intoLocalClient
rather than directly using@babel/core
, andcompileHook
has been renamed tocompileBabel7
. Some parts ofsrc/node.js
have been moved topackages/babel-register/src/worker/transform.js
because they need to run in the worker.src/is-in-register-worker.js
comment, but I also run those tests with the non-worker implementation since it doesn't hurt.@babel/register/experimental-worker
yet this is just to avoid exposing the new implementation, so that we can have different commits for the Babel 8 support and for the new Babel 7 feature (so that one can go in the changelog, and the other commit can be prefixed with[babel 8]
to make it easier to find while writing the Babel 8 changelog). Also, this PR blocks the ESM migration so I don't want to wait for the next minor 😛