Skip to content

Commit c5c2dd2

Browse files
committed
Run V8 on separate thread
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 a268a2c commit c5c2dd2

File tree

11 files changed

+3406
-2554
lines changed

11 files changed

+3406
-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)