Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions deps/v8/include/v8-microtask-dispatch.h
Original file line number Diff line number Diff line change
@@ -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_
43 changes: 42 additions & 1 deletion deps/v8/src/builtins/builtins-microtask-queue-gen.cc
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ class MicrotaskQueueBuiltinsAssembler : public CodeStubAssembler {
void PrepareForContext(TNode<Context> microtask_context, Label* bailout);
void RunSingleMicrotask(TNode<Context> current_context,
TNode<Microtask> 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<Context> current_context,
TNode<Microtask> microtask,
Label* dispatched, Label* if_exception,
TVariable<Object>* var_exception);
void IncrementFinishedMicrotaskCount(TNode<RawPtrT> microtask_queue);

TNode<Context> GetCurrentContext();
Expand Down Expand Up @@ -142,6 +149,25 @@ void MicrotaskQueueBuiltinsAssembler::ClearContinuationPreservedEmbedderData() {
}
#endif // V8_ENABLE_CONTINUATION_PRESERVED_EMBEDDER_DATA

void MicrotaskQueueBuiltinsAssembler::MaybeDispatchPromiseReactionJob(
TNode<Context> current_context, TNode<Microtask> microtask,
Label* dispatched, Label* if_exception, TVariable<Object>* var_exception) {
#ifdef V8_ENABLE_CONTINUATION_PRESERVED_EMBEDDER_DATA
Label run_inline(this);
const TNode<Object> continuation_preserved_embedder_data = LoadObjectField(
microtask, Microtask::kContinuationPreservedEmbedderDataOffset);
GotoIf(IsUndefined(continuation_preserved_embedder_data), &run_inline);
TNode<Object> 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<Context> current_context, TNode<Microtask> microtask) {
CSA_DCHECK(this, TaggedIsNotSmi(microtask));
Expand All @@ -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,
Expand Down Expand Up @@ -270,6 +297,9 @@ void MicrotaskQueueBuiltinsAssembler::RunSingleMicrotask(
TNode<Context> microtask_context = LoadObjectField<Context>(
microtask, PromiseReactionJobTask::kContextOffset);
TNode<NativeContext> native_context = LoadNativeContext(microtask_context);
MaybeDispatchPromiseReactionJob(current_context, microtask,
&job_dispatched, &if_exception,
&var_exception);
PrepareForContext(native_context, &done);

const TNode<Object> argument =
Expand Down Expand Up @@ -312,6 +342,9 @@ void MicrotaskQueueBuiltinsAssembler::RunSingleMicrotask(
TNode<Context> microtask_context = LoadObjectField<Context>(
microtask, PromiseReactionJobTask::kContextOffset);
TNode<NativeContext> native_context = LoadNativeContext(microtask_context);
MaybeDispatchPromiseReactionJob(current_context, microtask,
&job_dispatched, &if_exception,
&var_exception);
PrepareForContext(native_context, &done);

const TNode<Object> argument =
Expand Down Expand Up @@ -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.
Expand Down
159 changes: 159 additions & 0 deletions deps/v8/src/runtime/runtime-promise.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<Object> 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<Object> 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> microtask = args.at<Microtask>(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<MicrotaskQueue> queue = MicrotaskQueue::New(isolate);
queue->EnqueueMicrotask(*microtask);
v8::DispatchedMicrotask* task = new v8::DispatchedMicrotask{queue.get()};
bool taken = g_microtask_dispatch_callback(
reinterpret_cast<v8::Isolate*>(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<internal::Isolate*>(v8_isolate);
}

void RunDispatchedMicrotask(Isolate* v8_isolate, DispatchedMicrotask* task) {
internal::Isolate* isolate = reinterpret_cast<internal::Isolate*>(v8_isolate);
std::unique_ptr<internal::MicrotaskQueue> 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<internal::Isolate*>(v8_isolate);
internal::MicrotaskQueue* queue = isolate->default_microtask_queue();
if (queue == nullptr || queue->size() == 0) return false;
internal::HandleScope handle_scope(isolate);
internal::Tagged<internal::Microtask> 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<internal::Microtask> 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<internal::Address>(queue);
intptr_t* start = reinterpret_cast<intptr_t*>(
base + internal::MicrotaskQueue::kStartOffset);
intptr_t* size = reinterpret_cast<intptr_t*>(
base + internal::MicrotaskQueue::kSizeOffset);
*start = (*start + 1) % queue->capacity();
*size -= 1;
std::unique_ptr<internal::MicrotaskQueue> 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());
Expand Down
1 change: 1 addition & 0 deletions deps/v8/src/runtime/runtime.h
Original file line number Diff line number Diff line change
Expand Up @@ -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) \
Expand Down
Loading