Skip to content

Commit ec4b4f5

Browse files
committed
vm: support shared microtask queues
Add `vm.MicrotaskQueue` and `options.microtaskQueue` to allow multiple contexts to share an explicit V8 microtask queue and let users control synchronous checkpoint draining. Document how options.microtaskQueue and options.microtaskMode interact across createContext, runInNewContext, and script.runInNewContext. Update receiver type error expectation for runMicrotasks and add test coverage for runInNewContext with microtask queues. Fixes: #65555 Signed-off-by: Alessio Attilio <alessio.attilio@protonmail.com> Assisted-by: Antigravity
1 parent abb365a commit ec4b4f5

6 files changed

Lines changed: 422 additions & 11 deletions

File tree

‎doc/api/vm.md‎

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,19 @@ changes:
316316
* `microtaskMode` {string} If set to `afterEvaluate`, microtasks (tasks
317317
scheduled through `Promise`s and `async function`s) will be run immediately
318318
after the script has run. They are included in the `timeout` and
319-
`breakOnSigint` scopes in that case.
319+
`breakOnSigint` scopes in that case. If `microtaskQueue` (or
320+
`contextMicrotaskQueue`) is also specified, evaluating the script will
321+
drain that shared microtask queue (including any microtasks queued from
322+
other contexts sharing the queue). If `microtaskQueue` is not specified, a
323+
private microtask queue is created exclusively for this context.
324+
* `microtaskQueue` {vm.MicrotaskQueue} A microtask queue created with
325+
[`new vm.MicrotaskQueue()`][] or [`vm.createMicrotaskQueue()`][]. If
326+
specified, microtasks scheduled inside the new context will be placed on
327+
this queue. By default, microtasks placed on this queue are not
328+
automatically drained when script evaluation finishes; they remain queued
329+
until explicitly drained using [`microtaskQueue.runMicrotasks()`][], unless
330+
`microtaskMode: 'afterEvaluate'` is also specified. An alias for this
331+
option is `contextMicrotaskQueue`.
320332
* Returns: {any} the result of the very last statement executed in the script.
321333

322334
This method is a shortcut to `script.runInContext(vm.createContext(options), options)`.
@@ -1318,6 +1330,37 @@ added:
13181330

13191331
A `ModuleRequest` represents the request to import a module with given import attributes and phase.
13201332

1333+
## Class: `vm.MicrotaskQueue`
1334+
1335+
<!-- YAML
1336+
added: REPLACEME
1337+
-->
1338+
1339+
Represents an explicit microtask queue that can be shared across multiple
1340+
`vm.Context` instances and synchronously drained by the embedder.
1341+
1342+
By default, passing a `vm.MicrotaskQueue` to [`vm.createContext()`][] attaches the
1343+
context to that queue without automatically draining it after script evaluation;
1344+
microtasks remain queued until explicitly drained using
1345+
[`microtaskQueue.runMicrotasks()`][]. If automatic draining upon script completion
1346+
is also desired, pass `microtaskMode: 'afterEvaluate'` alongside `microtaskQueue`.
1347+
1348+
### `new vm.MicrotaskQueue()`
1349+
1350+
<!-- YAML
1351+
added: REPLACEME
1352+
-->
1353+
1354+
Creates a new `vm.MicrotaskQueue` instance.
1355+
1356+
### `microtaskQueue.runMicrotasks()`
1357+
1358+
<!-- YAML
1359+
added: REPLACEME
1360+
-->
1361+
1362+
Synchronously runs all microtasks currently queued in this microtask queue.
1363+
13211364
## `vm.compileFunction(code[, params[, options]])`
13221365

13231366
<!-- YAML
@@ -1474,6 +1517,19 @@ changes:
14741517
scheduled through `Promise`s and `async function`s) will be run immediately
14751518
after a script has run through [`script.runInContext()`][].
14761519
They are included in the `timeout` and `breakOnSigint` scopes in that case.
1520+
If `microtaskQueue` is also specified, evaluating a script in this context
1521+
will drain that shared microtask queue (including any pending microtasks
1522+
scheduled by other contexts sharing the queue). If `microtaskQueue` is not
1523+
specified, a private microtask queue is created exclusively for this
1524+
context.
1525+
* `microtaskQueue` {vm.MicrotaskQueue} A microtask queue created with
1526+
[`new vm.MicrotaskQueue()`][] or [`vm.createMicrotaskQueue()`][]. If
1527+
specified, microtasks scheduled inside this context will be placed on this
1528+
queue, allowing multiple contexts to share the same microtask queue.
1529+
By default, microtasks placed on this queue are not automatically drained
1530+
when script evaluation finishes; they remain queued until explicitly drained
1531+
using [`microtaskQueue.runMicrotasks()`][], unless `microtaskMode: 'afterEvaluate'`
1532+
is also specified.
14771533
* `importModuleDynamically`
14781534
{Function|vm.constants.USE\_MAIN\_CONTEXT\_DEFAULT\_LOADER}
14791535
Used to specify the how the modules should be loaded when `import()` is
@@ -1555,6 +1611,28 @@ Returns `true` if the given `object` object has been [contextified][] using
15551611
[`vm.createContext()`][], or if it's the global object of a context created
15561612
using [`vm.constants.DONT_CONTEXTIFY`][].
15571613

1614+
## `vm.createMicrotaskQueue()`
1615+
1616+
<!-- YAML
1617+
added: REPLACEME
1618+
-->
1619+
1620+
* Returns: {vm.MicrotaskQueue}
1621+
1622+
Creates a new [`vm.MicrotaskQueue`][] instance. Shortcut to
1623+
`new vm.MicrotaskQueue()`.
1624+
1625+
## `vm.isMicrotaskQueue(object)`
1626+
1627+
<!-- YAML
1628+
added: REPLACEME
1629+
-->
1630+
1631+
* `object` {any}
1632+
* Returns: {boolean}
1633+
1634+
Returns `true` if the given `object` is an instance of [`vm.MicrotaskQueue`][].
1635+
15581636
## `vm.measureMemory([options])`
15591637

15601638
<!-- YAML
@@ -1822,7 +1900,18 @@ changes:
18221900
* `microtaskMode` {string} If set to `afterEvaluate`, microtasks (tasks
18231901
scheduled through `Promise`s and `async function`s) will be run immediately
18241902
after the script has run. They are included in the `timeout` and
1825-
`breakOnSigint` scopes in that case.
1903+
`breakOnSigint` scopes in that case. If `microtaskQueue` is also specified,
1904+
evaluating the script will drain that shared microtask queue (including any
1905+
pending microtasks scheduled by other contexts sharing the queue). If
1906+
`microtaskQueue` is not specified, a private microtask queue is created
1907+
exclusively for this context.
1908+
* `microtaskQueue` {vm.MicrotaskQueue} A microtask queue created with
1909+
[`new vm.MicrotaskQueue()`][] or [`vm.createMicrotaskQueue()`][]. If
1910+
specified, microtasks scheduled inside the new context will be placed on
1911+
this queue. By default, microtasks placed on this queue are not
1912+
automatically drained when script evaluation finishes; they must be
1913+
explicitly drained using [`microtaskQueue.runMicrotasks()`][], unless
1914+
`microtaskMode: 'afterEvaluate'` is also specified.
18261915
* Returns: {any} the result of the very last statement executed in the script.
18271916

18281917
This method is a shortcut to
@@ -2577,16 +2666,20 @@ const { Script, SyntheticModule } = require('node:vm');
25772666
[`Error`]: errors.md#class-error
25782667
[`URL`]: url.md#class-url
25792668
[`eval()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval
2669+
[`microtaskQueue.runMicrotasks()`]: #microtaskqueuerunmicrotasks
2670+
[`new vm.MicrotaskQueue()`]: #new-vmmicrotaskqueue
25802671
[`optionsExpression`]: https://tc39.es/proposal-import-attributes/#sec-evaluate-import-call
25812672
[`script.runInContext()`]: #scriptrunincontextcontextifiedobject-options
25822673
[`script.runInThisContext()`]: #scriptruninthiscontextoptions
25832674
[`sourceTextModule.instantiate()`]: #sourcetextmoduleinstantiate
25842675
[`sourceTextModule.linkRequests(modules)`]: #sourcetextmodulelinkrequestsmodules
25852676
[`sourceTextModule.moduleRequests`]: #sourcetextmodulemodulerequests
25862677
[`url.origin`]: url.md#urlorigin
2678+
[`vm.MicrotaskQueue`]: #class-vmmicrotaskqueue
25872679
[`vm.compileFunction()`]: #vmcompilefunctioncode-params-options
25882680
[`vm.constants.DONT_CONTEXTIFY`]: #vmconstantsdont_contextify
25892681
[`vm.createContext()`]: #vmcreatecontextcontextobject-options
2682+
[`vm.createMicrotaskQueue()`]: #vmcreatemicrotaskqueue
25902683
[`vm.runInContext()`]: #vmrunincontextcode-contextifiedobject-options
25912684
[`vm.runInThisContext()`]: #vmruninthiscontextcode-options
25922685
[contextified]: #what-does-it-mean-to-contextify-an-object

‎lib/vm.js‎

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ const {
3232

3333
const {
3434
ContextifyScript,
35+
MicrotaskQueue: ContextifyMicrotaskQueue,
36+
isMicrotaskQueue: _isMicrotaskQueue,
3537
makeContext,
3638
constants,
3739
measureMemory: _measureMemory,
@@ -82,6 +84,16 @@ function isContext(object) {
8284
return _isContext(object);
8385
}
8486

87+
class MicrotaskQueue extends ContextifyMicrotaskQueue {}
88+
89+
function createMicrotaskQueue() {
90+
return new MicrotaskQueue();
91+
}
92+
93+
function isMicrotaskQueue(object) {
94+
return typeof object === 'object' && object !== null && _isMicrotaskQueue(object);
95+
}
96+
8597
class Script extends ContextifyScript {
8698
constructor(code, options = kEmptyObject) {
8799
code = `${code}`;
@@ -202,6 +214,7 @@ function getContextOptions(options) {
202214
origin: options.contextOrigin,
203215
codeGeneration: undefined,
204216
microtaskMode: options.microtaskMode,
217+
microtaskQueue: options.microtaskQueue ?? options.contextMicrotaskQueue,
205218
};
206219
if (contextOptions.name !== undefined)
207220
validateString(contextOptions.name, 'options.contextName');
@@ -235,6 +248,7 @@ function createContext(contextObject = { __proto__: ObjectPrototype }, options =
235248
origin,
236249
codeGeneration,
237250
microtaskMode,
251+
microtaskQueue,
238252
importModuleDynamically,
239253
} = options;
240254

@@ -255,12 +269,27 @@ function createContext(contextObject = { __proto__: ObjectPrototype }, options =
255269
validateOneOf(microtaskMode,
256270
'options.microtaskMode',
257271
['afterEvaluate', undefined]);
258-
const microtaskQueue = (microtaskMode === 'afterEvaluate');
272+
if (microtaskQueue !== undefined && !isMicrotaskQueue(microtaskQueue)) {
273+
throw new ERR_INVALID_ARG_TYPE(
274+
'options.microtaskQueue',
275+
'MicrotaskQueue',
276+
microtaskQueue,
277+
);
278+
}
259279

260280
const hostDefinedOptionId =
261281
getHostDefinedOptionId(importModuleDynamically, name);
262282

263-
const result = makeContext(contextObject, name, origin, strings, wasm, microtaskQueue, hostDefinedOptionId);
283+
const result = makeContext(
284+
contextObject,
285+
name,
286+
origin,
287+
strings,
288+
wasm,
289+
microtaskQueue ?? (microtaskMode === 'afterEvaluate'),
290+
hostDefinedOptionId,
291+
microtaskMode === 'afterEvaluate',
292+
);
264293
// Register the context scope callback after the context was initialized.
265294
registerImportModuleDynamically(result, importModuleDynamically);
266295
return result;
@@ -413,6 +442,9 @@ module.exports = {
413442
compileFunction,
414443
measureMemory,
415444
constants: vmConstants,
445+
MicrotaskQueue,
446+
createMicrotaskQueue,
447+
isMicrotaskQueue,
416448
};
417449

418450
// The vm module is patched to include vm.Module, vm.SourceTextModule

‎src/env_properties.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,7 @@
460460
V(lock_info_template, v8::DictionaryTemplate) \
461461
V(lock_query_template, v8::DictionaryTemplate) \
462462
V(message_port_constructor_template, v8::FunctionTemplate) \
463+
V(microtask_queue_constructor_template, v8::FunctionTemplate) \
463464
V(module_wrap_constructor_template, v8::FunctionTemplate) \
464465
V(mx_record_template, v8::DictionaryTemplate) \
465466
V(naptr_record_template, v8::DictionaryTemplate) \

‎src/node_contextify.cc‎

Lines changed: 92 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -153,10 +153,10 @@ ContextifyContext* ContextifyContext::New(Environment* env,
153153

154154
const SnapshotData* snapshot_data = env->isolate_data()->snapshot_data();
155155

156-
MicrotaskQueue* queue =
157-
options->own_microtask_queue
158-
? options->own_microtask_queue.get()
159-
: env->isolate()->GetCurrentContext()->GetMicrotaskQueue();
156+
MicrotaskQueue* queue = options->own_microtask_queue.get();
157+
if (queue == nullptr) queue = options->shared_microtask_queue.get();
158+
if (queue == nullptr)
159+
queue = env->isolate()->GetCurrentContext()->GetMicrotaskQueue();
160160

161161
Local<Context> v8_context;
162162
if (!(CreateV8Context(env->isolate(), object_template, snapshot_data, queue)
@@ -178,7 +178,9 @@ ContextifyContext::ContextifyContext(Environment* env,
178178
ContextOptions* options)
179179
: microtask_queue_(options->own_microtask_queue
180180
? options->own_microtask_queue.release()
181-
: nullptr) {
181+
: nullptr),
182+
shared_microtask_queue_(std::move(options->shared_microtask_queue)),
183+
drain_after_evaluate_(options->drain_after_evaluate) {
182184
CppgcMixin::Wrap(this, env, wrapper);
183185

184186
context_.Reset(env->isolate(), v8_context);
@@ -414,7 +416,7 @@ void ContextifyContext::MakeContext(const FunctionCallbackInfo<Value>& args) {
414416
Environment* env = Environment::GetCurrent(args);
415417
ContextOptions options;
416418

417-
CHECK_EQ(args.Length(), 7);
419+
CHECK_GE(args.Length(), 7);
418420
Local<Object> sandbox;
419421
if (args[0]->IsObject()) {
420422
sandbox = args[0].As<Object>();
@@ -445,11 +447,20 @@ void ContextifyContext::MakeContext(const FunctionCallbackInfo<Value>& args) {
445447
if (args[5]->IsBoolean() && args[5]->BooleanValue(env->isolate())) {
446448
options.own_microtask_queue =
447449
MicrotaskQueue::New(env->isolate(), MicrotasksPolicy::kExplicit);
450+
options.drain_after_evaluate = true;
451+
} else if (args[5]->IsObject()) {
452+
ContextifyMicrotaskQueue* queue;
453+
ASSIGN_OR_RETURN_UNWRAP_CPPGC(&queue, args[5].As<Object>());
454+
options.shared_microtask_queue = queue->microtask_queue();
448455
}
449456

450457
CHECK(args[6]->IsSymbol());
451458
options.host_defined_options_id = args[6].As<Symbol>();
452459

460+
if (args.Length() > 7 && args[7]->IsBoolean()) {
461+
options.drain_after_evaluate = args[7]->BooleanValue(env->isolate());
462+
}
463+
453464
TryCatchScope try_catch(env);
454465
ContextifyContext* context_ptr =
455466
ContextifyContext::New(env, sandbox, &options);
@@ -2027,13 +2038,87 @@ static void MeasureMemory(const FunctionCallbackInfo<Value>& args) {
20272038
args.GetReturnValue().Set(promise);
20282039
}
20292040

2041+
void ContextifyMicrotaskQueue::CreatePerIsolateProperties(
2042+
IsolateData* isolate_data, Local<ObjectTemplate> target) {
2043+
Isolate* isolate = isolate_data->isolate();
2044+
Local<String> class_name = FIXED_ONE_BYTE_STRING(isolate, "MicrotaskQueue");
2045+
2046+
Local<FunctionTemplate> tmpl = NewFunctionTemplate(isolate, New);
2047+
tmpl->InstanceTemplate()->SetInternalFieldCount(
2048+
ContextifyMicrotaskQueue::kInternalFieldCount);
2049+
tmpl->SetClassName(class_name);
2050+
SetProtoMethod(isolate, tmpl, "runMicrotasks", RunMicrotasks);
2051+
2052+
target->Set(isolate, "MicrotaskQueue", tmpl);
2053+
SetMethod(isolate, target, "isMicrotaskQueue", IsMicrotaskQueue);
2054+
isolate_data->set_microtask_queue_constructor_template(tmpl);
2055+
}
2056+
2057+
void ContextifyMicrotaskQueue::RegisterExternalReferences(
2058+
ExternalReferenceRegistry* registry) {
2059+
registry->Register(New);
2060+
registry->Register(RunMicrotasks);
2061+
registry->Register(IsMicrotaskQueue);
2062+
}
2063+
2064+
ContextifyMicrotaskQueue* ContextifyMicrotaskQueue::New(Environment* env,
2065+
Local<Object> object) {
2066+
DCHECK_NOT_NULL(env->isolate()->GetCppHeap());
2067+
return cppgc::MakeGarbageCollected<ContextifyMicrotaskQueue>(
2068+
env->cppgc_allocation_handle(), env, object);
2069+
}
2070+
2071+
void ContextifyMicrotaskQueue::New(const FunctionCallbackInfo<Value>& args) {
2072+
Environment* env = Environment::GetCurrent(args);
2073+
CHECK(args.IsConstructCall());
2074+
New(env, args.This());
2075+
}
2076+
2077+
ContextifyMicrotaskQueue::ContextifyMicrotaskQueue(Environment* env,
2078+
Local<Object> object)
2079+
: microtask_queue_(
2080+
MicrotaskQueue::New(env->isolate(), MicrotasksPolicy::kExplicit)) {
2081+
CppgcMixin::Wrap(this, env, object);
2082+
}
2083+
2084+
bool ContextifyMicrotaskQueue::InstanceOf(Environment* env,
2085+
const Local<Value>& value) {
2086+
return !value.IsEmpty() && value->IsObject() &&
2087+
env->microtask_queue_constructor_template()->HasInstance(value);
2088+
}
2089+
2090+
void ContextifyMicrotaskQueue::RunMicrotasks(
2091+
const FunctionCallbackInfo<Value>& args) {
2092+
Environment* env = Environment::GetCurrent(args);
2093+
if (!ContextifyMicrotaskQueue::InstanceOf(env, args.This())) {
2094+
THROW_ERR_INVALID_THIS(env,
2095+
"MicrotaskQueue methods can only be called on "
2096+
"MicrotaskQueue instances.");
2097+
return;
2098+
}
2099+
ContextifyMicrotaskQueue* queue;
2100+
ASSIGN_OR_RETURN_UNWRAP_CPPGC(&queue, args.This());
2101+
queue->microtask_queue_->PerformCheckpoint(args.GetIsolate());
2102+
}
2103+
2104+
void ContextifyMicrotaskQueue::IsMicrotaskQueue(
2105+
const FunctionCallbackInfo<Value>& args) {
2106+
Environment* env = Environment::GetCurrent(args);
2107+
bool is_queue =
2108+
args.Length() > 0 && args[0]->IsObject() && InstanceOf(env, args[0]) &&
2109+
CppgcMixin::Unwrap<ContextifyMicrotaskQueue>(args[0].As<Object>()) !=
2110+
nullptr;
2111+
args.GetReturnValue().Set(is_queue);
2112+
}
2113+
20302114
void CreatePerIsolateProperties(IsolateData* isolate_data,
20312115
Local<ObjectTemplate> target) {
20322116
Isolate* isolate = isolate_data->isolate();
20332117

20342118
ContextifyContext::CreatePerIsolateProperties(isolate_data, target);
20352119
ContextifyScript::CreatePerIsolateProperties(isolate_data, target);
20362120
ContextifyFunction::CreatePerIsolateProperties(isolate_data, target);
2121+
ContextifyMicrotaskQueue::CreatePerIsolateProperties(isolate_data, target);
20372122

20382123
SetMethod(isolate, target, "runInterruptible", RunInterruptible);
20392124

@@ -2083,6 +2168,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
20832168
ContextifyContext::RegisterExternalReferences(registry);
20842169
ContextifyScript::RegisterExternalReferences(registry);
20852170
ContextifyFunction::RegisterExternalReferences(registry);
2171+
ContextifyMicrotaskQueue::RegisterExternalReferences(registry);
20862172

20872173
registry->Register(CompileFunctionForCJSLoader);
20882174
registry->Register(RunInterruptible);

0 commit comments

Comments
 (0)