Skip to content
Merged
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
148 changes: 125 additions & 23 deletions ext/mini_racer_extension/mini_racer_extension.cc
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ typedef struct {
bool interrupted;
} ContextInfo;

typedef struct {
const char* data;
int raw_size;
} SnapshotInfo;

typedef struct {
bool parsed;
bool executed;
Expand All @@ -46,6 +51,7 @@ static VALUE rb_eScriptTerminatedError;
static VALUE rb_eParseError;
static VALUE rb_eScriptRuntimeError;
static VALUE rb_cJavaScriptFunction;
static VALUE rb_eSnapshotError;

static VALUE rb_cDateTime = Qnil;

Expand Down Expand Up @@ -299,6 +305,91 @@ static void unblock_eval(void *ptr) {
eval->context_info->interrupted = true;
}

static VALUE rb_snapshot_size(VALUE self, VALUE str) {
SnapshotInfo* snapshot_info;
Data_Get_Struct(self, SnapshotInfo, snapshot_info);

return INT2NUM(snapshot_info->raw_size);
}

static VALUE rb_snapshot_load(VALUE self, VALUE str) {
SnapshotInfo* snapshot_info;
Data_Get_Struct(self, SnapshotInfo, snapshot_info);

init_v8();

StartupData startup_data = V8::CreateSnapshotDataBlob(RSTRING_PTR(str));

if (startup_data.data == NULL && startup_data.raw_size == 0) {
rb_raise(rb_eSnapshotError, "Could not create snapshot, most likely the source is incorrect");
}

snapshot_info->data = startup_data.data;
snapshot_info->raw_size = startup_data.raw_size;

return Qnil;
}

static VALUE rb_snapshot_warmup(VALUE self, VALUE str) {
SnapshotInfo* snapshot_info;
Data_Get_Struct(self, SnapshotInfo, snapshot_info);

init_v8();

StartupData cold_startup_data = {snapshot_info->data, snapshot_info->raw_size};
StartupData warm_startup_data = V8::WarmUpSnapshotDataBlob(cold_startup_data, RSTRING_PTR(str));

if (warm_startup_data.data == NULL && warm_startup_data.raw_size == 0) {
rb_raise(rb_eSnapshotError, "Could not warm up snapshot, most likely the source is incorrect");
} else {
delete[] snapshot_info->data;

snapshot_info->data = warm_startup_data.data;
snapshot_info->raw_size = warm_startup_data.raw_size;
}

return Qnil;
}

static VALUE rb_context_init_with_snapshot(VALUE self, VALUE snapshot) {
ContextInfo* context_info;
Data_Get_Struct(self, ContextInfo, context_info);

init_v8();

context_info->allocator = new ArrayBufferAllocator();
context_info->interrupted = false;

Isolate::CreateParams create_params;
create_params.array_buffer_allocator = context_info->allocator;

StartupData startup_data;
if (!NIL_P(snapshot)) {
SnapshotInfo* snapshot_info;
Data_Get_Struct(snapshot, SnapshotInfo, snapshot_info);

startup_data = {snapshot_info->data, snapshot_info->raw_size};
create_params.snapshot_blob = &startup_data;
}

context_info->isolate = Isolate::New(create_params);

Locker lock(context_info->isolate);
Isolate::Scope isolate_scope(context_info->isolate);
HandleScope handle_scope(context_info->isolate);

Local<Context> context = Context::New(context_info->isolate);

context_info->context = new Persistent<Context>();
context_info->context->Reset(context_info->isolate, context);

if (Qnil == rb_cDateTime && rb_funcall(rb_cObject, rb_intern("const_defined?"), 1, rb_str_new2("DateTime")) == Qtrue)
{
rb_cDateTime = rb_const_get(rb_cObject, rb_intern("DateTime"));
}

return Qnil;
}

static VALUE rb_context_eval_unsafe(VALUE self, VALUE str) {

Expand Down Expand Up @@ -553,13 +644,18 @@ static VALUE rb_external_function_notify_v8(VALUE self) {

void deallocate(void * data) {
ContextInfo* context_info = (ContextInfo*)data;

{
Locker lock(context_info->isolate);
if (context_info->isolate) {
Locker lock(context_info->isolate);
}
}

{
context_info->context->Reset();
delete context_info->context;
if (context_info->context) {
context_info->context->Reset();
delete context_info->context;
}
}

{
Expand All @@ -578,37 +674,35 @@ void deallocate_external_function(void * data) {
xfree(data);
}

void deallocate_snapshot(void * data) {
SnapshotInfo* snapshot_info = (SnapshotInfo*)data;

delete[] snapshot_info->data;

xfree(snapshot_info);
}

VALUE allocate_external_function(VALUE klass) {
VALUE* self = ALLOC(VALUE);
return Data_Wrap_Struct(klass, NULL, deallocate_external_function, (void*)self);
}

VALUE allocate(VALUE klass) {
init_v8();

ContextInfo* context_info = ALLOC(ContextInfo);
context_info->allocator = new ArrayBufferAllocator();
context_info->allocator = NULL;
context_info->interrupted = false;
Isolate::CreateParams create_params;
create_params.array_buffer_allocator = context_info->allocator;

context_info->isolate = Isolate::New(create_params);

Locker lock(context_info->isolate);
Isolate::Scope isolate_scope(context_info->isolate);
HandleScope handle_scope(context_info->isolate);

Local<Context> context = Context::New(context_info->isolate);
context_info->isolate = NULL;
context_info->context = NULL;

context_info->context = new Persistent<Context>();
context_info->context->Reset(context_info->isolate, context);
return Data_Wrap_Struct(klass, NULL, deallocate, (void*)context_info);
}

if (Qnil == rb_cDateTime && rb_funcall(rb_cObject, rb_intern("const_defined?"), 1, rb_str_new2("DateTime")) == Qtrue)
{
rb_cDateTime = rb_const_get(rb_cObject, rb_intern("DateTime"));
}
VALUE allocate_snapshot(VALUE klass) {
SnapshotInfo* snapshot_info = ALLOC(SnapshotInfo);
snapshot_info->data = NULL;
snapshot_info->raw_size = 0;

return Data_Wrap_Struct(klass, NULL, deallocate, (void*)context_info);
return Data_Wrap_Struct(klass, NULL, deallocate_snapshot, (void*)snapshot_info);
}

static VALUE
Expand All @@ -625,20 +719,28 @@ extern "C" {
{
VALUE rb_mMiniRacer = rb_define_module("MiniRacer");
VALUE rb_cContext = rb_define_class_under(rb_mMiniRacer, "Context", rb_cObject);
VALUE rb_cSnapshot = rb_define_class_under(rb_mMiniRacer, "Snapshot", rb_cObject);

VALUE rb_eEvalError = rb_define_class_under(rb_mMiniRacer, "EvalError", rb_eStandardError);
rb_eScriptTerminatedError = rb_define_class_under(rb_mMiniRacer, "ScriptTerminatedError", rb_eEvalError);
rb_eParseError = rb_define_class_under(rb_mMiniRacer, "ParseError", rb_eEvalError);
rb_eScriptRuntimeError = rb_define_class_under(rb_mMiniRacer, "RuntimeError", rb_eEvalError);
rb_cJavaScriptFunction = rb_define_class_under(rb_mMiniRacer, "JavaScriptFunction", rb_cObject);
rb_eSnapshotError = rb_define_class_under(rb_mMiniRacer, "SnapshotError", rb_eStandardError);

VALUE rb_cExternalFunction = rb_define_class_under(rb_cContext, "ExternalFunction", rb_cObject);
rb_define_method(rb_cContext, "stop", (VALUE(*)(...))&rb_context_stop, 0);
rb_define_alloc_func(rb_cContext, allocate);
rb_define_alloc_func(rb_cSnapshot, allocate_snapshot);

rb_define_private_method(rb_cContext, "eval_unsafe",(VALUE(*)(...))&rb_context_eval_unsafe, 1);
rb_define_private_method(rb_cContext, "init_with_snapshot",(VALUE(*)(...))&rb_context_init_with_snapshot, 1);
rb_define_private_method(rb_cExternalFunction, "notify_v8", (VALUE(*)(...))&rb_external_function_notify_v8, 0);
rb_define_alloc_func(rb_cExternalFunction, allocate_external_function);

rb_define_method(rb_cSnapshot, "size", (VALUE(*)(...))&rb_snapshot_size, 0);
rb_define_method(rb_cSnapshot, "warmup", (VALUE(*)(...))&rb_snapshot_warmup, 1);
rb_define_private_method(rb_cSnapshot, "load", (VALUE(*)(...))&rb_snapshot_load, 1);
}

}
17 changes: 17 additions & 0 deletions lib/mini_racer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module MiniRacer
class EvalError < StandardError; end
class ScriptTerminatedError < EvalError; end
class ParseError < EvalError; end
class SnapshotError < StandardError; end

class RuntimeError < EvalError
def initialize(message)
Expand Down Expand Up @@ -80,9 +81,18 @@ def initialize(options = nil)
@timeout = nil
@current_exception = nil

snapshot = nil
if options
@timeout = options[:timeout]
snapshot = options[:snapshot]
end

unless snapshot.nil? || snapshot.is_a?(Snapshot)
raise ArgumentError, "snapshot must be a MiniRacer::Snapshot object, passed a #{snapshot.inspect}"
end

# defined in the C class
init_with_snapshot(snapshot)
end

def load(filename)
Expand All @@ -106,4 +116,11 @@ def attach(name, callback)

end

# `size` and `warmup` public methods are defined in the C class
class Snapshot
def initialize(str = '')
# defined in the C class
load(str)
end
end
end
70 changes: 70 additions & 0 deletions test/mini_racer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -287,4 +287,74 @@ def test_load
end
end

def test_it_can_use_snapshots
snapshot = MiniRacer::Snapshot.new('function hello() { return "world"; }; var foo = "bar";')

context = MiniRacer::Context.new(snapshot: snapshot)

assert_equal "world", context.eval("hello()")
assert_equal "bar", context.eval("foo")
end

def test_snapshot_size
snapshot = MiniRacer::Snapshot.new('var foo = "bar";')

# for some reason sizes seem to change across runs, so we just
# check it's a positive integer
assert(snapshot.size > 0)
end

def test_invalid_snapshots_throw_an_exception
assert_raises(MiniRacer::SnapshotError) do
MiniRacer::Snapshot.new('var foo = bar;')
end
end

def test_an_empty_snapshot_is_valid
MiniRacer::Snapshot.new('')
MiniRacer::Snapshot.new
end

def test_snapshots_can_be_warmed_up_with_no_side_effects
# shamelessly insipired by https://github.com/v8/v8/blob/5.3.254/test/cctest/test-serialize.cc#L792-L854
snapshot_source = <<-JS
function f() { return Math.sin(1); }
var a = 5;
JS

snapshot = MiniRacer::Snapshot.new(snapshot_source)

warmump_source = <<-JS
Math.tan(1);
var a = f();
Math.sin = 1;
JS

snapshot.warmup(warmump_source)

context = MiniRacer::Context.new(snapshot: snapshot)

assert_equal 5, context.eval("a")
assert_equal "function", context.eval("typeof(Math.sin)")
end

def test_invalid_warmup_sources_throw_an_exception
assert_raises(MiniRacer::SnapshotError) do
MiniRacer::Snapshot.new('Math.sin = 1;').warmup('var a = Math.sin(1);')
end
end

def test_warming_up_with_invalid_source_does_not_affect_the_snapshot_internal_state
snapshot = MiniRacer::Snapshot.new('Math.sin = 1;')

begin
snapshot.warmup('var a = Math.sin(1);')
rescue
# do nothing
end

context = MiniRacer::Context.new(snapshot: snapshot)

assert_equal 1, context.eval("Math.sin")
end
end