PERF: on boundary convert to and from json

Previously we would walk the object graph recursively and convert,
this proves to be extremely slow for large objects due to recursion and
huge amount of v8 contexts that were created

By converting objects to json on the boundary we also correct timeout
semantics and so on.
This commit is contained in:
Sam 2017-03-09 16:03:13 -05:00
parent 3a20c6d315
commit 42ffdd15b3
6 changed files with 79 additions and 26 deletions

View File

@ -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

View File

@ -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:

View File

@ -50,6 +50,7 @@ typedef struct {
bool parsed;
bool executed;
bool terminated;
bool json;
Persistent<Value>* value;
Persistent<Value>* message;
Persistent<Value>* 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> 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<Value>* persistent = new Persistent<Value>();
persistent->Reset(isolate, maybe_value.ToLocalChecked());
result->value = persistent;
// arrays and objects get converted to json
Local<Value> local_value = maybe_value.ToLocalChecked();
if ((local_value->IsObject() || local_value->IsArray()) &&
!local_value->IsDate() && !local_value->IsFunction()) {
Local<Object> JSON = context->Global()->Get(
String::NewFromUtf8(isolate, "JSON"))->ToObject();
Local<Function> stringify = JSON->Get(v8::String::NewFromUtf8(isolate, "stringify"))
.As<Function>();
Local<Object> object = local_value->ToObject();
const unsigned argc = 1;
Local<Value> argv[argc] = { object };
MaybeLocal<Value> json = stringify->Call(JSON, argc, argv);
if (json.IsEmpty()) {
result->executed = false;
} else {
result->json = true;
Persistent<Value>* persistent = new Persistent<Value>();
persistent->Reset(isolate, json.ToLocalChecked());
result->value = persistent;
}
} else {
Persistent<Value>* persistent = new Persistent<Value>();
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> &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> &value) {
}
if (value->IsObject()) {
TryCatch trycatch(isolate);
VALUE rb_hash = rb_hash_new();
TryCatch trycatch(isolate);
Local<Context> context = Context::New(isolate);
Local<Object> object = value->ToObject();
MaybeLocal<Array> 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<Value> tmp = Local<Value>::New(isolate, *eval_result.value);
result = convert_v8_to_ruby(isolate, tmp);
if (eval_result.json) {
Local<String> 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);

View File

@ -1,6 +1,7 @@
require "mini_racer/version"
require "mini_racer_extension"
require "thread"
require "json"
module MiniRacer

View File

@ -1,3 +1,3 @@
module MiniRacer
VERSION = "0.1.8"
VERSION = "0.1.9"
end

View File

@ -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