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:
parent
3a20c6d315
commit
42ffdd15b3
|
@ -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
|
||||
|
|
33
README.md
33
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:
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
require "mini_racer/version"
|
||||
require "mini_racer_extension"
|
||||
require "thread"
|
||||
require "json"
|
||||
|
||||
module MiniRacer
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
module MiniRacer
|
||||
VERSION = "0.1.8"
|
||||
VERSION = "0.1.9"
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue