From b74d09975edacbb41e184092aa6ce95ed23f12c1 Mon Sep 17 00:00:00 2001 From: Keita Urashima Date: Wed, 27 May 2026 19:32:59 +0900 Subject: [PATCH] FEATURE: add Context#perform_microtask_checkpoint Synchronously drains the V8 microtask queue, useful when a Ruby callback invoked during context.call/eval needs spec-compliant microtask ordering between synchronous `dispatchEvent` listeners (or any other sync JS sequence that schedules microtasks). Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG | 3 +++ .../mini_racer_extension.c | 13 +++++++++++ ext/mini_racer_extension/mini_racer_v8.cc | 12 ++++++++++ ext/mini_racer_extension/mini_racer_v8.h | 1 + test/mini_racer_test.rb | 22 +++++++++++++++++++ 5 files changed, 51 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 78f7b69..1fb18a7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -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 diff --git a/ext/mini_racer_extension/mini_racer_extension.c b/ext/mini_racer_extension/mini_racer_extension.c index ca37bb1..496d011 100644 --- a/ext/mini_racer_extension/mini_racer_extension.c +++ b/ext/mini_racer_extension/mini_racer_extension.c @@ -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); @@ -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; @@ -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); diff --git a/ext/mini_racer_extension/mini_racer_v8.cc b/ext/mini_racer_extension/mini_racer_v8.cc index 4f31e82..591d5e2 100644 --- a/ext/mini_racer_extension/mini_racer_v8.cc +++ b/ext/mini_racer_extension/mini_racer_v8.cc @@ -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; diff --git a/ext/mini_racer_extension/mini_racer_v8.h b/ext/mini_racer_extension/mini_racer_v8.h index 5bd47ba..57f12fb 100644 --- a/ext/mini_racer_extension/mini_racer_v8.h +++ b/ext/mini_racer_extension/mini_racer_v8.h @@ -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); diff --git a/test/mini_racer_test.rb b/test/mini_racer_test.rb index ef8f3d8..a9ffe38 100644 --- a/test/mini_racer_test.rb +++ b/test/mini_racer_test.rb @@ -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"