diff --git a/deps/v8/include/v8-microtask-dispatch.h b/deps/v8/include/v8-microtask-dispatch.h new file mode 100644 index 00000000000..5557ea581a5 --- /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 6b0c63b34ff..6992962361d 100644 --- a/deps/v8/src/builtins/builtins-microtask-queue-gen.cc +++ b/deps/v8/src/builtins/builtins-microtask-queue-gen.cc @@ -37,6 +37,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(); @@ -115,6 +122,24 @@ void MicrotaskQueueBuiltinsAssembler::PrepareForContext( SetCurrentContext(native_context); } +void MicrotaskQueueBuiltinsAssembler::MaybeDispatchPromiseReactionJob( + TNode current_context, TNode microtask, + Label* dispatched, Label* if_exception, TVariable* var_exception) { + Label run_inline(this); + const TNode preserved_embedder_data = LoadObjectField( + microtask, + PromiseReactionJobTask::kContinuationPreservedEmbedderDataOffset); + GotoIf(IsUndefined(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); +} + void MicrotaskQueueBuiltinsAssembler::RunSingleMicrotask( TNode current_context, TNode microtask) { CSA_DCHECK(this, TaggedIsNotSmi(microtask)); @@ -130,7 +155,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, @@ -225,6 +251,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 = @@ -237,6 +266,8 @@ void MicrotaskQueueBuiltinsAssembler::RunSingleMicrotask( TNode preserved_embedder_data = LoadObjectField( microtask, PromiseReactionJobTask::kContinuationPreservedEmbedderDataOffset); + const TNode previous_embedder_data = LoadContextElement( + native_context, Context::CONTINUATION_PRESERVED_EMBEDDER_DATA_INDEX); Label preserved_data_done(this); GotoIf(IsUndefined(preserved_embedder_data), &preserved_data_done); StoreContextElement(native_context, @@ -263,7 +294,7 @@ void MicrotaskQueueBuiltinsAssembler::RunSingleMicrotask( GotoIf(IsUndefined(preserved_embedder_data), &preserved_data_reset_done); StoreContextElement(native_context, Context::CONTINUATION_PRESERVED_EMBEDDER_DATA_INDEX, - UndefinedConstant()); + previous_embedder_data); Goto(&preserved_data_reset_done); BIND(&preserved_data_reset_done); @@ -278,6 +309,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 = @@ -290,6 +324,8 @@ void MicrotaskQueueBuiltinsAssembler::RunSingleMicrotask( TNode preserved_embedder_data = LoadObjectField( microtask, PromiseReactionJobTask::kContinuationPreservedEmbedderDataOffset); + const TNode previous_embedder_data = LoadContextElement( + native_context, Context::CONTINUATION_PRESERVED_EMBEDDER_DATA_INDEX); Label preserved_data_done(this); GotoIf(IsUndefined(preserved_embedder_data), &preserved_data_done); StoreContextElement(native_context, @@ -316,7 +352,7 @@ void MicrotaskQueueBuiltinsAssembler::RunSingleMicrotask( GotoIf(IsUndefined(preserved_embedder_data), &preserved_data_reset_done); StoreContextElement(native_context, Context::CONTINUATION_PRESERVED_EMBEDDER_DATA_INDEX, - UndefinedConstant()); + previous_embedder_data); Goto(&preserved_data_reset_done); BIND(&preserved_data_reset_done); @@ -328,6 +364,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 352879a5933..c33f1b198d6 100644 --- a/deps/v8/src/runtime/runtime-promise.cc +++ b/deps/v8/src/runtime/runtime-promise.cc @@ -2,14 +2,18 @@ // 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/logging/counters.h" #include "src/objects/elements.h" #include "src/objects/heap-object-inl.h" #include "src/objects/js-promise-inl.h" +#include "src/objects/microtask-inl.h" +#include "src/objects/promise-inl.h" #include "src/objects/objects-inl.h" #include "src/objects/oddball-inl.h" #include "src/runtime/runtime-utils.h" @@ -104,6 +108,142 @@ 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 by RunDispatchedMicrotask just before it runs its private queue, so the +// single job in that queue runs inline instead of being dispatched again. +bool g_run_next_dispatchable_inline = false; +} // namespace + +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(); + } + Handle microtask = args.at(0); + // A private single-job queue keeps the job alive (queues are GC roots) and + // lets RunDispatchedMicrotask reuse 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_SCHEDULED_EXCEPTION(isolate); + return taken ? ReadOnlyRoots(isolate).true_value() + : ReadOnlyRoots(isolate).false_value(); +} + +} // 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::HandleScope handle_scope(isolate); + internal::MaybeHandle maybe_exception; + internal::MaybeHandle maybe_result; + { + internal::HandleScopeImplementer::EnteredContextRewindScope rewind_scope( + isolate->handle_scope_implementer()); + internal::g_run_next_dispatchable_inline = true; + maybe_result = internal::Execution::TryRunMicrotasks(isolate, queue.get(), + &maybe_exception); + // Normally consumed by Runtime_DispatchMicrotask before the job starts. + internal::g_run_next_dispatchable_inline = false; + } + if (maybe_result.is_null() && maybe_exception.is_null()) { + isolate->OnTerminationDuringRunMicrotasks(); + } +} + +bool RunNextDispatchableMicrotask(Isolate* v8_isolate) { + 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::Microtask front = queue->get(0); + if (!front.IsPromiseFulfillReactionJobTask() && + !front.IsPromiseRejectReactionJobTask()) { + return false; + } + if (internal::PromiseReactionJobTask::cast(front) + .continuation_preserved_embedder_data() + .IsUndefined(isolate)) { + return false; + } + internal::Handle 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); + RunDispatchedMicrotask(v8_isolate, + new DispatchedMicrotask{private_queue.release()}); + return true; +} + +} // 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_PromiseStatus) { HandleScope scope(isolate); DCHECK_EQ(1, args.length()); diff --git a/deps/v8/src/runtime/runtime.h b/deps/v8/src/runtime/runtime.h index 877b277a5e2..ccdb3b851aa 100644 --- a/deps/v8/src/runtime/runtime.h +++ b/deps/v8/src/runtime/runtime.h @@ -238,6 +238,7 @@ namespace internal { F(ReThrow, 1, 1) \ F(ReThrowWithMessage, 2, 1) \ F(RunMicrotaskCallback, 2, 1) \ + F(DispatchMicrotask, 1, 1) \ F(PerformMicrotaskCheckpoint, 0, 1) \ F(SharedValueBarrierSlow, 1, 1) \ F(StackGuard, 0, 1) \