diff --git a/CHANGELOG b/CHANGELOG index 77e54fa..1887883 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +09-03-2017 + +- 0.1.9 + + - Perf: speed up ruby/node boundary performance when moving large objects + 06-02-2017 - 0.1.8 diff --git a/README.md b/README.md index 7988d0e..e3ff550 100644 --- a/README.md +++ b/README.md @@ -232,22 +232,31 @@ The `bench` folder contains benchmark. ### Benchmark minification of Discourse application.js (both minified and unminified) -- MiniRacer version 0.1 +MiniRacer outperforms node when minifying assets via execjs. + +- MiniRacer version 0.1.9 +- node version 6.10 - therubyracer version 0.12.2 -``` -$ ruby bench_uglify.rb -Benching with MiniRacer -MiniRacer minify discourse_app.js 13813.36ms -MiniRacer minify discourse_app_minified.js 18271.19ms -MiniRacer minify discourse_app.js twice (2 threads) 13587.21ms ``` -``` +$ bundle exec ruby bench.rb mini_racer +Benching with mini_racer +mini_racer minify discourse_app.js 9292.72063ms +mini_racer minify discourse_app_minified.js 11799.850171ms +mini_racer minify discourse_app.js twice (2 threads) 10269.570797ms + +sam@ubuntu exec_js_uglify % bundle exec ruby bench.rb node +Benching with node +node minify discourse_app.js 13302.715484ms +node minify discourse_app_minified.js 18100.761243ms +node minify discourse_app.js twice (2 threads) 14383.600207000001ms + +sam@ubuntu exec_js_uglify % bundle exec ruby bench.rb therubyracer Benching with therubyracer -MiniRacer minify discourse_app.js 151467.164ms -MiniRacer minify discourse_app_minified.js 158172.097ms -MiniRacer minify discourse_app.js twice (2 threads) - DOES NOT FINISH +therubyracer minify discourse_app.js 171683.01867700001ms +therubyracer minify discourse_app_minified.js 143138.88492ms +therubyracer minify discourse_app.js twice (2 threads) NEVER FINISH Killed: 9 ``` @@ -256,6 +265,8 @@ The huge performance disparity (MiniRacer is 10x faster) is due to MiniRacer run Note how the global interpreter lock release leads to 2 threads doing the same work taking the same wall time as 1 thread. +As a rule MiniRacer strives to always support and depend on the latest stable version of libv8. + ## Installation Add this line to your application's Gemfile: diff --git a/ext/mini_racer_extension/mini_racer_extension.cc b/ext/mini_racer_extension/mini_racer_extension.cc index 374426b..87e1b95 100644 --- a/ext/mini_racer_extension/mini_racer_extension.cc +++ b/ext/mini_racer_extension/mini_racer_extension.cc @@ -50,6 +50,7 @@ typedef struct { bool parsed; bool executed; bool terminated; + bool json; Persistent* value; Persistent* message; Persistent* backtrace; @@ -68,6 +69,7 @@ static VALUE rb_eScriptRuntimeError; static VALUE rb_cJavaScriptFunction; static VALUE rb_eSnapshotError; static VALUE rb_ePlatformAlreadyInitializedError; +static VALUE rb_mJSON; static VALUE rb_cFailedV8Conversion; static VALUE rb_cDateTime = Qnil; @@ -118,11 +120,10 @@ nogvl_context_eval(void* arg) { EvalParams* eval_params = (EvalParams*)arg; EvalResult* result = eval_params->result; Isolate* isolate = eval_params->context_info->isolate_info->isolate; + Isolate::Scope isolate_scope(isolate); HandleScope handle_scope(isolate); - TryCatch trycatch(isolate); - Local context = eval_params->context_info->context->Get(isolate); Context::Scope context_scope(context); @@ -135,6 +136,7 @@ nogvl_context_eval(void* arg) { result->parsed = !parsed_script.IsEmpty(); result->executed = false; result->terminated = false; + result->json = false; result->value = NULL; if (!result->parsed) { @@ -147,9 +149,36 @@ nogvl_context_eval(void* arg) { result->executed = !maybe_value.IsEmpty(); if (result->executed) { - Persistent* persistent = new Persistent(); - persistent->Reset(isolate, maybe_value.ToLocalChecked()); - result->value = persistent; + + // arrays and objects get converted to json + Local local_value = maybe_value.ToLocalChecked(); + if ((local_value->IsObject() || local_value->IsArray()) && + !local_value->IsDate() && !local_value->IsFunction()) { + Local JSON = context->Global()->Get( + String::NewFromUtf8(isolate, "JSON"))->ToObject(); + + Local stringify = JSON->Get(v8::String::NewFromUtf8(isolate, "stringify")) + .As(); + + Local object = local_value->ToObject(); + const unsigned argc = 1; + Local argv[argc] = { object }; + MaybeLocal json = stringify->Call(JSON, argc, argv); + + if (json.IsEmpty()) { + result->executed = false; + } else { + result->json = true; + Persistent* persistent = new Persistent(); + persistent->Reset(isolate, json.ToLocalChecked()); + result->value = persistent; + } + + } else { + Persistent* persistent = new Persistent(); + persistent->Reset(isolate, local_value); + result->value = persistent; + } } } @@ -190,6 +219,7 @@ nogvl_context_eval(void* arg) { static VALUE convert_v8_to_ruby(Isolate* isolate, Handle &value) { + Isolate::Scope isolate_scope(isolate); HandleScope scope(isolate); if (value->IsNull() || value->IsUndefined()){ @@ -239,11 +269,10 @@ static VALUE convert_v8_to_ruby(Isolate* isolate, Handle &value) { } if (value->IsObject()) { - - TryCatch trycatch(isolate); - VALUE rb_hash = rb_hash_new(); + TryCatch trycatch(isolate); Local context = Context::New(isolate); + Local object = value->ToObject(); MaybeLocal maybe_props = object->GetOwnPropertyNames(context); if (!maybe_props.IsEmpty()) { @@ -486,8 +515,6 @@ static VALUE rb_context_eval_unsafe(VALUE self, VALUE str) { Data_Get_Struct(self, ContextInfo, context_info); Isolate* isolate = context_info->isolate_info->isolate; - - { Locker lock(isolate); Isolate::Scope isolate_scope(isolate); @@ -553,14 +580,21 @@ static VALUE rb_context_eval_unsafe(VALUE self, VALUE str) { } } - // New scope for return value, must release GVL which + // New scope for return value { Locker lock(isolate); Isolate::Scope isolate_scope(isolate); HandleScope handle_scope(isolate); Local tmp = Local::New(isolate, *eval_result.value); - result = convert_v8_to_ruby(isolate, tmp); + + if (eval_result.json) { + Local rstr = tmp->ToString(); + VALUE json_string = rb_enc_str_new(*String::Utf8Value(rstr), rstr->Utf8Length(), rb_enc_find("utf-8")); + result = rb_funcall(rb_mJSON, rb_intern("parse"), 1, json_string); + } else { + result = convert_v8_to_ruby(isolate, tmp); + } eval_result.value->Reset(); delete eval_result.value; @@ -892,6 +926,7 @@ extern "C" { rb_eSnapshotError = rb_define_class_under(rb_mMiniRacer, "SnapshotError", rb_eStandardError); rb_ePlatformAlreadyInitializedError = rb_define_class_under(rb_mMiniRacer, "PlatformAlreadyInitialized", rb_eStandardError); rb_cFailedV8Conversion = rb_define_class_under(rb_mMiniRacer, "FailedV8Conversion", rb_cObject); + rb_mJSON = rb_define_module("JSON"); VALUE rb_cExternalFunction = rb_define_class_under(rb_cContext, "ExternalFunction", rb_cObject); rb_define_method(rb_cContext, "stop", (VALUE(*)(...))&rb_context_stop, 0); diff --git a/lib/mini_racer.rb b/lib/mini_racer.rb index 9c3a91b..d2eb9eb 100644 --- a/lib/mini_racer.rb +++ b/lib/mini_racer.rb @@ -1,6 +1,7 @@ require "mini_racer/version" require "mini_racer_extension" require "thread" +require "json" module MiniRacer diff --git a/lib/mini_racer/version.rb b/lib/mini_racer/version.rb index a9fab02..8e38f11 100644 --- a/lib/mini_racer/version.rb +++ b/lib/mini_racer/version.rb @@ -1,3 +1,3 @@ module MiniRacer - VERSION = "0.1.8" + VERSION = "0.1.9" end diff --git a/test/mini_racer_test.rb b/test/mini_racer_test.rb index 5a00bc8..25e31e9 100644 --- a/test/mini_racer_test.rb +++ b/test/mini_racer_test.rb @@ -39,7 +39,7 @@ class MiniRacerTest < Minitest::Test def test_object context = MiniRacer::Context.new # remember JavaScript is quirky {"1" : 1} magically turns to {1: 1} cause magic - assert_equal({1 => 2, "two" => "two"}, context.eval('var a={"1" : 2, "two" : "two"}; a')) + assert_equal({"1" => 2, "two" => "two"}, context.eval('var a={"1" : 2, "two" : "two"}; a')) end def test_it_returns_runtime_error @@ -191,7 +191,7 @@ raise FooError, "I like foos" def test_return_hash context = MiniRacer::Context.new context.attach("test", proc{{banana: :nose, "inner" => {42 => 42}}}) - assert_equal({"banana" => "nose", "inner" => {42 => 42}}, context.eval("test()")) + assert_equal({"banana" => "nose", "inner" => {"42" => 42}}, context.eval("test()")) end def test_return_date