mini_racer-csim 0.21.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG +351 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/LICENSE.txt +21 -0
- data/README.md +687 -0
- data/ext/mini_racer_extension/extconf.rb +75 -0
- data/ext/mini_racer_extension/mini_racer_extension.c +2899 -0
- data/ext/mini_racer_extension/mini_racer_v8.cc +2692 -0
- data/ext/mini_racer_extension/mini_racer_v8.h +70 -0
- data/ext/mini_racer_extension/serde.c +782 -0
- data/ext/mini_racer_loader/extconf.rb +13 -0
- data/ext/mini_racer_loader/mini_racer_loader.c +123 -0
- data/lib/mini_racer/shared.rb +395 -0
- data/lib/mini_racer/truffleruby.rb +479 -0
- data/lib/mini_racer/version.rb +9 -0
- data/lib/mini_racer-csim.rb +4 -0
- data/lib/mini_racer.rb +117 -0
- metadata +168 -0
data/README.md
ADDED
|
@@ -0,0 +1,687 @@
|
|
|
1
|
+
# MiniRacer
|
|
2
|
+
|
|
3
|
+
[](https://github.com/rubyjs/mini_racer/actions/workflows/ci.yml) 
|
|
4
|
+
|
|
5
|
+
Minimal, modern embedded V8 for Ruby.
|
|
6
|
+
|
|
7
|
+
MiniRacer provides a minimal two way bridge between the V8 JavaScript engine and Ruby.
|
|
8
|
+
|
|
9
|
+
It was created as an alternative to the excellent [therubyracer](https://github.com/cowboyd/therubyracer), which is [no longer maintained](https://github.com/rubyjs/therubyracer/issues/462). Unlike therubyracer, mini_racer only implements a minimal bridge. This reduces the surface area making upgrading v8 much simpler and exhaustive testing simpler.
|
|
10
|
+
|
|
11
|
+
MiniRacer has an adapter for [execjs](https://github.com/rails/execjs) so it can be used directly with Rails projects to minify assets, run babel or compile CoffeeScript.
|
|
12
|
+
|
|
13
|
+
## This repository is `mini_racer-csim` (a fork)
|
|
14
|
+
|
|
15
|
+
This is **`mini_racer-csim`**, a private fork of [`mini_racer`](https://github.com/rubyjs/mini_racer) maintained for [capybara-simulated](https://github.com/ursm/capybara-simulated). It adds browser-fidelity extensions (ES modules, realm reset, …) that capybara-simulated needs but most users do not — **if you are not using capybara-simulated, use upstream `mini_racer`.**
|
|
16
|
+
|
|
17
|
+
It stays a **drop-in replacement**: the gem is still loaded with `require "mini_racer"` and exposes the `MiniRacer` module, so existing code keeps working. Only the gem name (`mini_racer-csim`) differs.
|
|
18
|
+
|
|
19
|
+
### Additions over upstream
|
|
20
|
+
|
|
21
|
+
| Feature | API | Notes |
|
|
22
|
+
| --- | --- | --- |
|
|
23
|
+
| Bytecode cache | `Context#compile(src, cached_data:, produce_cache:)` → `Script`, `Script#run`, `Script#cache_rejected?` | Cross-process V8 bytecode caching to skip parsing; see [Bytecode cache for repeated script evaluation](#bytecode-cache-for-repeated-script-evaluation) below |
|
|
24
|
+
| ES Module API | `Context#compile_module` → `MiniRacer::Module` (`#instantiate` / `#evaluate` / `#namespace` / `#status` / `#cached_data` / `#dispose`); `Context#dynamic_import_resolver=` | V8's ES module pipeline, `import.meta.url`, dynamic `import()` |
|
|
25
|
+
| Batched module-graph loader | `Context#load_module_graph(resolve:, …)` | Loads an ESM graph in one batched, native (C++) pass; one `Module` per URL shared across every load path |
|
|
26
|
+
| Realm reset | `Context#reset_realm` | Discards the user realm (`globalThis`) while keeping the warm isolate (browser per-navigation model); re-binds attached host functions and the host namespace |
|
|
27
|
+
| Host namespace | `Context.new(host_namespace: "MiniRacer")` → `globalThis.MiniRacer.drainMicrotasks()` | Opt-in JS namespace exposing an inline, rendezvous-free microtask checkpoint |
|
|
28
|
+
| GVL release on boot | (automatic) | Releases the Ruby GVL while the V8 thread boots the isolate |
|
|
29
|
+
|
|
30
|
+
The fork is periodically rebased on upstream `mini_racer` to pick up V8 / `libv8-node` bumps and bug fixes.
|
|
31
|
+
|
|
32
|
+
## Supported Ruby Versions & Troubleshooting
|
|
33
|
+
|
|
34
|
+
MiniRacer only supports non-EOL versions of Ruby. See [Ruby Maintenance Branches](https://www.ruby-lang.org/en/downloads/branches/) for the list of non-EOL Rubies. If you require support for older versions of Ruby install an older version of the gem. [TruffleRuby](https://github.com/oracle/truffleruby) is also supported.
|
|
35
|
+
|
|
36
|
+
MiniRacer **does not support**
|
|
37
|
+
|
|
38
|
+
* [Ruby built on MinGW](https://github.com/rubyjs/mini_racer/issues/252#issuecomment-1201172236), "pure windows" no Cygwin, no WSL2 (see https://github.com/rubyjs/libv8-node/issues/9)
|
|
39
|
+
* [JRuby](https://www.jruby.org)
|
|
40
|
+
|
|
41
|
+
If you have a problem installing MiniRacer, please consider the following steps:
|
|
42
|
+
|
|
43
|
+
* make sure you try the latest released version of `mini_racer`
|
|
44
|
+
* make sure you have Rubygems >= 3.2.13 and bundler >= 2.2.13 installed: `gem update --system`
|
|
45
|
+
* if you are using bundler
|
|
46
|
+
* make sure it is actually using the latest bundler version: [`bundle update --bundler`](https://bundler.io/v2.4/man/bundle-update.1.html)
|
|
47
|
+
* make sure to have `PLATFORMS` set correctly in `Gemfile.lock` via [`bundle lock --add-platform`](https://bundler.io/v2.4/man/bundle-lock.1.html#SUPPORTING-OTHER-PLATFORMS)
|
|
48
|
+
* make sure to recompile/reinstall `mini_racer` and `libv8-node` after OS upgrades (for example via `gem uninstall --all mini_racer libv8-node`)
|
|
49
|
+
* make sure you are on the latest patch/teeny version of a supported Ruby branch
|
|
50
|
+
|
|
51
|
+
## Features
|
|
52
|
+
|
|
53
|
+
### Simple eval for JavaScript
|
|
54
|
+
|
|
55
|
+
You can simply eval one or many JavaScript snippets in a shared context
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
context = MiniRacer::Context.new
|
|
59
|
+
context.eval("var adder = (a,b)=>a+b;")
|
|
60
|
+
puts context.eval("adder(20,22)")
|
|
61
|
+
# => 42
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Attach global Ruby functions to your JavaScript context
|
|
65
|
+
|
|
66
|
+
You can attach one or many ruby proc that can be accessed via JavaScript
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
context = MiniRacer::Context.new
|
|
70
|
+
context.attach("math.adder", proc{|a,b| a+b})
|
|
71
|
+
puts context.eval("math.adder(20,22)")
|
|
72
|
+
# => 42
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
context = MiniRacer::Context.new
|
|
77
|
+
context.attach("array_and_hash", proc{{a: 1, b: [1, {a: 1}]}})
|
|
78
|
+
puts context.eval("array_and_hash()")
|
|
79
|
+
# => {"a" => 1, "b" => [1, {"a" => 1}]}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Return binary data from Ruby to JavaScript
|
|
83
|
+
|
|
84
|
+
Attached Ruby functions can return binary data as `Uint8Array` using `MiniRacer::Binary`:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
require "digest"
|
|
88
|
+
|
|
89
|
+
context = MiniRacer::Context.new
|
|
90
|
+
context.attach("sha256_raw", ->(data) {
|
|
91
|
+
MiniRacer::Binary.new(Digest::SHA256.digest(data))
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
# Inside JavaScript the return value is a Uint8Array
|
|
95
|
+
context.eval("sha256_raw('hello') instanceof Uint8Array") # => true
|
|
96
|
+
context.eval("sha256_raw('hello').length") # => 32
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
This is useful when you need to pass raw bytes (e.g., cryptographic digests, compressed data, binary file contents) from Ruby to JavaScript. The `MiniRacer::Binary` wrapper tells the bridge to serialize the data as a `Uint8Array` on the JavaScript side rather than a string.
|
|
100
|
+
|
|
101
|
+
### GIL free JavaScript execution
|
|
102
|
+
|
|
103
|
+
The Ruby Global interpreter lock is released when scripts are executing:
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
context = MiniRacer::Context.new
|
|
107
|
+
Thread.new do
|
|
108
|
+
sleep 1
|
|
109
|
+
context.stop
|
|
110
|
+
end
|
|
111
|
+
context.eval("while(true){}")
|
|
112
|
+
# => exception is raised
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
This allows you to execute multiple scripts in parallel.
|
|
116
|
+
|
|
117
|
+
### Timeout Support
|
|
118
|
+
|
|
119
|
+
Contexts can specify a default timeout for scripts
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
context = MiniRacer::Context.new(timeout: 1000)
|
|
123
|
+
context.eval("while(true){}")
|
|
124
|
+
# => exception is raised after 1 second (1000 ms)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Memory softlimit Support
|
|
128
|
+
|
|
129
|
+
Contexts can specify a memory softlimit for scripts
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
# terminates script if heap usage exceeds 200mb after V8 garbage collection has run
|
|
133
|
+
context = MiniRacer::Context.new(max_memory: 200_000_000)
|
|
134
|
+
context.eval("var a = new Array(10000); while(true) {a = a.concat(new Array(10000)) }")
|
|
135
|
+
# => V8OutOfMemoryError is raised
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Rich Debugging with File Name in Stack Trace Support
|
|
139
|
+
|
|
140
|
+
You can provide `filename:` to `#eval` which will be used in stack traces produced by V8:
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
context = MiniRacer::Context.new
|
|
144
|
+
context.eval("var foo = function() {bar();}", filename: "a/foo.js")
|
|
145
|
+
context.eval("bar()", filename: "a/bar.js")
|
|
146
|
+
|
|
147
|
+
# JavaScript at a/bar.js:1:1: ReferenceError: bar is not defined (MiniRacer::RuntimeError)
|
|
148
|
+
# …
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Bytecode cache for repeated script evaluation
|
|
152
|
+
|
|
153
|
+
`Context#compile` returns a `MiniRacer::Script` handle you can run multiple times,
|
|
154
|
+
and exposes V8's bytecode cache so subsequent Contexts can skip the parse step.
|
|
155
|
+
|
|
156
|
+
In a single process — e.g. warming a `Context` pool from one canonical compile:
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
# Warm the cache once — top-level compile, opt in with produce_cache: true.
|
|
160
|
+
warm = MiniRacer::Context.new
|
|
161
|
+
warmed = warm.compile(File.read("bundle.js"),
|
|
162
|
+
filename: "bundle.js",
|
|
163
|
+
produce_cache: true)
|
|
164
|
+
warmed.run
|
|
165
|
+
blob = warmed.cached_data # ASCII-8BIT String, hold onto it in memory
|
|
166
|
+
|
|
167
|
+
# Subsequent Contexts (e.g. a per-request pool) consume the blob and skip parsing.
|
|
168
|
+
ctx = MiniRacer::Context.new
|
|
169
|
+
script = ctx.compile(File.read("bundle.js"),
|
|
170
|
+
filename: "bundle.js",
|
|
171
|
+
cached_data: blob)
|
|
172
|
+
# script.cache_rejected? is false when V8 accepted the blob.
|
|
173
|
+
script.run
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Across processes (e.g. persisting blobs to disk), the consumer must boot from
|
|
177
|
+
**byte-identical snapshot data** — two separate `Snapshot.new(src)` calls produce
|
|
178
|
+
different blobs even for the same `src`, and V8 will then reject every cached
|
|
179
|
+
blob. Use `Snapshot#dump` / `Snapshot.load` to share canonical bytes:
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
# Build the snapshot once, persist its bytes.
|
|
183
|
+
snap_bytes = MiniRacer::Snapshot.new(snapshot_src).dump
|
|
184
|
+
File.binwrite("snapshot.bin", snap_bytes)
|
|
185
|
+
|
|
186
|
+
# Every process loads the same bytes.
|
|
187
|
+
snap = MiniRacer::Snapshot.load(File.binread("snapshot.bin"))
|
|
188
|
+
ctx = MiniRacer::Context.new(snapshot: snap)
|
|
189
|
+
script = ctx.compile(File.read("bundle.js"),
|
|
190
|
+
filename: "bundle.js",
|
|
191
|
+
cached_data: File.binread("bundle.js.cache"))
|
|
192
|
+
script.run
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`produce_cache` defaults to `false`; pass `true` to ask V8 for the cache blob.
|
|
196
|
+
When the supplied `cached_data` is accepted, `script.cached_data` returns `nil` so
|
|
197
|
+
callers can skip a redundant copy. When V8 produces a fresh blob (initial compile
|
|
198
|
+
with `produce_cache: true`, or a rejection while `produce_cache: true` was also
|
|
199
|
+
set), it returns the new bytes.
|
|
200
|
+
|
|
201
|
+
`MiniRacer::V8_CACHED_DATA_VERSION_TAG` exposes V8's
|
|
202
|
+
`ScriptCompiler::CachedDataVersionTag()` — mix it into your cache key alongside
|
|
203
|
+
the source hash so a libv8-node version bump invalidates stale blobs automatically.
|
|
204
|
+
The constant is populated on first `Context.new` (after `Platform.set_flags!`),
|
|
205
|
+
so read it after constructing at least one Context.
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
key = "#{Digest::SHA256.hexdigest(source)}-#{MiniRacer::V8_CACHED_DATA_VERSION_TAG}"
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Notes:
|
|
212
|
+
|
|
213
|
+
- A `Script` is bound to the `Context` that compiled it; reusing it on another
|
|
214
|
+
Context isn't supported.
|
|
215
|
+
- `Script#dispose` frees the underlying V8 handle eagerly. The Ruby GC finalizer
|
|
216
|
+
does not (taking the V8 lock from a finalizer thread risks deadlock), so
|
|
217
|
+
long-lived Contexts with many short-lived scripts accumulate handles until
|
|
218
|
+
`Context#dispose` clears them.
|
|
219
|
+
- `produce_cache: true` is only safe at the top level. From inside a host-fn
|
|
220
|
+
callback (i.e., re-entrant compile while a JS → Ruby → JS frame is on the
|
|
221
|
+
stack) it raises `MiniRacer::RuntimeError`, because V8's `CreateCodeCache`
|
|
222
|
+
walks live isolate state and corrupts the parser when re-entered. Warm the
|
|
223
|
+
cache from the top level once and pass it back via `cached_data:` from your
|
|
224
|
+
callbacks.
|
|
225
|
+
- Cross-process reuse is **incompatible with `MiniRacer::Platform.set_flags!(:single_threaded)`**.
|
|
226
|
+
V8's single-threaded mode embeds process-local state in the cache blob, so
|
|
227
|
+
every cached_data is rejected when consumed in a fresh process. Same-process
|
|
228
|
+
reuse still works under `:single_threaded`. If you need both cross-process
|
|
229
|
+
reuse and `:single_threaded` (e.g. for fork-safety reasons), disable
|
|
230
|
+
`:single_threaded` for the path that produces / consumes the cache.
|
|
231
|
+
- On TruffleRuby, `Script` is implemented as source replay (GraalJS has no
|
|
232
|
+
equivalent per-script bytecode cache reachable from `Polyglot::InnerContext`),
|
|
233
|
+
so `cached_data` and `produce_cache` are silently ignored and `cached_data`
|
|
234
|
+
always returns `nil`, and `MiniRacer::V8_CACHED_DATA_VERSION_TAG` is `0`.
|
|
235
|
+
|
|
236
|
+
### Fork Safety
|
|
237
|
+
|
|
238
|
+
Some Ruby web servers employ forking (for example unicorn or puma in clustered mode). V8 is not fork safe by default and sadly Ruby does not have support for fork notifications per [#5446](https://bugs.ruby-lang.org/issues/5446).
|
|
239
|
+
|
|
240
|
+
Since 0.6.1 mini_racer does support V8 single threaded platform mode which should remove most forking related issues. To enable run this before using `MiniRacer::Context`, for example in a Rails initializer:
|
|
241
|
+
|
|
242
|
+
```ruby
|
|
243
|
+
MiniRacer::Platform.set_flags!(:single_threaded)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
When using pre-fork `MiniRacer::Context` objects in `:single_threaded` mode,
|
|
247
|
+
ensure the process only forks while MiniRacer is quiescent: no thread may be
|
|
248
|
+
evaluating JavaScript, calling into a context, disposing/freeing a context,
|
|
249
|
+
running a Ruby callback from JavaScript, or otherwise using MiniRacer at the
|
|
250
|
+
instant of `fork`. In multi-threaded applications, guard all MiniRacer context
|
|
251
|
+
operations and the `fork` itself with the same application-level lock. Forking
|
|
252
|
+
while a MiniRacer operation is in progress can leave inherited pthread mutexes
|
|
253
|
+
in an unusable state in the child process.
|
|
254
|
+
|
|
255
|
+
If you want to ensure your application does not leak memory after fork either:
|
|
256
|
+
|
|
257
|
+
1. Ensure no `MiniRacer::Context` objects are created in the master process; or
|
|
258
|
+
2. Dispose manually of all `MiniRacer::Context` objects prior to forking
|
|
259
|
+
|
|
260
|
+
```ruby
|
|
261
|
+
# before fork
|
|
262
|
+
|
|
263
|
+
require "objspace"
|
|
264
|
+
ObjectSpace.each_object(MiniRacer::Context){|c| c.dispose}
|
|
265
|
+
|
|
266
|
+
# fork here
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Threadsafe
|
|
270
|
+
|
|
271
|
+
Context usage is threadsafe
|
|
272
|
+
|
|
273
|
+
```ruby
|
|
274
|
+
context = MiniRacer::Context.new
|
|
275
|
+
context.eval("counter=0; plus=()=>counter++;")
|
|
276
|
+
|
|
277
|
+
(1..10).map do
|
|
278
|
+
Thread.new {
|
|
279
|
+
context.eval("plus()")
|
|
280
|
+
}
|
|
281
|
+
end.each(&:join)
|
|
282
|
+
|
|
283
|
+
puts context.eval("counter")
|
|
284
|
+
# => 10
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Snapshots
|
|
288
|
+
|
|
289
|
+
Contexts can be created with pre-loaded snapshots:
|
|
290
|
+
|
|
291
|
+
```ruby
|
|
292
|
+
snapshot = MiniRacer::Snapshot.new("function hello() { return 'world!'; }")
|
|
293
|
+
|
|
294
|
+
context = MiniRacer::Context.new(snapshot: snapshot)
|
|
295
|
+
|
|
296
|
+
context.eval("hello()")
|
|
297
|
+
# => "world!"
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Snapshots can come in handy for example if you want your contexts to be pre-loaded for efficiency. It uses [V8 snapshots](http://v8project.blogspot.com/2015/09/custom-startup-snapshots.html) under the hood; see [this link](http://v8project.blogspot.com/2015/09/custom-startup-snapshots.html) for caveats using these, in particular:
|
|
301
|
+
|
|
302
|
+
> There is an important limitation to snapshots: they can only capture V8’s
|
|
303
|
+
> heap. Any interaction from V8 with the outside is off-limits when creating the
|
|
304
|
+
> snapshot. Such interactions include:
|
|
305
|
+
>
|
|
306
|
+
> * defining and calling API callbacks (i.e. functions created via v8::FunctionTemplate)
|
|
307
|
+
> * creating typed arrays, since the backing store may be allocated outside of V8
|
|
308
|
+
>
|
|
309
|
+
> And of course, values derived from sources such as `Math.random` or `Date.now`
|
|
310
|
+
> are fixed once the snapshot has been captured. They are no longer really random
|
|
311
|
+
> nor reflect the current time.
|
|
312
|
+
|
|
313
|
+
Also note that snapshots can be warmed up, using the `warmup!` method, which allows you to call functions which are otherwise lazily compiled to get them to compile right away; any side effect of your warm up code being then dismissed. [More details on warming up here](https://github.com/electron/electron/issues/169#issuecomment-76783481), and a small example:
|
|
314
|
+
|
|
315
|
+
```ruby
|
|
316
|
+
snapshot = MiniRacer::Snapshot.new("var counter = 0; function hello() { counter++; return 'world! '; }")
|
|
317
|
+
|
|
318
|
+
snapshot.warmup!("hello()")
|
|
319
|
+
|
|
320
|
+
context = MiniRacer::Context.new(snapshot: snapshot)
|
|
321
|
+
|
|
322
|
+
context.eval("hello()")
|
|
323
|
+
# => "world! 1"
|
|
324
|
+
context.eval("counter")
|
|
325
|
+
# => 1
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Snapshots can also be persisted to disk for faster startup:
|
|
329
|
+
|
|
330
|
+
```ruby
|
|
331
|
+
# Save a snapshot to disk
|
|
332
|
+
snapshot = MiniRacer::Snapshot.new('var foo = "bar";')
|
|
333
|
+
File.binwrite("snapshot.bin", snapshot.dump)
|
|
334
|
+
|
|
335
|
+
# Load it back in a later process
|
|
336
|
+
blob = File.binread("snapshot.bin")
|
|
337
|
+
snapshot = MiniRacer::Snapshot.load(blob)
|
|
338
|
+
context = MiniRacer::Context.new(snapshot: snapshot)
|
|
339
|
+
context.eval("foo")
|
|
340
|
+
# => "bar"
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Note that snapshots are architecture and V8-version specific. A snapshot created on one platform (e.g., ARM64 macOS) cannot be loaded on a different platform (e.g., x86_64 Linux). Snapshots are best used for same-machine caching or homogeneous deployment environments.
|
|
344
|
+
|
|
345
|
+
**Security note:** Only load snapshots from trusted sources. V8 snapshots are not designed to be safely loaded from untrusted input—malformed or malicious snapshot data may cause crashes or memory corruption.
|
|
346
|
+
|
|
347
|
+
### Garbage collection
|
|
348
|
+
|
|
349
|
+
You can make the garbage collector more aggressive by defining the context with `MiniRacer::Context.new(ensure_gc_after_idle: 1000)`. Using this will ensure V8 will run a full GC using `context.low_memory_notification` 1 second after the last eval on the context. Low memory notifications ensure long living contexts use minimal amounts of memory.
|
|
350
|
+
|
|
351
|
+
### V8 Runtime flags
|
|
352
|
+
|
|
353
|
+
It is possible to set V8 Runtime flags:
|
|
354
|
+
|
|
355
|
+
```ruby
|
|
356
|
+
MiniRacer::Platform.set_flags! :noconcurrent_recompilation, max_inlining_levels: 10
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
This can come in handy if you want to use MiniRacer with Unicorn, which doesn't seem to always appreciate V8's liberal use of threading:
|
|
360
|
+
|
|
361
|
+
```ruby
|
|
362
|
+
MiniRacer::Platform.set_flags! :noconcurrent_recompilation, :noconcurrent_sweeping
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
Or else to unlock experimental features in V8, for example tail recursion optimization:
|
|
366
|
+
|
|
367
|
+
```ruby
|
|
368
|
+
MiniRacer::Platform.set_flags! :harmony
|
|
369
|
+
|
|
370
|
+
js = <<-JS
|
|
371
|
+
'use strict';
|
|
372
|
+
var f = function f(n){
|
|
373
|
+
if (n <= 0) {
|
|
374
|
+
return 'foo';
|
|
375
|
+
}
|
|
376
|
+
return f(n - 1);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
f(1e6);
|
|
380
|
+
JS
|
|
381
|
+
|
|
382
|
+
context = MiniRacer::Context.new
|
|
383
|
+
|
|
384
|
+
context.eval js
|
|
385
|
+
# => "foo"
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
The same code without the harmony runtime flag results in a `MiniRacer::RuntimeError: RangeError: Maximum call stack size exceeded` exception.
|
|
389
|
+
Please refer to http://node.green/ as a reference on other harmony features.
|
|
390
|
+
|
|
391
|
+
A list of all V8 runtime flags can be found using `node --v8-options`, or else by perusing [the V8 source code for flags (make sure to use the right version of V8)](https://github.com/v8/v8/blob/master/src/flags/flag-definitions.h).
|
|
392
|
+
|
|
393
|
+
Note that runtime flags must be set before any other operation (e.g. creating a context or a snapshot), otherwise an exception will be thrown.
|
|
394
|
+
|
|
395
|
+
Flags:
|
|
396
|
+
|
|
397
|
+
* `:expose_gc`: Will expose `gc()` which you can run in JavaScript to issue a GC run.
|
|
398
|
+
* `:max_old_space_size`: defaults to 1400 (megs) on 64 bit, you can restrict memory usage by limiting this.
|
|
399
|
+
|
|
400
|
+
**NOTE TO READER** our documentation could be awesome we could be properly documenting all the flags, they are hugely useful, if you feel like documenting a few more, PLEASE DO, PRs are welcome.
|
|
401
|
+
|
|
402
|
+
## Controlling memory
|
|
403
|
+
|
|
404
|
+
When hosting v8 you may want to keep track of memory usage, use `#heap_stats` to get memory usage:
|
|
405
|
+
|
|
406
|
+
```ruby
|
|
407
|
+
context = MiniRacer::Context.new
|
|
408
|
+
# use context
|
|
409
|
+
p context.heap_stats
|
|
410
|
+
# {:total_physical_size=>1280640,
|
|
411
|
+
# :total_heap_size_executable=>4194304,
|
|
412
|
+
# :total_heap_size=>3100672,
|
|
413
|
+
# :used_heap_size=>1205376,
|
|
414
|
+
# :heap_size_limit=>1501560832}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
If you wish to dispose of a context before waiting on the GC use `#dispose`:
|
|
418
|
+
|
|
419
|
+
```ruby
|
|
420
|
+
context = MiniRacer::Context.new
|
|
421
|
+
context.eval("let a='testing';")
|
|
422
|
+
context.dispose
|
|
423
|
+
context.eval("a = 2")
|
|
424
|
+
# MiniRacer::ContextDisposedError
|
|
425
|
+
|
|
426
|
+
# nothing works on the context from now on, it's a shell waiting to be disposed
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
A MiniRacer context can also be dumped in a heapsnapshot file using `#write_heap_snapshot(file_or_io)`
|
|
430
|
+
|
|
431
|
+
```ruby
|
|
432
|
+
context = MiniRacer::Context.new
|
|
433
|
+
# use context
|
|
434
|
+
context.write_heap_snapshot("test.heapsnapshot")
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
This file can then be loaded in the "memory" tab of the [Chrome DevTools](https://developer.chrome.com/docs/devtools/memory-problems/heap-snapshots/#view_snapshots).
|
|
438
|
+
|
|
439
|
+
### Function call
|
|
440
|
+
|
|
441
|
+
This calls the function passed as first argument:
|
|
442
|
+
|
|
443
|
+
```ruby
|
|
444
|
+
context = MiniRacer::Context.new
|
|
445
|
+
context.eval("function hello(name) { return `Hello, ${name}!` }")
|
|
446
|
+
context.call("hello", "George")
|
|
447
|
+
# "Hello, George!"
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
Performance is slightly better than running `context.eval("hello('George')")` since:
|
|
451
|
+
|
|
452
|
+
* compilation of eval'd string is avoided
|
|
453
|
+
* function arguments don't need to be converted to JSON
|
|
454
|
+
|
|
455
|
+
### Microtask checkpoints
|
|
456
|
+
|
|
457
|
+
V8 drains its microtask queue (e.g. callbacks queued via `Promise.resolve().then(...)`) automatically when script execution returns to the embedder, so most code "just works":
|
|
458
|
+
|
|
459
|
+
```ruby
|
|
460
|
+
context = MiniRacer::Context.new
|
|
461
|
+
context.eval(<<~JS)
|
|
462
|
+
let x = 0;
|
|
463
|
+
Promise.resolve().then(() => x = 99);
|
|
464
|
+
JS
|
|
465
|
+
context.eval("x")
|
|
466
|
+
# => 99
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
When JavaScript invokes a Ruby callback synchronously and you need queued microtasks to drain mid-execution — e.g. for spec-compliant ordering across a chain of synchronous `dispatchEvent` listeners — call `context.perform_microtask_checkpoint` from the callback:
|
|
470
|
+
|
|
471
|
+
```ruby
|
|
472
|
+
context = MiniRacer::Context.new
|
|
473
|
+
context.attach("drain", -> { context.perform_microtask_checkpoint })
|
|
474
|
+
context.eval(<<~JS)
|
|
475
|
+
globalThis.log = [];
|
|
476
|
+
Promise.resolve().then(() => log.push("microtask"));
|
|
477
|
+
log.push("before");
|
|
478
|
+
drain();
|
|
479
|
+
log.push("after");
|
|
480
|
+
JS
|
|
481
|
+
context.eval("log")
|
|
482
|
+
# => ["before", "microtask", "after"]
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
Without `drain()` the order would be `["before", "after", "microtask"]` because the microtask only runs once the outermost script returns. `perform_microtask_checkpoint` is a thin wrapper over V8's `MicrotasksScope::PerformCheckpoint`.
|
|
486
|
+
|
|
487
|
+
When the drain has to happen from within JavaScript itself — for example between each listener in a synchronous `dispatchEvent` chain — the same checkpoint is available to JS as `drainMicrotasks()`. It runs inline on the V8 thread without the Ruby ↔ V8 round-trip, so no `attach` is required.
|
|
488
|
+
|
|
489
|
+
It is exposed through an opt-in **host namespace** — a single object (in the spirit of Deno's `Deno` or Bun's `Bun`) that mini_racer hangs its non-standard helpers off. Pass `host_namespace:` to enable it; by default nothing is injected and the global stays clean:
|
|
490
|
+
|
|
491
|
+
```ruby
|
|
492
|
+
context = MiniRacer::Context.new(host_namespace: "MiniRacer")
|
|
493
|
+
context.eval(<<~JS)
|
|
494
|
+
globalThis.log = [];
|
|
495
|
+
Promise.resolve().then(() => log.push("microtask"));
|
|
496
|
+
log.push("before");
|
|
497
|
+
MiniRacer.drainMicrotasks();
|
|
498
|
+
log.push("after");
|
|
499
|
+
JS
|
|
500
|
+
context.eval("log")
|
|
501
|
+
# => ["before", "microtask", "after"]
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
`host_namespace:` accepts a String (the global name to use — it must be a valid JavaScript identifier), `true` (the default name `"MiniRacer"`), or `nil`/`false` (the default — inject nothing). The namespace object is defined non-enumerable so it does not appear in `Object.keys(globalThis)`, while its methods are ordinary properties discoverable via `Object.keys(MiniRacer)`. Like `perform_microtask_checkpoint`, `drainMicrotasks()` is a no-op while a microtask checkpoint is already in progress, and it lets watchdog/out-of-memory termination propagate to the enclosing `eval`/`call`. (The host namespace is V8-only; it is not installed on the TruffleRuby backend.)
|
|
505
|
+
|
|
506
|
+
### ES modules
|
|
507
|
+
|
|
508
|
+
`Context#compile_module` exposes V8's ES module API for code that uses
|
|
509
|
+
`import` / `export` syntax. Unlike `eval` (which only accepts script-level
|
|
510
|
+
syntax), modules can have static imports that resolve to other modules and
|
|
511
|
+
expose named exports through a real Module Namespace Object.
|
|
512
|
+
|
|
513
|
+
```ruby
|
|
514
|
+
context = MiniRacer::Context.new
|
|
515
|
+
|
|
516
|
+
dep = context.compile_module("export const base = 10", filename: "dep.js")
|
|
517
|
+
main = context.compile_module(<<~JS, filename: "main.js")
|
|
518
|
+
import { base } from 'dep'
|
|
519
|
+
export const doubled = base * 2
|
|
520
|
+
JS
|
|
521
|
+
|
|
522
|
+
main.instantiate {|specifier, referrer| dep } # called once per static import
|
|
523
|
+
dep.evaluate
|
|
524
|
+
main.evaluate
|
|
525
|
+
|
|
526
|
+
main.namespace # => {"doubled" => 20}
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
* `Context#compile_module(source, filename:)` — parses the source as a
|
|
530
|
+
module; the returned `MiniRacer::Module` is bound to its Context. The
|
|
531
|
+
`filename` is also exposed to the module as `import.meta.url`.
|
|
532
|
+
* `Module#instantiate { |specifier, referrer_url| ... }` — walks the static
|
|
533
|
+
import graph. The resolver block is called once per import declaration
|
|
534
|
+
with the raw specifier string and the importing module's filename, so
|
|
535
|
+
relative specifiers (`./foo`, `../bar`) can be resolved against the
|
|
536
|
+
referrer. It must return another `MiniRacer::Module` (typically from a
|
|
537
|
+
per-Context cache). Imports can also be resolved lazily from inside the
|
|
538
|
+
block via further `Context#compile_module` calls.
|
|
539
|
+
* `Module#evaluate` — runs the module body. Returns the evaluation result
|
|
540
|
+
(`nil` for the typical `export const …` shape). Modules with top-level
|
|
541
|
+
`await` raise `MiniRacer::RuntimeError` for now.
|
|
542
|
+
* `Module#namespace` — returns the Module Namespace Object as a Hash
|
|
543
|
+
(`{ "default" => …, "namedExport" => … }`). Available after
|
|
544
|
+
`instantiate` succeeds; `evaluate` populates the values.
|
|
545
|
+
* `Module#status` — one of `:uninstantiated`, `:instantiating`,
|
|
546
|
+
`:instantiated`, `:evaluating`, `:evaluated`, `:errored`.
|
|
547
|
+
* `Module#dispose` / `Module#disposed?` — eager handle release, mirroring
|
|
548
|
+
the convention used elsewhere.
|
|
549
|
+
* `Context#dynamic_import_resolver = proc { |specifier, referrer_url| ... }`
|
|
550
|
+
— handler for JS `import(...)` expressions. The proc must return a
|
|
551
|
+
`MiniRacer::Module` (already instantiated; `evaluate` is driven for you
|
|
552
|
+
if pending). Set to `nil` to reject all dynamic imports. Drain the
|
|
553
|
+
microtask queue with `Context#perform_microtask_checkpoint` to see the
|
|
554
|
+
result in a `.then` callback or after `await`.
|
|
555
|
+
|
|
556
|
+
```ruby
|
|
557
|
+
context.dynamic_import_resolver = ->(spec, _ref) { cache.fetch(spec) }
|
|
558
|
+
context.eval(%(import('dep').then(ns => globalThis.r = ns.x)), filename: 'caller.js')
|
|
559
|
+
context.perform_microtask_checkpoint
|
|
560
|
+
context.eval('globalThis.r') # => 42
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
Notes:
|
|
564
|
+
|
|
565
|
+
- A `Module` is bound to the `Context` that compiled it; resolvers must
|
|
566
|
+
return modules from the same Context.
|
|
567
|
+
- `Module#dispose` frees the underlying V8 handle eagerly. The Ruby GC
|
|
568
|
+
finalizer does not (taking the V8 lock from a finalizer thread risks
|
|
569
|
+
deadlock), so long-lived Contexts with many short-lived modules
|
|
570
|
+
accumulate handles until `Context#dispose` clears them.
|
|
571
|
+
- Top-level await is not yet supported; `evaluate` raises if the
|
|
572
|
+
module's evaluation promise stays pending after the microtask drain.
|
|
573
|
+
- On TruffleRuby, `Context#compile_module` raises `NotImplementedError`
|
|
574
|
+
— GraalJS has its own module-loading mechanism that doesn't map onto
|
|
575
|
+
this handle-based API. PRs to bridge are welcome.
|
|
576
|
+
|
|
577
|
+
## Performance
|
|
578
|
+
|
|
579
|
+
The `bench` folder contains benchmark.
|
|
580
|
+
|
|
581
|
+
### Benchmark minification of Discourse application.js (both minified and non-minified)
|
|
582
|
+
|
|
583
|
+
MiniRacer outperforms node when minifying assets via execjs.
|
|
584
|
+
|
|
585
|
+
* MiniRacer version 0.1.9
|
|
586
|
+
* node version 6.10
|
|
587
|
+
* therubyracer version 0.12.2
|
|
588
|
+
|
|
589
|
+
```terminal
|
|
590
|
+
$ bundle exec ruby bench.rb mini_racer
|
|
591
|
+
Benching with mini_racer
|
|
592
|
+
mini_racer minify discourse_app.js 9292.72063ms
|
|
593
|
+
mini_racer minify discourse_app_minified.js 11799.850171ms
|
|
594
|
+
mini_racer minify discourse_app.js twice (2 threads) 10269.570797ms
|
|
595
|
+
|
|
596
|
+
sam@ubuntu exec_js_uglify % bundle exec ruby bench.rb node
|
|
597
|
+
Benching with node
|
|
598
|
+
node minify discourse_app.js 13302.715484ms
|
|
599
|
+
node minify discourse_app_minified.js 18100.761243ms
|
|
600
|
+
node minify discourse_app.js twice (2 threads) 14383.600207000001ms
|
|
601
|
+
|
|
602
|
+
sam@ubuntu exec_js_uglify % bundle exec ruby bench.rb therubyracer
|
|
603
|
+
Benching with therubyracer
|
|
604
|
+
therubyracer minify discourse_app.js 171683.01867700001ms
|
|
605
|
+
therubyracer minify discourse_app_minified.js 143138.88492ms
|
|
606
|
+
therubyracer minify discourse_app.js twice (2 threads) NEVER FINISH
|
|
607
|
+
|
|
608
|
+
Killed: 9
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
The huge performance disparity (MiniRacer is 10x faster) is due to MiniRacer running latest version of V8. In July 2016 there is a queued upgrade to therubyracer which should bring some of the perf inline.
|
|
612
|
+
|
|
613
|
+
Note how the global interpreter lock release leads to 2 threads doing the same work taking the same wall time as 1 thread.
|
|
614
|
+
|
|
615
|
+
As a rule MiniRacer strives to always support and depend on the latest stable version of libv8.
|
|
616
|
+
|
|
617
|
+
## Source Maps
|
|
618
|
+
|
|
619
|
+
MiniRacer can fully support source maps but must be configured correctly to do so. [Check out this example](./examples/source-map-support/) for a working implementation.
|
|
620
|
+
|
|
621
|
+
## Installation
|
|
622
|
+
|
|
623
|
+
Add this line to your application's Gemfile:
|
|
624
|
+
|
|
625
|
+
```ruby
|
|
626
|
+
gem "mini_racer"
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
And then execute:
|
|
630
|
+
|
|
631
|
+
```terminal
|
|
632
|
+
$ bundle
|
|
633
|
+
|
|
634
|
+
Or install it yourself as:
|
|
635
|
+
|
|
636
|
+
```terminal
|
|
637
|
+
$ gem install mini_racer
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
**Note** using v8.h and compiling MiniRacer requires a C++20 capable compiler.
|
|
641
|
+
gcc >= 12.2 and Xcode >= 13 are, at the time of writing, known to work.
|
|
642
|
+
|
|
643
|
+
## Similar Projects
|
|
644
|
+
|
|
645
|
+
### therubyracer
|
|
646
|
+
|
|
647
|
+
* https://github.com/cowboyd/therubyracer
|
|
648
|
+
* Most comprehensive bridge available
|
|
649
|
+
* Provides the ability to "eval" JavaScript
|
|
650
|
+
* Provides the ability to invoke Ruby code from JavaScript
|
|
651
|
+
* Hold references to JavaScript objects and methods in your Ruby code
|
|
652
|
+
* Hold references to Ruby objects and methods in JavaScript code
|
|
653
|
+
* Uses libv8, so installation is fast
|
|
654
|
+
* Supports timeouts for JavaScript execution
|
|
655
|
+
* Does not release global interpreter lock, so performance is constrained to a single thread
|
|
656
|
+
* Currently (May 2016) only supports v8 version 3.16.14 (Released approx November 2013), plans to upgrade by July 2016
|
|
657
|
+
* Supports execjs
|
|
658
|
+
|
|
659
|
+
### v8eval
|
|
660
|
+
|
|
661
|
+
* https://github.com/sony/v8eval
|
|
662
|
+
* Provides the ability to "eval" JavaScript using the latest V8 engine
|
|
663
|
+
* Does not depend on the [libv8](https://github.com/cowboyd/libv8) gem, installation can take 10-20 mins as V8 needs to be downloaded and compiled.
|
|
664
|
+
* Does not release global interpreter lock when executing JavaScript
|
|
665
|
+
* Does not allow you to invoke Ruby code from JavaScript
|
|
666
|
+
* Multi runtime support due to SWIG based bindings
|
|
667
|
+
* Supports a JavaScript debugger
|
|
668
|
+
* Does not support timeouts for JavaScript execution
|
|
669
|
+
* No support for execjs (can not be used with Rails uglifier and coffeescript gems)
|
|
670
|
+
|
|
671
|
+
### therubyrhino
|
|
672
|
+
|
|
673
|
+
* https://github.com/cowboyd/therubyrhino
|
|
674
|
+
* API compatible with therubyracer
|
|
675
|
+
* Uses Mozilla's Rhino engine https://github.com/mozilla/rhino
|
|
676
|
+
* Requires JRuby
|
|
677
|
+
* Support for timeouts for JavaScript execution
|
|
678
|
+
* Concurrent cause .... JRuby
|
|
679
|
+
* Supports execjs
|
|
680
|
+
|
|
681
|
+
## Contributing
|
|
682
|
+
|
|
683
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/rubyjs/mini_racer. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
|
684
|
+
|
|
685
|
+
## License
|
|
686
|
+
|
|
687
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|