Skip to content

Run single-threaded V8 dispatches on reusable thread#416

Open
sam-saffron-jarvis wants to merge 1 commit into
rubyjs:mainfrom
sam-saffron-jarvis:fix-single-threaded-v8-thread
Open

Run single-threaded V8 dispatches on reusable thread#416
sam-saffron-jarvis wants to merge 1 commit into
rubyjs:mainfrom
sam-saffron-jarvis:fix-single-threaded-v8-thread

Conversation

@sam-saffron-jarvis
Copy link
Copy Markdown

@sam-saffron-jarvis sam-saffron-jarvis commented May 21, 2026

Summary

Fixes :single_threaded so V8 still uses the single-threaded platform, but MiniRacer no longer runs V8 dispatches directly on the Ruby caller thread.

Instead, each context lazily starts a reusable single-threaded V8 runner thread. The important bit: the runner waits outside V8, and only enters V8 for an individual dispatch:

Ruby caller thread
  rb_nogvl
    ensure reusable runner thread exists for this pid
    signal request
    wait for response / callback

runner thread
  wait outside V8
  on request:
    enter V8 with Locker/IsolateScope/HandleScope
    dispatch request
    exit V8 completely
  wait outside V8 again

JS→Ruby callbacks rendezvous back to the original Ruby caller thread through the existing condition-variable protocol, so the V8 runner thread does not directly reacquire the GVL or run Ruby code.

This addresses #415. In that workload, the previous same-Ruby-thread :single_threaded path could crash during V8 exception unwinding in v8::internal::wasm::StackMemory::jslimit(). The crash appears to depend on the embedding shape where V8 runs on a Ruby thread and callbacks re-enter Ruby/GVL from inside the V8 stack.

No extra public option is added. MiniRacer::Platform.set_flags!(:single_threaded) keeps the existing public API.

Fork behavior

This preserves the important historical property of :single_threaded: an inherited context can still be used after fork.

The reusable runner is tracked per process id. If a context is inherited into a child process, the parent runner thread is gone, so the next dispatch notices the pid change and starts a new runner thread in the child.

The runner does not hold V8's Locker or Isolate::Scope while idle, so the normal idle-at-fork case does not inherit a V8 thread parked inside the isolate.

Changes

  • Replace direct same-Ruby-thread v8_single_threaded_enter(...) dispatches with a reusable per-context runner thread.
  • The runner enters/exits V8 around each dispatch instead of holding V8 state while idle.
  • Make v8_roundtrip() use the condition-variable rendezvous path for :single_threaded too.
  • Keep Ruby callbacks running on the original Ruby caller thread, not on the V8 runner thread.
  • Track the runner pid and recreate the runner after fork.
  • Keep the existing single-threaded persistent V8 handles, so contexts remain alive between calls and across ordinary fork boundaries.

Validation

MiniRacer test suite:

103 runs, 188 assertions, 0 failures, 0 errors, 3 skips

Forking smoke test:

bundle exec ruby test/test_forking.rb
# exit 0

Fork stress:

for i in $(seq 1 10); do bundle exec ruby test/test_forking.rb || exit 1; done
# fork-stress-ok

Single-threaded Ruby callback smoke:

bundle exec ruby -Ilib -e 'require "mini_racer"; MiniRacer::Platform.set_flags! :single_threaded; c=MiniRacer::Context.new; c.attach("rb", proc { |x| x + 1 }); raise unless c.eval("rb(41)") == 42; puts "callback ok"'
# callback ok

Standalone #415 replay now completes with the normal existing flag:

MiniRacer::Platform.set_flags!(:single_threaded)
visited, current_url=http://www.example.com/about status=200
checking .about__stats-item.site-creation-date span text="Created 2 months ago" iterations=1
iteration 0: found element text="Created 2 months ago"; asking has_text?/visible?
done without crash

The original Discourse repro from #415 also passes unchanged:

1 example, 0 failures

@sam-saffron-jarvis sam-saffron-jarvis force-pushed the fix-single-threaded-v8-thread branch from e79cb5d to eeaa806 Compare May 21, 2026 01:07
@sam-saffron-jarvis sam-saffron-jarvis changed the title Add dedicated V8 thread mode for single-threaded platform Run single-threaded V8 work on dedicated thread May 21, 2026
@sam-saffron-jarvis sam-saffron-jarvis force-pushed the fix-single-threaded-v8-thread branch from eeaa806 to b9c1ab0 Compare May 21, 2026 01:42
@sam-saffron-jarvis sam-saffron-jarvis changed the title Run single-threaded V8 work on dedicated thread Run single-threaded V8 dispatches on short-lived threads May 21, 2026
@sam-saffron-jarvis sam-saffron-jarvis force-pushed the fix-single-threaded-v8-thread branch from b9c1ab0 to 25ba0c0 Compare May 21, 2026 01:48
@sam-saffron-jarvis sam-saffron-jarvis changed the title Run single-threaded V8 dispatches on short-lived threads Run single-threaded V8 dispatches on reusable thread May 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant