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_
50 changes: 47 additions & 3 deletions deps/v8/src/builtins/builtins-microtask-queue-gen.cc
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,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 @@ -115,6 +122,24 @@ void MicrotaskQueueBuiltinsAssembler::PrepareForContext(
SetCurrentContext(native_context);
}

void MicrotaskQueueBuiltinsAssembler::MaybeDispatchPromiseReactionJob(
TNode<Context> current_context, TNode<Microtask> microtask,
Label* dispatched, Label* if_exception, TVariable<Object>* var_exception) {
Label run_inline(this);
const TNode<Object> preserved_embedder_data = LoadObjectField(
microtask,
PromiseReactionJobTask::kContinuationPreservedEmbedderDataOffset);
GotoIf(IsUndefined(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);
}

void MicrotaskQueueBuiltinsAssembler::RunSingleMicrotask(
TNode<Context> current_context, TNode<Microtask> microtask) {
CSA_DCHECK(this, TaggedIsNotSmi(microtask));
Expand All @@ -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,
Expand Down Expand Up @@ -225,6 +251,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 All @@ -237,6 +266,8 @@ void MicrotaskQueueBuiltinsAssembler::RunSingleMicrotask(
TNode<Object> preserved_embedder_data = LoadObjectField(
microtask,
PromiseReactionJobTask::kContinuationPreservedEmbedderDataOffset);
const TNode<Object> 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,
Expand All @@ -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);

Expand All @@ -278,6 +309,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 All @@ -290,6 +324,8 @@ void MicrotaskQueueBuiltinsAssembler::RunSingleMicrotask(
TNode<Object> preserved_embedder_data = LoadObjectField(
microtask,
PromiseReactionJobTask::kContinuationPreservedEmbedderDataOffset);
const TNode<Object> 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,
Expand All @@ -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);

Expand All @@ -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.
Expand Down
140 changes: 140 additions & 0 deletions deps/v8/src/runtime/runtime-promise.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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> microtask = args.at<Microtask>(0);
// A private single-job queue keeps the job alive (queues are GC roots) and
// lets RunDispatchedMicrotask reuse 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_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<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::HandleScope handle_scope(isolate);
internal::MaybeHandle<internal::Object> maybe_exception;
internal::MaybeHandle<internal::Object> 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<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::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<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);
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());
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 @@ -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) \
Expand Down
Loading