diff --git a/CHANGELOG b/CHANGELOG index 6a7ebda..7dc084a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -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 diff --git a/README.md b/README.md index a0e0289..4e8e3b7 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/ext/mini_racer_extension/mini_racer_extension.c b/ext/mini_racer_extension/mini_racer_extension.c index ee91f43..7cd1fa1 100644 --- a/ext/mini_racer_extension/mini_racer_extension.c +++ b/ext/mini_racer_extension/mini_racer_extension.c @@ -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; @@ -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); diff --git a/test/mini_racer_test.rb b/test/mini_racer_test.rb index 0134dda..211b052 100644 --- a/test/mini_racer_test.rb +++ b/test/mini_racer_test.rb @@ -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;")