diff --git a/deps/v8/include/v8-microtask-dispatch.h b/deps/v8/include/v8-microtask-dispatch.h new file mode 100644 index 000000000000..5557ea581a50 --- /dev/null +++ b/deps/v8/include/v8-microtask-dispatch.h @@ -0,0 +1,61 @@ +// Copyright 2026 Qualia Labs. Prototype, not upstream V8. +// +// Lets an embedder run a promise reaction job somewhere other than inline in +// the microtask loop, e.g. on a coroutine (node-fibers) stack. Only reaction +// jobs whose continuation-preserved embedder data (CPED, captured when the +// reaction was registered) is not undefined are offered to the embedder. + +#ifndef INCLUDE_V8_MICROTASK_DISPATCH_H_ +#define INCLUDE_V8_MICROTASK_DISPATCH_H_ + +#include "v8config.h" // NOLINT(build/include_directory) + +namespace v8 { + +class Isolate; + +// Opaque handle to a single promise reaction job taken out of the microtask +// queue. +struct DispatchedMicrotask; + +// Called by the microtask loop for a promise reaction job with non-undefined +// CPED. Return true to take ownership of |task|; the embedder must then call +// RunDispatchedMicrotask(isolate, task) exactly once (before the next +// microtask runs, or later). Return false to have V8 run the job inline as +// usual; V8 then frees |task|. +using MicrotaskDispatchCallback = bool (*)(Isolate* isolate, + DispatchedMicrotask* task, + void* data); + +V8_EXPORT void SetMicrotaskDispatchCallback(Isolate* isolate, + MicrotaskDispatchCallback callback, + void* data); + +// Runs |task| on the current stack with the usual microtask bookkeeping +// (context, CPED, promise hooks, exception reporting) and frees it. +V8_EXPORT void RunDispatchedMicrotask(Isolate* isolate, + DispatchedMicrotask* task); + +// If the next job in the isolate's default microtask queue is a promise +// reaction job with non-undefined CPED, takes it off the queue and runs it on +// the current stack, as RunDispatchedMicrotask does, and returns true. +// Otherwise returns false and leaves the queue alone. Lets an embedder that is +// running a dispatched job keep running the dispatchable jobs right behind it +// without switching stacks for each one. Only call it while the job that was +// dispatched is still nested inside the microtask loop, so the order of jobs +// is unchanged. +V8_EXPORT bool RunNextDispatchableMicrotask(Isolate* isolate); + +} // namespace v8 + +// Unmangled entry points, so an addon can find them with dlsym() and still +// load (with the feature off) on a node binary without this patch. +extern "C" { +V8_EXPORT void v8_qualia_SetMicrotaskDispatchCallback( + v8::Isolate* isolate, v8::MicrotaskDispatchCallback callback, void* data); +V8_EXPORT void v8_qualia_RunDispatchedMicrotask(v8::Isolate* isolate, + v8::DispatchedMicrotask* task); +V8_EXPORT bool v8_qualia_RunNextDispatchableMicrotask(v8::Isolate* isolate); +} + +#endif // INCLUDE_V8_MICROTASK_DISPATCH_H_ diff --git a/deps/v8/src/builtins/builtins-microtask-queue-gen.cc b/deps/v8/src/builtins/builtins-microtask-queue-gen.cc index 0d8c09753f93..16525ce0888f 100644 --- a/deps/v8/src/builtins/builtins-microtask-queue-gen.cc +++ b/deps/v8/src/builtins/builtins-microtask-queue-gen.cc @@ -39,6 +39,13 @@ class MicrotaskQueueBuiltinsAssembler : public CodeStubAssembler { void PrepareForContext(TNode microtask_context, Label* bailout); void RunSingleMicrotask(TNode current_context, TNode microtask); + // Prototype (Qualia): offers a promise reaction job whose continuation- + // preserved embedder data is not undefined to the embedder (see + // include/v8-microtask-dispatch.h). Jumps to |dispatched| if it took it. + void MaybeDispatchPromiseReactionJob(TNode current_context, + TNode microtask, + Label* dispatched, Label* if_exception, + TVariable* var_exception); void IncrementFinishedMicrotaskCount(TNode microtask_queue); TNode GetCurrentContext(); @@ -142,6 +149,25 @@ void MicrotaskQueueBuiltinsAssembler::ClearContinuationPreservedEmbedderData() { } #endif // V8_ENABLE_CONTINUATION_PRESERVED_EMBEDDER_DATA +void MicrotaskQueueBuiltinsAssembler::MaybeDispatchPromiseReactionJob( + TNode current_context, TNode microtask, + Label* dispatched, Label* if_exception, TVariable* var_exception) { +#ifdef V8_ENABLE_CONTINUATION_PRESERVED_EMBEDDER_DATA + Label run_inline(this); + const TNode continuation_preserved_embedder_data = LoadObjectField( + microtask, Microtask::kContinuationPreservedEmbedderDataOffset); + GotoIf(IsUndefined(continuation_preserved_embedder_data), &run_inline); + TNode result; + { + ScopedExceptionHandler handler(this, if_exception, var_exception); + result = CallRuntime(Runtime::kDispatchMicrotask, current_context, + microtask); + } + Branch(TaggedEqual(result, TrueConstant()), dispatched, &run_inline); + BIND(&run_inline); +#endif // V8_ENABLE_CONTINUATION_PRESERVED_EMBEDDER_DATA +} + void MicrotaskQueueBuiltinsAssembler::RunSingleMicrotask( TNode current_context, TNode microtask) { CSA_DCHECK(this, TaggedIsNotSmi(microtask)); @@ -158,7 +184,8 @@ void MicrotaskQueueBuiltinsAssembler::RunSingleMicrotask( is_promise_fulfill_reaction_job(this), is_promise_reject_reaction_job(this), is_promise_resolve_thenable_job(this), - is_unreachable(this, Label::kDeferred), done(this); + is_unreachable(this, Label::kDeferred), done(this), + job_dispatched(this, Label::kDeferred); int32_t case_values[] = {CALLABLE_TASK_TYPE, CALLBACK_TASK_TYPE, PROMISE_FULFILL_REACTION_JOB_TASK_TYPE, @@ -270,6 +297,9 @@ void MicrotaskQueueBuiltinsAssembler::RunSingleMicrotask( TNode microtask_context = LoadObjectField( microtask, PromiseReactionJobTask::kContextOffset); TNode native_context = LoadNativeContext(microtask_context); + MaybeDispatchPromiseReactionJob(current_context, microtask, + &job_dispatched, &if_exception, + &var_exception); PrepareForContext(native_context, &done); const TNode argument = @@ -312,6 +342,9 @@ void MicrotaskQueueBuiltinsAssembler::RunSingleMicrotask( TNode microtask_context = LoadObjectField( microtask, PromiseReactionJobTask::kContextOffset); TNode native_context = LoadNativeContext(microtask_context); + MaybeDispatchPromiseReactionJob(current_context, microtask, + &job_dispatched, &if_exception, + &var_exception); PrepareForContext(native_context, &done); const TNode argument = @@ -351,6 +384,14 @@ void MicrotaskQueueBuiltinsAssembler::RunSingleMicrotask( BIND(&is_unreachable); Unreachable(); + BIND(&job_dispatched); + { + // The embedder ran (or will run) the job elsewhere. + RewindEnteredContext(saved_entered_context_count); + SetCurrentContext(current_context); + Goto(&done); + } + BIND(&if_exception); { // Report unhandled exceptions from microtasks. diff --git a/deps/v8/src/runtime/runtime-promise.cc b/deps/v8/src/runtime/runtime-promise.cc index 262b9aa5aa69..b1cbca11ac81 100644 --- a/deps/v8/src/runtime/runtime-promise.cc +++ b/deps/v8/src/runtime/runtime-promise.cc @@ -2,11 +2,15 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include "include/v8-microtask-dispatch.h" #include "src/api/api-inl.h" #include "src/debug/debug.h" #include "src/execution/arguments-inl.h" +#include "src/execution/execution.h" #include "src/execution/microtask-queue.h" #include "src/objects/js-promise-inl.h" +#include "src/objects/microtask-inl.h" +#include "src/objects/promise-inl.h" namespace v8 { namespace internal { @@ -95,6 +99,161 @@ RUNTIME_FUNCTION(Runtime_RunMicrotaskCallback) { return ReadOnlyRoots(isolate).undefined_value(); } +// Prototype (Qualia): hand promise reaction jobs that carry continuation- +// preserved embedder data to the embedder, so it can run them on another +// stack (node-fibers). See include/v8-microtask-dispatch.h. +} // namespace internal + +struct DispatchedMicrotask { + internal::MicrotaskQueue* queue; +}; + +namespace internal { + +namespace { +v8::MicrotaskDispatchCallback g_microtask_dispatch_callback = nullptr; +void* g_microtask_dispatch_data = nullptr; +Isolate* g_microtask_dispatch_isolate = nullptr; +// Set just before a private single-job queue runs, so that job runs inline +// instead of being dispatched again. +bool g_run_next_dispatchable_inline = false; +} // namespace + +// Runs the one job in |queue| on the current stack, with the bookkeeping +// MicrotaskQueue::RunMicrotasks does around a drain (the job sets its own CPED; +// the caller's CPED is restored afterwards). Bypasses RunMicrotasks itself so +// the default queue's suppression and running state aren't touched while the +// job may be parked on a coroutine. +static void RunPrivateMicrotaskQueue(Isolate* isolate, MicrotaskQueue* queue) { + HandleScope handle_scope(isolate); +#ifdef V8_ENABLE_CONTINUATION_PRESERVED_EMBEDDER_DATA + DirectHandle outer_embedder_data( + isolate->isolate_data()->continuation_preserved_embedder_data(), isolate); + isolate->isolate_data()->set_continuation_preserved_embedder_data( + ReadOnlyRoots(isolate).undefined_value()); +#endif // V8_ENABLE_CONTINUATION_PRESERVED_EMBEDDER_DATA + { + HandleScopeImplementer::EnteredContextRewindScope rewind_scope( + isolate->handle_scope_implementer()); + g_run_next_dispatchable_inline = true; + MaybeDirectHandle result = + Execution::TryRunMicrotasks(isolate, queue); + USE(result); + // Normally consumed by Runtime_DispatchMicrotask before the job starts. + g_run_next_dispatchable_inline = false; + } +#ifdef V8_ENABLE_CONTINUATION_PRESERVED_EMBEDDER_DATA + isolate->isolate_data()->set_continuation_preserved_embedder_data( + *outer_embedder_data); +#endif // V8_ENABLE_CONTINUATION_PRESERVED_EMBEDDER_DATA + if (isolate->is_execution_terminating()) { + isolate->OnTerminationDuringRunMicrotasks(); + } +} + +RUNTIME_FUNCTION(Runtime_DispatchMicrotask) { + HandleScope scope(isolate); + DCHECK_EQ(1, args.length()); + if (g_run_next_dispatchable_inline) { + g_run_next_dispatchable_inline = false; + return ReadOnlyRoots(isolate).false_value(); + } + if (g_microtask_dispatch_callback == nullptr || + g_microtask_dispatch_isolate != isolate) { + return ReadOnlyRoots(isolate).false_value(); + } + DirectHandle microtask = args.at(0); + // A private single-job queue keeps the job alive (queues are GC roots) and + // lets the job run through the RunMicrotasks builtin. + std::unique_ptr queue = MicrotaskQueue::New(isolate); + queue->EnqueueMicrotask(*microtask); + v8::DispatchedMicrotask* task = new v8::DispatchedMicrotask{queue.get()}; + bool taken = g_microtask_dispatch_callback( + reinterpret_cast(isolate), task, g_microtask_dispatch_data); + if (taken) { + queue.release(); // Owned (and possibly already freed) by the embedder. + } else { + delete task; + } + RETURN_FAILURE_IF_EXCEPTION(isolate); + return isolate->heap()->ToBoolean(taken); +} + +} // namespace internal + +void SetMicrotaskDispatchCallback(Isolate* v8_isolate, + MicrotaskDispatchCallback callback, + void* data) { + internal::g_microtask_dispatch_callback = callback; + internal::g_microtask_dispatch_data = data; + internal::g_microtask_dispatch_isolate = + reinterpret_cast(v8_isolate); +} + +void RunDispatchedMicrotask(Isolate* v8_isolate, DispatchedMicrotask* task) { + internal::Isolate* isolate = reinterpret_cast(v8_isolate); + std::unique_ptr queue(task->queue); + delete task; + internal::RunPrivateMicrotaskQueue(isolate, queue.get()); +} + +bool RunNextDispatchableMicrotask(Isolate* v8_isolate) { +#ifdef V8_ENABLE_CONTINUATION_PRESERVED_EMBEDDER_DATA + internal::Isolate* isolate = reinterpret_cast(v8_isolate); + internal::MicrotaskQueue* queue = isolate->default_microtask_queue(); + if (queue == nullptr || queue->size() == 0) return false; + internal::HandleScope handle_scope(isolate); + internal::Tagged front = queue->get(0); + if (!internal::IsPromiseFulfillReactionJobTask(front) && + !internal::IsPromiseRejectReactionJobTask(front)) { + return false; + } + if (internal::IsUndefined(front->continuation_preserved_embedder_data(), + isolate)) { + return false; + } + internal::DirectHandle microtask(front, isolate); + // Take it off the front of the queue the way the RunMicrotasks builtin does + // (that loop re-reads start and size before every job, so the drain this is + // nested in carries on with the job after it). + internal::Address base = reinterpret_cast(queue); + intptr_t* start = reinterpret_cast( + base + internal::MicrotaskQueue::kStartOffset); + intptr_t* size = reinterpret_cast( + base + internal::MicrotaskQueue::kSizeOffset); + *start = (*start + 1) % queue->capacity(); + *size -= 1; + std::unique_ptr private_queue = + internal::MicrotaskQueue::New(isolate); + private_queue->EnqueueMicrotask(*microtask); + internal::RunPrivateMicrotaskQueue(isolate, private_queue.get()); + return true; +#else + return false; +#endif // V8_ENABLE_CONTINUATION_PRESERVED_EMBEDDER_DATA +} + +} // namespace v8 + +extern "C" { +void v8_qualia_SetMicrotaskDispatchCallback( + v8::Isolate* isolate, v8::MicrotaskDispatchCallback callback, void* data) { + v8::SetMicrotaskDispatchCallback(isolate, callback, data); +} + +void v8_qualia_RunDispatchedMicrotask(v8::Isolate* isolate, + v8::DispatchedMicrotask* task) { + v8::RunDispatchedMicrotask(isolate, task); +} + +bool v8_qualia_RunNextDispatchableMicrotask(v8::Isolate* isolate) { + return v8::RunNextDispatchableMicrotask(isolate); +} +} + +namespace v8 { +namespace internal { + RUNTIME_FUNCTION(Runtime_PromiseHookInit) { HandleScope scope(isolate); DCHECK_EQ(2, args.length()); diff --git a/deps/v8/src/runtime/runtime.h b/deps/v8/src/runtime/runtime.h index f9c8207c1c10..fe376703becc 100644 --- a/deps/v8/src/runtime/runtime.h +++ b/deps/v8/src/runtime/runtime.h @@ -293,6 +293,7 @@ namespace internal { F(PropagateException, 0, 1) \ F(ReportMessageFromMicrotask, 1, 1) \ F(RunMicrotaskCallback, 2, 1) \ + F(DispatchMicrotask, 1, 1) \ F(PerformMicrotaskCheckpoint, 0, 1) \ F(SharedValueBarrierSlow, 1, 1) \ F(StackGuard, 0, 1) \