diff --git a/benchmark/green_threads/roundtrip.js b/benchmark/green_threads/roundtrip.js new file mode 100644 index 000000000000..8eda4c9bd36b --- /dev/null +++ b/benchmark/green_threads/roundtrip.js @@ -0,0 +1,40 @@ +'use strict'; +const common = require('../common.js'); +const { once } = require('events'); + +const bench = common.createBenchmark(main, { + kind: ['green-thread', 'worker'], + concurrency: [1, 16], + n: [10000], +}, { + flags: ['--experimental-green-threads', '--no-warnings'], +}); + +// Sends `n` messages in total, spread over `concurrency` threads, each of +// which echoes every message back before it gets the next one. +async function main({ kind, concurrency, n }) { + let threads; + if (kind === 'green-thread') { + const { GreenThread } = require('node:green_threads'); + threads = Array.from({ length: concurrency }, () => new GreenThread( + 'onmessage = ({ data }) => postMessage(data);')); + } else { + const { Worker } = require('worker_threads'); + threads = Array.from({ length: concurrency }, () => new Worker(` + const { parentPort } = require('worker_threads'); + parentPort.on('message', (data) => parentPort.postMessage(data)); + `, { eval: true })); + await Promise.all(threads.map((w) => once(w, 'online'))); + } + const perThread = Math.ceil(n / concurrency); + bench.start(); + await Promise.all(threads.map(async (thread) => { + for (let i = 0; i < perThread; i++) { + const reply = once(thread, 'message'); + thread.postMessage(i); + await reply; + } + })); + bench.end(perThread * concurrency); + await Promise.all(threads.map((thread) => thread.terminate())); +} diff --git a/benchmark/green_threads/rpc.js b/benchmark/green_threads/rpc.js new file mode 100644 index 000000000000..a16033bb6444 --- /dev/null +++ b/benchmark/green_threads/rpc.js @@ -0,0 +1,52 @@ +'use strict'; +const common = require('../common.js'); + +const bench = common.createBenchmark(main, { + direction: ['green-to-host', 'host-to-green'], + concurrency: [1, 16], + n: [10000], +}, { + flags: ['--experimental-green-threads', '--no-warnings'], +}); + +// Makes `n` RPC calls in total, spread over `concurrency` green threads, each +// of which makes or receives one call at a time. +async function main({ direction, concurrency, n }) { + const { GreenThread } = require('node:green_threads'); + const perThread = Math.ceil(n / concurrency); + let started; + const ready = new Promise((resolve) => { started = resolve; }); + const echoes = []; + const greenThreads = Array.from({ length: concurrency }, () => { + return new GreenThread(direction === 'green-to-host' ? ` + onmessage = async ({ data: n }) => { + for (let i = 0; i < n; i++) await env.echo(i); + postMessage(0); + }; + ` : 'env.register((value) => value);', { + env: { + echo: (value) => value, + register(echo) { + echoes.push(echo); + if (echoes.length === concurrency) started(); + }, + }, + }); + }); + if (direction === 'host-to-green') await ready; + bench.start(); + await Promise.all(greenThreads.map(async (greenThread, index) => { + if (direction === 'green-to-host') { + const done = new Promise((resolve) => { + greenThread.once('message', resolve); + }); + greenThread.postMessage(perThread); + await done; + } else { + const echo = echoes[index]; + for (let i = 0; i < perThread; i++) await echo(i); + } + })); + bench.end(perThread * concurrency); + await Promise.all(greenThreads.map((greenThread) => greenThread.terminate())); +} diff --git a/benchmark/green_threads/startup.js b/benchmark/green_threads/startup.js new file mode 100644 index 000000000000..d3bc7fbc344d --- /dev/null +++ b/benchmark/green_threads/startup.js @@ -0,0 +1,49 @@ +'use strict'; +const common = require('../common.js'); +const { once } = require('events'); + +const bench = common.createBenchmark(main, { + kind: ['green-thread', 'green-thread-warm', 'worker', 'vm-context'], + n: [50], +}, { + flags: ['--experimental-green-threads', '--no-warnings'], +}); + +// Starts `n` of `kind` at once, and measures until each has sent a message. +async function main({ kind, n }) { + switch (kind) { + case 'green-thread': + case 'green-thread-warm': { + const { GreenThread, setWarmPoolSize } = require('node:green_threads'); + if (kind === 'green-thread-warm') await setWarmPoolSize(n); + bench.start(); + const greenThreads = Array.from( + { length: n }, () => new GreenThread('postMessage(0);')); + await Promise.all(greenThreads.map((g) => once(g, 'message'))); + bench.end(n); + await setWarmPoolSize(0); + await Promise.all(greenThreads.map((g) => g.terminate())); + break; + } + case 'worker': { + const { Worker } = require('worker_threads'); + bench.start(); + const workers = Array.from({ length: n }, () => new Worker( + "require('worker_threads').parentPort.postMessage(0);", + { eval: true })); + await Promise.all(workers.map((w) => once(w, 'message'))); + bench.end(n); + await Promise.all(workers.map((w) => w.terminate())); + break; + } + case 'vm-context': { + const vm = require('vm'); + bench.start(); + for (let i = 0; i < n; i++) { + vm.runInContext('0', vm.createContext()); + } + bench.end(n); + break; + } + } +} diff --git a/doc/api/cli.md b/doc/api/cli.md index 415b65ac5efa..84e14219b555 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -1469,6 +1469,16 @@ added: Enable exposition of [EventSource Web API][] on the global scope. +### `--experimental-green-threads` + + + +> Stability: 1 - Experimental + +Enable the experimental [`node:green_threads`][] module. + ### `--experimental-import-meta-resolve` + + + +> Stability: 1.0 - Early development + + + +The `node:green_threads` module runs JavaScript in lightweight, isolated +threads of execution. It is available only when the +[`--experimental-green-threads`][] flag is used. + +```mjs +import { GreenThread } from 'node:green_threads'; +``` + +```cjs +const { GreenThread } = require('node:green_threads'); +``` + +This module is only available under the `node:` scheme. Loading it emits an +`ExperimentalWarning`. + +Each green thread has its own V8 isolate, so it has its own heap and garbage +collector, and it can be terminated without affecting any other thread. +Unlike a [`Worker`][], a green thread does not get its own OS thread, event +loop, or Node.js environment. Green threads are scheduled onto a small, +process-wide pool of OS threads, which makes them much cheaper to create. + +A green thread only has the JavaScript built-ins defined by ECMAScript, plus +the following globals: + +* `postMessage(value[, transfer])`: Sends a structured clone of `value` to the + host. The host receives it as a [`'message'`][] event. `transfer` is as for + [`greenThread.postMessage()`][]. +* `onmessage`: Set this to a function to receive messages sent with + [`greenThread.postMessage()`][]. The function is called with an object + whose `data` property holds the message, and whose `ports` property holds + the [`MessagePort`][]s that were transferred with it. +* `close()`: Stops the green thread once the current task has completed. The + exit code is `0`. +* `queueMicrotask(callback)` +* `setTimeout(callback, delay)`, `clearTimeout(id)`, + `setInterval(callback, delay)`, `clearInterval(id)`. Extra arguments are not + passed to `callback`. +* `console.log()`, `console.info()`, `console.warn()`, `console.error()`, + `console.debug()`: Forwarded to the host's `console`. Arguments that can't + be cloned are forwarded as strings. +* [`EventTarget`][], [`Event`][], [`AbortController`][], [`AbortSignal`][] and + [`DOMException`][]. There is no DOM tree, so events are only dispatched to + their target. Errors thrown by event listeners are uncaught errors of the + green thread. +* [`MessageChannel`][], [`MessagePort`][] and `MessageEvent`. See + [Message ports][]. +* `env`: The `env` option of the [`GreenThread`][] constructor, if it was + given. See [RPC][]. +* [`RpcTarget`][]: See [RPC][]. + +Green threads have no access to `require()`, `process`, or any other Node.js +API. They can only import the modules that the host supplies. See +[ES modules][]. + +```cjs +const { GreenThread } = require('node:green_threads'); + +const greenThread = new GreenThread(` + onmessage = ({ data }) => postMessage(data * 2); +`); +greenThread.on('message', (value) => { + console.log(value); // Prints: 42 + greenThread.terminate(); +}); +greenThread.postMessage(21); +``` + +An uncaught exception or unhandled promise rejection inside a green thread +emits an [`'error'`][] event on the host, followed by an [`'exit'`][] event +with exit code `1`. + +### Errors + +Errors that a green thread throws reach the host as copies: in +[`'error'`][] events, as rejections of [`greenThread.evaluate()`][], and from +calls to stubs, in both directions. As with errors of [`Worker`][]s, the +copies keep the class of the error, or of the nearest built-in error class +it extends, such as `TypeError`, and its data properties, own and inherited, +such as `name`, `message`, `stack` and `code`. `cause`, and the `errors` of +an `AggregateError`, are copied the same way. They are real errors: +`Error.isError()` is `true` for them. Properties that can't be cloned are +left out. [`DOMException`][]s stay `DOMException`s. Other thrown values are +structured clones, or strings if they can't be cloned. + +Errors that are passed as values, such as in messages or results, are +structured clones, as with [`port.postMessage()`][]. + +## RPC + +A green thread can only use what its host passes it. Besides messages, the +host can give it an `env` object whose functions the green thread can call, +and the green thread can pass functions and objects back. Calls return +promises, which are fulfilled with structured clones of what the call returns. + +```cjs +const { GreenThread, RpcTarget } = require('node:green_threads'); + +class Counter extends RpcTarget { + #count = 0; + increment() { + return ++this.#count; + } +} + +const store = new Map(); +const greenThread = new GreenThread(` + (async () => { + await env.db.put('greeting', 'hello'); + const counter = await env.newCounter(); + await counter.increment(); + postMessage([await env.db.get('greeting'), await counter.increment()]); + })(); +`, { + env: { + db: { + get: (key) => store.get(key), + put: (key, value) => { store.set(key, value); }, + }, + newCounter: () => new Counter(), + }, +}); +greenThread.on('message', (value) => { + console.log(value); // Prints: [ 'hello', 2 ] + greenThread.terminate(); +}); +``` + +These values are not cloned, but passed as stubs that call the side they came +from: + +* Functions in the `env` object, at any depth within plain objects and arrays. +* Functions that are passed directly as arguments, or returned. +* [`RpcTarget`][] instances, anywhere in arguments and results. Their stubs + have the methods of the instance, including inherited ones, but no other + properties. + +A stub that is passed back to the side it came from is the original function +or object again. Each side can call the other's stubs: a green thread can +pass the host a callback, or an `RpcTarget` of its own. + +[`AbortSignal`][]s can be passed too, anywhere in arguments and results. The +other side gets an `AbortSignal` that aborts with a clone of the reason when +the original does. + +Errors thrown by a call reject its promise with a copy of the error, as +described in [Errors][]. + +Once a side no longer uses a stub, and it is garbage collected, or disposed +with `stub[Symbol.dispose]()`, the other side releases the function or object +that it refers to. When a green thread exits, calls that are pending, and +calls to its stubs on the host, are rejected with [`ERR_GREEN_THREAD_EXITED`][]. + +The result of a call that a green thread makes continues in the invocation +context that made it, so that timers it creates run on the same pool thread, +as if no call had been made. + +## Message ports + +Green threads have the web's [`MessageChannel`][] and [`MessagePort`][]. Their +ports are the same as those of `node:worker_threads` underneath, so a port can +connect green threads with each other, with their host, and with +[`Worker`][]s. Ports are transferred +with `postMessage()` on either side, or with a port's own `postMessage()`, by +listing them in the transfer list: + +```cjs +const { GreenThread } = require('node:green_threads'); +const { MessageChannel } = require('node:worker_threads'); + +const greenThread = new GreenThread(` + onmessage = ({ ports: [port] }) => { + port.onmessage = ({ data }) => port.postMessage(data * 2); + }; +`); +const { port1, port2 } = new MessageChannel(); +greenThread.postMessage('port', [port2]); +port1.once('message', (value) => { + console.log(value); // Prints: 42 + port1.close(); + greenThread.terminate(); +}); +port1.postMessage(21); +``` + +In green threads, ports are [`EventTarget`][]s. They emit `'message'` and +`'messageerror'` events, which are `MessageEvent`s, and a `'close'` event once +either port of the channel is closed. As in `node:worker_threads`, a port starts +delivering messages once `start()` is called, `onmessage` is set, or a +`'message'` listener is added. Ports stay open until they are closed or +transferred, or their green thread exits. + +Ports can carry what `postMessage()` can, including transferred `ArrayBuffer`s +and other ports, but not [RPC][] stubs. A message from Node.js that a green +thread can't receive, such as one with a {FileHandle} or a +`WebAssembly.Module`, is a `'messageerror'` event whose `data` is a +`DataCloneError`. + +## ES modules + +With `type: 'module'`, `source` is an ES module. It can import other modules, +which the host supplies, and its exports can be called from the host: + +```cjs +const { GreenThread } = require('node:green_threads'); + +const greenThread = new GreenThread(` + import { scale } from './scale.js'; + export function resize(width, height) { + return [scale(width), scale(height)]; + } +`, { + type: 'module', + filename: 'file:///app/main.js', + modules: { + 'file:///app/scale.js': 'export const scale = (x) => x / 2;', + }, +}); +greenThread.exports.then(async ({ resize }) => { + console.log(await resize(640, 480)); // Prints: [ 320, 240 ] + greenThread.terminate(); +}); +``` + +Green threads have no access to the file system, so modules come from the +host: + +* Specifiers that are URLs, or start with `/`, `./` or `../`, are resolved as + URLs, relative to the URL of the importing module. Other specifiers are + resolved with the `resolve` hook. `node:` URLs can't be imported. +* Modules are taken from `modules`, by URL, and otherwise from the `load` + hook. + +The hooks run on the host's thread, and may be asynchronous. They are called +at most once for each specifier and module: + +* `resolve(specifier, context)`: `context` has the `referrer`, the URL of the + importing module, and the import `attributes`. Returns `{ url }`, where + `url` is an absolute URL, or `undefined` if the specifier can't be resolved. +* `load(url, context)`: `context` has the import `attributes`. Returns + `{ source, format }`, where `source` is a string, an `ArrayBuffer` or a + `TypedArray` containing UTF-8, and `format` is `'module'` (the default) or + `'json'`, or `undefined` if there is no such module. JSON modules have to be + imported `with { type: 'json' }`. + +What can't be imported is an `ERR_MODULE_NOT_FOUND` error. A green thread +fetches all modules that a module imports before it runs any of them, also +for `import()`. Scripts can use `import()` too. `import.meta.url` is the +module's URL. + +[`greenThread.exports`][] is a promise for the exports of the main module. +Its functions are stubs, as with [RPC][]: each call runs in the green thread, +in an invocation context of its own, as messages do. Other exports are +copies, taken when the main module has been evaluated. Exported classes +can't be constructed from the host. Export a function that returns an +[`RpcTarget`][] instead. + +## Class: `RpcTarget` + + + +The base class of objects that are passed as stubs, whose methods the other +side can call. Green threads have it as a global too. + +## Scheduling + +A green thread is not tied to an OS thread. Each message, evaluation, and +timer callback is a separate task, and the tasks of a green thread run one at +a time, in order, but not necessarily on the same OS thread. Other than +through [CPU time limits][], green threads are not preempted: a task that +never returns keeps one pool thread busy until the green thread is +terminated. Other green threads keep running on the remaining pool threads. + +Timers belong to the message or evaluation that created them, or to the +initial script, and their callbacks run on the same OS thread as that task. + +## Resource limits + +Each green thread can have its own CPU time and memory limits, which are set +with the `resourceLimits` option of the [`GreenThread`][] constructor. A green +thread that reaches a limit is stopped, without affecting any other thread. + +### CPU time limits + +`maxTaskCpuTimeMs` limits the CPU time of each task, and `maxCpuTimeMs` that of +all tasks of the green thread together. A task includes the microtasks that +run after it, such as promise reactions. Time spent waiting, for example for +a timer, does not count. + +When a task reaches either limit, it is interrupted, even if it never yields: +the green thread emits an [`'error'`][] event with an +[`ERR_GREEN_THREAD_CPU_LIMIT`][] error and exits. Limits are enforced within +about a millisecond, and by the time a task is interrupted it may have used +slightly more CPU time than allowed. + +```cjs +const { GreenThread } = require('node:green_threads'); + +const greenThread = new GreenThread('for (;;);', { + resourceLimits: { maxTaskCpuTimeMs: 50 }, +}); +greenThread.on('error', (error) => { + console.log(error.code); // Prints: ERR_GREEN_THREAD_CPU_LIMIT +}); +``` + +### Memory limits + +`maxMemoryMb` limits the memory used by the JavaScript heap and by +`ArrayBuffer`s together. An `ArrayBuffer` allocation that would exceed the +limit throws a `RangeError`, which the green thread can catch. To leave room +for the heap to grow until the next garbage collection, such allocations fail +once the heap and `ArrayBuffer`s together would use more than 15/16 of the +limit. When the heap +together with the `ArrayBuffer`s still in use exceeds the limit, the green +thread emits an [`'error'`][] event with an [`ERR_GREEN_THREAD_OUT_OF_MEMORY`][] +error and exits. This is checked after each garbage collection, so memory use +may briefly exceed the limit. See [`greenThread.postMessage()`][] for +`ArrayBuffer`s that are transferred. + +`maxOldGenerationSizeMb` and `maxYoungGenerationSizeMb` limit only the heap, +like the [`Worker`][] options of the same names. + +### Green threads that don't stop + +Resource limits and [`greenThread.terminate()`][] interrupt the running +JavaScript code. In rare cases, V8 compiles code in a way that no longer +handles interrupts, so the green thread keeps running. Node.js then stops +waiting for it after a grace period, one second by default: the green thread +emits the [`'error'`][] +event for the limit it reached, if any, and the [`'exit'`][] event with exit +code `1`, and the process emits a `GreenThreadWarning`. The green thread is +abandoned: it never runs again, and the memory it uses is not freed until the +process exits. + +Node.js then stops the pool thread that was running it as soon as that thread +runs JavaScript code, so that it can run other green threads again. Where that +isn't possible, or doesn't succeed within 10 seconds by default, Node.js +aborts the process. Stopping pool threads is only supported on Linux on x64 +and arm64. [`greenThreads.setUnresponsiveTimeouts()`][] changes both +durations. + +## `greenThreads.setUnresponsiveTimeouts(timeouts)` + + + +* `timeouts` {Object} + * `gracePeriodMs` {integer} How long a green thread has to stop after it was + asked to, in milliseconds, before it is abandoned. Between `1` and + `2 ** 31 - 1`. **Default:** `1000`. + * `abortTimeoutMs` {integer|Infinity} How long Node.js tries to stop the + pool thread running an abandoned green thread, in milliseconds, before it + aborts the process. Between `0` and `2 ** 31 - 1`, or `Infinity` to never + abort. **Default:** `10000`. + +Sets, for the whole process, how Node.js handles [green threads that don't +stop][]. Omitted values are not changed. The new values apply to green threads +that are asked to stop from then on. + +A short grace period may abandon green threads that would have stopped, for +example after a long garbage collection. With an `abortTimeoutMs` of +`Infinity`, a pool thread that can't be stopped keeps using CPU time until the +process exits, and the process doesn't wait for it when it exits. + +## `greenThreads.setWarmPoolSize(size)` + + + +* `size` {integer} An integer between `0` and `1024`. +* Returns: {Promise} Fulfilled with `undefined` once the pool holds `size` + isolates that are ready to use. + +Sets how many green threads are prepared ahead of time, for the whole process. +**Default:** `0`. + +```cjs +const { GreenThread, setWarmPoolSize } = require('node:green_threads'); + +(async () => { + await setWarmPoolSize(16); + // These start without having to create an isolate. + const greenThreads = Array.from({ length: 16 }, () => new GreenThread('')); +})(); +``` + +Most of the time it takes to start a green thread is spent creating its V8 +isolate. Isolates in the warm pool are created in the background, when pool +threads have nothing else to do, so green threads that use them start almost +immediately. Each isolate is used by one green thread only: the pool is +refilled after a green thread takes one. Isolates in the warm pool use memory, +about 3 MB each. + +Only green threads without the `maxMemoryMb`, `maxOldGenerationSizeMb` and +`maxYoungGenerationSizeMb` options, and in their default isolate group, use the +warm pool. + +## Isolate groups + +When Node.js is built with `--experimental-enable-pointer-compression`, V8 +limits the heaps of all isolates in an isolate group to 4 GB together. Isolates +in different groups are independent of each other: each group has its own +pointer compression cage and its own copy of the read-only part of the heap. +[`IsolateGroup.supported`][] tells whether this build of Node.js supports +creating isolate groups. + +Where it does, each green thread gets an isolate group of its own by default, +so that green threads can't use up the heap space of the main thread or of +each other. Isolates in groups that only green threads use also start from +V8's own startup snapshot, which is smaller than the one Node.js uses, so they +use less memory. The `isolateGroup` option of the [`GreenThread`][] constructor +selects the group instead: + +* `'new'`: a group of its own. The default where isolate groups are supported. +* `'default'`: V8's default group, which the main thread and workers are in + unless they have groups of their own. The default where isolate groups are + not supported. +* An [`IsolateGroup`][]: a group that green threads share. + +```cjs +const { GreenThread, IsolateGroup } = require('node:green_threads'); + +if (IsolateGroup.supported) { + const group = new IsolateGroup(); + // Both green threads together can use at most 4 GB of heap. + const a = new GreenThread('', { isolateGroup: group }); + const b = new GreenThread('', { isolateGroup: group }); +} +``` + +## Class: `IsolateGroup` + + + +A V8 isolate group that green threads can share. See [Isolate groups][]. + +### `new IsolateGroup()` + + + +Creates an isolate group. Throws an +[`ERR_GREEN_THREAD_ISOLATE_GROUPS_UNSUPPORTED`][] error if this build of Node.js +doesn't support isolate groups. + +### Static property: `IsolateGroup.supported` + + + +* Type: {boolean} + +Whether this build of Node.js supports isolate groups. + +## Class: `GreenThread` + + + +* Extends: {EventEmitter} + +### `new GreenThread(source[, options])` + + + +* `source` {string} The script, or with `type: 'module'` the main module, to + run in the green thread. +* `options` {Object} + * `env` {Object} The `env` global of the green thread. Functions in it, at + any depth within plain objects and arrays, and [`RpcTarget`][]s, are + passed as stubs. Everything else is cloned. See [RPC][]. + * `type` {string} `'script'` or `'module'`. See [ES modules][]. + **Default:** `'script'`. + * `filename` {string} The file name used in stack traces. For modules, the + URL of the main module, as an absolute URL or path. + **Default:** `'green-thread.js'`, or `'file:///green-thread.js'` for + modules. + * `modules` {Object} Modules that the green thread can import, by URL. Each + value is the module's source, or an object like those returned by + `load()`. See [ES modules][]. + * `resolve` {Function} Resolves specifiers that are neither URLs nor + relative. See [ES modules][]. + * `load` {Function} Loads modules that are not in `modules`. See + [ES modules][]. + * `isolateGroup` {string|IsolateGroup} The isolate group of the green + thread: `'new'`, `'default'`, or an {IsolateGroup}. See + [Isolate groups][]. **Default:** `'new'` if this build of Node.js supports + isolate groups, `'default'` otherwise. + * `resourceLimits` {Object} + * `maxOldGenerationSizeMb` {number} The maximum size of the main heap, in + MB. `0` uses the V8 default. **Default:** `0`. + * `maxYoungGenerationSizeMb` {number} The maximum size of the heap space + for recently created objects, in MB. `0` uses the V8 default. + **Default:** `0`. + * `maxMemoryMb` {number} The maximum size of the heap and `ArrayBuffer`s + together, in MB. Also limits the main heap, unless + `maxOldGenerationSizeMb` is set. `0` means no limit. **Default:** `0`. + * `maxCpuTimeMs` {number} The maximum CPU time of all tasks together, in + milliseconds. `0` means no limit. **Default:** `0`. + * `maxTaskCpuTimeMs` {number} The maximum CPU time of each task, in + milliseconds. `0` means no limit. **Default:** `0`. + +Starts a green thread that runs `source`. See [Resource limits][] for what +happens when a limit is reached. + +### `greenThread.exports` + + + +* Type: {Promise|undefined} + +For green threads with `type: 'module'`, a promise for the exports of the +main module. It is fulfilled once the main module has been evaluated, +including any top-level `await`, and rejected if the module can't be loaded +or throws, or the green thread exits first. `undefined` for scripts. See +[ES modules][]. + +### Event: `'error'` + + + +* `error` {any} + +Emitted when the green thread throws an uncaught exception, has an unhandled +promise rejection, or reaches a [resource limit][Resource limits]. The green +thread then exits. + +### Event: `'exit'` + + + +* `exitCode` {integer} + +Emitted once the green thread has stopped. The exit code is `0` if the green +thread called `close()`, and `1` otherwise. + +### Event: `'message'` + + + +* `value` {any} + +Emitted when the green thread calls `postMessage()`. + +### `greenThread.evaluate(source[, options])` + + + +* `source` {string} +* `options` {Object} + * `filename` {string} The file name used in stack traces. + **Default:** `'green-thread-eval.js'`. +* Returns: {Promise} + +Runs `source` as a script in the green thread. The promise is fulfilled with a +structured clone of the script's completion value. If that value is a promise, +the green thread waits for it to settle first. The promise is rejected if the +script throws, or if the green thread exits first, with an +[`ERR_GREEN_THREAD_EXITED`][] error. + +Exceptions thrown by `source` do not stop the green thread. + +```cjs +const { GreenThread } = require('node:green_threads'); + +const greenThread = new GreenThread('globalThis.counter = 0;'); +(async () => { + console.log(await greenThread.evaluate('++counter')); // Prints: 1 + await greenThread.terminate(); +})(); +``` + +### `greenThread.exitCode` + + + +* Type: {integer|undefined} + +The exit code, or `undefined` while the green thread is running. + +### `greenThread.postMessage(value[, transfer])` + + + +* `value` {any} +* `transfer` {Array|Object} `ArrayBuffer`s and [`MessagePort`][]s to transfer + rather than copy, or an object whose `transfer` property is such an array. + +Sends a structured clone of `value` to the green thread's `onmessage` handler. +Messages are dropped if `onmessage` is not set. Calling this after the green +thread has exited has no effect. + +`ArrayBuffer`s and ports in `transfer` are moved to the receiver, and +detached on the sending side, as with [`port.postMessage()`][]. See +[Message ports][]. If the sender or the receiver +has a [`maxMemoryMb`][Memory limits] limit, their contents are copied, so that +they count towards the limit only while the green thread has them. Receiving +an `ArrayBuffer` that the receiving green thread has no memory left for is an +uncaught `RangeError` in it. + +`SharedArrayBuffer`s are shared rather than copied. `Atomics.wait()` throws in +green threads, since it would block the pool thread, and with it other green +threads. + +### `greenThread.ref()` + + + +* Returns: {GreenThread} + +Undoes a previous `unref()` call. This is the default: a running green thread +keeps the event loop alive. + +### `greenThread.terminate()` + + + +* Returns: {Promise} Fulfilled with the exit code. + +Stops the green thread as soon as possible, interrupting any JavaScript that +is running. + +### `greenThread.unref()` + + + +* Returns: {GreenThread} + +Allows the process to exit while the green thread is still running. Any +green threads that are still running when the process exits are terminated. + +### `greenThread[Symbol.asyncDispose]()` + + + +* Returns: {Promise} + +Calls [`greenThread.terminate()`][]. + +[CPU time limits]: #cpu-time-limits +[ES modules]: #es-modules +[Errors]: #errors +[Isolate groups]: #isolate-groups +[Memory limits]: #memory-limits +[Message ports]: #message-ports +[RPC]: #rpc +[Resource limits]: #resource-limits +[`'error'`]: #event-error +[`'exit'`]: #event-exit +[`'message'`]: #event-message +[`--experimental-green-threads`]: cli.md#--experimental-green-threads +[`AbortController`]: globals.md#class-abortcontroller +[`AbortSignal`]: globals.md#class-abortsignal +[`DOMException`]: globals.md#class-domexception +[`ERR_GREEN_THREAD_CPU_LIMIT`]: errors.md#err_green_thread_cpu_limit +[`ERR_GREEN_THREAD_EXITED`]: errors.md#err_green_thread_exited +[`ERR_GREEN_THREAD_ISOLATE_GROUPS_UNSUPPORTED`]: errors.md#err_green_thread_isolate_groups_unsupported +[`ERR_GREEN_THREAD_OUT_OF_MEMORY`]: errors.md#err_green_thread_out_of_memory +[`EventTarget`]: events.md#class-eventtarget +[`Event`]: events.md#class-event +[`GreenThread`]: #new-greenthreadsource-options +[`IsolateGroup.supported`]: #static-property-isolategroupsupported +[`IsolateGroup`]: #class-isolategroup +[`MessageChannel`]: worker_threads.md#class-messagechannel +[`MessagePort`]: worker_threads.md#class-messageport +[`RpcTarget`]: #class-rpctarget +[`Worker`]: worker_threads.md#class-worker +[`greenThread.evaluate()`]: #greenthreadevaluatesource-options +[`greenThread.exports`]: #greenthreadexports +[`greenThread.postMessage()`]: #greenthreadpostmessagevalue-transfer +[`greenThread.terminate()`]: #greenthreadterminate +[`greenThreads.setUnresponsiveTimeouts()`]: #greenthreadssetunresponsivetimeoutstimeouts +[`port.postMessage()`]: worker_threads.md#portpostmessagevalue-transferlist +[green threads that don't stop]: #green-threads-that-dont-stop diff --git a/doc/api/index.md b/doc/api/index.md index a30724c064e1..33d89ec642dc 100644 --- a/doc/api/index.md +++ b/doc/api/index.md @@ -29,6 +29,7 @@ * [Events](events.md) * [File system](fs.md) * [FFI](ffi.md) +* [Green threads](green_threads.md) * [Globals](globals.md) * [HTTP](http.md) * [HTTP/2](http2.md) diff --git a/doc/node-config-schema.json b/doc/node-config-schema.json index 0d2cb83b517d..6b5a6f2d76cc 100644 --- a/doc/node-config-schema.json +++ b/doc/node-config-schema.json @@ -351,6 +351,10 @@ "type": "boolean", "description": "expose experimental Navigator API on the global scope" }, + "experimental-green-threads": { + "type": "boolean", + "description": "experimental node:green_threads module" + }, "experimental-import-meta-resolve": { "type": "boolean", "description": "experimental ES Module import.meta.resolve() parentURL support" diff --git a/doc/node.1 b/doc/node.1 index 7419eb9a34d2..5f6ee3e5362c 100644 --- a/doc/node.1 +++ b/doc/node.1 @@ -861,6 +861,9 @@ dtls documentation for details. .It Fl -experimental-eventsource Enable exposition of EventSource Web API on the global scope. . +.It Fl -experimental-green-threads +Enable the experimental \fBnode:green_threads\fR module. +. .It Fl -experimental-import-meta-resolve Enable experimental \fBimport.meta.resolve()\fR parent URL support, which allows passing a second \fBparentURL\fR argument for contextual resolution. @@ -2269,6 +2272,8 @@ one is included in the list below. .It \fB--experimental-eventsource\fR .It +\fB--experimental-green-threads\fR +.It \fB--experimental-import-meta-resolve\fR .It \fB--experimental-import-text\fR diff --git a/lib/green_threads.js b/lib/green_threads.js new file mode 100644 index 000000000000..c17dde173bb5 --- /dev/null +++ b/lib/green_threads.js @@ -0,0 +1,517 @@ +'use strict'; + +const { + ObjectKeys, + PromisePrototypeThen, + PromiseReject, + PromiseResolve, + PromiseWithResolvers, + ReflectApply, + SafeMap, + SafeWeakMap, + SymbolAsyncDispose, +} = primordials; + +const EventEmitter = require('events'); +const { + codes: { + ERR_GREEN_THREAD_CPU_LIMIT, + ERR_GREEN_THREAD_EXITED, + ERR_GREEN_THREAD_ISOLATE_GROUPS_UNSUPPORTED, + ERR_GREEN_THREAD_OUT_OF_MEMORY, + ERR_GREEN_THREAD_RPC_DISPOSED, + ERR_GREEN_THREAD_RPC_NO_SUCH_METHOD, + ERR_GREEN_THREAD_RPC_UNKNOWN_EXPORT, + ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, + ERR_INVALID_RETURN_PROPERTY_VALUE, + ERR_INVALID_RETURN_VALUE, + }, +} = require('internal/errors'); +const { + emitExperimentalWarning, + kEmptyObject, + lazyDOMExceptionClass, +} = require('internal/util'); +const { + AbortController, + AbortSignal, +} = require('internal/abort_controller'); +const { createRpc } = require('internal/green_threads/rpc'); +const { kTestingFlags } = require('internal/green_threads/testing'); +const { isAbsolute } = require('path'); +const { URL, pathToFileURL } = require('internal/url'); +const { isAnyArrayBuffer, isArrayBufferView } = require('internal/util/types'); +const { + validateFunction, + validateInteger, + validateNumber, + validateOneOf, + validateObject, + validateString, +} = require('internal/validators'); +const { + GreenThread: GreenThreadHandle, + kEventMessage, + kEventConsole, + kEventEvalResult, + kEventError, + kEventExit, + kEventAbandoned, + kEventRpc, + kErrorOutOfMemory, + kErrorCpuLimit, + setWarmPoolSize: setWarmPoolSizeBinding, + setUnresponsiveTimeouts: setUnresponsiveTimeoutsBinding, + WarmPoolWaiter, + IsolateGroup: IsolateGroupHandle, + isolateGroupsSupported, + kIsolateGroupDefault, + kIsolateGroupNew, + kIsolateGroupShared, + rpcBrand, +} = internalBinding('green_thread'); + +emitExperimentalWarning('Green threads'); + +const kConsoleMethods = ['log', 'info', 'warn', 'error', 'debug']; + +let hostConsole; + +const isolateGroupHandles = new SafeWeakMap(); + +const { RpcTarget, RpcSession, decodeError } = createRpc({ + brand: rpcBrand, + AbortController, + AbortSignal, + DOMException: lazyDOMExceptionClass(), + errors: { + disposed: () => new ERR_GREEN_THREAD_RPC_DISPOSED(), + noSuchMethod: (name) => new ERR_GREEN_THREAD_RPC_NO_SUCH_METHOD(name), + unknownExport: () => new ERR_GREEN_THREAD_RPC_UNKNOWN_EXPORT(), + }, +}); + +/** + * A V8 isolate group that green threads can share. Isolates in the same + * group share a pointer compression cage, which limits their heaps to 4 GB + * together, and the read-only part of their heaps. + */ +class IsolateGroup { + constructor() { + if (!isolateGroupsSupported) { + throw new ERR_GREEN_THREAD_ISOLATE_GROUPS_UNSUPPORTED(); + } + isolateGroupHandles.set(this, new IsolateGroupHandle()); + } + + /** + * Whether this build of Node.js can create isolate groups. + * @type {boolean} + */ + static get supported() { + return isolateGroupsSupported; + } +} + +function getIsolateGroupArgs(isolateGroup) { + if (isolateGroup === undefined) return [undefined, undefined]; + if (isolateGroup === 'default') return [kIsolateGroupDefault, undefined]; + if (isolateGroup === 'new') { + if (!isolateGroupsSupported) { + throw new ERR_GREEN_THREAD_ISOLATE_GROUPS_UNSUPPORTED(); + } + return [kIsolateGroupNew, undefined]; + } + const handle = isolateGroupHandles.get(isolateGroup); + if (handle === undefined) { + throw new ERR_INVALID_ARG_VALUE('options.isolateGroup', isolateGroup, + "must be 'default', 'new', or an " + + 'IsolateGroup'); + } + return [kIsolateGroupShared, handle]; +} + +// The href of a module's URL, which may be given as an absolute path. +function toModuleUrl(value, name) { + validateString(value, name); + if (URL.canParse(value)) return new URL(value).href; + if (isAbsolute(value)) return pathToFileURL(value).href; + throw new ERR_INVALID_ARG_VALUE(name, value, + 'must be an absolute URL or path'); +} + +let utf8Decoder; + +// A module in the bundle, or what the load() hook returned, as passed to the +// green thread: its source as a string, or { source, format }. +function toModuleEntry(entry, invalid) { + if (typeof entry === 'string') return entry; + if (typeof entry !== 'object' || entry === null) { + throw invalid('a string or an object', undefined, entry); + } + let { source } = entry; + const { format = 'module' } = entry; + if (isAnyArrayBuffer(source) || isArrayBufferView(source)) { + utf8Decoder ??= new (require('internal/encoding').TextDecoder)(); + source = utf8Decoder.decode(source); + } else if (typeof source !== 'string') { + throw invalid('a string, ArrayBuffer, or Uint8Array', 'source', source); + } + if (format !== 'module' && format !== 'json') { + throw invalid("'module' or 'json'", 'format', format); + } + return format === 'module' ? source : { source, format }; +} + +function toBundle(modules) { + validateObject(modules, 'options.modules'); + const bundle = { __proto__: null }; + const keys = ObjectKeys(modules); + for (let i = 0; i < keys.length; i++) { + const name = `options.modules[${keys[i]}]`; + bundle[toModuleUrl(keys[i], name)] = toModuleEntry( + modules[keys[i]], (expected, property, value) => { + return new ERR_INVALID_ARG_TYPE( + property === undefined ? name : `${name}.${property}`, + expected, value); + }); + } + return bundle; +} + +// The host's module loader hooks, which a green thread calls through RPC. +class ModuleLoader extends RpcTarget { + #resolve; + #load; + + constructor(resolve, load) { + super(); + this.#resolve = resolve; + this.#load = load; + } + + async resolve(specifier, context) { + const result = await this.#resolve(specifier, context); + if (result === undefined) return undefined; + if (typeof result !== 'object' || result === null) { + throw new ERR_INVALID_RETURN_VALUE('an object or undefined', 'resolve', + result); + } + if (typeof result.url !== 'string' || !URL.canParse(result.url)) { + throw new ERR_INVALID_RETURN_PROPERTY_VALUE('an absolute URL string', + 'resolve', 'url', + result.url); + } + return { __proto__: null, url: new URL(result.url).href }; + } + + async load(url, context) { + const result = await this.#load(url, context); + if (result === undefined) return undefined; + if (typeof result !== 'object' || result === null) { + throw new ERR_INVALID_RETURN_VALUE('an object or undefined', 'load', + result); + } + return toModuleEntry(result, (expected, property, value) => { + return new ERR_INVALID_RETURN_PROPERTY_VALUE(expected, 'load', + property, value); + }); + } +} + +class GreenThread extends EventEmitter { + #handle; + #rpc; + #exitCode; + #exports; + #exited = PromiseWithResolvers(); + #nextEvalId = 0; + #pendingEvals = new SafeMap(); + + /** + * @param {string} source + * @param {{ + * env?: object, + * filename?: string, + * isolateGroup?: 'default' | 'new' | IsolateGroup, + * resourceLimits?: { + * maxOldGenerationSizeMb?: number, + * maxYoungGenerationSizeMb?: number, + * maxMemoryMb?: number, + * maxCpuTimeMs?: number, + * maxTaskCpuTimeMs?: number, + * }, + * }} [options] + */ + constructor(source, options = kEmptyObject) { + super(); + validateString(source, 'source'); + validateObject(options, 'options'); + const { + env, + type = 'script', + isolateGroup, + resourceLimits = kEmptyObject, + modules, + resolve, + load, + } = options; + let { filename } = options; + const { 0: groupKind, 1: groupHandle } = getIsolateGroupArgs(isolateGroup); + validateOneOf(type, 'options.type', ['script', 'module']); + if (type === 'module') { + filename = toModuleUrl(filename ?? 'file:///green-thread.js', + 'options.filename'); + } else { + filename ??= 'green-thread.js'; + validateString(filename, 'options.filename'); + } + if (env !== undefined) validateObject(env, 'options.env'); + if (resolve !== undefined) validateFunction(resolve, 'options.resolve'); + if (load !== undefined) validateFunction(load, 'options.load'); + const init = { __proto__: null }; + let hasInit = false; + if (env !== undefined) { + init.env = env; + hasInit = true; + } + if (modules !== undefined) { + init.modules = toBundle(modules); + hasInit = true; + } + if (resolve !== undefined || load !== undefined) { + init.loader = new ModuleLoader(resolve, load); + init.hasResolve = resolve !== undefined; + init.hasLoad = load !== undefined; + hasInit = true; + } + validateObject(resourceLimits, 'options.resourceLimits'); + const { + maxOldGenerationSizeMb = 0, + maxYoungGenerationSizeMb = 0, + maxMemoryMb = 0, + maxCpuTimeMs = 0, + maxTaskCpuTimeMs = 0, + } = resourceLimits; + validateNumber(maxOldGenerationSizeMb, + 'options.resourceLimits.maxOldGenerationSizeMb', 0); + validateNumber(maxYoungGenerationSizeMb, + 'options.resourceLimits.maxYoungGenerationSizeMb', 0); + validateNumber(maxMemoryMb, 'options.resourceLimits.maxMemoryMb', 0); + validateNumber(maxCpuTimeMs, 'options.resourceLimits.maxCpuTimeMs', 0); + validateNumber(maxTaskCpuTimeMs, + 'options.resourceLimits.maxTaskCpuTimeMs', 0); + + this.#rpc = new RpcSession((message, isCall, context = 0) => { + this.#handle.rpc(message, context); + }); + if (type === 'module') { + this.#exports = PromiseWithResolvers(); + // Rejections that nothing waits for are reported as 'error' events. + PromisePrototypeThen(this.#exports.promise, undefined, () => {}); + this.#rpc.onexports = (value) => this.#exports.resolve(value); + } + this.#handle = new GreenThreadHandle(source, + filename, + maxOldGenerationSizeMb, + maxYoungGenerationSizeMb, + options[kTestingFlags] ?? 0, + maxMemoryMb, + maxCpuTimeMs, + maxTaskCpuTimeMs, + groupKind, + groupHandle, + this.#rpc, + hasInit ? this.#rpc.wrapEnv(init) : + undefined, + type === 'module'); + this.#handle.onevent = (type, id, ok, value) => { + this.#onEvent(type, id, ok, value); + }; + } + + /** + * For module green threads, a promise for the main module's exports, once + * it has been evaluated. Functions are stubs, whose calls run in the green + * thread, and other values are copies. `undefined` for scripts. + * @type {Promise|undefined} + */ + get exports() { + return this.#exports?.promise; + } + + /** + * The exit code, or `undefined` while the green thread is running. + * @type {number|undefined} + */ + get exitCode() { + return this.#exitCode; + } + + /** + * Sends a structured clone of `value` to the green thread's `onmessage`. + * @param {any} value + * @param {ArrayBuffer[] | { transfer?: ArrayBuffer[] }} [transfer] + * ArrayBuffers to transfer rather than copy. + */ + postMessage(value, transfer) { + if (this.#exitCode !== undefined) return; + this.#handle.postMessage(value, transfer); + } + + /** + * Runs `source` as a script in the green thread. Resolves with a + * structured clone of the completion value, awaiting it if it is a promise. + * @param {string} source + * @param {{ filename?: string }} [options] + * @returns {Promise} + */ + evaluate(source, options = kEmptyObject) { + validateString(source, 'source'); + validateObject(options, 'options'); + const { filename = 'green-thread-eval.js' } = options; + validateString(filename, 'options.filename'); + if (this.#exitCode !== undefined) { + return PromiseReject(new ERR_GREEN_THREAD_EXITED()); + } + const id = ++this.#nextEvalId; + const pending = PromiseWithResolvers(); + this.#pendingEvals.set(id, pending); + this.#handle.evaluate(id, source, filename); + return pending.promise; + } + + /** + * Stops the green thread, interrupting any running code. + * @returns {Promise} The exit code. + */ + terminate() { + if (this.#exitCode !== undefined) return PromiseResolve(this.#exitCode); + this.#handle.terminate(); + return this.#exited.promise; + } + + ref() { + if (this.#exitCode === undefined) this.#handle.ref(); + return this; + } + + unref() { + if (this.#exitCode === undefined) this.#handle.unref(); + return this; + } + + async [SymbolAsyncDispose]() { + await this.terminate(); + } + + #onEvent(type, id, ok, value) { + switch (type) { + case kEventMessage: + this.emit('message', value); + break; + case kEventConsole: { + hostConsole ??= require('internal/console/global'); + ReflectApply(hostConsole[kConsoleMethods[id]], hostConsole, value); + break; + } + case kEventEvalResult: { + const pending = this.#pendingEvals.get(id); + if (pending === undefined) break; + this.#pendingEvals.delete(id); + if (ok) { + pending.resolve(value); + } else { + pending.reject(decodeError(value)); + } + break; + } + case kEventError: + if (id === kErrorOutOfMemory) { + value = new ERR_GREEN_THREAD_OUT_OF_MEMORY(); + } else if (id === kErrorCpuLimit) { + value = new ERR_GREEN_THREAD_CPU_LIMIT(); + } else { + value = decodeError(value); + } + this.#exports?.reject(value); + this.emit('error', value); + break; + case kEventAbandoned: + process.emitWarning( + `A green thread did not stop within ${id} ms of being terminated ` + + 'and was abandoned. It keeps the memory it uses until the process ' + + 'exits.', 'GreenThreadWarning'); + break; + case kEventRpc: + this.#rpc.onMessage(value, id); + break; + case kEventExit: + this.#onExit(id); + break; + } + } + + #onExit(code) { + this.#exitCode = code; + this.#handle.close(); + this.#rpc.close(new ERR_GREEN_THREAD_EXITED()); + for (const { reject } of this.#pendingEvals.values()) { + reject(new ERR_GREEN_THREAD_EXITED()); + } + this.#pendingEvals.clear(); + this.#exports?.reject(new ERR_GREEN_THREAD_EXITED()); + this.#exited.resolve(code); + this.emit('exit', code); + } +} + +/** + * Sets how many green thread isolates are kept initialized ahead of time, + * for the whole process. + * @param {number} size + * @returns {Promise} Fulfilled once the pool holds `size` initialized + * isolates. + */ +function setWarmPoolSize(size) { + validateInteger(size, 'size', 0, 1024); + const { promise, resolve } = PromiseWithResolvers(); + const waiter = new WarmPoolWaiter(); + waiter.onready = () => { + waiter.close(); + resolve(); + }; + setWarmPoolSizeBinding(size, waiter); + return promise; +} + +/** + * Sets, for the whole process, how long green threads that don't stop when + * asked to have before they are abandoned, and how long Node.js then tries to + * stop the pool thread running them before it aborts the process. + * @param {{ + * gracePeriodMs?: number, + * abortTimeoutMs?: number, + * }} timeouts + */ +function setUnresponsiveTimeouts(timeouts) { + validateObject(timeouts, 'timeouts'); + const { gracePeriodMs, abortTimeoutMs } = timeouts; + if (gracePeriodMs !== undefined) { + validateInteger(gracePeriodMs, 'timeouts.gracePeriodMs', 1, 2 ** 31 - 1); + } + if (abortTimeoutMs !== undefined && abortTimeoutMs !== Infinity) { + validateInteger(abortTimeoutMs, 'timeouts.abortTimeoutMs', 0, + 2 ** 31 - 1); + } + setUnresponsiveTimeoutsBinding(gracePeriodMs, abortTimeoutMs); +} + +module.exports = { + GreenThread, + IsolateGroup, + RpcTarget, + setUnresponsiveTimeouts, + setWarmPoolSize, +}; diff --git a/lib/internal/bootstrap/realm.js b/lib/internal/bootstrap/realm.js index fd3e4eb64c80..8eec8664152d 100644 --- a/lib/internal/bootstrap/realm.js +++ b/lib/internal/bootstrap/realm.js @@ -201,6 +201,7 @@ const builtinModulePolicies = [ ['bench/reporters', true, '--experimental-bench'], ['dtls', true, '--experimental-dtls'], ['ffi', true, '--experimental-ffi'], + ['green_threads', true, '--experimental-green-threads'], ['sea', true, null], ['sqlite', true, '--experimental-sqlite'], ['quic', true, '--experimental-quic'], diff --git a/lib/internal/errors.js b/lib/internal/errors.js index e2ee2cdd340d..8126332b3a78 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1259,6 +1259,22 @@ E('ERR_FS_CP_UNKNOWN', 'Cannot copy an unknown file type', SystemError); E('ERR_FS_EISDIR', 'Path is a directory', SystemError, HideStackFramesError); E('ERR_FS_FILE_TOO_LARGE', 'File size (%s) is greater than 2 GiB', RangeError); E('ERR_FS_WATCH_QUEUE_OVERFLOW', 'fs.watch() queued more than %d events', Error); +E('ERR_GREEN_THREAD_CPU_LIMIT', + 'The green thread was terminated due to exceeding its CPU time limit', + Error); +E('ERR_GREEN_THREAD_EXITED', 'The green thread has exited', Error); +E('ERR_GREEN_THREAD_ISOLATE_GROUPS_UNSUPPORTED', + 'This build of Node.js does not support isolate groups, which require ' + + 'pointer compression with multiple cages', Error); +E('ERR_GREEN_THREAD_OUT_OF_MEMORY', + 'The green thread was terminated due to reaching its memory limit', Error); +E('ERR_GREEN_THREAD_RPC_DISPOSED', 'The RPC stub has been disposed', TypeError); +E('ERR_GREEN_THREAD_RPC_NO_SUCH_METHOD', (name) => { + return name === null ? 'The RPC target is not a function' : + `The RPC target has no method "${name}"`; +}, TypeError); +E('ERR_GREEN_THREAD_RPC_UNKNOWN_EXPORT', 'The RPC target has been released', + TypeError); E('ERR_HTTP2_ALTSVC_INVALID_ORIGIN', 'HTTP/2 ALTSVC frames require a valid origin', TypeError); E('ERR_HTTP2_ALTSVC_LENGTH', diff --git a/lib/internal/green_threads/rpc.js b/lib/internal/green_threads/rpc.js new file mode 100644 index 000000000000..fcf1e2a25d45 --- /dev/null +++ b/lib/internal/green_threads/rpc.js @@ -0,0 +1,764 @@ +'use strict'; + +// RPC between a green thread and its host. Both sides run this module: the +// host with Node.js' primordials, a green thread with its own (see +// internal/green_threads/runtime/primordials), so it only uses primordials +// that both have, and gets everything else from createRpc(). +// +// Each side exports objects that the other side can call: RpcTargets, and +// functions. They are passed as stubs, whose methods return promises. A +// stub passed back to the side that exported it turns into the original +// object again. AbortSignals are passed as signals that abort when the +// original does. +// +// Values are serialized by C++, which treats objects with `brand`, and +// AbortSignals, as host objects: it writes what exportHostObject() returns +// for them ahead of the value, and imports them with importHostObjects() +// before reading the value. + +const { + AggregateError, + ArrayIsArray, + ArrayPrototypePush, + Error, + EvalError, + FunctionPrototype, + FunctionPrototypeCall, + ObjectDefineProperty, + ObjectGetOwnPropertyDescriptor, + ObjectGetOwnPropertyNames, + ObjectGetPrototypeOf, + ObjectKeys, + ObjectPrototype, + ObjectPrototypeToString, + PromisePrototypeThen, + PromiseReject, + PromiseResolve, + PromiseWithResolvers, + RangeError, + ReferenceError, + ReflectApply, + ReflectConstruct, + SafeFinalizationRegistry, + SafeMap, + SafeSet, + SafeWeakRef, + String, + Symbol, + SymbolDispose, + SyntaxError, + TypeError, + URIError, +} = primordials; + +// Message types. Messages are arrays that start with their type. +const kCall = 0; // [kCall, callId, exportId, method or null, args] +const kResolve = 1; // [kResolve, callId, value] +const kReject = 2; // [kReject, callId, encoded error] +const kRelease = 3; // [kRelease, exportId, count] +const kAbort = 4; // [kAbort, exportId, encoded reason] +const kExports = 5; // [kExports, value]: a green thread's module exports + +// Kinds of host objects. Their descriptors are arrays that start with their +// kind. +const kTarget = 0; // [kTarget, exportId, isFunction, methodNames] +const kReturn = 1; // [kReturn, exportId] +const kSignal = 2; // [kSignal, exportId, aborted, encoded reason] + +// Thrown values. Errors are sent with their constructor, the nearest of +// those in errorConstructors, and all their data properties, like workers' +// errors (see internal/error_serdes), since structured clone drops all but a +// few of them. DOMExceptions, which structured clone doesn't support, are +// sent with what they consist of. Must match src/node_green_thread.cc. +const kErrorValue = 0; // [kErrorValue, value] +const kErrorDOMException = 1; // [kErrorDOMException, message, name, stack] +const kErrorObject = 2; // [kErrorObject, constructor, properties] +const kErrorString = 3; // [kErrorString, string]: not cloneable + +const errorConstructors = { + __proto__: null, + AggregateError, + Error, + EvalError, + RangeError, + ReferenceError, + SyntaxError, + TypeError, + URIError, +}; + +function isErrorObject(value) { + try { + return typeof value === 'object' && value !== null && + ObjectPrototypeToString(value) === '[object Error]'; + } catch { + return false; + } +} + +function errorConstructorName(error) { + for (let object = error; object !== null; + object = ObjectGetPrototypeOf(object)) { + const constructor = + ObjectGetOwnPropertyDescriptor(object, 'constructor')?.value; + if (typeof constructor !== 'function') continue; + const name = ObjectGetOwnPropertyDescriptor(constructor, 'name')?.value; + if (typeof name === 'string' && errorConstructors[name] !== undefined) { + return name; + } + } + return 'Error'; +} + +// The data properties of `error`, own and inherited, by name, as +// { value, writable, enumerable, configurable, thrown }. Getters are called. +// `cause`, and the elements of `errors` (as of AggregateErrors), are encoded +// with encodeThrown(), which `thrown` says. With `minimal`, only those with +// primitive values, which can always be cloned. +function errorProperties(error, minimal, seen) { + const properties = { __proto__: null }; + const chain = []; + for (let object = error; object !== null; + object = ObjectGetPrototypeOf(object)) { + ArrayPrototypePush(chain, object); + } + // From the prototypes to the error itself, so that own properties win. + for (let i = chain.length - 1; i >= 0; i--) { + const names = ObjectGetOwnPropertyNames(chain[i]); + for (let j = 0; j < names.length; j++) { + const name = names[j]; + let descriptor; + try { + descriptor = ObjectGetOwnPropertyDescriptor(chain[i], name); + if (descriptor.get !== undefined && name !== '__proto__') { + descriptor = { + __proto__: null, + value: FunctionPrototypeCall(descriptor.get, error), + writable: true, + enumerable: descriptor.enumerable, + configurable: true, + }; + } + } catch { + continue; + } + if (!('value' in descriptor)) continue; + let { value } = descriptor; + let thrown = false; + if (name === 'cause') { + value = encodeThrown(value, minimal, seen); + thrown = true; + } else if (name === 'errors' && ArrayIsArray(value)) { + const errors = []; + for (let k = 0; k < value.length; k++) { + ArrayPrototypePush(errors, encodeThrown(value[k], minimal, seen)); + } + value = errors; + thrown = true; + } else if (typeof value === 'function' || typeof value === 'symbol' || + (minimal && typeof value === 'object' && value !== null)) { + continue; + } + properties[name] = { + __proto__: null, + value, + writable: !!descriptor.writable, + enumerable: !!descriptor.enumerable, + configurable: !!descriptor.configurable, + thrown, + }; + } + } + return properties; +} + +function toStringOrDefault(value) { + try { + return String(value); + } catch { + return 'Error'; + } +} + +/** + * Encodes a thrown value so that it can be cloned, keeping all of an + * error's data properties. With `minimal`, the result can always be + * cloned, but only keeps those with primitive values. + * @param {any} value + * @param {boolean} [minimal] + * @returns {any[]} + */ +function encodeThrown(value, minimal = false, seen = new SafeSet()) { + if (typeof value === 'symbol' || typeof value === 'function') { + return [kErrorString, toStringOrDefault(value)]; + } + if (!isErrorObject(value)) { + if (minimal && typeof value === 'object' && value !== null) { + return [kErrorString, toStringOrDefault(value)]; + } + return [kErrorValue, value]; + } + if (seen.has(value)) return [kErrorValue, undefined]; + seen.add(value); + try { + return [kErrorObject, errorConstructorName(value), + errorProperties(value, minimal, seen)]; + } catch { + return [kErrorString, toStringOrDefault(value)]; + } +} + +// The inverse of encodeThrown(). Errors are created with their constructor, +// so that they are real errors, and get the properties that they don't +// inherit already. +function decodeThrown(encoded) { + switch (encoded?.[0]) { + case kErrorValue: + case kErrorString: + return encoded[1]; + case kErrorObject: { + const constructor = errorConstructors[encoded[1]] ?? Error; + const error = ReflectConstruct(constructor, + constructor === AggregateError ? [[]] : []); + const properties = encoded[2]; + const names = ObjectKeys(properties); + for (let i = 0; i < names.length; i++) { + const name = names[i]; + const descriptor = properties[name]; + let { value } = descriptor; + if (descriptor.thrown) { + if (ArrayIsArray(value) && name === 'errors') { + const errors = []; + for (let k = 0; k < value.length; k++) { + ArrayPrototypePush(errors, decodeThrown(value[k])); + } + value = errors; + } else { + value = decodeThrown(value); + } + } + const own = ObjectGetOwnPropertyDescriptor(error, name); + if (own === undefined && !descriptor.thrown && error[name] === value) { + continue; + } + try { + ObjectDefineProperty(error, name, { + __proto__: null, + value, + writable: !!descriptor.writable, + enumerable: !!descriptor.enumerable, + configurable: !!descriptor.configurable, + }); + } catch { + // Not configurable. + } + } + return error; + } + } + return encoded; +} + +/** + * @param {{ + * brand: symbol, + * AbortController: Function, + * AbortSignal: Function, + * DOMException: Function, + * errors: { + * disposed: () => Error, + * noSuchMethod: (name: string) => Error, + * unknownExport: () => Error, + * }, + * }} config + * @returns {{ RpcTarget: Function, RpcSession: Function }} + */ +function createRpc(config) { + const { + brand, + AbortController, + AbortSignal, + DOMException, + errors, + } = config; + const abortSignalPrototype = AbortSignal.prototype; + + /** + * The base class of objects whose methods the other side can call. Their + * other properties are not passed. + */ + class RpcTarget { + constructor() { + this[brand] = true; + } + } + + // A function passed directly as an argument or result, which V8 doesn't + // let C++ serialize as a host object itself. + class FunctionReference { + constructor(fn) { + this.fn = fn; + this[brand] = true; + } + } + + function wrapValue(value) { + return typeof value === 'function' ? new FunctionReference(value) : value; + } + + function wrapArgs(args) { + const wrapped = []; + for (let i = 0; i < args.length; i++) { + ArrayPrototypePush(wrapped, wrapValue(args[i])); + } + return wrapped; + } + + // The names of the methods of `target` that may be called: those it has + // or inherits, except from Object.prototype and Function.prototype. + function methodNames(target) { + const names = []; + const seen = new SafeSet(['constructor']); + for (let object = target; + object !== null && object !== ObjectPrototype && + object !== FunctionPrototype && object !== RpcTarget.prototype; + object = ObjectGetPrototypeOf(object)) { + const ownNames = ObjectGetOwnPropertyNames(object); + for (let i = 0; i < ownNames.length; i++) { + const name = ownNames[i]; + if (seen.has(name)) continue; + seen.add(name); + const descriptor = ObjectGetOwnPropertyDescriptor(object, name); + if (typeof descriptor.value === 'function') { + ArrayPrototypePush(names, name); + } + } + } + return names; + } + + function encodeError(error, minimal = false) { + if (error instanceof DOMException) { + return [kErrorDOMException, error.message, error.name, error.stack]; + } + return encodeThrown(error, minimal); + } + + function decodeError(encoded) { + if (encoded?.[0] === kErrorDOMException) { + const error = new DOMException(encoded[1], encoded[2]); + ObjectDefineProperty(error, 'stack', { + __proto__: null, + value: encoded[3], + writable: true, + configurable: true, + }); + return error; + } + return decodeThrown(encoded); + } + + function isAbortSignal(value) { + for (let object = value; object !== null; + object = ObjectGetPrototypeOf(object)) { + if (object === abortSignalPrototype) return true; + } + return false; + } + + const kController = Symbol('kController'); + + // The state of a stub, which `brand` refers to. + class Stub { + constructor(session, id) { + this.session = session; + this.id = id; + this.disposed = false; + this.object = null; + } + } + + const stubPrototype = { + __proto__: null, + [SymbolDispose]() { + const stub = this[brand]; + if (stub instanceof Stub) stub.session.dispose(stub); + }, + }; + + function createStub(session, id, isFunction, names) { + const stub = new Stub(session, id); + let object; + if (isFunction) { + object = function(...args) { + return session.call(stub, null, args); + }; + object[SymbolDispose] = stubPrototype[SymbolDispose]; + } else { + object = { __proto__: stubPrototype }; + for (let i = 0; i < names.length; i++) { + const name = names[i]; + ObjectDefineProperty(object, name, { + __proto__: null, + value: (...args) => session.call(stub, name, args), + writable: false, + enumerable: true, + configurable: false, + }); + } + } + object[brand] = stub; + stub.object = object; + return object; + } + + /** + * One side of the connection between a green thread and its host. + */ + class RpcSession { + // Sends a message. `isCall` is true for calls, whose result the other + // side sends back with `context`, the context of the call on this side. + #send; + #closed = null; + + // Objects that this side exported, by ID: { value, refs, methods, + // removeListener }. The other side holds `refs` references to each. + #exports = new SafeMap(); + #exportIds = new SafeMap(); + #nextExportId = 0; + + // Stubs for objects that the other side exported, by ID: { stub, + // count }, where `stub` is a SafeWeakRef and the other side has been told + // about `count` references. + #imports = new SafeMap(); + #registry = new SafeFinalizationRegistry((id) => this.#onCollected(id)); + + // Calls made by this side, by ID. + #pending = new SafeMap(); + #nextCallId = 0; + + // Signals whose abort arrived before they did. Never needed for signals + // that were aborted before they were passed, which carry their reason. + #pendingAborts = new SafeMap(); + + /** + * @param {(message: any[], isCall: boolean, context?: number) => void} send + * Serializes and sends `message`. Throws if it can't be serialized. + */ + constructor(send) { + this.#send = send; + } + + /** + * Called with the value that the other side sent with sendExports(). + * @type {((value: any) => void) | null} + */ + onexports = null; + + /** + * Sends `value`, wrapped like the env, to the other side's onexports. + * Throws if it can't be serialized. + * @param {any} value + */ + sendExports(value) { + if (this.#closed !== null) return; + this.#send([kExports, this.wrapEnv(value)], false); + } + + get abortSignalPrototype() { + return abortSignalPrototype; + } + + /** + * Wraps a value so that it can be serialized with this session's hooks: + * functions at the top level, or anywhere in plain objects and arrays, + * are passed as stubs. + * @param {any} value + * @returns {any} + */ + wrapEnv(value) { + if (typeof value === 'function') return new FunctionReference(value); + if (typeof value !== 'object' || value === null || value[brand] || + isAbortSignal(value)) { + return value; + } + if (ArrayIsArray(value)) { + const result = []; + for (let i = 0; i < value.length; i++) { + ArrayPrototypePush(result, this.wrapEnv(value[i])); + } + return result; + } + const prototype = ObjectGetPrototypeOf(value); + if (prototype !== ObjectPrototype && prototype !== null) return value; + const result = { __proto__: null }; + const names = ObjectGetOwnPropertyNames(value); + for (let i = 0; i < names.length; i++) { + result[names[i]] = this.wrapEnv(value[names[i]]); + } + return result; + } + + // Called by C++ while serializing. + exportHostObject(object) { + if (isAbortSignal(object)) return this.#exportSignal(object); + const target = object instanceof FunctionReference ? object.fn : object; + const stub = target[brand]; + if (stub instanceof Stub && stub.session === this) { + if (stub.disposed) throw errors.disposed(); + return [kReturn, stub.id]; + } + const isFunction = typeof target === 'function'; + let id = this.#exportIds.get(target); + if (id === undefined) { + id = ++this.#nextExportId; + this.#exportIds.set(target, id); + this.#exports.set(id, { + value: target, + refs: 0, + methods: isFunction ? null : new SafeSet(methodNames(target)), + removeListener: null, + }); + } + const entry = this.#exports.get(id); + entry.refs++; + const names = []; + if (!isFunction) { + for (const name of entry.methods) ArrayPrototypePush(names, name); + } + return [kTarget, id, isFunction, names]; + } + + #exportSignal(signal) { + let id = this.#exportIds.get(signal); + if (id === undefined) { + id = ++this.#nextExportId; + this.#exportIds.set(signal, id); + const entry = { + value: signal, + refs: 0, + methods: null, + removeListener: null, + }; + this.#exports.set(id, entry); + if (!signal.aborted) { + const listener = () => { + entry.removeListener = null; + this.#trySend([kAbort, id, encodeError(signal.reason)], false); + }; + signal.addEventListener('abort', listener, { once: true }); + entry.removeListener = () => { + signal.removeEventListener('abort', listener); + }; + } + } + this.#exports.get(id).refs++; + if (signal.aborted) { + return [kSignal, id, true, encodeError(signal.reason)]; + } + return [kSignal, id, false, null]; + } + + // Called by C++ before deserializing a value, with the descriptors of + // the host objects in it. + importHostObjects(descriptors) { + const objects = []; + for (let i = 0; i < descriptors.length; i++) { + ArrayPrototypePush(objects, this.#importHostObject(descriptors[i])); + } + return objects; + } + + #importHostObject(descriptor) { + switch (descriptor[0]) { + case kReturn: { + const entry = this.#exports.get(descriptor[1]); + if (entry === undefined) throw errors.unknownExport(); + return entry.value; + } + case kTarget: + return this.#import(descriptor[1], () => { + return createStub(this, descriptor[1], descriptor[2], + descriptor[3]); + }); + case kSignal: + return this.#import(descriptor[1], () => { + const controller = new AbortController(); + if (descriptor[2]) { + controller.abort(decodeError(descriptor[3])); + } else if (this.#pendingAborts.has(descriptor[1])) { + controller.abort(this.#pendingAborts.get(descriptor[1])); + this.#pendingAborts.delete(descriptor[1]); + } + const { signal } = controller; + signal[kController] = controller; + return signal; + }); + } + throw errors.unknownExport(); + } + + #import(id, create) { + let entry = this.#imports.get(id); + let object = entry?.stub.deref(); + if (object === undefined) { + object = create(); + if (entry === undefined) { + entry = { stub: null, count: 0 }; + this.#imports.set(id, entry); + } + entry.stub = new SafeWeakRef(object); + this.#registry.register(object, id, object); + } + entry.count++; + return object; + } + + #onCollected(id) { + const entry = this.#imports.get(id); + // A new stub for the same export may be in use. + if (entry === undefined || entry.stub.deref() !== undefined) return; + this.#release(id, entry); + } + + #release(id, entry) { + this.#imports.delete(id); + this.#trySend([kRelease, id, entry.count], false); + } + + dispose(stub) { + if (stub.disposed) return; + stub.disposed = true; + const entry = this.#imports.get(stub.id); + if (entry === undefined || entry.stub.deref() !== stub.object) return; + this.#registry.unregister(stub.object); + this.#release(stub.id, entry); + } + + call(stub, method, args) { + if (this.#closed !== null) return PromiseReject(this.#closed); + if (stub.disposed) return PromiseReject(errors.disposed()); + const callId = ++this.#nextCallId; + const pending = PromiseWithResolvers(); + this.#pending.set(callId, pending); + try { + this.#send([kCall, callId, stub.id, method, wrapArgs(args)], true); + } catch (error) { + this.#pending.delete(callId); + pending.reject(error); + } + return pending.promise; + } + + // Sends a message that nothing waits for. It can't fail to serialize. + #trySend(message, isCall, context) { + if (this.#closed !== null) return; + this.#send(message, isCall, context); + } + + /** + * Handles a message from the other side. `context` is passed back with + * the result of a call. + */ + onMessage(message, context) { + switch (message[0]) { + case kCall: + this.#onCall(message[1], message[2], message[3], message[4], + context); + break; + case kResolve: + case kReject: { + const pending = this.#pending.get(message[1]); + if (pending === undefined) break; + this.#pending.delete(message[1]); + if (message[0] === kResolve) { + pending.resolve(message[2]); + } else { + pending.reject(decodeError(message[2])); + } + break; + } + case kRelease: { + const entry = this.#exports.get(message[1]); + if (entry === undefined) break; + entry.refs -= message[2]; + if (entry.refs > 0) break; + this.#exports.delete(message[1]); + this.#exportIds.delete(entry.value); + entry.removeListener?.(); + break; + } + case kExports: + this.onexports?.(message[1]); + break; + case kAbort: { + const reason = decodeError(message[2]); + const signal = this.#imports.get(message[1])?.stub.deref(); + if (signal === undefined) { + this.#pendingAborts.set(message[1], reason); + } else { + signal[kController].abort(reason); + } + break; + } + } + } + + #onCall(callId, id, method, args, context) { + const entry = this.#exports.get(id); + let result; + try { + if (entry === undefined) throw errors.unknownExport(); + const { value } = entry; + let fn; + if (method === null) { + fn = entry.methods === null ? value : undefined; + } else if (entry.methods?.has(method)) { + fn = value[method]; + } + if (typeof fn !== 'function') throw errors.noSuchMethod(method); + result = PromiseResolve(ReflectApply(fn, value, args)); + } catch (error) { + result = PromiseReject(error); + } + PromisePrototypeThen(result, (value) => { + try { + this.#trySend([kResolve, callId, wrapValue(value)], false, context); + } catch (error) { + this.#sendError(callId, error, context); + } + }, (error) => { + this.#sendError(callId, error, context); + }); + } + + #sendError(callId, error, context) { + try { + this.#trySend([kReject, callId, encodeError(error)], false, context); + } catch { + // Not cloneable. + this.#trySend([kReject, callId, encodeError(error, true)], false, + context); + } + } + + /** + * Rejects all pending calls with `error`, and drops all exports. Nothing + * is sent after this. + */ + close(error) { + if (this.#closed !== null) return; + this.#closed = error; + for (const { reject } of this.#pending.values()) reject(error); + this.#pending.clear(); + for (const entry of this.#exports.values()) entry.removeListener?.(); + this.#exports.clear(); + this.#exportIds.clear(); + this.#imports.clear(); + } + } + + return { RpcTarget, RpcSession, encodeError, decodeError }; +} + +module.exports = { createRpc }; diff --git a/lib/internal/green_threads/runtime/bootstrap.js b/lib/internal/green_threads/runtime/bootstrap.js new file mode 100644 index 000000000000..be4b01938eda --- /dev/null +++ b/lib/internal/green_threads/runtime/bootstrap.js @@ -0,0 +1,97 @@ +'use strict'; + +// Runs in the context of every green thread, before any user code, and +// installs the web APIs that green threads have. Its exports are used by +// src/node_green_thread.cc. + +const { + ObjectDefineProperty, + PromisePrototypeThen, + globalThis, +} = primordials; + +// Taken before any user code runs. +const { queueMicrotask } = globalThis; + +const { DOMException } = internalBinding('per_context'); +const { rpcBrand, rpcSend } = internalBinding('green_thread_runtime'); +const { + AbortController, + AbortSignal, + Event, + EventTarget, +} = require('internal/green_threads/runtime/event_target'); +const { + MessageChannel, + MessageEvent, + MessagePort, + createMessagePort, + dispatchPortClose, + dispatchPortMessage, +} = require('internal/green_threads/runtime/message_port'); +const { rpc: rpcErrors } = require('internal/green_threads/runtime/errors'); +const modules = require('internal/green_threads/runtime/modules'); +const { createRpc } = require('internal/green_threads/rpc'); + +const { RpcTarget, RpcSession, encodeError } = createRpc({ + brand: rpcBrand, + AbortController, + AbortSignal, + DOMException, + errors: rpcErrors, +}); +const session = new RpcSession(rpcSend); + +function defineGlobal(name, value) { + ObjectDefineProperty(globalThis, name, { + __proto__: null, + value, + writable: true, + enumerable: false, + configurable: true, + }); +} + +defineGlobal('AbortController', AbortController); +defineGlobal('AbortSignal', AbortSignal); +defineGlobal('DOMException', DOMException); +defineGlobal('Event', Event); +defineGlobal('EventTarget', EventTarget); +defineGlobal('MessageChannel', MessageChannel); +defineGlobal('MessageEvent', MessageEvent); +defineGlobal('MessagePort', MessagePort); +defineGlobal('RpcTarget', RpcTarget); + +// An uncaught exception of the green thread. +function reportError(error) { + queueMicrotask(() => { throw error; }); +} + +// Used by C++ to serialize and deliver RPC messages, and the env, to deliver +// the messages of ports, and to run ES modules. +module.exports = { + // With what the host passed: { env, modules, loader, hasResolve, + // hasLoad }, each of them optional. + initialize(init) { + if ('env' in init) defineGlobal('env', init.env); + modules.configure(init); + }, + // Runs the main module, and sends its namespace to the host. + runMain(source, url) { + PromisePrototypeThen(modules.runMain(source, url), (namespace) => { + session.sendExports(namespace); + }, reportError); + }, + importModule: (specifier, referrer, attributes) => + modules.importModule(specifier, referrer, attributes), + // Encodes errors that are sent to the host: uncaught ones, and those of + // evaluate(). + encodeError: (error, minimal) => encodeError(error, minimal), + abortSignalPrototype: AbortSignal.prototype, + createMessagePort, + dispatchPortClose, + dispatchPortMessage, + exportHostObject: (object) => session.exportHostObject(object), + importHostObjects: (descriptors) => session.importHostObjects(descriptors), + onRpc: (message) => session.onMessage(message), +}; diff --git a/lib/internal/green_threads/runtime/errors.js b/lib/internal/green_threads/runtime/errors.js new file mode 100644 index 000000000000..976421f16f03 --- /dev/null +++ b/lib/internal/green_threads/runtime/errors.js @@ -0,0 +1,59 @@ +'use strict'; + +// Errors with Node.js-style codes, for green threads, which don't have +// internal/errors. + +const { + Error, + RangeError, + TypeError, +} = primordials; + +function codedError(Base, code, message) { + const error = new Base(message); + error.code = code; + return error; +} + +module.exports = { + invalidArgType: (message) => codedError(TypeError, 'ERR_INVALID_ARG_TYPE', + message), + missingArgs: (message) => codedError(TypeError, 'ERR_MISSING_ARGS', message), + outOfRange: (message) => codedError(RangeError, 'ERR_OUT_OF_RANGE', message), + illegalConstructor: () => codedError(TypeError, 'ERR_ILLEGAL_CONSTRUCTOR', + 'Illegal constructor'), + // Like those of Node.js' ES module loader. + modules: { + notFound: (specifier, referrer = undefined) => codedError( + Error, 'ERR_MODULE_NOT_FOUND', + referrer === undefined ? `Cannot find module '${specifier}'` : + `Cannot find module '${specifier}' imported from ${referrer}`), + unsupportedScheme: (url) => codedError( + Error, 'ERR_UNSUPPORTED_ESM_URL_SCHEME', + `Importing ${url} is not supported in green threads`), + unknownFormat: (format, url) => codedError( + RangeError, 'ERR_UNKNOWN_MODULE_FORMAT', + `Unknown module format: ${format} for URL ${url}`), + attributeMissing: (url, key, value) => codedError( + TypeError, 'ERR_IMPORT_ATTRIBUTE_MISSING', + `Module "${url}" needs an import attribute of "${key}: ${value}"`), + attributeIncompatible: (url, type) => codedError( + TypeError, 'ERR_IMPORT_ATTRIBUTE_TYPE_INCOMPATIBLE', + `Module "${url}" is not of type "${type}"`), + attributeUnsupported: (key, value) => codedError( + TypeError, 'ERR_IMPORT_ATTRIBUTE_UNSUPPORTED', + `Import attribute "${key}" with value "${value}" is not supported`), + }, + // Must match those in internal/errors. + rpc: { + disposed: () => codedError(TypeError, 'ERR_GREEN_THREAD_RPC_DISPOSED', + 'The RPC stub has been disposed'), + noSuchMethod: (name) => codedError( + TypeError, 'ERR_GREEN_THREAD_RPC_NO_SUCH_METHOD', + name === null ? 'The RPC target is not a function' : + `The RPC target has no method "${name}"`), + unknownExport: () => codedError( + TypeError, 'ERR_GREEN_THREAD_RPC_UNKNOWN_EXPORT', + 'The RPC target has been released'), + }, +}; diff --git a/lib/internal/green_threads/runtime/event_target.js b/lib/internal/green_threads/runtime/event_target.js new file mode 100644 index 000000000000..1badfb433b38 --- /dev/null +++ b/lib/internal/green_threads/runtime/event_target.js @@ -0,0 +1,396 @@ +'use strict'; + +// Event, EventTarget, AbortSignal and AbortController for green threads. +// There is no DOM tree, so events are only ever dispatched to their target. +// Node.js' own implementation depends on too much of Node.js to run in a +// green thread. + +const { + ArrayPrototypeFindIndex, + ArrayPrototypePush, + ArrayPrototypeSlice, + ArrayPrototypeSplice, + DateNow, + FunctionPrototypeCall, + NumberIsNaN, + ObjectDefineProperties, + SafeMap, + Symbol, + SymbolToStringTag, + globalThis, +} = primordials; + +// Taken before any user code runs. +const { queueMicrotask, setTimeout } = globalThis; + +const { DOMException } = internalBinding('per_context'); +const { + illegalConstructor, + invalidArgType, + missingArgs, + outOfRange, +} = require('internal/green_threads/runtime/errors'); + +const kCreate = Symbol('kCreate'); +const timeOrigin = DateNow(); + +let setEventTarget; +let setEventCurrentTarget; +let setEventPhase; +let setEventTrusted; +let setEventDispatching; +let isEventDispatching; +let isEventStopped; +let isEvent; + +class Event { + #type; + #bubbles; + #cancelable; + #composed; + #timeStamp = DateNow() - timeOrigin; + #defaultPrevented = false; + #target = null; + #currentTarget = null; + #eventPhase = 0; + #isTrusted = false; + #dispatching = false; + #propagationStopped = false; + #immediatePropagationStopped = false; + #inPassiveListener = false; + + constructor(type, options = undefined) { + if (arguments.length === 0) { + throw missingArgs('The "type" argument must be specified'); + } + if (options != null && typeof options !== 'object') { + throw invalidArgType('The "options" argument must be an object'); + } + this.#type = `${type}`; + this.#bubbles = !!options?.bubbles; + this.#cancelable = !!options?.cancelable; + this.#composed = !!options?.composed; + } + + get type() { return this.#type; } + get target() { return this.#target; } + get srcElement() { return this.#target; } + get currentTarget() { return this.#currentTarget; } + get eventPhase() { return this.#eventPhase; } + get bubbles() { return this.#bubbles; } + get cancelable() { return this.#cancelable; } + get composed() { return this.#composed; } + get defaultPrevented() { return this.#cancelable && this.#defaultPrevented; } + get timeStamp() { return this.#timeStamp; } + get isTrusted() { return this.#isTrusted; } + get returnValue() { return !this.defaultPrevented; } + set returnValue(value) { if (!value) this.preventDefault(); } + get cancelBubble() { return this.#propagationStopped; } + set cancelBubble(value) { if (value) this.#propagationStopped = true; } + + composedPath() { + return this.#dispatching ? [this.#currentTarget] : []; + } + + stopPropagation() { + this.#propagationStopped = true; + } + + stopImmediatePropagation() { + this.#propagationStopped = true; + this.#immediatePropagationStopped = true; + } + + preventDefault() { + if (this.#cancelable && !this.#inPassiveListener) { + this.#defaultPrevented = true; + } + } + + static { + setEventTarget = (event, target) => { event.#target = target; }; + setEventCurrentTarget = (event, target) => { + event.#currentTarget = target; + }; + setEventPhase = (event, phase) => { event.#eventPhase = phase; }; + setEventTrusted = (event) => { event.#isTrusted = true; }; + setEventDispatching = (event, dispatching, passive = false) => { + event.#dispatching = dispatching; + event.#inPassiveListener = passive; + }; + isEventDispatching = (event) => event.#dispatching; + isEventStopped = (event) => event.#immediatePropagationStopped; + isEvent = (value) => typeof value === 'object' && value !== null && + #type in value; + } +} + +ObjectDefineProperties(Event, { + NONE: { __proto__: null, value: 0, enumerable: true }, + CAPTURING_PHASE: { __proto__: null, value: 1, enumerable: true }, + AT_TARGET: { __proto__: null, value: 2, enumerable: true }, + BUBBLING_PHASE: { __proto__: null, value: 3, enumerable: true }, +}); +ObjectDefineProperties(Event.prototype, { + [SymbolToStringTag]: { + __proto__: null, + value: 'Event', + configurable: true, + }, +}); + +function flattenOptions(options) { + if (typeof options === 'boolean') return { capture: options }; + if (options == null) return { capture: false }; + if (typeof options !== 'object') { + throw invalidArgType('The "options" argument must be an object'); + } + return { + capture: !!options.capture, + once: !!options.once, + passive: !!options.passive, + signal: options.signal, + }; +} + +// Errors thrown by listeners don't stop the dispatch. They are reported like +// any other uncaught exception. +function reportListenerError(error) { + queueMicrotask(() => { throw error; }); +} + +class EventTarget { + // Event type => listener records, in the order they were added. + #listeners = new SafeMap(); + + addEventListener(type, callback, options = undefined) { + if (arguments.length < 2) { + throw missingArgs('The "type" and "listener" arguments must be specified'); + } + const { capture, once, passive, signal } = flattenOptions(options); + if (signal !== undefined && !isAbortSignal(signal)) { + throw invalidArgType('The "options.signal" property must be an AbortSignal'); + } + if (callback == null || signal?.aborted) return; + if (typeof callback !== 'function' && typeof callback !== 'object') { + throw invalidArgType('The "listener" argument must be a function or an object'); + } + type = `${type}`; + let listeners = this.#listeners.get(type); + if (listeners === undefined) { + listeners = []; + this.#listeners.set(type, listeners); + } + const index = ArrayPrototypeFindIndex(listeners, (listener) => { + return listener.callback === callback && listener.capture === capture; + }); + if (index !== -1) return; + ArrayPrototypePush(listeners, { + callback, + capture, + once, + passive, + removed: false, + }); + if (signal !== undefined) { + FunctionPrototypeCall( + EventTarget.prototype.addEventListener, signal, 'abort', () => { + this.removeEventListener(type, callback, { capture }); + }, { once: true }); + } + } + + removeEventListener(type, callback, options = undefined) { + if (arguments.length < 2) { + throw missingArgs('The "type" and "listener" arguments must be specified'); + } + const { capture } = flattenOptions(options); + const listeners = this.#listeners.get(`${type}`); + if (listeners === undefined) return; + const index = ArrayPrototypeFindIndex(listeners, (listener) => { + return listener.callback === callback && listener.capture === capture; + }); + if (index === -1) return; + listeners[index].removed = true; + ArrayPrototypeSplice(listeners, index, 1); + } + + dispatchEvent(event) { + if (!isEvent(event)) { + throw invalidArgType('The "event" argument must be an Event'); + } + if (isEventDispatching(event)) { + throw new DOMException('The event is already being dispatched', + 'InvalidStateError'); + } + setEventTarget(event, this); + setEventCurrentTarget(event, this); + setEventPhase(event, Event.AT_TARGET); + const listeners = this.#listeners.get(event.type); + for (const listener of ArrayPrototypeSlice(listeners ?? [])) { + if (listener.removed) continue; + if (listener.once) { + this.removeEventListener(event.type, listener.callback, + { capture: listener.capture }); + } + setEventDispatching(event, true, listener.passive); + try { + const { callback } = listener; + if (typeof callback === 'function') { + FunctionPrototypeCall(callback, this, event); + } else if (typeof callback?.handleEvent === 'function') { + FunctionPrototypeCall(callback.handleEvent, callback, event); + } + } catch (error) { + reportListenerError(error); + } + if (isEventStopped(event)) break; + } + setEventDispatching(event, false); + setEventCurrentTarget(event, null); + setEventPhase(event, Event.NONE); + return !event.defaultPrevented; + } +} + +ObjectDefineProperties(EventTarget.prototype, { + [SymbolToStringTag]: { + __proto__: null, + value: 'EventTarget', + configurable: true, + }, +}); + +let isAbortSignal; +let abortSignal; + +class AbortSignal extends EventTarget { + #aborted = false; + #reason = undefined; + #onabort = null; + #onabortListener = null; + + constructor(key) { + if (key !== kCreate) throw illegalConstructor(); + super(); + } + + get aborted() { return this.#aborted; } + get reason() { return this.#reason; } + + throwIfAborted() { + if (this.#aborted) throw this.#reason; + } + + get onabort() { return this.#onabort; } + set onabort(handler) { + if (this.#onabortListener !== null) { + this.removeEventListener('abort', this.#onabortListener); + this.#onabortListener = null; + } + this.#onabort = typeof handler === 'function' ? handler : null; + if (this.#onabort !== null) { + this.#onabortListener = (event) => { + FunctionPrototypeCall(this.#onabort, this, event); + }; + this.addEventListener('abort', this.#onabortListener); + } + } + + static abort(reason = new DOMException('This operation was aborted', + 'AbortError')) { + const signal = new AbortSignal(kCreate); + signal.#aborted = true; + signal.#reason = reason; + return signal; + } + + static timeout(milliseconds) { + milliseconds = +milliseconds; + if (NumberIsNaN(milliseconds) || milliseconds < 0) { + throw outOfRange('The "milliseconds" argument must be a non-negative number'); + } + const signal = new AbortSignal(kCreate); + setTimeout(() => { + abortSignal(signal, new DOMException( + 'The operation was aborted due to timeout', 'TimeoutError')); + }, milliseconds); + return signal; + } + + static any(signals) { + const sources = []; + for (const source of signals) { + if (!isAbortSignal(source)) { + throw invalidArgType('The "signals" argument must be an iterable of AbortSignals'); + } + ArrayPrototypePush(sources, source); + } + const signal = new AbortSignal(kCreate); + for (const source of sources) { + if (source.#aborted) { + signal.#aborted = true; + signal.#reason = source.#reason; + return signal; + } + } + for (const source of sources) { + FunctionPrototypeCall( + EventTarget.prototype.addEventListener, source, 'abort', () => { + abortSignal(signal, source.#reason); + }, { once: true }); + } + return signal; + } + + static { + isAbortSignal = (value) => typeof value === 'object' && value !== null && + #aborted in value; + abortSignal = (signal, reason) => { + if (signal.#aborted) return; + signal.#aborted = true; + signal.#reason = reason; + const event = new Event('abort'); + setEventTrusted(event); + signal.dispatchEvent(event); + }; + } +} + +ObjectDefineProperties(AbortSignal.prototype, { + [SymbolToStringTag]: { + __proto__: null, + value: 'AbortSignal', + configurable: true, + }, +}); + +class AbortController { + #signal = new AbortSignal(kCreate); + + get signal() { return this.#signal; } + + abort(reason = new DOMException('This operation was aborted', + 'AbortError')) { + abortSignal(this.#signal, reason); + } +} + +ObjectDefineProperties(AbortController.prototype, { + [SymbolToStringTag]: { + __proto__: null, + value: 'AbortController', + configurable: true, + }, +}); + +module.exports = { + AbortController, + AbortSignal, + Event, + EventTarget, + abortSignal, + createAbortSignal: () => new AbortSignal(kCreate), + isAbortSignal, + markEventTrusted: (event) => setEventTrusted(event), +}; diff --git a/lib/internal/green_threads/runtime/message_port.js b/lib/internal/green_threads/runtime/message_port.js new file mode 100644 index 000000000000..38b1882c8311 --- /dev/null +++ b/lib/internal/green_threads/runtime/message_port.js @@ -0,0 +1,158 @@ +'use strict'; + +// MessageChannel, MessagePort and MessageEvent for green threads. The ports +// are Node.js' MessagePorts underneath, which src/node_green_thread.cc +// receives messages for, so they can connect green threads with each other, +// with their host and with workers. + +const { + ArrayPrototypeSlice, + FunctionPrototypeCall, + ObjectDefineProperties, + ObjectFreeze, + ReflectApply, + Symbol, + SymbolToStringTag, +} = primordials; + +const { + messageChannel, + portClose, + portId, + portPostMessage, + portStart, +} = internalBinding('green_thread_runtime'); +const { + Event, + EventTarget, + markEventTrusted, +} = require('internal/green_threads/runtime/event_target'); +const { illegalConstructor } = require('internal/green_threads/runtime/errors'); + +const kCreate = Symbol('kCreate'); + +class MessageEvent extends Event { + #data; + #ports; + + constructor(type, options = undefined) { + super(type, options); + this.#data = options?.data ?? null; + this.#ports = ObjectFreeze( + options?.ports == null ? [] : ArrayPrototypeSlice(options.ports)); + } + + get data() { return this.#data; } + get ports() { return this.#ports; } + get origin() { return ''; } + get lastEventId() { return ''; } + get source() { return null; } +} + +ObjectDefineProperties(MessageEvent.prototype, { + [SymbolToStringTag]: { + __proto__: null, + value: 'MessageEvent', + configurable: true, + }, +}); + +class MessagePort extends EventTarget { + #handlers = { __proto__: null }; + + constructor(key, id) { + if (key !== kCreate) throw illegalConstructor(); + super(); + this[portId] = id; + } + + postMessage(message, transfer = undefined) { + portPostMessage(this, message, transfer); + } + + start() { + portStart(this); + } + + close() { + portClose(this); + } + + // Like Node.js' MessagePorts, ports start once they have 'message' + // listeners. + addEventListener(type, listener, options = undefined) { + ReflectApply(super.addEventListener, this, arguments); + if (`${type}` === 'message' && listener != null) this.start(); + } + + get onmessage() { return this.#getHandler('message'); } + set onmessage(handler) { this.#setHandler('message', handler); } + get onmessageerror() { return this.#getHandler('messageerror'); } + set onmessageerror(handler) { this.#setHandler('messageerror', handler); } + + #getHandler(type) { + return this.#handlers[type]?.handler ?? null; + } + + #setHandler(type, handler) { + const previous = this.#handlers[type]; + if (previous !== undefined) { + this.removeEventListener(type, previous.listener); + delete this.#handlers[type]; + } + if (typeof handler !== 'function') return; + const listener = (event) => FunctionPrototypeCall(handler, this, event); + this.#handlers[type] = { handler, listener }; + this.addEventListener(type, listener); + } +} + +ObjectDefineProperties(MessagePort.prototype, { + [SymbolToStringTag]: { + __proto__: null, + value: 'MessagePort', + configurable: true, + }, +}); + +class MessageChannel { + #port1; + #port2; + + constructor() { + const { 0: port1, 1: port2 } = messageChannel(); + this.#port1 = port1; + this.#port2 = port2; + } + + get port1() { return this.#port1; } + get port2() { return this.#port2; } +} + +ObjectDefineProperties(MessageChannel.prototype, { + [SymbolToStringTag]: { + __proto__: null, + value: 'MessageChannel', + configurable: true, + }, +}); + +function dispatch(port, event) { + markEventTrusted(event); + port.dispatchEvent(event); +} + +module.exports = { + MessageChannel, + MessageEvent, + MessagePort, + // Used by src/node_green_thread.cc. + createMessagePort: (id) => new MessagePort(kCreate, id), + dispatchPortMessage(port, data, ports, ok) { + dispatch(port, new MessageEvent(ok ? 'message' : 'messageerror', + { data, ports })); + }, + dispatchPortClose(port) { + dispatch(port, new Event('close')); + }, +}; diff --git a/lib/internal/green_threads/runtime/modules.js b/lib/internal/green_threads/runtime/modules.js new file mode 100644 index 000000000000..f140ec47680e --- /dev/null +++ b/lib/internal/green_threads/runtime/modules.js @@ -0,0 +1,223 @@ +'use strict'; + +// The ES module loader of green threads. Modules come from the bundle that +// the host passed, or from the host's load() hook. Specifiers that are URLs, +// or relative to the importing module's URL, are resolved as URLs, others +// with the host's resolve() hook. A module's whole graph is fetched before +// it is linked, since V8 links modules synchronously. + +const { + ArrayPrototypePush, + JSONParse, + ObjectKeys, + PromisePrototypeThen, + PromiseWithResolvers, + SafeMap, + SafeSet, + StringPrototypeStartsWith, +} = primordials; + +const { + compileModule, + createJsonModule, + evaluateModule, + instantiateModule, + linkModule, + moduleNamespace, + moduleRequests, + resolveUrl, +} = internalBinding('green_thread_runtime'); +const { modules: errors } = require('internal/green_threads/runtime/errors'); + +// URL => { source, format } or source. +let bundle = { __proto__: null }; +// A stub of the host's loader, with resolve() and load() methods, if the +// host has hooks. +let hostLoader = null; +let hasResolveHook = false; +let hasLoadHook = false; + +// Like Promise.all(), without using what user code can change. +function all(promises) { + const { promise, resolve, reject } = PromiseWithResolvers(); + const results = []; + let remaining = promises.length; + if (remaining === 0) resolve(results); + for (let i = 0; i < promises.length; i++) { + PromisePrototypeThen(promises[i], (value) => { + results[i] = value; + if (--remaining === 0) resolve(results); + }, reject); + } + return promise; +} + +// `${type}:${url}` => promise of the ID of the module. +const records = new SafeMap(); +// ID => promise of the IDs of the modules that its requests resolve to, +// once it's linked. +const dependencies = new SafeMap(); +// ID => URL. +const urls = new SafeMap(); + +function configure({ modules, loader, hasResolve, hasLoad }) { + if (modules != null) bundle = modules; + if (loader != null) { + hostLoader = loader; + hasResolveHook = !!hasResolve; + hasLoadHook = !!hasLoad; + } +} + +function isRelativeSpecifier(specifier) { + return StringPrototypeStartsWith(specifier, './') || + StringPrototypeStartsWith(specifier, '../') || + StringPrototypeStartsWith(specifier, '/'); +} + +function checkScheme(url) { + if (StringPrototypeStartsWith(url, 'node:')) { + throw errors.unsupportedScheme(url); + } + return url; +} + +async function resolve(specifier, referrer, attributes) { + if (isRelativeSpecifier(specifier)) { + const url = referrer === undefined ? undefined : + resolveUrl(specifier, referrer); + if (url !== undefined) return checkScheme(url); + } else { + const url = resolveUrl(specifier); + if (url !== undefined) return checkScheme(url); + } + if (hasResolveHook) { + const result = await hostLoader.resolve(specifier, + { referrer, attributes }); + if (result !== undefined) return checkScheme(result.url); + } + throw errors.notFound(specifier, referrer); +} + +function attributeType(attributes) { + const keys = ObjectKeys(attributes); + for (let i = 0; i < keys.length; i++) { + if (keys[i] !== 'type') { + throw errors.attributeUnsupported(keys[i], attributes[keys[i]]); + } + } + const { type } = attributes; + if (type !== undefined && type !== 'json') { + throw errors.attributeUnsupported('type', type); + } + return type; +} + +// Resolves with the ID of the module at `url`, which is compiled once for +// each type. +function getRecord(url, attributes) { + const type = attributeType(attributes); + const key = `${type ?? 'module'}:${url}`; + let record = records.get(key); + if (record === undefined) { + record = fetchRecord(url, type); + records.set(key, record); + } + return record; +} + +async function fetchRecord(url, type) { + let entry = bundle[url]; + if (entry === undefined && hasLoadHook) { + entry = await hostLoader.load(url, { + attributes: type === undefined ? {} : { type }, + }); + } + if (entry === undefined) throw errors.notFound(url); + // The host checked it. + const { source, format = 'module' } = + typeof entry === 'string' ? { source: entry } : entry; + let id; + if (format === 'json') { + if (type !== 'json') throw errors.attributeMissing(url, 'type', 'json'); + id = createJsonModule(url, JSONParse(source)); + } else if (format === 'module') { + if (type === 'json') throw errors.attributeIncompatible(url, 'json'); + id = compileModule(source, url); + } else { + throw errors.unknownFormat(format, url); + } + urls.set(id, url); + return id; +} + +function getDependencies(id) { + const url = urls.get(id); + let result = dependencies.get(id); + if (result === undefined) { + result = (async () => { + const requests = moduleRequests(id); + const ids = []; + for (let i = 0; i < requests.length; i++) { + const { 0: specifier, 1: attributes } = requests[i]; + ArrayPrototypePush(ids, (async () => { + return getRecord(await resolve(specifier, url, attributes), + attributes); + })()); + } + const resolved = await all(ids); + linkModule(id, resolved); + return resolved; + })(); + dependencies.set(id, result); + } + return result; +} + +// Fetches and links the graph of the module `id`. +async function loadGraph(id) { + const visited = new SafeSet(); + const visit = async (id) => { + if (visited.has(id)) return; + visited.add(id); + const ids = await getDependencies(id); + const visits = []; + for (let i = 0; i < ids.length; i++) ArrayPrototypePush(visits, visit(ids[i])); + await all(visits); + }; + await visit(id); +} + +async function run(id) { + await loadGraph(id); + instantiateModule(id); + await evaluateModule(id); + return moduleNamespace(id); +} + +/** + * Imports the module `specifier` for import(), from a module or script at + * `referrer`, if known. + * @returns {Promise} The namespace of the module. + */ +async function importModule(specifier, referrer, attributes) { + const url = await resolve(specifier, referrer, attributes); + return run(await getRecord(url, attributes)); +} + +/** + * Runs the main module of a green thread, at `url`. + * @returns {Promise} The namespace of the module. + */ +function runMain(source, url) { + const id = compileModule(source, url); + urls.set(id, url); + records.set(`module:${url}`, id); + return run(id); +} + +module.exports = { + configure, + importModule, + runMain, +}; diff --git a/lib/internal/green_threads/runtime/primordials.js b/lib/internal/green_threads/runtime/primordials.js new file mode 100644 index 000000000000..a00a245107f4 --- /dev/null +++ b/lib/internal/green_threads/runtime/primordials.js @@ -0,0 +1,128 @@ +'use strict'; +/* eslint-disable node-core/prefer-primordials, no-restricted-globals */ + +// The primordials of green threads: the part of Node.js' primordials (see +// lib/internal/per_context/primordials.js) that the green thread runtime and +// internal/per_context/domexception use, with the same names and semantics. +// Creating all of Node.js' primordials takes about 3 ms, most of the time it +// takes to start a green thread. Add what the runtime uses here. + +const { + defineProperty: ReflectDefineProperty, + getOwnPropertyDescriptor: ReflectGetOwnPropertyDescriptor, + ownKeys: ReflectOwnKeys, +} = Reflect; +const { bind, call } = Function.prototype; +const uncurryThis = bind.bind(call); + +function define(name, value) { + ReflectDefineProperty(primordials, name, { + __proto__: null, + value, + writable: false, + enumerable: false, + configurable: false, + }); +} + +// Copied as they are. +for (const name of [ + 'AggregateError', + 'Array', + 'Error', + 'EvalError', + 'FinalizationRegistry', + 'Map', + 'Promise', + 'RangeError', + 'ReferenceError', + 'Set', + 'String', + 'Symbol', + 'SyntaxError', + 'TypeError', + 'URIError', + 'WeakMap', + 'WeakRef', +]) { + define(name, globalThis[name]); +} +define('globalThis', globalThis); + +// `${Global}${Member}`. Functions are called with their global as `this`. +for (const { 0: global, 1: members } of [ + ['Array', ['isArray']], + ['Date', ['now']], + ['JSON', ['parse']], + ['Number', ['isFinite', 'isInteger', 'isNaN']], + ['Object', [ + 'defineProperties', + 'defineProperty', + 'freeze', + 'getOwnPropertyDescriptor', + 'getOwnPropertyNames', + 'getPrototypeOf', + 'keys', + 'setPrototypeOf', + ]], + ['Promise', ['reject', 'resolve', 'withResolvers']], + ['Reflect', ['apply', 'construct']], + ['Symbol', ['dispose', 'iterator', 'toStringTag']], +]) { + const target = globalThis[global]; + for (const member of members) { + let value = target[member]; + if (typeof value === 'function') value = bind.call(value, target); + define(`${global}${member[0].toUpperCase()}${member.slice(1)}`, value); + } +} + +// `${Global}Prototype`, and `${Global}Prototype${Method}`, which takes the +// receiver as its first argument. +for (const { 0: global, 1: methods } of [ + ['Array', ['findIndex', 'forEach', 'push', 'slice', 'splice']], + ['Error', []], + ['Function', ['call']], + ['Map', ['delete', 'get', 'set']], + ['Object', ['toString']], + ['Promise', ['then']], + ['String', ['startsWith']], +]) { + const prototype = globalThis[global].prototype; + define(`${global}Prototype`, prototype); + for (const method of methods) { + define(`${global}Prototype${method[0].toUpperCase()}${method.slice(1)}`, + uncurryThis(prototype[method])); + } +} + +// Collections that user code can't change the behavior of. Like Node.js' +// makeSafe(), whose iterator wrapping never applies to them. +const makeSafe = (unsafe, safe) => { + for (const { 0: src, 1: dest } of [[unsafe.prototype, safe.prototype], + [unsafe, safe]]) { + for (const key of ReflectOwnKeys(src)) { + if (!ReflectGetOwnPropertyDescriptor(dest, key)) { + ReflectDefineProperty( + dest, + key, + { __proto__: null, ...ReflectGetOwnPropertyDescriptor(src, key) }); + } + } + } + Object.setPrototypeOf(safe.prototype, null); + Object.freeze(safe.prototype); + Object.freeze(safe); + return safe; +}; + +define('SafeMap', makeSafe(Map, class SafeMap extends Map {})); +define('SafeSet', makeSafe(Set, class SafeSet extends Set {})); +define('SafeWeakMap', makeSafe(WeakMap, class SafeWeakMap extends WeakMap {})); +define('SafeFinalizationRegistry', makeSafe( + FinalizationRegistry, + class SafeFinalizationRegistry extends FinalizationRegistry {}, +)); +define('SafeWeakRef', makeSafe(WeakRef, class SafeWeakRef extends WeakRef {})); + +Object.freeze(primordials); diff --git a/lib/internal/green_threads/testing.js b/lib/internal/green_threads/testing.js new file mode 100644 index 000000000000..68d073aed57e --- /dev/null +++ b/lib/internal/green_threads/testing.js @@ -0,0 +1,12 @@ +'use strict'; + +// Options of node:green_threads for tests only. + +const { + Symbol, +} = primordials; + +module.exports = { + // The `flags` argument of the binding's GreenThread constructor. + kTestingFlags: Symbol('kTestingFlags'), +}; diff --git a/node.gyp b/node.gyp index 714609b03e66..b19d5c13b7d4 100644 --- a/node.gyp +++ b/node.gyp @@ -140,6 +140,7 @@ 'src/node_errors.cc', 'src/node_external_reference.cc', 'src/node_file.cc', + 'src/node_green_thread.cc', 'src/node_file_utils.cc', 'src/node_http_parser.cc', 'src/node_http2.cc', diff --git a/src/api/environment.cc b/src/api/environment.cc index 4a7b70168e6d..124a7ea8ae0b 100644 --- a/src/api/environment.cc +++ b/src/api/environment.cc @@ -342,14 +342,14 @@ bool IsFirstSnapshotBlob(const char* data) { return first_snapshot_recorded && data == first_snapshot_blob.data; } -// TODO(joyeecheung): we may want to expose this, but then we need to be -// careful about what we override in the params. -Isolate* NewIsolate(Isolate::CreateParams* params, - uv_loop_t* event_loop, - MultiIsolatePlatform* platform, - const SnapshotData* snapshot_data, - const IsolateSettings& settings) { - IsolateGroup group = GetOrCreateIsolateGroup(); +template +static Isolate* NewIsolateImpl(Isolate::CreateParams* params, + MultiIsolatePlatform* platform, + const SnapshotData* snapshot_data, + const IsolateSettings& settings, + const IsolateGroup& group, + bool use_first_snapshot, + RegisterFn register_isolate) { Isolate* isolate = Isolate::Allocate(group); if (isolate == nullptr) return nullptr; @@ -357,11 +357,11 @@ Isolate* NewIsolate(Isolate::CreateParams* params, SnapshotBuilder::InitializeIsolateParams(snapshot_data, params); } - UseFirstSnapshot(params); + if (use_first_snapshot) UseFirstSnapshot(params); // Register the isolate on the platform before the isolate gets initialized, // so that the isolate can access the platform during initialization. - platform->RegisterIsolate(isolate, event_loop); + register_isolate(isolate); // Ensure that there is always a CppHeap. if (settings.cpp_heap == nullptr) { @@ -378,6 +378,41 @@ Isolate* NewIsolate(Isolate::CreateParams* params, return isolate; } +// TODO(joyeecheung): we may want to expose this, but then we need to be +// careful about what we override in the params. +Isolate* NewIsolate(Isolate::CreateParams* params, + uv_loop_t* event_loop, + MultiIsolatePlatform* platform, + const SnapshotData* snapshot_data, + const IsolateSettings& settings) { + return NewIsolateImpl(params, + platform, + snapshot_data, + settings, + GetOrCreateIsolateGroup(), + true, + [&](Isolate* isolate) { + platform->RegisterIsolate(isolate, event_loop); + }); +} + +Isolate* NewIsolate(Isolate::CreateParams* params, + IsolatePlatformDelegate* delegate, + MultiIsolatePlatform* platform, + const IsolateSettings& settings, + const IsolateGroup& group, + bool use_first_snapshot) { + return NewIsolateImpl(params, + platform, + nullptr, + settings, + group, + use_first_snapshot, + [&](Isolate* isolate) { + platform->RegisterIsolate(isolate, delegate); + }); +} + Isolate* NewIsolate(ArrayBufferAllocator* allocator, uv_loop_t* event_loop, MultiIsolatePlatform* platform, @@ -1168,6 +1203,7 @@ void DefaultProcessExitHandlerInternal(Environment* env, ExitCode exit_code) { env->set_stopping(true); env->set_can_call_into_js(false); env->stop_sub_worker_contexts(); + const bool green_threads_disposed = green_thread::TerminateAll(); env->isolate()->DumpAndResetStats(); // The tracing agent could be in the process of writing data using the // threadpool. Stop it before shutting down libuv. The rest of the tracing @@ -1179,7 +1215,7 @@ void DefaultProcessExitHandlerInternal(Environment* env, ExitCode exit_code) { // And make sure V8Platform don not call into Libuv threadpool, see Dispose // in node_v8_platform-inl.h uv_library_shutdown(); - DisposePlatform(); + if (green_threads_disposed) DisposePlatform(); #if HAVE_OPENSSL crypto::CleanupCachedRootCertificates(); diff --git a/src/async_wrap.h b/src/async_wrap.h index a14d84088b38..883e6f87bcb4 100644 --- a/src/async_wrap.h +++ b/src/async_wrap.h @@ -42,6 +42,7 @@ namespace node { V(FSEVENTWRAP) \ V(FSREQCALLBACK) \ V(GLOBREQUEST) \ + V(GREENTHREAD) \ V(FSREQPROMISE) \ V(GETADDRINFOREQWRAP) \ V(GETNAMEINFOREQWRAP) \ diff --git a/src/cppgc_helpers-inl.h b/src/cppgc_helpers-inl.h index 4db197d694f8..90a22b18b638 100644 --- a/src/cppgc_helpers-inl.h +++ b/src/cppgc_helpers-inl.h @@ -15,7 +15,7 @@ void CppgcMixin::Wrap(T* ptr, Realm* realm, v8::Local obj) { ptr->traced_reference_ = v8::TracedReference(isolate, obj); // Note that ptr must be of concrete type T in Wrap. auto* wrappable = static_cast(ptr); - v8::Object::Wrap(isolate, obj, wrappable); + v8::Object::Wrap(isolate, obj, wrappable); // Keep the layout consistent with BaseObjects. obj->SetAlignedPointerInInternalField( kEmbedderType, diff --git a/src/cppgc_helpers.h b/src/cppgc_helpers.h index 3dfd31bcb29e..0372ffe2c8de 100644 --- a/src/cppgc_helpers.h +++ b/src/cppgc_helpers.h @@ -15,6 +15,12 @@ namespace node { +// The tag of the pointers from JS wrappers to cppgc-managed objects. V8 only +// defines a default tag when the sandbox is disabled, since embedders that use +// the sandbox should use specific tags; this one has the same value. +constexpr v8::CppHeapPointerTag kCppgcWrappableTag = + v8::CppHeapPointerTag::kFirstEmbedderWrappableTag; + class Environment; class Realm; class CppgcWrapperListNode; diff --git a/src/env.cc b/src/env.cc index c30b6e2f5049..364b9844690f 100644 --- a/src/env.cc +++ b/src/env.cc @@ -1,6 +1,7 @@ #include "env.h" #include "async_wrap.h" #include "base_object-inl.h" +#include "cppgc_helpers.h" #include "debug_utils-inl.h" #include "diagnosticfilename-inl.h" #include "memory_tracker-inl.h" @@ -668,8 +669,7 @@ IsolateData::~IsolateData() { void SetCppgcReference(Isolate* isolate, Local object, v8::Object::Wrappable* wrappable) { - v8::Object::Wrap( - isolate, object, wrappable); + v8::Object::Wrap(isolate, object, wrappable); } void IsolateData::MemoryInfo(MemoryTracker* tracker) const { diff --git a/src/node.cc b/src/node.cc index 0b5b305d6881..4219417af637 100644 --- a/src/node.cc +++ b/src/node.cc @@ -1552,11 +1552,16 @@ std::vector GetRuntimeEnvironmentDefaults() { void TearDownOncePerProcess() { const uint32_t flags = init_process_flags.load(); + // Green threads that aren't owned by an Environment, e.g. warm ones, must + // be gone before V8 is. Abandoned ones never are. + const bool green_threads_disposed = green_thread::TerminateAll(); ResetStdio(); if (!(flags & ProcessInitializationFlags::kNoDefaultSignalHandling)) { ResetSignalHandlers(); } + if (!green_threads_disposed) return; + if (!(flags & ProcessInitializationFlags::kNoInitializeCppgc)) { cppgc::ShutdownProcess(); } diff --git a/src/node_binding.cc b/src/node_binding.cc index 3c7dee9bb33d..f940dd1f6d90 100644 --- a/src/node_binding.cc +++ b/src/node_binding.cc @@ -71,6 +71,7 @@ V(fs_dir) \ V(fs_event_wrap) \ V(glob) \ + V(green_thread) \ V(heap_utils) \ V(http2) \ V(http_parser) \ diff --git a/src/node_builtins.cc b/src/node_builtins.cc index 21af988c95d8..73676ca84701 100644 --- a/src/node_builtins.cc +++ b/src/node_builtins.cc @@ -124,7 +124,9 @@ BuiltinLoader::BuiltinCategories BuiltinLoader::GetBuiltinCategories() const { "internal/bootstrap/", "internal/per_context/", "internal/deps/", - "internal/main/" + "internal/main/", + // Only run in green threads. + "internal/green_threads/runtime/", }; builtin_categories.cannot_be_required = std::set { @@ -162,6 +164,7 @@ BuiltinLoader::BuiltinCategories BuiltinLoader::GetBuiltinCategories() const { "bench/reporters", // Experimental. "dtls", // Experimental. "ffi", // Experimental. + "green_threads", // Experimental. "quic", // Experimental. "sqlite", // Experimental. "stream/iter", // Experimental. diff --git a/src/node_external_reference.h b/src/node_external_reference.h index abdf6a36cbcb..e3a7836440cc 100644 --- a/src/node_external_reference.h +++ b/src/node_external_reference.h @@ -78,6 +78,7 @@ class ExternalReferenceRegistry { V(fs_dir) \ V(fs_event_wrap) \ V(glob) \ + V(green_thread) \ V(handle_wrap) \ V(heap_utils) \ V(http_parser) \ diff --git a/src/node_green_thread.cc b/src/node_green_thread.cc new file mode 100644 index 000000000000..9ddfe491cb02 --- /dev/null +++ b/src/node_green_thread.cc @@ -0,0 +1,4430 @@ +#include "ada.h" +#include "async_wrap-inl.h" +#include "env-inl.h" +#include "handle_wrap.h" +#include "node_builtins.h" +#include "node_errors.h" +#include "node_external_reference.h" +#include "node_internals.h" +#include "node_messaging.h" +#include "node_mutex.h" +#include "util-inl.h" +#include "uv.h" +#include "v8.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Where a pool thread that runs JS which ignores termination can be cut off. +#if defined(__linux__) && (defined(__x86_64__) || defined(__aarch64__)) +#define NODE_GREEN_THREAD_CAN_CUT_OFF 1 +#include +#include +#endif + +namespace node { +namespace green_thread { + +using v8::Array; +using v8::Boolean; +using v8::ConstructorBehavior; +using v8::Context; +using v8::Exception; +using v8::Function; +using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; +using v8::Global; +using v8::HandleScope; +using v8::IdleTask; +using v8::Int32; +using v8::Integer; +using v8::Isolate; +using v8::Just; +using v8::Local; +using v8::LocalVector; +using v8::Locker; +using v8::Maybe; +using v8::MaybeLocal; +using v8::Message; +using v8::Name; +using v8::Nothing; +using v8::Number; +using v8::Object; +using v8::ObjectTemplate; +using v8::Promise; +using v8::PromiseRejectEvent; +using v8::PromiseRejectMessage; +using v8::PropertyAttribute; +using v8::Script; +using v8::ScriptCompiler; +using v8::ScriptOrigin; +using v8::SourceLocation; +using v8::String; +using v8::Task; +using v8::TaskRunner; +using v8::TryCatch; +using v8::Uint32; +using v8::Undefined; +using v8::Value; +using v8::ValueDeserializer; +using v8::ValueSerializer; + +namespace { + +constexpr size_t kStackSize = 4 * 1024 * 1024; +constexpr size_t kStackBufferSize = 192 * 1024; +constexpr unsigned int kMaxPoolThreads = 8; +constexpr size_t kMB = 1024 * 1024; +constexpr size_t kExtraHeapAllowance = 16 * kMB; +// The maximum number of work items an isolate runs per slice. +constexpr size_t kSliceBudget = 64; +// The maximum number of slices a pool thread runs before returning to its +// loop. +constexpr size_t kReadyBudget = 16; + +// Event types sent from a green thread to its host. +enum EventType : int32_t { + kEventMessage = 0, + kEventConsole, + kEventEvalResult, + kEventError, + kEventExit, + kEventWarmPoolFull, + kEventAbandoned, + kEventRpc, +}; + +// Values of the `id` field of kEventError events. +enum ErrorKind : int32_t { + kErrorException = 0, + kErrorOutOfMemory, + kErrorCpuLimit, +}; + +// Flags passed to the GreenThread constructor. +enum Flags : uint32_t { + // Installs __greenThreadInfo() and __greenThreadBlock(), for tests. + kFlagTestingGlobals = 1 << 0, + // Makes the isolate ignore termination, like JS that V8 has compiled into + // code that doesn't handle interrupts would, for tests. + kFlagTestingIgnoreTermination = 1 << 1, +}; + +// How long a green thread has to stop after it has been asked to, before it +// is abandoned, and how long after that the backup watchdog tries to cut off +// the pool thread that runs it, before aborting the process. In nanoseconds; +// UINT64_MAX means never. Set with setUnresponsiveTimeouts(). +std::atomic unresponsive_grace_period{1000 * 1000000ull}; +std::atomic cut_off_period{10000 * 1000000ull}; + +// Now plus `delay` nanoseconds, in uv_hrtime() time. +uint64_t DeadlineAfter(uint64_t delay) { + const uint64_t now = uv_hrtime(); + return delay > UINT64_MAX - now ? UINT64_MAX : now + delay; +} +constexpr uint64_t kCutOffRetryInterval = 5 * 1000000ull; + +#ifdef NODE_GREEN_THREAD_CAN_CUT_OFF +// A real-time signal, which Node.js doesn't otherwise use, and which JS can't +// install handlers for. +int CutOffSignal() { + return SIGRTMIN + 7; +} +#endif + +using Bytes = std::vector; +using worker::MessagePortData; +using PortDataList = std::vector>; + +// A value serialized by Serialize(), with the ArrayBuffers and MessagePorts +// that were transferred and the SharedArrayBuffers that are shared with it. +// It uses the format of Node.js' messages, so that it can be sent through +// MessagePorts. +struct SerializedValue { + Bytes data; + std::vector> array_buffers; + std::vector> shared_array_buffers; + // Taken by Deserialize(). Shared so that work that delivers the value can + // be copied. + std::shared_ptr ports; + + bool empty() const { return data.empty(); } +}; + +class GreenIsolate; +class GreenThreadPool; +class PoolThread; + +// NOLINTNEXTLINE(runtime/thread_local) +thread_local PoolThread* current_pool_thread = nullptr; + +// A pool thread owns a libuv loop. It runs the callbacks posted to it, in +// order, and slices of the green isolates that are ready to run on any +// thread. Pool threads live until the process exits. +class PoolThread { + public: + using Callback = std::function; + + PoolThread(GreenThreadPool* pool, size_t index) + : pool_(pool), index_(index) {} + + void Start() { + CHECK_EQ(uv_loop_init(&loop_), 0); + CHECK_EQ(uv_async_init(&loop_, &wakeup_, OnWakeup), 0); + + uv_thread_options_t options; + options.flags = UV_THREAD_HAS_STACK_SIZE; + options.stack_size = kStackSize; + CHECK_EQ( + uv_thread_create_ex( + &tid_, + &options, + [](void* arg) { + PoolThread* self = static_cast(arg); + const uintptr_t stack_top = reinterpret_cast(&arg); + // Leave some room for C++ code below the V8 limit. + self->stack_limit_ = stack_top - (kStackSize - kStackBufferSize); + char name[16]; + snprintf(name, sizeof(name), "GreenThread%zu", self->index_); + uv_thread_setname(name); + current_pool_thread = self; + uv_run(&self->loop_, UV_RUN_DEFAULT); + }, + this), + 0); + } + + // May be called from any thread. + void Post(Callback callback) { + { + Mutex::ScopedLock lock(mutex_); + queue_.push_back(std::move(callback)); + } + Wake(); + } + + // May be called from any thread. + void Wake() { CHECK_EQ(uv_async_send(&wakeup_), 0); } + + // May be called from any thread. + void RunOrPost(Callback callback) { + if (current_pool_thread == this) { + callback(); + } else { + Post(std::move(callback)); + } + } + + bool is_idle() const { return !busy_.load(); } + size_t index() const { return index_; } + + // The green isolate whose work this thread is running, if any, and the one + // that the backup watchdog wants to cut off. See RunGuarded(). + sigjmp_buf* cut_off_jump() { return &cut_off_jump_; } + GreenIsolate* running() const { return running_.load(); } + void set_running(GreenIsolate* green) { running_ = green; } + GreenIsolate* cut_off_target() const { return cut_off_target_.load(); } + void set_cut_off_target(GreenIsolate* green) { cut_off_target_ = green; } + + // May be called from any thread. Returns false if cutting threads off is + // not supported. + inline bool SendCutOffSignal(GreenIsolate* target); + uv_loop_t* loop() { return &loop_; } + uintptr_t stack_limit() const { return stack_limit_; } + + private: + inline static void OnWakeup(uv_async_t* handle); + + bool HasCallbacks() { + Mutex::ScopedLock lock(mutex_); + return !queue_.empty(); + } + + GreenThreadPool* const pool_; + const size_t index_; + uv_thread_t tid_; + uv_loop_t loop_; + uv_async_t wakeup_; + Mutex mutex_; + std::deque queue_; + uintptr_t stack_limit_ = 0; + std::atomic busy_{false}; + sigjmp_buf cut_off_jump_; + std::atomic running_{nullptr}; + std::atomic cut_off_target_{nullptr}; +}; + +// The process-wide set of pool threads, and the queue of green isolates that +// are ready to run on any of them. It is created on first use and +// intentionally never destroyed. +class GreenThreadPool { + public: + static GreenThreadPool* Get() { + static GreenThreadPool* pool = new GreenThreadPool(); + return pool; + } + + PoolThread* Next() { + size_t index = next_.fetch_add(1, std::memory_order_relaxed); + return threads_[index % threads_.size()].get(); + } + + size_t size() const { return threads_.size(); } + + // May be called from any thread. Wakes `preferred` if it is idle, so that + // isolates tend to stay on the same thread, or else any idle thread. If all + // threads are busy, the first one to finish its work picks `green` up. + void ScheduleReady(std::shared_ptr green, + PoolThread* preferred) { + { + Mutex::ScopedLock lock(ready_mutex_); + ready_.push_back(std::move(green)); + } + if (preferred != nullptr && preferred->is_idle()) { + preferred->Wake(); + } else { + WakeIdleThread(nullptr); + } + } + + bool HasReady() { + Mutex::ScopedLock lock(ready_mutex_); + return !ready_.empty(); + } + + // Queues work that only runs when a pool thread has nothing else to do. + // May be called from any thread. + void PostBackground(std::function callback) { + { + Mutex::ScopedLock lock(ready_mutex_); + background_.push_back(std::move(callback)); + } + WakeIdleThread(nullptr); + } + + // Whether there is background work that may run now. + bool HasBackground() { + Mutex::ScopedLock lock(ready_mutex_); + return !background_.empty() && background_running_ < MaxBackground(); + } + + // Runs one item of background work, if there is any that may run now. + void RunBackground() { + std::function callback; + { + Mutex::ScopedLock lock(ready_mutex_); + if (background_.empty() || background_running_ >= MaxBackground()) { + return; + } + callback = std::move(background_.front()); + background_.pop_front(); + background_running_++; + } + callback(); + Mutex::ScopedLock lock(ready_mutex_); + background_running_--; + } + + inline void RunReady(PoolThread* thread, size_t budget); + + private: + GreenThreadPool() { + unsigned int count = + std::clamp(uv_available_parallelism(), 1u, kMaxPoolThreads); + for (unsigned int i = 0; i < count; i++) { + threads_.push_back(std::make_unique(this, i)); + } + for (auto& thread : threads_) thread->Start(); + } + + void WakeIdleThread(PoolThread* except) { + const size_t start = next_.fetch_add(1, std::memory_order_relaxed); + for (size_t i = 0; i < threads_.size(); i++) { + PoolThread* thread = threads_[(start + i) % threads_.size()].get(); + if (thread != except && thread->is_idle()) { + thread->Wake(); + return; + } + } + } + + inline std::shared_ptr TakeReady(PoolThread* thread); + + // Background work can take a while, e.g. creating an isolate, and can't be + // interrupted, so some threads are always left for other work. + size_t MaxBackground() const { + return std::max(1, threads_.size() / 2); + } + + std::vector> threads_; + std::atomic next_{0}; + Mutex ready_mutex_; + std::deque> ready_; + std::deque> background_; + size_t background_running_ = 0; +}; + +void PoolThread::OnWakeup(uv_async_t* handle) { + PoolThread* self = ContainerOf(&PoolThread::wakeup_, handle); + self->busy_ = true; + std::deque callbacks; + { + Mutex::ScopedLock lock(self->mutex_); + callbacks.swap(self->queue_); + } + for (Callback& callback : callbacks) callback(); + self->pool_->RunReady(self, kReadyBudget); + if (!self->HasCallbacks() && !self->pool_->HasReady()) { + self->pool_->RunBackground(); + } + + // Return to the loop so that its timers can run, but come back right away + // if there is more work. + if (self->HasCallbacks() || self->pool_->HasReady() || + self->pool_->HasBackground()) { + self->Wake(); + return; + } + self->busy_ = false; + // ScheduleReady() and PostBackground() may have skipped this thread while + // it was still busy. + if (self->pool_->HasReady() || self->pool_->HasBackground()) { + self->busy_ = true; + self->Wake(); + } +} + +// A libuv timer on a given pool thread. It may be started and closed from any +// thread; the handle itself is only touched on its thread, and stays alive +// until libuv is done with it. +class TimerHandle final : public std::enable_shared_from_this { + public: + TimerHandle(PoolThread* thread, + uint64_t timeout, + uint64_t repeat, + std::function on_fire) + : thread_(thread), + timeout_(timeout), + repeat_(repeat), + on_fire_(std::move(on_fire)) {} + + void Start() { + thread_->RunOrPost([self = shared_from_this()]() { + if (self->closed_) return; + CHECK_EQ(uv_timer_init(self->thread_->loop(), &self->handle_), 0); + self->handle_.data = self.get(); + uv_timer_start(&self->handle_, OnTimer, self->timeout_, self->repeat_); + self->started_ = true; + self->self_ = self; + }); + } + + void Close() { + thread_->RunOrPost([self = shared_from_this()]() { + if (self->closed_) return; + self->closed_ = true; + self->on_fire_ = nullptr; + if (!self->started_) return; + uv_close(reinterpret_cast(&self->handle_), + [](uv_handle_t* handle) { + static_cast(handle->data)->self_.reset(); + }); + }); + } + + private: + static void OnTimer(uv_timer_t* handle) { + TimerHandle* self = static_cast(handle->data); + // Copy, since the callback may close the timer. + std::function on_fire = self->on_fire_; + if (on_fire) on_fire(); + } + + PoolThread* const thread_; + const uint64_t timeout_; + const uint64_t repeat_; + uv_timer_t handle_; + // Only accessed on `thread_`. + std::function on_fire_; + bool started_ = false; + bool closed_ = false; + std::shared_ptr self_; +}; + +// The CPU time used by the calling thread, in nanoseconds. +uint64_t ThreadCpuTime() { +#ifdef CLOCK_THREAD_CPUTIME_ID + // More precise than getrusage(), which is only updated on scheduler ticks + // on some systems. + timespec ts; + if (clock_gettime(CLOCK_THREAD_CPUTIME_ID, &ts) == 0) { + return static_cast(ts.tv_sec) * 1000000000 + + static_cast(ts.tv_nsec); + } +#endif + uv_rusage_t usage; + if (uv_getrusage_thread(&usage) != 0) return 0; + auto ns = [](const uv_timeval_t& tv) { + return static_cast(tv.tv_sec) * 1000000000 + + static_cast(tv.tv_usec) * 1000; + }; + return ns(usage.ru_utime) + ns(usage.ru_stime); +} + +// An ArrayBuffer allocator that counts what it has allocated, and refuses +// allocations that would take the isolate over its memory limit, together +// with the size of the JS heap as of the last GC. Free() may be called from +// any thread. +class LimitedAllocator final : public v8::ArrayBuffer::Allocator { + public: + // In sandbox builds, memory has to come from the sandbox of the isolate's + // group. + LimitedAllocator(size_t limit, const v8::IsolateGroup& group) +#if defined(V8_COMPRESS_POINTERS) && \ + !defined(V8_COMPRESS_POINTERS_IN_SHARED_CAGE) + : allocator_(v8::ArrayBuffer::Allocator::NewDefaultAllocator(group)), +#else + : allocator_(v8::ArrayBuffer::Allocator::NewDefaultAllocator()), +#endif + limit_(limit) { + } + + v8::PageAllocator* GetPageAllocator() override { + return allocator_->GetPageAllocator(); + } + + void* Allocate(size_t length) override { + if (!Reserve(length)) return nullptr; + void* data = allocator_->Allocate(length); + if (data == nullptr) allocated_ -= length; + return data; + } + + void* AllocateUninitialized(size_t length) override { + if (!Reserve(length)) return nullptr; + void* data = allocator_->AllocateUninitialized(length); + if (data == nullptr) allocated_ -= length; + return data; + } + + void Free(void* data, size_t length) override { + allocator_->Free(data, length); + allocated_ -= length; + } + + size_t limit() const { + return limit_; + } + size_t allocated() const { + return allocated_.load(); + } + void set_heap_used(size_t heap_used) { + heap_used_ = heap_used; + } + + // A copy of `store` in memory that doesn't count towards the limit, for + // ArrayBuffers that are transferred away. Returns nullptr if allocating + // fails. + static std::unique_ptr CopyUntracked( + std::shared_ptr self, const v8::BackingStore& store) { + const size_t length = store.ByteLength(); + void* data = self->allocator_->AllocateUninitialized(length); + if (data == nullptr && length > 0) return nullptr; + if (length > 0) memcpy(data, store.Data(), length); + auto* holder = new std::shared_ptr(std::move(self)); + return v8::ArrayBuffer::NewBackingStore( + data, + length, + [](void* data, size_t length, void* holder_data) { + auto* holder = + static_cast*>(holder_data); + (*holder)->allocator_->Free(data, length); + delete holder; + }, + holder); + } + + private: + // Leaves some of the limit for the heap to grow into before the next GC, + // which checks the heap and ArrayBuffers together, so that running out of + // memory through ArrayBuffers throws a RangeError rather than terminating + // the isolate. + bool Reserve(size_t length) { + size_t allocated = allocated_.fetch_add(length) + length; + if (limit_ == 0 || + allocated + heap_used_.load() <= limit_ - limit_ / kHeadroomDivisor) { + return true; + } + allocated_ -= length; + return false; + } + + static constexpr size_t kHeadroomDivisor = 16; + + std::unique_ptr allocator_; + const size_t limit_; + std::atomic allocated_{0}; + std::atomic heap_used_{0}; +}; + +// Calls a callback once a deadline (in uv_hrtime() nanoseconds) has passed, +// on its own thread. Used to interrupt green threads that may have used up +// their CPU time. Created on first use and intentionally never destroyed. +class Watchdog { + public: + using Callback = std::function; + + static Watchdog* Get() { + static Watchdog* watchdog = new Watchdog(); + return watchdog; + } + + // Replaces the deadline of `key`, if any. May be called from any thread. + void Arm(const void* key, uint64_t deadline, Callback callback) { + Mutex::ScopedLock lock(mutex_); + entries_[key] = {deadline, std::move(callback)}; + cv_.Signal(lock); + } + + // May be called from any thread. The callback of `key` may still be + // running, or about to run, when this returns. + void Disarm(const void* key) { + Mutex::ScopedLock lock(mutex_); + entries_.erase(key); + } + + private: + struct Entry { + uint64_t deadline; + Callback callback; + }; + + Watchdog() { + CHECK_EQ( + uv_thread_create( + &tid_, [](void* arg) { static_cast(arg)->Run(); }, this), + 0); + } + + void Run() { + uv_thread_setname("GreenWatchdog"); + Mutex::ScopedLock lock(mutex_); + for (;;) { + if (entries_.empty()) { + cv_.Wait(lock); + continue; + } + const uint64_t now = uv_hrtime(); + uint64_t next = UINT64_MAX; + std::vector due; + for (auto it = entries_.begin(); it != entries_.end();) { + if (it->second.deadline <= now) { + due.push_back(std::move(it->second.callback)); + it = entries_.erase(it); + } else { + next = std::min(next, it->second.deadline); + ++it; + } + } + if (!due.empty()) { + Mutex::ScopedUnlock unlock(lock); + for (Callback& callback : due) callback(); + continue; + } + cv_.TimedWait(lock, next - now); + } + } + + uv_thread_t tid_; + Mutex mutex_; + ConditionVariable cv_; + std::unordered_map entries_; +}; + +// The private symbol that RpcTargets and RPC stubs are branded with, in both +// green threads and their hosts. See internal/green_threads/rpc. +Local RpcBrand(Isolate* isolate) { + return v8::Private::ForApi( + isolate, FIXED_ONE_BYTE_STRING(isolate, "node:green_thread:rpc")); +} + +// The private symbol that holds the ID of a green thread's MessagePort. See +// internal/green_threads/runtime/message_port. +Local PortIdSymbol(Isolate* isolate) { + return v8::Private::ForApi( + isolate, FIXED_ONE_BYTE_STRING(isolate, "node:green_thread:port_id")); +} + +// Calls `hooks[name](arg)`, where `hooks` is an RpcSession's hooks: see +// internal/green_threads/runtime/bootstrap and lib/green_threads.js. +MaybeLocal CallRpcHook(Isolate* isolate, + Local context, + Local hooks, + const char* name, + Local arg) { + Local hook; + if (!hooks->Get(context, OneByteString(isolate, name)).ToLocal(&hook)) { + return {}; + } + CHECK(hook->IsFunction()); + return hook.As()->Call(context, hooks, 1, &arg); +} + +// How the host or a green thread represents MessagePorts. +class PortAdapter { + public: + virtual ~PortAdapter() = default; + // Whether `object` is a MessagePort, and if so, whether it is detached. + virtual bool IsPort(Local object, bool* detached) = 0; + // Takes the data of a port that is not detached, which detaches it. + virtual std::unique_ptr TakePort(Local port) = 0; + // Creates a port for data that was transferred. + virtual MaybeLocal CreatePort( + std::unique_ptr data) = 0; +}; + +// What Node.js' messages use for the host object index of an object that is +// serialized in place, such as process.env. +constexpr uint32_t kNormalObject = static_cast(-1); + +class SerializerDelegate final : public ValueSerializer::Delegate { + public: + // With `rpc_hooks`, RpcTargets, RPC stubs and AbortSignals are serialized + // as indices into descriptors(), which holds what the hooks' + // exportHostObject() returns for them. + SerializerDelegate(Isolate* isolate, + Local context, + bool is_host, + Local rpc_hooks = {}) + : isolate_(isolate), + context_(context), + is_host_(is_host), + rpc_hooks_(rpc_hooks) { + if (rpc_hooks_.IsEmpty()) return; + rpc_brand_ = RpcBrand(isolate); + Local prototype; + if (rpc_hooks_ + ->Get(context, + FIXED_ONE_BYTE_STRING(isolate, "abortSignalPrototype")) + .ToLocal(&prototype)) { + abort_signal_prototype_ = prototype; + } + } + + void set_serializer(ValueSerializer* serializer) { serializer_ = serializer; } + // Ports in `transfer_ports` are written as their indices in it, as in + // Node.js' messages. + void set_ports(PortAdapter* ports, + const LocalVector* transfer_ports) { + ports_ = ports; + transfer_ports_ = transfer_ports; + } + void set_shared_array_buffers( + std::vector>* shared_array_buffers) { + shared_array_buffers_ = shared_array_buffers; + } + + Maybe GetSharedArrayBufferId( + Isolate* isolate, Local buffer) override { + std::shared_ptr store = buffer->GetBackingStore(); + for (size_t i = 0; i < shared_array_buffers_->size(); i++) { + if ((*shared_array_buffers_)[i] == store) { + return Just(static_cast(i)); + } + } + shared_array_buffers_->push_back(std::move(store)); + return Just(static_cast(shared_array_buffers_->size() - 1)); + } + + void ThrowDataCloneError(Local message) override { + if (is_host_) { + worker::ThrowDataCloneException(context_, message); + return; + } + // Green threads have no DOMException, so use a plain Error with the + // same name. + Local error = Exception::Error(message); + error.As() + ->Set(context_, + FIXED_ONE_BYTE_STRING(isolate_, "name"), + FIXED_ONE_BYTE_STRING(isolate_, "DataCloneError")) + .Check(); + isolate_->ThrowException(error); + } + + bool HasCustomHostObject(Isolate* isolate) override { + return !rpc_hooks_.IsEmpty() || ports_ != nullptr; + } + + Maybe IsHostObject(Isolate* isolate, Local object) override { + // What V8 does without custom host objects. + if (object->InternalFieldCount() > 0) return Just(true); + bool detached; + if (ports_ != nullptr && ports_->IsPort(object, &detached)) { + return Just(true); + } + return IsRpcObject(object); + } + + Maybe WriteHostObject(Isolate* isolate, Local object) override { + if (ports_ != nullptr) { + for (size_t i = 0; i < transfer_ports_->size(); i++) { + if ((*transfer_ports_)[i] == object) { + serializer_->WriteUint32(static_cast(i)); + return Just(true); + } + } + bool detached; + if (ports_->IsPort(object, &detached)) { + ThrowDataCloneError(FIXED_ONE_BYTE_STRING( + isolate, "A MessagePort must be in the transfer list to be sent")); + return Nothing(); + } + } + Maybe is_rpc_object = IsRpcObject(object); + if (is_rpc_object.IsNothing()) return Nothing(); + if (!is_rpc_object.FromJust()) { + return ValueSerializer::Delegate::WriteHostObject(isolate, object); + } + Local descriptor; + if (!CallRpcHook(isolate, context_, rpc_hooks_, "exportHostObject", object) + .ToLocal(&descriptor)) { + return Nothing(); + } + serializer_->WriteUint32(static_cast(descriptors_.size())); + // V8 writes each object in a HandleScope of its own. + descriptors_.emplace_back(isolate, descriptor); + return Just(true); + } + + // Once the value is serialized. + Local descriptors() { + if (descriptors_.empty()) return {}; + LocalVector values(isolate_); + values.reserve(descriptors_.size()); + for (const Global& descriptor : descriptors_) { + values.push_back(descriptor.Get(isolate_)); + } + return v8::Array::New(isolate_, values.data(), values.size()); + } + + private: + Maybe IsRpcObject(Local object) { + if (rpc_hooks_.IsEmpty()) return Just(false); + Maybe branded = object->HasPrivate(context_, rpc_brand_); + if (branded.IsNothing() || branded.FromJust()) return branded; + if (abort_signal_prototype_.IsEmpty()) return Just(false); + // Like `instanceof AbortSignal`, without calling into JS. + constexpr int kMaxDepth = 8; + Local prototype = object->GetPrototype(); + for (int i = 0; i < kMaxDepth && prototype->IsObject(); i++) { + if (prototype == abort_signal_prototype_) return Just(true); + prototype = prototype.As()->GetPrototype(); + } + return Just(false); + } + + Isolate* isolate_; + Local context_; + bool is_host_; + Local rpc_hooks_; + Local rpc_brand_; + Local abort_signal_prototype_; + ValueSerializer* serializer_ = nullptr; + std::vector> descriptors_; + std::vector>* shared_array_buffers_ = + nullptr; + PortAdapter* ports_ = nullptr; + const LocalVector* transfer_ports_ = nullptr; +}; + +// Reads the host objects written by SerializerDelegate, which were imported +// before deserializing, since JS can't run while V8 deserializes. +class DeserializerDelegate final : public ValueDeserializer::Delegate { + public: + DeserializerDelegate(Local context, + Local objects, + const std::vector>* + shared_array_buffers) + : context_(context), + objects_(objects), + shared_array_buffers_(shared_array_buffers) {} + + MaybeLocal GetSharedArrayBufferFromId( + Isolate* isolate, uint32_t id) override { + if (shared_array_buffers_ == nullptr || + id >= shared_array_buffers_->size()) { + isolate->ThrowException(Exception::Error( + FIXED_ONE_BYTE_STRING(isolate, "Invalid SharedArrayBuffer"))); + return {}; + } + return v8::SharedArrayBuffer::New(isolate, (*shared_array_buffers_)[id]); + } + + void set_deserializer(ValueDeserializer* deserializer) { + deserializer_ = deserializer; + } + + MaybeLocal ReadHostObject(Isolate* isolate) override { + uint32_t index; + Local object; + if (!deserializer_->ReadUint32(&index)) return {}; + if (index == kNormalObject) { + if (!deserializer_->ReadValue(context_).ToLocal(&object)) return {}; + CHECK(object->IsObject()); + return object.As(); + } + if (objects_.IsEmpty() || index >= objects_->Length() || + !objects_->Get(context_, index).ToLocal(&object) || + !object->IsObject()) { + isolate->ThrowException(Exception::Error( + FIXED_ONE_BYTE_STRING(isolate, "Invalid RPC host object"))); + return {}; + } + return object.As(); + } + + private: + Local context_; + Local objects_; + const std::vector>* shared_array_buffers_; + ValueDeserializer* deserializer_ = nullptr; +}; + +// The transfer list of postMessage(value, transfer), where `transfer` is an +// array, or an object with a `transfer` property. +MaybeLocal GetTransferList(Local context, Local arg) { + if (!arg->IsObject() || arg->IsArray()) return arg; + return arg.As()->Get( + context, FIXED_ONE_BYTE_STRING(Isolate::GetCurrent(), "transfer")); +} + +// Serializes `value` alone, adding the SharedArrayBuffers in it to +// `shared_array_buffers`, and transferring the ArrayBuffers in `transfer` and +// the ports in `transfer_ports`. +std::optional SerializeBytes( + Isolate* isolate, + Local context, + Local value, + bool is_host, + Local rpc_hooks, + const LocalVector& transfer, + PortAdapter* ports, + const LocalVector& transfer_ports, + std::vector>* shared_array_buffers, + Local* descriptors) { + SerializerDelegate delegate(isolate, context, is_host, rpc_hooks); + // Before V8 asks the delegate whether it has custom host objects. + if (ports != nullptr) delegate.set_ports(ports, &transfer_ports); + ValueSerializer serializer(isolate, &delegate); + delegate.set_serializer(&serializer); + delegate.set_shared_array_buffers(shared_array_buffers); + serializer.WriteHeader(); + for (size_t i = 0; i < transfer.size(); i++) { + serializer.TransferArrayBuffer(static_cast(i), transfer[i]); + } + if (serializer.WriteValue(context, value).IsNothing()) return std::nullopt; + if (descriptors != nullptr) *descriptors = delegate.descriptors(); + std::pair buffer = serializer.Release(); + Bytes bytes(buffer.first, buffer.first + buffer.second); + free(buffer.first); + return bytes; +} + +struct TransferOptions { + // An array of what to transfer, or undefined. + Local list; + // If MessagePorts can be transferred, how they are represented. + PortAdapter* ports = nullptr; + // The port that sends the value, which can't be transferred. + Local source_port; + // The allocator of a sender with a memory limit. Transferred ArrayBuffers + // are copied, so that they no longer count towards the limit. + std::shared_ptr limited_allocator; +}; + +// Throws a JS exception and returns std::nullopt if `value` is not cloneable, +// or the transfer list is not an array of distinct, detachable ArrayBuffers +// and ports. With `rpc_hooks`, the data starts with the length of the +// serialized descriptors of its host objects, and those descriptors. +std::optional Serialize(Isolate* isolate, + Local context, + Local value, + bool is_host, + Local rpc_hooks = {}, + const TransferOptions& options = {}) { + SerializedValue result; + LocalVector transfer(isolate); + LocalVector transfer_ports(isolate); + const Local transfer_list = options.list; + const std::shared_ptr& limited_allocator = + options.limited_allocator; + if (!transfer_list.IsEmpty() && !transfer_list->IsUndefined()) { + SerializerDelegate delegate(isolate, context, is_host); + auto fail = [&](const char* message) { + delegate.ThrowDataCloneError(OneByteString(isolate, message)); + return std::nullopt; + }; + if (!transfer_list->IsArray()) { + return fail("The transfer list must be an array"); + } + Local array = transfer_list.As(); + for (uint32_t i = 0; i < array->Length(); i++) { + Local item; + if (!array->Get(context, i).ToLocal(&item)) return std::nullopt; + bool detached = false; + if (item->IsObject() && options.ports != nullptr && + options.ports->IsPort(item.As(), &detached)) { + Local port = item.As(); + if (port == options.source_port) { + return fail("Transfer list contains source port"); + } + if (detached) { + return fail("MessagePort in transfer list is already detached"); + } + if (std::find(transfer_ports.begin(), transfer_ports.end(), port) != + transfer_ports.end()) { + return fail("A MessagePort is in the transfer list more than once"); + } + transfer_ports.push_back(port); + continue; + } + if (!item->IsArrayBuffer()) { + return fail( + options.ports != nullptr + ? "Only ArrayBuffers and MessagePorts can be transferred" + : "Only ArrayBuffers can be transferred"); + } + Local buffer = item.As(); + if (!buffer->IsDetachable() || buffer->WasDetached()) { + return fail("An ArrayBuffer is detached or can't be detached"); + } + if (std::find(transfer.begin(), transfer.end(), buffer) != + transfer.end()) { + return fail("An ArrayBuffer is in the transfer list more than once"); + } + transfer.push_back(buffer); + } + } + + Local descriptors; + std::optional bytes = + SerializeBytes(isolate, + context, + value, + is_host, + rpc_hooks, + transfer, + options.ports, + transfer_ports, + &result.shared_array_buffers, + rpc_hooks.IsEmpty() ? nullptr : &descriptors); + if (!bytes) return std::nullopt; + if (!rpc_hooks.IsEmpty()) { + Bytes descriptor_bytes; + if (!descriptors.IsEmpty()) { + std::optional serialized = + SerializeBytes(isolate, + context, + descriptors, + is_host, + {}, + LocalVector(isolate), + nullptr, + LocalVector(isolate), + &result.shared_array_buffers, + nullptr); + if (!serialized) return std::nullopt; + descriptor_bytes = std::move(*serialized); + } + const uint32_t length = descriptor_bytes.size(); + const uint8_t* length_bytes = reinterpret_cast(&length); + result.data.insert( + result.data.end(), length_bytes, length_bytes + sizeof(length)); + result.data.insert( + result.data.end(), descriptor_bytes.begin(), descriptor_bytes.end()); + } + result.data.insert(result.data.end(), bytes->begin(), bytes->end()); + + for (Local buffer : transfer) { + std::shared_ptr store = buffer->GetBackingStore(); + if (limited_allocator && !store->IsResizableByUserJavaScript()) { + store = LimitedAllocator::CopyUntracked(limited_allocator, *store); + if (!store) { + isolate->ThrowException(Exception::RangeError( + FIXED_ONE_BYTE_STRING(isolate, "Array buffer allocation failed"))); + return std::nullopt; + } + } + result.array_buffers.push_back(std::move(store)); + } + // Nothing can fail anymore. + for (Local buffer : transfer) { + CHECK(buffer->Detach(Local()).FromJust()); + } + if (!transfer_ports.empty()) { + result.ports = std::make_shared(); + for (Local port : transfer_ports) { + result.ports->push_back(options.ports->TakePort(port)); + } + } + return result; +} + +MaybeLocal DeserializeBytes( + Isolate* isolate, + Local context, + const uint8_t* data, + size_t size, + const LocalVector& transferred, + const std::vector>& shared_array_buffers, + Local host_objects = {}) { + DeserializerDelegate delegate(context, host_objects, &shared_array_buffers); + ValueDeserializer deserializer(isolate, data, size, &delegate); + delegate.set_deserializer(&deserializer); + if (deserializer.ReadHeader(context).IsNothing()) return {}; + for (size_t i = 0; i < transferred.size(); i++) { + deserializer.TransferArrayBuffer(static_cast(i), transferred[i]); + } + return deserializer.ReadValue(context); +} + +// Deserializes what Serialize() returned, once. With `rpc_hooks`, imports the +// host objects with the hooks' importHostObjects() first. With +// `copy_array_buffers`, for receivers with a memory limit, transferred +// ArrayBuffers are copied into memory of the isolate's own. +MaybeLocal Deserialize(Isolate* isolate, + Local context, + const SerializedValue& value, + Local rpc_hooks = {}, + bool copy_array_buffers = false, + PortAdapter* ports = nullptr, + Local* port_list = nullptr) { + Local port_objects; + if (value.ports && !value.ports->empty()) { + CHECK(rpc_hooks.IsEmpty()); + CHECK_NOT_NULL(ports); + PortDataList data = std::move(*value.ports); + LocalVector objects(isolate); + for (std::unique_ptr& port_data : data) { + Local object; + if (!ports->CreatePort(std::move(port_data)).ToLocal(&object)) return {}; + objects.push_back(object); + } + port_objects = v8::Array::New(isolate, objects.data(), objects.size()); + } + if (port_list != nullptr) { + *port_list = + port_objects.IsEmpty() ? v8::Array::New(isolate, 0) : port_objects; + } + + LocalVector transferred(isolate); + for (const std::shared_ptr& store : value.array_buffers) { + if (!copy_array_buffers || store->IsResizableByUserJavaScript()) { + transferred.push_back(v8::ArrayBuffer::New(isolate, store)); + continue; + } + std::unique_ptr copy = v8::ArrayBuffer::NewBackingStore( + isolate, + store->ByteLength(), + v8::BackingStoreInitializationMode::kUninitialized, + v8::BackingStoreOnFailureMode::kReturnNull); + if (!copy) { + isolate->ThrowException(Exception::RangeError( + FIXED_ONE_BYTE_STRING(isolate, "Array buffer allocation failed"))); + return {}; + } + if (store->ByteLength() > 0) { + memcpy(copy->Data(), store->Data(), store->ByteLength()); + } + transferred.push_back(v8::ArrayBuffer::New(isolate, std::move(copy))); + } + + const Bytes& bytes = value.data; + if (rpc_hooks.IsEmpty()) { + return DeserializeBytes(isolate, + context, + bytes.data(), + bytes.size(), + transferred, + value.shared_array_buffers, + port_objects); + } + uint32_t length; + CHECK_GE(bytes.size(), sizeof(length)); + memcpy(&length, bytes.data(), sizeof(length)); + CHECK_LE(length, bytes.size() - sizeof(length)); + const uint8_t* descriptors = bytes.data() + sizeof(length); + Local host_objects; + if (length > 0) { + Local descriptor_array; + Local objects; + if (!DeserializeBytes(isolate, + context, + descriptors, + length, + LocalVector(isolate), + value.shared_array_buffers) + .ToLocal(&descriptor_array) || + !CallRpcHook( + isolate, context, rpc_hooks, "importHostObjects", descriptor_array) + .ToLocal(&objects)) { + return {}; + } + CHECK(objects->IsArray()); + host_objects = objects.As(); + } + return DeserializeBytes(isolate, + context, + descriptors + length, + bytes.size() - sizeof(length) - length, + transferred, + value.shared_array_buffers, + host_objects); +} + +// A Node.js message with the contents of `value`, for MessagePorts. +std::shared_ptr ToMessage(SerializedValue&& value) { + MallocedBuffer payload(value.data.size()); + memcpy(payload.data, value.data.data(), value.data.size()); + auto message = std::make_shared(std::move(payload)); + for (std::shared_ptr& store : value.array_buffers) { + message->AddArrayBuffer(std::move(store)); + } + for (std::shared_ptr& store : value.shared_array_buffers) { + message->AddSharedArrayBuffer(std::move(store)); + } + if (value.ports) { + for (std::unique_ptr& port : *value.ports) { + message->AddTransferable(std::move(port)); + } + } + return message; +} + +// The contents of a Node.js message that isn't the close message. Returns +// std::nullopt if it has contents that green threads can't receive: objects +// other than MessagePorts that were transferred or cloned with it, such as +// FileHandles, or WebAssembly modules. +std::optional FromMessage(worker::Message* message) { + if (message->has_wasm_modules_or_shared_values()) return std::nullopt; + SerializedValue value; + for (std::unique_ptr& data : + message->mutable_transferables()) { + if (data->AsMessagePortData() == nullptr) return std::nullopt; + } + const MallocedBuffer& payload = message->payload(); + value.data.assign(payload.data, payload.data + payload.size); + value.array_buffers = std::move(message->array_buffers()); + value.shared_array_buffers = message->shared_array_buffers(); + if (!message->mutable_transferables().empty()) { + value.ports = std::make_shared(); + for (std::unique_ptr& data : + message->mutable_transferables()) { + value.ports->emplace_back(data.release()->AsMessagePortData()); + } + message->mutable_transferables().clear(); + } + return value; +} + +struct HostEvent { + EventType type; + int32_t id = 0; + bool ok = true; + SerializedValue payload; +}; + +// Carries events from a green thread to its host. The host closes it before +// closing its uv_async_t, after which events are dropped. +class HostChannel { + public: + explicit HostChannel(uv_async_t* async) : async_(async) {} + + void Push(HostEvent&& event) { + Mutex::ScopedLock lock(mutex_); + if (async_ == nullptr) return; + queue_.push_back(std::move(event)); + CHECK_EQ(uv_async_send(async_), 0); + } + + std::deque Take() { + std::deque events; + Mutex::ScopedLock lock(mutex_); + events.swap(queue_); + return events; + } + + void Close() { + Mutex::ScopedLock lock(mutex_); + async_ = nullptr; + queue_.clear(); + } + + private: + Mutex mutex_; + uv_async_t* async_; + std::deque queue_; +}; + +// NOLINTNEXTLINE(runtime/thread_local) +thread_local GreenIsolate* current_green_isolate = nullptr; + +// All green isolates that have not been disposed yet, so that they can be +// stopped before the process exits. +// Leaked, since pool threads may still use it while exit() runs static +// destructors. +Mutex& live_green_isolates_mutex = *new Mutex(); +std::unordered_map>& + live_green_isolates = + *new std::unordered_map>(); +// The sources and code cache of built-in modules, for pool threads. Set by +// the first Environment that loads the binding; leaked. +Mutex shared_builtin_loader_mutex; +builtins::BuiltinLoader* shared_builtin_loader = nullptr; + +// Green isolates that didn't stop when asked to, and were abandoned. They are +// never freed. Guarded by live_green_isolates_mutex. +std::vector>* abandoned_green_isolates = + new std::vector>(); + +// The foreground task runner V8 uses for a green isolate. Tasks are run like +// other work for the isolate, with the isolate locked. +class GreenTaskRunner final : public TaskRunner { + public: + explicit GreenTaskRunner(std::weak_ptr owner) + : owner_(std::move(owner)) {} + + bool IdleTasksEnabled() override { return false; } + bool NonNestableTasksEnabled() const override { return true; } + bool NonNestableDelayedTasksEnabled() const override { return true; } + + private: + void PostTaskImpl(std::unique_ptr task, + const SourceLocation& location) override { + Post(std::move(task), 0); + } + void PostNonNestableTaskImpl(std::unique_ptr task, + const SourceLocation& location) override { + Post(std::move(task), 0); + } + void PostDelayedTaskImpl(std::unique_ptr task, + double delay_in_seconds, + const SourceLocation& location) override { + Post(std::move(task), delay_in_seconds); + } + void PostNonNestableDelayedTaskImpl(std::unique_ptr task, + double delay_in_seconds, + const SourceLocation& location) override { + Post(std::move(task), delay_in_seconds); + } + void PostIdleTaskImpl(std::unique_ptr task, + const SourceLocation& location) override { + UNREACHABLE(); + } + + inline void Post(std::unique_ptr task, double delay_in_seconds); + + std::weak_ptr owner_; +}; + +// The state of one green thread: a V8 isolate, its invocation contexts and +// their timers. +// +// Work for the isolate is queued in FIFO order. Each item is either unbound, +// and may run on any pool thread, or bound to the pool thread that owns the +// I/O it continues. A pool thread that holds the isolate's (logical) lock +// runs a slice: it runs items from the front of the queue until its budget is +// used up or the next item is bound to another thread. Threads never block +// waiting for an isolate. Except where noted, methods run in a slice, i.e. +// with the logical lock held. +enum class IsolateGroupKind : int32_t { kDefault = 0, kNew = 1, kShared = 2 }; + +// Like Node.js' own isolates, green isolates get a group of their own if the +// build supports that. +IsolateGroupKind DefaultIsolateGroupKind() { + return v8::IsolateGroup::CanCreateNewGroups() ? IsolateGroupKind::kNew + : IsolateGroupKind::kDefault; +} + +// Identifies isolate groups in tests. 0 is V8's default group. +std::atomic next_isolate_group_id{1}; + +// A MessagePort of a green thread, which its runtime's MessagePort object +// refers to by its ID. Owned by its green isolate, and only used by it, but +// for OnIncomingMessage(). +class GreenPort final : public MessagePortData::Owner { + public: + GreenPort(GreenIsolate* green, uint32_t id) : green_(green), id_(id) {} + ~GreenPort() override { Detach(); } + + void Attach(std::unique_ptr data) { + data_ = std::move(data); + data_->SetOwner(this); + } + + // Once this returns, OnIncomingMessage() is no longer called. + std::unique_ptr Detach() { + if (data_) data_->SetOwner(nullptr); + return std::move(data_); + } + + // Null once the port is closed or transferred. + MessagePortData* data() const { return data_.get(); } + + // May be called from any thread. + void OnIncomingMessage() override { Notify(); } + // Makes the green isolate take the port's messages, unless that is queued. + void Notify(); + // Before taking the port's messages. + void ClearNotified() { notified_ = false; } + + Global object; + // Whether it delivers messages other than the close message. + bool started = false; + + private: + // Outlives the port. Abandoned green isolates are never freed. + GreenIsolate* const green_; + const uint32_t id_; + std::unique_ptr data_; + std::atomic notified_ = false; +}; + +class GreenIsolate final : public IsolatePlatformDelegate, + public PortAdapter, + public std::enable_shared_from_this { + public: + using Work = std::function; + + struct Options { + std::string source; + std::string filename; + // Whether `source` is an ES module rather than a script. + bool is_module = false; + // What the runtime's initialize() gets, serialized, if anything: the + // `env` global, the module bundle and the module loader hooks. + SerializedValue init; + bool testing_globals = false; + bool testing_ignore_termination = false; + // Fixed when the isolate is created. + size_t max_old_generation_size = 0; + size_t max_young_generation_size = 0; + size_t max_memory = 0; + // In nanoseconds. + uint64_t max_cpu_time = 0; + uint64_t max_task_cpu_time = 0; + // The isolate group to create the isolate in: V8's default group, a new + // one, or `shared_group`. `group_id` identifies the group in tests. + IsolateGroupKind group = DefaultIsolateGroupKind(); + std::optional shared_group; + uint64_t group_id = 0; + + // Whether an isolate created with the default options (i.e. a warm one) + // can be used. + bool CanUseDefaultIsolate() const { + return max_old_generation_size == 0 && max_young_generation_size == 0 && + max_memory == 0 && group == DefaultIsolateGroupKind(); + } + }; + + explicit GreenIsolate(MultiIsolatePlatform* platform) : platform_(platform) {} + + // Creates the isolate and its context, using the isolate options from + // `options`. Must be called once, right after construction. May be called + // from any thread but a pool thread. + void Prepare(const Options& options) { + task_runner_ = std::make_shared(weak_from_this()); + { + Mutex::ScopedLock lock(live_green_isolates_mutex); + live_green_isolates.emplace(this, weak_from_this()); + } + Submit([options](GreenIsolate* self) { + self->Initialize(options); + { + Mutex::ScopedLock lock(self->scheduler_mutex_); + self->low_priority_ = false; + } + if (self->on_initialized_) { + self->on_initialized_(self->state_ == State::kWarm && !self->fatal_); + } + }); + } + + // Makes Prepare()'s work background work, which only runs when pool threads + // have nothing else to do. Must be called before Prepare(). + void set_low_priority() { low_priority_ = true; } + + // Called on a pool thread once Prepare()'s work has run, with whether the + // isolate is ready to be activated. Must be set before Prepare(). + void set_on_initialized(std::function on_initialized) { + on_initialized_ = std::move(on_initialized); + } + + // Connects the green thread to its host and runs its script, possibly + // before Prepare()'s work has run. Must be called once. May be called from + // any thread but a pool thread. + void Activate(std::shared_ptr host, Options options) { + ignore_termination_ = options.testing_ignore_termination; + { + Mutex::ScopedLock lock(host_mutex_); + host_ = std::move(host); + } + { + Mutex::ScopedLock lock(scheduler_mutex_); + if (!stopped_) { + queue_.push_back( + {nullptr, [options = std::move(options)](GreenIsolate* self) { + self->Run(options); + }}); + MaybeScheduleLocked(); + return; + } + } + // It failed to initialize, or was terminated, before it had a host. + PushExit(1); + } + + // IsolatePlatformDelegate. May be called from any thread. + std::shared_ptr GetForegroundTaskRunner() override { + return task_runner_; + } + bool IdleTasksEnabled() override { return false; } + + // May be called from any thread. + void PostMessage(SerializedValue data) { + Submit([data = std::move(data)](GreenIsolate* self) { + self->RunTask(kNewInvocation, + [&](Isolate* isolate, Local context) { + self->DeliverMessage(isolate, context, data); + }); + }); + } + + // May be called from any thread. `context_id` is the invocation context + // of the call that this is the result of, or 0. + void PostRpc(SerializedValue data, uint32_t context_id) { + Submit([data = std::move(data), context_id](GreenIsolate* self) { + self->RunTask(context_id == 0 ? kNewInvocation : context_id, + [&](Isolate* isolate, Local context) { + self->DeliverRpc(isolate, context, data); + }); + if (context_id == 0) return; + auto it = self->contexts_.find(context_id); + if (it != self->contexts_.end() && it->second.pending > 0) { + it->second.pending--; + self->MaybeReleaseInvocationContext(context_id); + } + }); + } + + // May be called from any thread. + void Evaluate(int32_t id, std::string source, std::string filename) { + Submit([=](GreenIsolate* self) { + self->RunTask( + kNewInvocation, [&](Isolate* isolate, Local context) { + self->EvaluateSource(isolate, context, id, source, filename); + }); + }); + } + + // May be called from any thread. Delivers the messages of a port. + void DrainPortLater(uint32_t id) { + Submit([id](GreenIsolate* self) { self->DrainPort(id); }); + } + + // May be called from any thread. Interrupts running JS, if any. + void Terminate() { + terminate_requested_ = true; + { + Mutex::ScopedLock lock(isolate_mutex_); + if (isolate_ != nullptr) TerminateExecution(isolate_); + } + Mutex::ScopedLock lock(scheduler_mutex_); + MaybeScheduleLocked(); + } + + // May be called from any thread but a pool thread. Terminates the green + // thread and waits until its isolate is disposed. Unless a pool thread is + // running the isolate right now, which termination interrupts, it is shut + // down on the calling thread, so that this never waits for pool threads + // that are busy with other isolates. If the green thread doesn't stop, this + // waits until it is abandoned, or, if `wait_for_thread` is true, until the + // pool thread that runs it has been cut off, or the process aborted. + void TerminateAndWait(bool wait_for_thread) { + CHECK_NULL(current_pool_thread); + Terminate(); + bool shut_down_here = false; + { + Mutex::ScopedLock lock(scheduler_mutex_); + if (!stopped_ && !locked_) { + locked_ = true; + shut_down_here = true; + } + } + if (shut_down_here) { + Shutdown(); + Mutex::ScopedLock lock(scheduler_mutex_); + locked_ = false; + } + Mutex::ScopedLock lock(disposal_mutex_); + while (!disposed_ && !cut_off_ && (wait_for_thread || !abandoned_)) { + disposal_cv_.Wait(lock); + } + } + + // Whether ArrayBuffers that are transferred to or from this isolate must be + // copied, so that they count towards its memory limit only while it uses + // them. + bool has_memory_limit() const { + return allocator_ != nullptr && allocator_->limit() > 0; + } + + // Code that V8 generated for this isolate, or V8's built-in code. While a + // thread runs such code, it holds no locks. Read by signal handlers. + bool IsGeneratedCode(uintptr_t pc) const { + auto contains = [pc](const std::pair& range) { + return pc >= range.first && pc - range.first < range.second; + }; + return contains(code_range_) || contains(embedded_code_range_); + } + + // Runs `work`, unless the backup watchdog cuts it off, in which case this + // returns false: the signal handler then jumps back here, leaving behind + // the frames of the work, which never return. + static V8_NOINLINE bool RunGuarded(PoolThread* thread, + GreenIsolate* green, + Work* work) { + if (sigsetjmp(*thread->cut_off_jump(), 0) != 0) return false; + thread->set_running(green); + (*work)(green); + thread->set_running(nullptr); + return true; + } + + // May be called from any thread. + void PostV8Task(std::unique_ptr task, double delay_in_seconds) { + if (delay_in_seconds <= 0) return Submit(MakeV8TaskWork(std::move(task))); + + // A due delayed task may run on any thread; its timer just needs a loop. + // The timer's callback must not own the task, since the callback may + // outlive the isolate. Until the task is due, it's owned by + // delayed_v8_tasks_, and then by the queue, which are both emptied before + // the isolate is freed. + uint64_t key; + { + Mutex::ScopedLock lock(scheduler_mutex_); + if (stopped_) return; + key = ++next_delayed_v8_task_id_; + } + auto handle = std::make_shared( + GreenThreadPool::Get()->Next(), + static_cast(delay_in_seconds * 1000), + 0, + [weak = weak_from_this(), key]() { + std::shared_ptr self = weak.lock(); + if (!self) return; + Mutex::ScopedLock lock(self->scheduler_mutex_); + auto it = self->delayed_v8_tasks_.find(key); + if (it == self->delayed_v8_tasks_.end()) return; + it->second.first->Close(); + self->queue_.push_back( + {nullptr, MakeV8TaskWork(std::move(it->second.second))}); + self->delayed_v8_tasks_.erase(it); + self->MaybeScheduleLocked(); + }); + { + Mutex::ScopedLock lock(scheduler_mutex_); + if (stopped_) return; + delayed_v8_tasks_.emplace(key, std::make_pair(handle, std::move(task))); + } + handle->Start(); + } + + // May be called from any thread. + PoolThread* last_thread() const { return last_thread_.load(); } + + // Runs a slice on `thread`. Must only be called after MaybeScheduleLocked() + // has scheduled this isolate. + void RunSlice(PoolThread* thread) { + { + Mutex::ScopedLock lock(scheduler_mutex_); + CHECK(scheduled_); + scheduled_ = false; + // TerminateAndWait() took the lock to shut the isolate down. + if (locked_ || stopped_) return; + locked_ = true; + } + last_thread_ = thread; + + for (size_t n = 0; n < kSliceBudget && !ShouldShutdown(); n++) { + Work work; + { + Mutex::ScopedLock lock(scheduler_mutex_); + if (queue_.empty()) break; + PoolThread* bound = queue_.front().thread; + if (bound != nullptr && bound != thread) break; + work = std::move(queue_.front().work); + queue_.pop_front(); + } + work_epoch_++; + running_thread_ = thread; + const bool completed = RunGuarded(thread, this, &work); + running_thread_ = nullptr; + if (!completed) return OnCutOff(thread, std::move(work)); + work_epoch_++; + } + // Only after `work` is gone, since it may own V8 tasks that must not + // outlive the isolate. + if (ShouldShutdown()) Shutdown(); + + Mutex::ScopedLock lock(scheduler_mutex_); + locked_ = false; + MaybeScheduleLocked(); + } + + private: + // kWarm: initialized, but not activated yet. + enum class State { kPending, kWarm, kRunning, kStopped }; + + // Context IDs passed to RunTask(). + static constexpr uint32_t kNewInvocation = 0; + static constexpr uint32_t kDefaultInvocation = 1; + + // The context of an invocation: the initial script (the default context), + // a message, an evaluation, or everything that continues them. Its timers + // run on the pool thread that ran the invocation, where their handles live. + struct InvocationContext { + PoolThread* thread; + uint32_t pending = 0; + }; + + // The ES modules of the green thread. The runtime's module loader + // (internal/green_threads/runtime/modules) refers to them by their IDs. + struct ModuleRecord { + Global module; + std::string url; + // The value of a JSON module. + Global json; + // The modules that its requests resolve to, in the order of + // GetModuleRequests(), once the loader has linked it. + std::vector dependencies; + bool linked = false; + }; + + struct TimerRecord { + Global callback; + bool repeat; + uint32_t context_id; + std::shared_ptr handle; + // Set while a fire of this timer is queued, so that an interval whose + // isolate is busy elsewhere fires once when it's due, not once per + // missed interval. + std::shared_ptr> fire_queued; + }; + + struct QueuedWork { + PoolThread* thread; + Work work; + }; + + // Delayed V8 tasks that are not due yet, with their timers. The tasks are + // owned separately so that they can be destroyed before the isolate. + using DelayedV8Tasks = std::unordered_map< + uint64_t, + std::pair, std::unique_ptr>>; + + class CurrentScope { + public: + explicit CurrentScope(GreenIsolate* green) + : previous_(current_green_isolate) { + current_green_isolate = green; + } + ~CurrentScope() { current_green_isolate = previous_; } + + private: + GreenIsolate* previous_; + }; + + // May be called from any thread. If `thread` is set, `work` only runs on it. + void Submit(Work work, PoolThread* thread = nullptr) { + Mutex::ScopedLock lock(scheduler_mutex_); + if (stopped_) return; + queue_.push_back({thread, std::move(work)}); + MaybeScheduleLocked(); + } + + void MaybeScheduleLocked() { + if (locked_ || scheduled_ || stopped_) return; + if (queue_.empty() && !terminate_requested_) return; + scheduled_ = true; + // Termination doesn't wait for the thread the next item is bound to. + PoolThread* bound = terminate_requested_ ? nullptr : queue_.front().thread; + if (bound != nullptr) { + bound->Post( + [self = shared_from_this(), bound]() { self->RunSlice(bound); }); + } else if (low_priority_) { + GreenThreadPool::Get()->PostBackground([self = shared_from_this()]() { + self->RunSlice(current_pool_thread); + }); + } else { + GreenThreadPool::Get()->ScheduleReady(shared_from_this(), + last_thread_.load()); + } + } + + // The returned work owns `task`. Work is only ever owned by the queue or by + // the slice running it, both of which are gone before the isolate is freed. + static Work MakeV8TaskWork(std::unique_ptr task) { + auto holder = std::make_shared>(std::move(task)); + return [holder](GreenIsolate* self) { + std::unique_ptr task = std::move(*holder); + self->RunTask(kDefaultInvocation, + [&](Isolate*, Local) { task->Run(); }); + }; + } + + // Whether JS may run (again) in the current task. Once a termination has + // unwound the stack, IsExecutionTerminating() is false again, and a + // TerminateExecution() request made while the isolate was unlocked is + // discarded when the lock is acquired, so the flags are checked as well. + bool CanRunJS(Isolate* isolate) const { + return !terminate_requested_ && !fatal_ && !out_of_memory_ && + !cpu_limit_exceeded_ && !isolate->IsExecutionTerminating(); + } + + bool ShouldShutdown() const { + return state_ != State::kStopped && + (fatal_ || terminate_requested_ || close_requested_); + } + + void Initialize(const Options& options) { + CHECK_EQ(state_, State::kPending); + PoolThread* thread = current_pool_thread; + + Isolate::CreateParams params; + v8::IsolateGroup group = v8::IsolateGroup::GetDefault(); + group_id_ = 0; + if (options.group == IsolateGroupKind::kNew) { + group = v8::IsolateGroup::Create(); + group_id_ = next_isolate_group_id++; + } else if (options.group == IsolateGroupKind::kShared) { + group = *options.shared_group; + group_id_ = options.group_id; + } + // Isolates in V8's default group must use the snapshot that all other + // isolates in it use, Node.js' own. Groups that only contain green + // isolates can use V8's built-in snapshot, which is much smaller, since + // green threads don't need any of Node.js' per-isolate state. + uses_node_snapshot_ = group == v8::IsolateGroup::GetDefault(); + allocator_ = std::make_shared(options.max_memory, group); + params.array_buffer_allocator_shared = allocator_; + size_t max_old_generation_size = options.max_old_generation_size; + if (max_old_generation_size == 0) { + max_old_generation_size = options.max_memory; + } + if (max_old_generation_size > 0) { + params.constraints.set_max_old_generation_size_in_bytes( + max_old_generation_size); + } + if (options.max_young_generation_size > 0) { + params.constraints.set_max_young_generation_size_in_bytes( + options.max_young_generation_size); + } + + // Node.js' default callbacks expect a Node.js context, so skip or replace + // them. + IsolateSettings settings; + settings.flags = SHOULD_NOT_SET_PREPARE_STACK_TRACE_CALLBACK | + SHOULD_NOT_SET_WASM_STREAMING_CALLBACK; + settings.promise_reject_callback = OnPromiseReject; + settings.allow_wasm_code_generation_callback = + [](Local, Local) { return true; }; + + Isolate* isolate = NewIsolate( + ¶ms, this, platform_, settings, group, uses_node_snapshot_); + if (isolate == nullptr) { + fatal_ = true; + return; + } + // It would block the pool thread, and with it other green threads. + isolate->SetAllowAtomicsWait(false); + isolate->SetHostImportModuleDynamicallyCallback(ImportModuleDynamically); + isolate->SetHostInitializeImportMetaObjectCallback(InitializeImportMeta); + { + Mutex::ScopedLock lock(isolate_mutex_); + isolate_ = isolate; + } + { + void* start; + size_t length; + isolate->GetCodeRange(&start, &length); + code_range_ = {reinterpret_cast(start), length}; + const void* embedded_start; + isolate->GetEmbeddedCodeRange(&embedded_start, &length); + embedded_code_range_ = {reinterpret_cast(embedded_start), + length}; + } + + const uint32_t default_context_id = CreateInvocationContext(thread); + CHECK_EQ(default_context_id, kDefaultInvocation); + { + Locker locker(isolate); + Isolate::Scope isolate_scope(isolate); + isolate->SetStackLimit(thread->stack_limit()); + HandleScope handle_scope(isolate); + isolate->AddNearHeapLimitCallback(NearHeapLimit, this); + if (options.max_memory > 0) { + isolate->AddGCEpilogueCallback(AfterGarbageCollection, this); + } + isolate->AddMessageListenerWithErrorLevel( + OnMessage, Isolate::MessageErrorLevel::kMessageError); + // The default context of the startup snapshot contains only what V8 + // creates. + Local context = Context::New(isolate); + if (context.IsEmpty()) { + fatal_ = true; + } else { + Context::Scope context_scope(context); + context_.Reset(isolate, context); + if (!InstallGlobals(isolate, context) || + !RunBootstrap(isolate, context)) { + fatal_ = true; + } + // Until the first GC, ArrayBuffer allocations are checked against + // the heap as it is now. + v8::HeapStatistics stats; + isolate->GetHeapStatistics(&stats); + allocator_->set_heap_used(stats.used_heap_size()); + } + } + if (fatal_) return; + state_ = State::kWarm; + } + + // Runs the green thread's script. Always queued after Initialize(). + void Run(const Options& options) { + if (state_ != State::kWarm) return; + state_ = State::kRunning; + max_cpu_time_ = options.max_cpu_time; + max_task_cpu_time_ = options.max_task_cpu_time; + RunTask(kDefaultInvocation, [&](Isolate* isolate, Local context) { + if (options.testing_globals && (!DefineFunction(isolate, + context, + context->Global(), + "__greenThreadInfo", + InfoCallback) || + !DefineFunction(isolate, + context, + context->Global(), + "__greenThreadBlock", + BlockCallback))) { + return; + } + if (!options.init.empty()) { + Local init; + if (!Deserialize(isolate, context, options.init, runtime_.Get(isolate)) + .ToLocal(&init) || + CallRuntime(isolate, context, "initialize", 1, &init).IsEmpty()) { + return; + } + } + if (options.is_module) { + Local argv[2]; + if (!ToV8Value(context, options.source, isolate).ToLocal(&argv[0]) || + !ToV8Value(context, options.filename, isolate).ToLocal(&argv[1])) { + return; + } + USE(CallRuntime(isolate, context, "runMain", arraysize(argv), argv)); + return; + } + Local