Skip to content

Commit 7cfa8fd

Browse files
Introduce Snapshot.load (#395)
This enables persisting snapshots to disk for faster startup. Complements the existing `Snapshot#dump` method. ```ruby # Save a snapshot to disk snapshot = MiniRacer::Snapshot.new('var foo = "bar";') File.binwrite("snapshot.bin", snapshot.dump) # Load it back in a later process blob = File.binread("snapshot.bin") snapshot = MiniRacer::Snapshot.load(blob) context = MiniRacer::Context.new(snapshot: snapshot) context.eval("foo") # => "bar" ``` Co-authored-by: Sam Saffron <sam.saffron@gmail.com>
1 parent 84d86bc commit 7cfa8fd

5 files changed

Lines changed: 74 additions & 1 deletion

File tree

CHANGELOG

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
- 0.20.0 - 24-02-2026
2+
- Add Snapshot.load to restore snapshots from binary data, enabling disk persistence
3+
14
- 0.19.2 - 24-12-2025
25
- upgrade to node 24.12.0
36

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,25 @@ context.eval("counter")
193193
# => 1
194194
```
195195

196+
Snapshots can also be persisted to disk for faster startup:
197+
198+
```ruby
199+
# Save a snapshot to disk
200+
snapshot = MiniRacer::Snapshot.new('var foo = "bar";')
201+
File.binwrite("snapshot.bin", snapshot.dump)
202+
203+
# Load it back in a later process
204+
blob = File.binread("snapshot.bin")
205+
snapshot = MiniRacer::Snapshot.load(blob)
206+
context = MiniRacer::Context.new(snapshot: snapshot)
207+
context.eval("foo")
208+
# => "bar"
209+
```
210+
211+
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.
212+
213+
**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.
214+
196215
### Garbage collection
197216

198217
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.

ext/mini_racer_extension/mini_racer_extension.c

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1690,6 +1690,20 @@ static VALUE snapshot_dump(VALUE self)
16901690
return ss->blob;
16911691
}
16921692

1693+
static VALUE snapshot_load(VALUE klass, VALUE blob)
1694+
{
1695+
Snapshot *ss;
1696+
VALUE self;
1697+
1698+
Check_Type(blob, T_STRING);
1699+
self = snapshot_alloc(klass);
1700+
TypedData_Get_Struct(self, Snapshot, &snapshot_type, ss);
1701+
ss->blob = rb_str_dup(blob);
1702+
rb_enc_associate(ss->blob, rb_ascii8bit_encoding());
1703+
ENC_CODERANGE_SET(ss->blob, ENC_CODERANGE_VALID);
1704+
return self;
1705+
}
1706+
16931707
static VALUE snapshot_size0(VALUE self)
16941708
{
16951709
Snapshot *ss;
@@ -1742,6 +1756,7 @@ void Init_mini_racer_extension(void)
17421756
rb_define_method(c, "warmup!", snapshot_warmup, 1);
17431757
rb_define_method(c, "dump", snapshot_dump, 0);
17441758
rb_define_method(c, "size", snapshot_size0, 0);
1759+
rb_define_singleton_method(c, "load", snapshot_load, 1);
17451760
rb_define_alloc_func(c, snapshot_alloc);
17461761

17471762
c = rb_define_class_under(m, "Platform", rb_cObject);

lib/mini_racer/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# frozen_string_literal: true
22

33
module MiniRacer
4-
VERSION = "0.19.2"
4+
VERSION = "0.20.0"
55
LIBV8_NODE_VERSION = "~> 24.12.0.1"
66
end

test/mini_racer_test.rb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,42 @@ def test_snapshot_dump
485485
assert_equal(snapshot.size, dump.length)
486486
end
487487

488+
def test_snapshot_load
489+
if RUBY_ENGINE == "truffleruby"
490+
skip "TruffleRuby does not yet implement snapshots"
491+
end
492+
snapshot = MiniRacer::Snapshot.new('var foo = "bar"; function hello() { return "world"; }')
493+
blob = snapshot.dump
494+
495+
restored = MiniRacer::Snapshot.load(blob)
496+
497+
assert_equal(snapshot.size, restored.size)
498+
assert_equal(Encoding::ASCII_8BIT, restored.dump.encoding)
499+
assert(restored.dump.valid_encoding?, "restored snapshot dump should have valid encoding")
500+
ctx = MiniRacer::Context.new(snapshot: restored)
501+
assert_equal("bar", ctx.eval("foo"))
502+
assert_equal("world", ctx.eval("hello()"))
503+
end
504+
505+
def test_snapshot_load_with_non_binary_encoding
506+
if RUBY_ENGINE == "truffleruby"
507+
skip "TruffleRuby does not yet implement snapshots"
508+
end
509+
snapshot = MiniRacer::Snapshot.new('var foo = "bar";')
510+
# Force non-binary encoding to exercise the coderange fix.
511+
# Binary data interpreted as UTF-8 will have broken encoding.
512+
blob = snapshot.dump.dup.force_encoding("UTF-8")
513+
assert_equal(Encoding::UTF_8, blob.encoding)
514+
assert(!blob.valid_encoding?, "test precondition: blob should have broken UTF-8 encoding")
515+
516+
restored = MiniRacer::Snapshot.load(blob)
517+
518+
assert_equal(Encoding::ASCII_8BIT, restored.dump.encoding)
519+
assert(restored.dump.valid_encoding?, "restored snapshot should have valid binary encoding")
520+
ctx = MiniRacer::Context.new(snapshot: restored)
521+
assert_equal("bar", ctx.eval("foo"))
522+
end
523+
488524
def test_invalid_snapshots_throw_an_exception
489525
begin
490526
MiniRacer::Snapshot.new("var foo = bar;")

0 commit comments

Comments
 (0)