Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
- unreleased
- Add Snapshot.load to restore snapshots from binary data, enabling disk persistence

- 0.19.2 - 24-12-2025
- upgrade to node 24.12.0

Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,25 @@ context.eval("counter")
# => 1
```

Snapshots can also be persisted to disk for faster startup:

```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"
```

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.

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

### Garbage collection

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.
Expand Down
15 changes: 15 additions & 0 deletions ext/mini_racer_extension/mini_racer_extension.c
Original file line number Diff line number Diff line change
Expand Up @@ -1690,6 +1690,20 @@ static VALUE snapshot_dump(VALUE self)
return ss->blob;
}

static VALUE snapshot_load(VALUE klass, VALUE blob)
{
Snapshot *ss;
VALUE self;

Check_Type(blob, T_STRING);
self = snapshot_alloc(klass);
TypedData_Get_Struct(self, Snapshot, &snapshot_type, ss);
ss->blob = rb_str_dup(blob);
rb_enc_associate(ss->blob, rb_ascii8bit_encoding());
ENC_CODERANGE_SET(ss->blob, ENC_CODERANGE_VALID);
return self;
}

static VALUE snapshot_size0(VALUE self)
{
Snapshot *ss;
Expand Down Expand Up @@ -1742,6 +1756,7 @@ void Init_mini_racer_extension(void)
rb_define_method(c, "warmup!", snapshot_warmup, 1);
rb_define_method(c, "dump", snapshot_dump, 0);
rb_define_method(c, "size", snapshot_size0, 0);
rb_define_singleton_method(c, "load", snapshot_load, 1);
rb_define_alloc_func(c, snapshot_alloc);

c = rb_define_class_under(m, "Platform", rb_cObject);
Expand Down
36 changes: 36 additions & 0 deletions test/mini_racer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,42 @@ def test_snapshot_dump
assert_equal(snapshot.size, dump.length)
end

def test_snapshot_load
if RUBY_ENGINE == "truffleruby"
skip "TruffleRuby does not yet implement snapshots"
end
snapshot = MiniRacer::Snapshot.new('var foo = "bar"; function hello() { return "world"; }')
blob = snapshot.dump

restored = MiniRacer::Snapshot.load(blob)

assert_equal(snapshot.size, restored.size)
assert_equal(Encoding::ASCII_8BIT, restored.dump.encoding)
assert(restored.dump.valid_encoding?, "restored snapshot dump should have valid encoding")
ctx = MiniRacer::Context.new(snapshot: restored)
assert_equal("bar", ctx.eval("foo"))
assert_equal("world", ctx.eval("hello()"))
end

def test_snapshot_load_with_non_binary_encoding
if RUBY_ENGINE == "truffleruby"
skip "TruffleRuby does not yet implement snapshots"
end
snapshot = MiniRacer::Snapshot.new('var foo = "bar";')
# Force non-binary encoding to exercise the coderange fix.
# Binary data interpreted as UTF-8 will have broken encoding.
blob = snapshot.dump.dup.force_encoding("UTF-8")
assert_equal(Encoding::UTF_8, blob.encoding)
assert(!blob.valid_encoding?, "test precondition: blob should have broken UTF-8 encoding")

restored = MiniRacer::Snapshot.load(blob)

assert_equal(Encoding::ASCII_8BIT, restored.dump.encoding)
assert(restored.dump.valid_encoding?, "restored snapshot should have valid binary encoding")
ctx = MiniRacer::Context.new(snapshot: restored)
assert_equal("bar", ctx.eval("foo"))
end

def test_invalid_snapshots_throw_an_exception
begin
MiniRacer::Snapshot.new("var foo = bar;")
Expand Down