mini_racer_rs 1.0.0 → 1.0.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 41191c7e0a6bf53c0371de7ba31f7067b3d83b0095579d6955a75a9021c4a5b0
4
- data.tar.gz: 7b0424bbabd9fcd45122dc81b6b449e3c30d7ffb218ec43eff973ae4d8c70772
3
+ metadata.gz: 1b53466394528b78785dc0cac7e884a5b59f0200ca037d405c95375152a12d72
4
+ data.tar.gz: 06b9b34066ec568f0a41186f5f7c533c3a17f7acb90a625ec8e9fff22b3069f5
5
5
  SHA512:
6
- metadata.gz: b864cac2b4d1f3108deea75e4bc5a10de16c94ce73be5c5824f7443922a86d94f6d1ff933a1008632c971f1eafdef57a452631661bc82807799b339ff7e15ca5
7
- data.tar.gz: '0391a874e20db2b91f6c1916efa0c39f05178b68e9223dc2709aeb2fa6ad6026de9bba898f8bea5f8764193df36c0291fb8af8afab738029db3ef4a537354bb4'
6
+ metadata.gz: 7e9f4ee092b37374b56a701831bd4ba868af74de863c220a9f58aae583505cc3c4000106107cfe8c8e709552863d5b484b8715cd016306ecb06241f2d84fe759
7
+ data.tar.gz: d5dc38c171b8d11683d53840d6c671cbf90ae042dba02f1a46e9786f343e555356808e41e2fb460b00a440c326a2da23f737b1149ab96d359b4e2269981c8b36
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.2](https://github.com/andymai/mini_racer_rs/compare/v1.0.1...v1.0.2) (2026-10-18)
4
+
5
+
6
+ ### Performance
7
+
8
+ * **engine:** share one copy of a large repeated source across contexts ([#85](https://github.com/andymai/mini_racer_rs/issues/85)) ([9fe050a](https://github.com/andymai/mini_racer_rs/commit/9fe050a7a612314264e255f9bcc40ca146e93571))
9
+
10
+ ## [1.0.1](https://github.com/andymai/mini_racer_rs/compare/v1.0.0...v1.0.1) (2026-10-18)
11
+
12
+
13
+ ### Performance
14
+
15
+ * **engine:** build small call arguments in the runner and write primitive results directly ([#83](https://github.com/andymai/mini_racer_rs/issues/83)) ([76afcbd](https://github.com/andymai/mini_racer_rs/commit/76afcbd7bb5df36da6797b95d8dae7049ee80f53))
16
+
3
17
  ## 1.0.0 (2026-10-18)
4
18
 
5
19
 
data/COMPATIBILITY.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  | Engine | Done | Planned | Deviation |
6
6
  |---|---|---|---|
7
- | CRuby | 54 | 0 | 25 |
7
+ | CRuby | 54 | 0 | 26 |
8
8
  | TruffleRuby | 58 | 0 | 0 |
9
9
 
10
10
  ## Surface
@@ -180,6 +180,12 @@ mini_racer_rs supports glibc Linux on x86_64 and aarch64, plus macOS on arm64 an
180
180
 
181
181
  Approved 2026-10-16.
182
182
 
183
+ ### `behavior:shared-source-heap-stats@ruby`
184
+
185
+ `Context#heap_stats` can report large source text differently from mini_racer 0.22.1. When a context evaluates a source of 1 MiB or more that the process has evaluated before under the same file name and startup snapshot, mini_racer_rs gives V8 one copy shared by every such context and held outside V8's heap. It reports that source in `:external_memory` and omits it from `:used_heap_size`. V8 counts the shared source by its length in characters, so two-byte text reports half its bytes. mini_racer 0.22.1 reports `:external_memory` as 0 and counts each context's own copy in `:used_heap_size`. JavaScript sees the same source text in both gems. Contexts using `max_memory:` and sources that are not valid UTF-8 retain individual copies. Setting `MINI_RACER_RS_CODE_CACHE=off` before creating the first context disables sharing and makes `heap_stats` count every source as mini_racer does.
186
+
187
+ Approved 2026-10-18.
188
+
183
189
  ### `behavior:snapshot-filter-context@ruby`
184
190
 
185
191
  mini_racer_rs returns the same filtered results from a snapshot-based context as from a context without a snapshot. In a context made from `MiniRacer::Snapshot.new("Object.getOwnPropertyDescriptors = () => ({})")`, `({a: 1, f() {}})` returns `{"a" => 1}`, and `[1, function() {}]` returns `[1]`. mini_racer 0.22.1 returns `nil` for both. Both gems copy results that V8 refuses to serialize as is through a filter in a separate safe context. mini_racer_rs keeps an untouched context in each snapshot blob and uses it for that filter, while mini_racer 0.22.1 builds the safe context from the snapshot, exposing the filter's built-ins to snapshot code. User code still sees the replacement in both gems: `Object.getOwnPropertyDescriptors({a: 1})` returns `{}`.
data/Cargo.lock CHANGED
@@ -458,7 +458,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
458
458
 
459
459
  [[package]]
460
460
  name = "mini_racer_engine"
461
- version = "1.0.0"
461
+ version = "1.0.2"
462
462
  dependencies = [
463
463
  "deno_core_icudata",
464
464
  "libc",
@@ -470,7 +470,7 @@ dependencies = [
470
470
 
471
471
  [[package]]
472
472
  name = "mini_racer_rs"
473
- version = "1.0.0"
473
+ version = "1.0.2"
474
474
  dependencies = [
475
475
  "magnus",
476
476
  "mini_racer_engine",
@@ -481,7 +481,7 @@ dependencies = [
481
481
 
482
482
  [[package]]
483
483
  name = "mini_racer_sync"
484
- version = "1.0.0"
484
+ version = "1.0.2"
485
485
  dependencies = [
486
486
  "loom",
487
487
  "thiserror",
@@ -489,7 +489,7 @@ dependencies = [
489
489
 
490
490
  [[package]]
491
491
  name = "mini_racer_wire"
492
- version = "1.0.0"
492
+ version = "1.0.2"
493
493
  dependencies = [
494
494
  "proptest",
495
495
  "thiserror",
data/Cargo.toml CHANGED
@@ -14,7 +14,7 @@ license = "MIT"
14
14
  publish = false
15
15
  repository = "https://github.com/andymai/mini_racer_rs"
16
16
  rust-version = "1.91"
17
- version = "1.0.0"
17
+ version = "1.0.2"
18
18
 
19
19
  [workspace.dependencies]
20
20
  deno_core_icudata = "=0.78.0"
data/README.md CHANGED
@@ -17,7 +17,7 @@ A drop-in replacement for mini_racer, implemented in Rust on V8 15.2.
17
17
 
18
18
  mini_racer_rs implements the `MiniRacer` Ruby API from mini_racer 0.22.1, with faster Ruby and JavaScript round trips, V8 15.2, prebuilt native gems, fiber scheduler support, and safer handling of difficult values. It defines the `MiniRacer` namespace and ships `lib/mini_racer.rb`, so `require "mini_racer"` and ExecJS continue to work.
19
19
 
20
- > **Pre-release:** Nothing is published to RubyGems.org yet, and the release workflow is held. Install from the repository until the first release. mini_racer_rs is an independent project and is not affiliated with the Discourse staff who maintain mini_racer.
20
+ > mini_racer_rs is an independent project and is not affiliated with the Discourse staff who maintain mini_racer.
21
21
 
22
22
  ## Highlights
23
23
 
@@ -46,7 +46,7 @@ The package switch is one Gemfile line:
46
46
 
47
47
  Run `bundle install`. Bundler is required because RubyGems may otherwise activate the original `mini_racer` first. Loading both gems raises a `LoadError` that names the conflict. Reverse the Gemfile change to switch back.
48
48
 
49
- Until the first release publishes native gems, build from a checkout as described in [CONTRIBUTING.md](CONTRIBUTING.md). Run `bin/setup`. Source builds require Rust 1.91 or newer and libclang. Once published, installing a native gem will compile nothing.
49
+ On x86_64-linux-gnu, aarch64-linux-gnu, arm64-darwin, and x86_64-darwin, `bundle install` installs a native gem and compiles nothing. Elsewhere, the source gem builds the extension and requires Rust 1.91 or newer and libclang. To build from a checkout, follow [CONTRIBUTING.md](CONTRIBUTING.md) and run `bin/setup`.
50
50
 
51
51
  ## Quick start
52
52
 
@@ -143,8 +143,6 @@ See [COMPATIBILITY.md](COMPATIBILITY.md) for every difference.
143
143
 
144
144
  - You deploy on musl-based Linux such as Alpine. mini_racer supports it through libv8-node's musl gems, while mini_racer_rs does not install there. Neither gem supports Windows.
145
145
 
146
- - You need a gem from RubyGems.org today. mini_racer_rs is pre-release, while mini_racer is the established gem maintained by Discourse staff.
147
-
148
146
  - Download size matters more than compile time. The largest mini_racer_rs native gem is 65.6 MB because it includes extensions for three Ruby versions. The mini_racer download on x86_64 Linux is 47.1 MB, followed by compilation.
149
147
 
150
148
  ### Gems that depend on mini_racer
@@ -185,7 +183,7 @@ bundle exec ruby bench/run.rb --cpus 0-7
185
183
 
186
184
  | Engine | Done | Planned | Deviation |
187
185
  |---|---|---|---|
188
- | CRuby | 54 | 0 | 25 |
186
+ | CRuby | 54 | 0 | 26 |
189
187
  | TruffleRuby | 58 | 0 | 0 |
190
188
 
191
189
  See [COMPATIBILITY.md](COMPATIBILITY.md).
@@ -8,7 +8,9 @@ use mini_racer_sync::{
8
8
  Channel, ChannelError, DispatchKind, Inherited, Interrupt, Phase, Side, TermError, ToRuby, lock,
9
9
  };
10
10
 
11
- use crate::control::{CallbackReply, ContextConfig, ControlBlock, Depth, JsText, Request, Wire};
11
+ use crate::control::{
12
+ CallRequest, CallbackReply, ContextConfig, ControlBlock, Depth, JsText, Request, Wire,
13
+ };
12
14
  use crate::notify::NotifyFd;
13
15
  use crate::process::fork::{self, Generation};
14
16
  use crate::process::platform::Mode;
@@ -287,16 +289,14 @@ impl ContextHandle {
287
289
  )
288
290
  }
289
291
 
290
- /// `request` is the wire form `mini_racer` sends: one array holding the
291
- /// function's dotted path, then its arguments.
292
- pub fn call(&self, request: Vec<u8>, host: &mut impl Host) -> Result<Vec<u8>, EngineError> {
292
+ pub fn call(&self, request: CallRequest, host: &mut impl Host) -> Result<Vec<u8>, EngineError> {
293
293
  self.invoke(request, false, host)
294
294
  }
295
295
 
296
296
  /// `call`, then awaits a promise result, as `eval_await` does.
297
297
  pub fn call_await(
298
298
  &self,
299
- request: Vec<u8>,
299
+ request: CallRequest,
300
300
  host: &mut impl Host,
301
301
  ) -> Result<Vec<u8>, EngineError> {
302
302
  self.invoke(request, true, host)
@@ -304,7 +304,7 @@ impl ContextHandle {
304
304
 
305
305
  fn invoke(
306
306
  &self,
307
- request: Vec<u8>,
307
+ request: CallRequest,
308
308
  awaited: bool,
309
309
  host: &mut impl Host,
310
310
  ) -> Result<Vec<u8>, EngineError> {
@@ -784,6 +784,11 @@ fn adopt(inherited: &ControlBlock, carried: Inherited) -> Held {
784
784
  let block = Arc::new(ControlBlock::with_tasks(inherited.config.clone(), tasks));
785
785
  *lock(&block.custody) = Some(isolate);
786
786
  block.term.adopt(carried);
787
+ // The isolate keeps whatever termination V8 still held for it.
788
+ block.termination_requested.store(
789
+ inherited.termination_requested.load(Ordering::Acquire),
790
+ Ordering::Release,
791
+ );
787
792
  match start(&block, generation, Start::Adopted) {
788
793
  Ok(()) => Held::Block(block),
789
794
  Err(_) => orphaned(true, false),
@@ -1,3 +1,4 @@
1
+ use std::sync::atomic::{AtomicBool, Ordering};
1
2
  use std::sync::{Arc, Mutex, OnceLock};
2
3
  use std::time::{Duration, Instant};
3
4
 
@@ -66,6 +67,17 @@ impl From<&str> for JsText {
66
67
  }
67
68
  }
68
69
 
70
+ /// A call in the wire form `mini_racer` sends: one array holding the
71
+ /// function's dotted path, then its arguments.
72
+ ///
73
+ /// `objects` and `properties` count the objects the arguments hold and their
74
+ /// properties in all, which decide whether the runner builds the arguments.
75
+ pub struct CallRequest {
76
+ pub bytes: Vec<u8>,
77
+ pub objects: usize,
78
+ pub properties: usize,
79
+ }
80
+
69
81
  pub enum Request {
70
82
  Init {
71
83
  depth: Depth,
@@ -78,7 +90,7 @@ pub enum Request {
78
90
  awaited: bool,
79
91
  },
80
92
  Call {
81
- request: Vec<u8>,
93
+ request: CallRequest,
82
94
  depth: Depth,
83
95
  awaited: bool,
84
96
  },
@@ -117,13 +129,15 @@ pub enum Request {
117
129
  pub struct IsolateTerminator {
118
130
  handle: v8::IsolateHandle,
119
131
  tasks: Arc<Tasks>,
132
+ requested: Arc<AtomicBool>,
120
133
  }
121
134
 
122
135
  impl IsolateTerminator {
123
- pub fn new(handle: v8::IsolateHandle, tasks: &Arc<Tasks>) -> Self {
136
+ pub fn new(block: &ControlBlock, handle: v8::IsolateHandle) -> Self {
124
137
  Self {
125
138
  handle,
126
- tasks: Arc::clone(tasks),
139
+ tasks: Arc::clone(&block.tasks),
140
+ requested: Arc::clone(&block.termination_requested),
127
141
  }
128
142
  }
129
143
  }
@@ -132,12 +146,14 @@ impl Terminator for IsolateTerminator {
132
146
  // A runner awaiting a promise sleeps until a task is due; the ring wakes
133
147
  // it to find the termination.
134
148
  fn terminate(&self) {
149
+ self.requested.store(true, Ordering::Release);
135
150
  self.handle.terminate_execution();
136
151
  self.tasks.ring();
137
152
  }
138
153
 
139
154
  fn cancel(&self) {
140
155
  self.handle.cancel_terminate_execution();
156
+ self.requested.store(false, Ordering::Release);
141
157
  }
142
158
  }
143
159
 
@@ -178,6 +194,10 @@ pub struct ControlBlock {
178
194
  // V8's foreground tasks for this context's isolate, which the runner runs
179
195
  // while awaiting a promise or pumping.
180
196
  pub tasks: Arc<Tasks>,
197
+ /// Set before anything asks V8 to terminate this context's script, and
198
+ /// cleared only once V8 has cancelled every such request, so a runner
199
+ /// building values itself knows when V8's own reader would stop instead.
200
+ pub termination_requested: Arc<AtomicBool>,
181
201
  pub phase: RunnerPhase,
182
202
  pub isolate: OnceLock<v8::IsolateHandle>,
183
203
  // The isolate a snapshot is being built in on this runner, if any.
@@ -230,6 +250,7 @@ impl ControlBlock {
230
250
  timer: Mutex::default(),
231
251
  notify: NotifyFd::default(),
232
252
  tasks: Arc::new(tasks),
253
+ termination_requested: Arc::default(),
233
254
  phase: RunnerPhase::default(),
234
255
  isolate: OnceLock::new(),
235
256
  building: Mutex::new(None),
@@ -4,6 +4,7 @@ mod error;
4
4
  mod fast_calls;
5
5
  mod notify;
6
6
  pub mod process;
7
+ mod request;
7
8
  mod runner;
8
9
  mod serialize;
9
10
  mod snapshot;
@@ -15,6 +16,6 @@ mod spin;
15
16
  mod sys;
16
17
 
17
18
  pub use context::{ContextHandle, Host, Unparker, WaitHandle, WaitOutcome};
18
- pub use control::{CallbackReply, ContextConfig, JsText};
19
+ pub use control::{CallRequest, CallbackReply, ContextConfig, JsText};
19
20
  pub use error::{EngineError, FlagError, InvalidSnapshot, JsError, JsErrorKind, TerminationCause};
20
21
  pub use snapshot::{SnapshotBlob, WeakSnapshotBlob};
@@ -77,15 +77,26 @@ impl Drop for Building {
77
77
 
78
78
  /// How the runner compiles a large script.
79
79
  pub(crate) enum Plan {
80
- /// From source: a first sighting, a key being built elsewhere, a refused
81
- /// cache, or the cache is off.
82
- Compile,
80
+ /// From source: a first sighting or the cache is off, or with its key, a
81
+ /// key being built elsewhere or a refused cache.
82
+ Compile(Option<Key>),
83
83
  /// From source, then a cache made once the dispatch is over.
84
84
  Make(Building),
85
85
  /// From the entry, falling back to source if V8 refuses it.
86
86
  Consume(Arc<Entry>, Key),
87
87
  }
88
88
 
89
+ impl Plan {
90
+ /// The key of a script the process has seen before.
91
+ pub(crate) const fn key(&self) -> Option<&Key> {
92
+ match self {
93
+ Self::Compile(key) => key.as_ref(),
94
+ Self::Make(building) => Some(&building.key),
95
+ Self::Consume(_, key) => Some(key),
96
+ }
97
+ }
98
+ }
99
+
89
100
  impl CodeCache {
90
101
  pub(crate) fn plan(
91
102
  &'static self,
@@ -94,7 +105,7 @@ impl CodeCache {
94
105
  snapshot: Option<u64>,
95
106
  ) -> Plan {
96
107
  if !enabled() {
97
- return Plan::Compile;
108
+ return Plan::Compile(None);
98
109
  }
99
110
  let sighting = self.sighting(filename, source, snapshot);
100
111
  let seen = touch(&self.sightings, sighting, MAX_SIGHTINGS);
@@ -103,7 +114,7 @@ impl CodeCache {
103
114
  .iter()
104
115
  .any(|entry| entry.key.sighting == sighting);
105
116
  if !seen && !cached {
106
- return Plan::Compile;
117
+ return Plan::Compile(None);
107
118
  }
108
119
  let key = Key {
109
120
  filename: filename.clone(),
@@ -121,9 +132,10 @@ impl CodeCache {
121
132
  }
122
133
  drop(entries);
123
134
  if lock(&self.refused).contains(&key) {
124
- return Plan::Compile;
135
+ return Plan::Compile(Some(key));
125
136
  }
126
- self.begin(key).map_or(Plan::Compile, Plan::Make)
137
+ self.begin(key.clone())
138
+ .map_or(Plan::Compile(Some(key)), Plan::Make)
127
139
  }
128
140
 
129
141
  /// Claims `key` for this context to build, unless another is building it.
@@ -300,7 +312,7 @@ mod tests {
300
312
  let cache = cache();
301
313
  assert!(matches!(
302
314
  cache.plan(&name(), &source(1), None),
303
- Plan::Compile
315
+ Plan::Compile(None)
304
316
  ));
305
317
  cached(cache, &source(1), Box::from(&b"compiled"[..]));
306
318
  assert!(matches!(
@@ -309,11 +321,11 @@ mod tests {
309
321
  ));
310
322
  assert!(matches!(
311
323
  cache.plan(&name(), &source(1), Some(7)),
312
- Plan::Compile
324
+ Plan::Compile(None)
313
325
  ));
314
326
  assert!(matches!(
315
327
  cache.plan(&JsText::from("other.js"), &source(1), None),
316
- Plan::Compile
328
+ Plan::Compile(None)
317
329
  ));
318
330
  }
319
331
 
@@ -355,10 +367,9 @@ mod tests {
355
367
  let _ = cache.plan(&name(), &source(3), None);
356
368
  let first = cache.plan(&name(), &source(3), None);
357
369
  assert!(matches!(first, Plan::Make(..)));
358
- assert!(matches!(
359
- cache.plan(&name(), &source(3), None),
360
- Plan::Compile
361
- ));
370
+ let elsewhere = cache.plan(&name(), &source(3), None);
371
+ assert!(matches!(elsewhere, Plan::Compile(Some(_))));
372
+ assert!(elsewhere.key() == first.key());
362
373
  drop(first);
363
374
  assert!(matches!(
364
375
  cache.plan(&name(), &source(3), None),
@@ -402,7 +413,7 @@ mod tests {
402
413
  assert_eq!(cache.stored(), 1);
403
414
  assert!(matches!(
404
415
  cache.plan(&name(), &source(7), None),
405
- Plan::Compile
416
+ Plan::Compile(Some(_))
406
417
  ));
407
418
  assert!(matches!(
408
419
  cache.plan(&name(), &source(6), None),
@@ -7,6 +7,7 @@ use mini_racer_sync::{ForkGate, Phase, lock};
7
7
 
8
8
  use crate::control::ControlBlock;
9
9
  use crate::process::code_cache::CodeCache;
10
+ use crate::process::sources::Sources;
10
11
  use crate::process::tasks::TaskRegistry;
11
12
  use crate::process::watchdog::Watchdog;
12
13
  use crate::process::workers::Workers;
@@ -51,13 +52,14 @@ pub struct Generation {
51
52
  watchdog: Mutex<Option<Arc<Watchdog>>>,
52
53
  workers: Workers,
53
54
  code_cache: CodeCache,
55
+ sources: Sources,
54
56
  next: OnceLock<Box<Self>>,
55
57
  }
56
58
 
57
59
  static ROOT: OnceLock<Generation> = OnceLock::new();
58
60
 
59
61
  impl Generation {
60
- fn new(epoch: u64, quiesced: bool, code_cache: CodeCache) -> Self {
62
+ fn new(epoch: u64, quiesced: bool, code_cache: CodeCache, sources: Sources) -> Self {
61
63
  Self {
62
64
  epoch,
63
65
  quiesced,
@@ -69,6 +71,7 @@ impl Generation {
69
71
  watchdog: Mutex::new(None),
70
72
  workers: Workers::default(),
71
73
  code_cache,
74
+ sources,
72
75
  next: OnceLock::new(),
73
76
  }
74
77
  }
@@ -94,6 +97,11 @@ impl Generation {
94
97
  &self.code_cache
95
98
  }
96
99
 
100
+ #[must_use]
101
+ pub const fn sources(&self) -> &Sources {
102
+ &self.sources
103
+ }
104
+
97
105
  pub fn register(&self, block: &Arc<ControlBlock>) {
98
106
  let mut blocks = lock(&self.blocks);
99
107
  blocks.retain(|block| block.strong_count() > 0);
@@ -115,7 +123,8 @@ impl Generation {
115
123
 
116
124
  pub fn current() -> &'static Generation {
117
125
  let epoch = epoch();
118
- let mut generation = ROOT.get_or_init(|| Generation::new(epoch, true, CodeCache::default()));
126
+ let mut generation =
127
+ ROOT.get_or_init(|| Generation::new(epoch, true, CodeCache::default(), Sources::default()));
119
128
  while generation.epoch != epoch {
120
129
  let quiesced = generation.quiesced && generation.prepared.load(Ordering::SeqCst);
121
130
  let next = generation.epoch.saturating_add(1);
@@ -125,6 +134,7 @@ pub fn current() -> &'static Generation {
125
134
  next,
126
135
  quiesced,
127
136
  parent.code_cache.inherited(),
137
+ parent.sources.inherited(),
128
138
  ))
129
139
  });
130
140
  }