|
| 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.) |
0 commit comments