Skip to content

Commit 76fa027

Browse files
committed
Run V8 on separate thread (rubyjs#325)
Rationale, implementation and known bugs are documented in DESIGN.md but the elevator pitch is that Ruby and V8 don't like sharing the same system stack. mini_racer_extension.cc has been split into mini_racer_extension.c and mini_racer_v8.cc. The former deals with Ruby, the latter with JS. This work has been sponsored by Discourse.
1 parent 6f5d75a commit 76fa027

File tree

11 files changed

+3412
-2554
lines changed

11 files changed

+3412
-2554
lines changed

DESIGN.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
rationale
2+
=========
3+
4+
Before the commit that added this document, Ruby and V8 shared the same system
5+
stack but it's been observed that they don't always co-exist peacefully there.
6+
7+
Symptoms range from unexpected JS stack overflow exceptions and hitting debug
8+
checks in V8, to outright segmentation faults.
9+
10+
To mitigate that, V8 runs on separate threads now.
11+
12+
implementation
13+
==============
14+
15+
Each `MiniRacer::Context` is paired with a native system thread that runs V8.
16+
17+
Multiple Ruby threads can concurrently access the `MiniRacer::Context`.
18+
MiniRacer ensures mutual exclusion. Ruby threads won't trample each other.
19+
20+
Ruby threads communicate with the V8 thread through a mutex-and-condition-variable
21+
protected request/response memory buffer.
22+
23+
The wire format is V8's native (de)serialization format. An encoder/decoder
24+
has been added to MiniRacer.
25+
26+
Requests and (some) responses are prefixed with a single character
27+
that indicates the desired action: `'C'` is `context.call(...)`,
28+
`'E'` is `context.eval(...)`, and so on.
29+
30+
A response from the V8 thread either starts with:
31+
32+
- `'\xff'`, indicating a normal response that should be deserialized as-is
33+
34+
- `'c'`, signaling an in-band request (not a response!) to call a Ruby function
35+
registered with `context.attach(...)`. In turn, the Ruby thread replies with
36+
a `'c'` response containing the return value from the Ruby function.
37+
38+
Special care has been taken to ensure Ruby and JS functions can call each other
39+
recursively without deadlocking. The Ruby thread uses a recursive mutex that
40+
excludes other Ruby threads but still allows reentrancy from the same thread.
41+
42+
The exact request and response payloads are documented in the source code but
43+
they are almost universally:
44+
45+
- either a single value (e.g. `true` or `false`), or
46+
47+
- a two or three element array (ex. `[filename, source]` for `context.eval(...)`), or
48+
49+
- for responses, an errback-style `[response, error]` array, where `error`
50+
is a multi-line string that contains the error message on the first line,
51+
and, optionally, the stack trace. If not empty, the error string is turned
52+
into a Ruby exception and raised.
53+
54+
deliberate changes & known bugs
55+
===============================
56+
57+
- `MiniRacer::Platform.set_flags! :single_threaded` still runs everything on
58+
the same thread but is prone to crashes in Ruby < 3.4.0 due to a Ruby runtime
59+
bug that clobbers thread-local variables.
60+
61+
- The `Isolate` class is gone. Maintaining a one-to-many relationship between
62+
isolates and contexts in a multi-threaded environment had a bad cost/benefit
63+
ratio. `Isolate` methods like `isolate.low_memory_notification` have been
64+
moved to `Context`, ex., `context.low_memory_notification`.
65+
66+
- The `marshal_stack_depth` argument is still accepted but ignored; it's no
67+
longer necessary.
68+
69+
- The `ensure_gc_after_idle` argument is a no-op in `:single_threaded` mode.
70+
71+
- The `timeout` argument no longer interrupts long-running Ruby code. Killing
72+
or interrupting a Ruby thread executing arbitrary code is fraught with peril.
73+
74+
- Returning an invalid JS `Date` object (think `new Date(NaN)`) now raises a
75+
`RangeError` instead of silently returning a bogus `Time` object.
76+
77+
- Not all JS objects map 1-to-1 to Ruby objects. Typed arrays and arraybuffers
78+
are currently mapped to `Encoding::ASCII_8BIT`strings as the closest Ruby
79+
equivalent to a byte buffer.
80+
81+
- Not all JS objects are serializable/cloneable. Where possible, such objects
82+
are substituted with a cloneable representation, else a `MiniRacer::RuntimeError`
83+
is raised.
84+
85+
Promises, argument objects, map and set iterators, etc., are substituted,
86+
either with an empty object (promises, argument objects), or by turning them
87+
into arrays (map/set iterators.)
88+
89+
Function objects are substituted with a marker so they can be represented
90+
as `MiniRacer::JavaScriptFunction` objects on the Ruby side.
91+
92+
SharedArrayBuffers are not cloneable by design but aren't really usable in
93+
`mini_racer` in the first place (no way to share them between isolates.)

ext/mini_racer_extension/extconf.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
require 'mkmf'
22

3+
$srcs = ["mini_racer_extension.c", "mini_racer_v8.cc"]
4+
35
if RUBY_ENGINE == "truffleruby"
46
File.write("Makefile", dummy_makefile($srcdir).join(""))
57
return

0 commit comments

Comments
 (0)