Skip to content
Merged
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
3 changes: 3 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
- (Unreleased)
- Add `Context#perform_microtask_checkpoint` to synchronously drain the V8 microtask queue, useful for spec-compliant `dispatchEvent` sequencing inside Ruby callbacks

- 0.21.1 - 25-05-2026
- Run `:single_threaded` V8 dispatches on a reusable mini_racer-owned native thread so V8 does not execute on Ruby-owned threads
- Stop and join the reusable `:single_threaded` runner when contexts are disposed
Expand Down
13 changes: 13 additions & 0 deletions ext/mini_racer_extension/mini_racer_extension.c
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,7 @@ static void dispatch1(Context *c, const uint8_t *p, size_t n)
case 'C': return v8_timedwait(c, p+1, n-1, v8_call);
case 'E': return v8_timedwait(c, p+1, n-1, v8_eval);
case 'H': return v8_heap_snapshot(c->pst);
case 'M': return v8_perform_microtask_checkpoint(c->pst);
case 'P': return v8_pump_message_loop(c->pst);
case 'S': return v8_heap_stats(c->pst);
case 'T': return v8_snapshot(c->pst, p+1, n-1);
Expand Down Expand Up @@ -1469,6 +1470,17 @@ static VALUE context_heap_snapshot(VALUE self)
return rb_utf8_str_new((char *)res.buf, res.len);
}

static VALUE context_perform_microtask_checkpoint(VALUE self)
{
Context *c;
Buf b;

TypedData_Get_Struct(self, Context, &context_type, c);
buf_init(&b);
buf_putc(&b, 'M'); // (M)icrotask checkpoint, returns nil
return rendezvous(c, &b); // takes ownership of |b|
}

static VALUE context_pump_message_loop(VALUE self)
{
Context *c;
Expand Down Expand Up @@ -1824,6 +1836,7 @@ void Init_mini_racer_extension(void)
rb_define_method(c, "eval", context_eval, -1);
rb_define_method(c, "heap_stats", context_heap_stats, 0);
rb_define_method(c, "heap_snapshot", context_heap_snapshot, 0);
rb_define_method(c, "perform_microtask_checkpoint", context_perform_microtask_checkpoint, 0);
rb_define_method(c, "pump_message_loop", context_pump_message_loop, 0);
rb_define_method(c, "low_memory_notification", context_low_memory_notification, 0);
rb_define_alloc_func(c, context_alloc);
Expand Down
12 changes: 12 additions & 0 deletions ext/mini_racer_extension/mini_racer_v8.cc
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,18 @@ extern "C" void v8_heap_snapshot(State *pst)
v8_reply(st.ruby_context, os.buf.data(), os.buf.size()); // not serialized because big
}

extern "C" void v8_perform_microtask_checkpoint(State *pst)
{
// Leave any termination active so the enclosing v8_call/v8_eval frame
// surfaces OOM (set by v8_gc_callback) or watchdog termination to Ruby.
State& st = *pst;
v8::TryCatch try_catch(st.isolate);
try_catch.SetVerbose(st.verbose_exceptions);
v8::HandleScope handle_scope(st.isolate);
v8::MicrotasksScope::PerformCheckpoint(st.isolate);
reply_retry(st, v8::Undefined(st.isolate));
}

extern "C" void v8_pump_message_loop(State *pst)
{
State& st = *pst;
Expand Down
1 change: 1 addition & 0 deletions ext/mini_racer_extension/mini_racer_v8.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ void v8_call(struct State *pst, const uint8_t *p, size_t n);
void v8_eval(struct State *pst, const uint8_t *p, size_t n);
void v8_heap_stats(struct State *pst);
void v8_heap_snapshot(struct State *pst);
void v8_perform_microtask_checkpoint(struct State *pst);
void v8_pump_message_loop(struct State *pst);
void v8_snapshot(struct State *pst, const uint8_t *p, size_t n);
void v8_warmup(struct State *pst, const uint8_t *p, size_t n);
Expand Down
22 changes: 22 additions & 0 deletions test/mini_racer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -986,6 +986,28 @@ def test_promise
assert_equal(v, 99)
end

def test_perform_microtask_checkpoint_returns_nil
context = MiniRacer::Context.new
assert_nil(context.perform_microtask_checkpoint)
end

def test_perform_microtask_checkpoint_drains_from_callback
context = MiniRacer::Context.new
seen = []

context.attach('note', ->(s) { seen << s })
context.attach('drain', -> { context.perform_microtask_checkpoint })

context.eval <<~JS
Promise.resolve().then(() => note('microtask-fired'));
note('before-drain');
drain();
note('after-drain');
JS

assert_equal(%w[before-drain microtask-fired after-drain], seen)
end

def test_webassembly
if RUBY_ENGINE == "truffleruby"
skip "TruffleRuby does not enable WebAssembly by default"
Expand Down
Loading