Memory profiling and control features
- #heap_stats for v8 heap stats - #dispose to quickly dispose of heap - fix minor memory leak
This commit is contained in:
parent
8ebd248a24
commit
f7ec907547
|
@ -1,3 +1,11 @@
|
|||
13-07-2017
|
||||
|
||||
- 0.1.10
|
||||
|
||||
- Fix leak: minor memory leak when disposing a context (20 bytes per context)
|
||||
- Feature: added #heap_stats so you can get visibility from context to actual memory usage of isolate
|
||||
- Feature: added #dispose so you reclaim all v8 memory right away as opposed to waiting for GC
|
||||
|
||||
09-03-2017
|
||||
|
||||
- 0.1.9
|
||||
|
|
27
README.md
27
README.md
|
@ -226,6 +226,33 @@ A list of all V8 runtime flags can be found using `node --v8-options`, or else b
|
|||
|
||||
Note that runtime flags must be set before any other operation (e.g. creating a context, a snapshot or an isolate), otherwise an exception will be thrown.
|
||||
|
||||
## Controlling memory
|
||||
|
||||
When hosting v8 you may want to keep track of memory usage, use #heap_stats to get memory usage:
|
||||
|
||||
```ruby
|
||||
context = MiniRacer::Context.new(timeout: 5)
|
||||
context.eval("let a='testing';")
|
||||
p context.heap_stats
|
||||
# {:total_physical_size=>1280640,
|
||||
# :total_heap_size_executable=>4194304,
|
||||
# :total_heap_size=>3100672,
|
||||
# :used_heap_size=>1205376,
|
||||
# :heap_size_limit=>1501560832}
|
||||
```
|
||||
|
||||
If you wish to dispose of a context before waiting on the GC use
|
||||
|
||||
```ruby
|
||||
context = MiniRacer::Context.new(timeout: 5)
|
||||
context.eval("let a='testing';")
|
||||
context.dispose
|
||||
context.eval("a = 2")
|
||||
# MiniRacer::ContextDisposedError
|
||||
|
||||
# nothing works on the context from now on, its a shell waiting to be disposed
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
The `bench` folder contains benchmark.
|
||||
|
|
|
@ -17,8 +17,12 @@ class ArrayBufferAllocator : public v8::ArrayBuffer::Allocator {
|
|||
void* data = AllocateUninitialized(length);
|
||||
return data == NULL ? data : memset(data, 0, length);
|
||||
}
|
||||
virtual void* AllocateUninitialized(size_t length) { return malloc(length); }
|
||||
virtual void Free(void* data, size_t) { free(data); }
|
||||
virtual void* AllocateUninitialized(size_t length) {
|
||||
return malloc(length);
|
||||
}
|
||||
virtual void Free(void* data, size_t) {
|
||||
free(data);
|
||||
}
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
|
@ -31,6 +35,7 @@ typedef struct {
|
|||
ArrayBufferAllocator* allocator;
|
||||
StartupData* startup_data;
|
||||
bool interrupted;
|
||||
bool disposed;
|
||||
pid_t pid;
|
||||
|
||||
// how many references to this isolate exist
|
||||
|
@ -38,7 +43,7 @@ typedef struct {
|
|||
// objects, Ruby will destroy ruby objects first, then call the
|
||||
// extenstion's deallocators. In this case, that means it would
|
||||
// call `deallocate_isolate` _before_ `deallocate`, causing a segfault
|
||||
int refs_count;
|
||||
volatile int refs_count;
|
||||
} IsolateInfo;
|
||||
|
||||
typedef struct {
|
||||
|
@ -484,14 +489,14 @@ static VALUE rb_context_init_with_isolate(VALUE self, VALUE isolate) {
|
|||
isolate_info->refs_count++;
|
||||
|
||||
{
|
||||
Locker lock(isolate_info->isolate);
|
||||
Isolate::Scope isolate_scope(isolate_info->isolate);
|
||||
HandleScope handle_scope(isolate_info->isolate);
|
||||
Locker lock(isolate_info->isolate);
|
||||
Isolate::Scope isolate_scope(isolate_info->isolate);
|
||||
HandleScope handle_scope(isolate_info->isolate);
|
||||
|
||||
Local<Context> context = Context::New(isolate_info->isolate);
|
||||
Local<Context> context = Context::New(isolate_info->isolate);
|
||||
|
||||
context_info->context = new Persistent<Context>();
|
||||
context_info->context->Reset(isolate_info->isolate, context);
|
||||
context_info->context = new Persistent<Context>();
|
||||
context_info->context->Reset(isolate_info->isolate, context);
|
||||
}
|
||||
|
||||
if (Qnil == rb_cDateTime && rb_funcall(rb_cObject, rb_intern("const_defined?"), 1, rb_str_new2("DateTime")) == Qtrue)
|
||||
|
@ -786,15 +791,10 @@ static VALUE rb_external_function_notify_v8(VALUE self) {
|
|||
return Qnil;
|
||||
}
|
||||
|
||||
void maybe_free_isolate_info(IsolateInfo* isolate_info) {
|
||||
// an isolate can only be freed if no Isolate or Context (ruby) object
|
||||
// still need it
|
||||
if (isolate_info == NULL || isolate_info->refs_count > 0) {
|
||||
return;
|
||||
}
|
||||
void free_isolate(IsolateInfo* isolate_info) {
|
||||
|
||||
if (isolate_info->isolate) {
|
||||
Locker lock(isolate_info->isolate);
|
||||
Locker lock(isolate_info->isolate);
|
||||
}
|
||||
|
||||
if (isolate_info->isolate) {
|
||||
|
@ -817,18 +817,25 @@ void maybe_free_isolate_info(IsolateInfo* isolate_info) {
|
|||
}
|
||||
|
||||
delete isolate_info->allocator;
|
||||
xfree(isolate_info);
|
||||
isolate_info->disposed = true;
|
||||
}
|
||||
|
||||
void deallocate_isolate(void* data) {
|
||||
IsolateInfo* isolate_info = (IsolateInfo*) data;
|
||||
void maybe_free_isolate(IsolateInfo* isolate_info) {
|
||||
// an isolate can only be freed if no Isolate or Context (ruby) object
|
||||
// still need it
|
||||
//
|
||||
// there is a sequence issue here where Ruby may call the deallocator on the
|
||||
// context object after it calles the dallocator on the isolate
|
||||
if (isolate_info->refs_count != 0 || isolate_info->disposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
isolate_info->refs_count--;
|
||||
|
||||
maybe_free_isolate_info(isolate_info);
|
||||
free_isolate(isolate_info);
|
||||
}
|
||||
|
||||
void deallocate(void* data) {
|
||||
|
||||
void free_context(void* data) {
|
||||
|
||||
ContextInfo* context_info = (ContextInfo*)data;
|
||||
IsolateInfo* isolate_info = context_info->isolate_info;
|
||||
|
||||
|
@ -837,23 +844,56 @@ void deallocate(void* data) {
|
|||
v8::Isolate::Scope isolate_scope(isolate_info->isolate);
|
||||
context_info->context->Reset();
|
||||
delete context_info->context;
|
||||
}
|
||||
|
||||
if (isolate_info) {
|
||||
isolate_info->refs_count--;
|
||||
maybe_free_isolate_info(isolate_info);
|
||||
context_info->context = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void deallocate_context(void* data) {
|
||||
|
||||
ContextInfo* context_info = (ContextInfo*)data;
|
||||
IsolateInfo* isolate_info = context_info->isolate_info;
|
||||
|
||||
free_context(data);
|
||||
|
||||
if (isolate_info) {
|
||||
isolate_info->refs_count--;
|
||||
maybe_free_isolate(isolate_info);
|
||||
}
|
||||
}
|
||||
|
||||
void deallocate_isolate(void* data) {
|
||||
|
||||
IsolateInfo* isolate_info = (IsolateInfo*) data;
|
||||
|
||||
isolate_info->refs_count--;
|
||||
maybe_free_isolate(isolate_info);
|
||||
|
||||
if (isolate_info->refs_count == 0) {
|
||||
xfree(isolate_info);
|
||||
}
|
||||
}
|
||||
|
||||
void deallocate(void* data) {
|
||||
ContextInfo* context_info = (ContextInfo*)data;
|
||||
IsolateInfo* isolate_info = context_info->isolate_info;
|
||||
|
||||
deallocate_context(data);
|
||||
|
||||
if (isolate_info && isolate_info->refs_count == 0) {
|
||||
xfree(isolate_info);
|
||||
}
|
||||
|
||||
xfree(data);
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -887,10 +927,42 @@ VALUE allocate_isolate(VALUE klass) {
|
|||
isolate_info->interrupted = false;
|
||||
isolate_info->refs_count = 0;
|
||||
isolate_info->pid = getpid();
|
||||
isolate_info->disposed = false;
|
||||
|
||||
return Data_Wrap_Struct(klass, NULL, deallocate_isolate, (void*)isolate_info);
|
||||
}
|
||||
|
||||
static VALUE
|
||||
rb_heap_stats(VALUE self) {
|
||||
|
||||
ContextInfo* context_info;
|
||||
Data_Get_Struct(self, ContextInfo, context_info);
|
||||
|
||||
if (!context_info->isolate_info) {
|
||||
return Qnil;
|
||||
}
|
||||
|
||||
Isolate* isolate = context_info->isolate_info->isolate;
|
||||
|
||||
if (!isolate) {
|
||||
return Qnil;
|
||||
}
|
||||
|
||||
v8::HeapStatistics stats;
|
||||
|
||||
isolate->GetHeapStatistics(&stats);
|
||||
|
||||
VALUE rval = rb_hash_new();
|
||||
|
||||
rb_hash_aset(rval, ID2SYM(rb_intern("total_physical_size")), ULONG2NUM(stats.total_physical_size()));
|
||||
rb_hash_aset(rval, ID2SYM(rb_intern("total_heap_size_executable")), ULONG2NUM(stats.total_heap_size_executable()));
|
||||
rb_hash_aset(rval, ID2SYM(rb_intern("total_heap_size")), ULONG2NUM(stats.total_heap_size()));
|
||||
rb_hash_aset(rval, ID2SYM(rb_intern("used_heap_size")), ULONG2NUM(stats.used_heap_size()));
|
||||
rb_hash_aset(rval, ID2SYM(rb_intern("heap_size_limit")), ULONG2NUM(stats.heap_size_limit()));
|
||||
|
||||
return rval;
|
||||
}
|
||||
|
||||
static VALUE
|
||||
rb_context_stop(VALUE self) {
|
||||
|
||||
|
@ -908,6 +980,22 @@ rb_context_stop(VALUE self) {
|
|||
return Qnil;
|
||||
}
|
||||
|
||||
static VALUE
|
||||
rb_context_dispose(VALUE self) {
|
||||
|
||||
ContextInfo* context_info;
|
||||
Data_Get_Struct(self, ContextInfo, context_info);
|
||||
|
||||
free_context(context_info);
|
||||
|
||||
if (context_info->isolate_info && context_info->isolate_info->refs_count == 2) {
|
||||
// special case, we only have isolate + context so we can burn the
|
||||
// isolate as well
|
||||
free_isolate(context_info->isolate_info);
|
||||
}
|
||||
return Qnil;
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
void Init_mini_racer_extension ( void )
|
||||
|
@ -918,18 +1006,25 @@ extern "C" {
|
|||
VALUE rb_cIsolate = rb_define_class_under(rb_mMiniRacer, "Isolate", rb_cObject);
|
||||
VALUE rb_cPlatform = rb_define_class_under(rb_mMiniRacer, "Platform", rb_cObject);
|
||||
|
||||
VALUE rb_eEvalError = rb_define_class_under(rb_mMiniRacer, "EvalError", rb_eStandardError);
|
||||
VALUE rb_eError = rb_define_class_under(rb_mMiniRacer, "Error", rb_eStandardError);
|
||||
|
||||
VALUE rb_eEvalError = rb_define_class_under(rb_mMiniRacer, "EvalError", rb_eError);
|
||||
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);
|
||||
rb_ePlatformAlreadyInitializedError = rb_define_class_under(rb_mMiniRacer, "PlatformAlreadyInitialized", rb_eStandardError);
|
||||
rb_eSnapshotError = rb_define_class_under(rb_mMiniRacer, "SnapshotError", rb_eError);
|
||||
rb_ePlatformAlreadyInitializedError = rb_define_class_under(rb_mMiniRacer, "PlatformAlreadyInitialized", rb_eError);
|
||||
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);
|
||||
rb_define_method(rb_cContext, "dispose_unsafe", (VALUE(*)(...))&rb_context_dispose, 0);
|
||||
rb_define_method(rb_cContext, "heap_stats", (VALUE(*)(...))&rb_heap_stats, 0);
|
||||
|
||||
rb_define_alloc_func(rb_cContext, allocate);
|
||||
rb_define_alloc_func(rb_cSnapshot, allocate_snapshot);
|
||||
rb_define_alloc_func(rb_cIsolate, allocate_isolate);
|
||||
|
|
|
@ -5,11 +5,15 @@ require "json"
|
|||
|
||||
module MiniRacer
|
||||
|
||||
class EvalError < StandardError; end
|
||||
class ScriptTerminatedError < EvalError; end
|
||||
class Error < ::StandardError; end
|
||||
|
||||
class ContextDisposedError < Error; end
|
||||
class SnapshotError < Error; end
|
||||
class PlatformAlreadyInitialized < Error; end
|
||||
|
||||
class EvalError < Error; end
|
||||
class ParseError < EvalError; end
|
||||
class SnapshotError < StandardError; end
|
||||
class PlatformAlreadyInitialized < StandardError; end
|
||||
class ScriptTerminatedError < EvalError; end
|
||||
|
||||
class FailedV8Conversion
|
||||
attr_reader :info
|
||||
|
@ -161,6 +165,8 @@ module MiniRacer
|
|||
end
|
||||
|
||||
def eval(str)
|
||||
raise(ContextDisposedError, 'attempted to call eval on a disposed context!') if @disposed
|
||||
|
||||
@eval_thread = Thread.current
|
||||
isolate.with_lock do
|
||||
@current_exception = nil
|
||||
|
@ -172,6 +178,15 @@ module MiniRacer
|
|||
@eval_thread = nil
|
||||
end
|
||||
|
||||
def dispose
|
||||
if !@disposed
|
||||
isolate.with_lock do
|
||||
dispose_unsafe
|
||||
end
|
||||
@disposed = true
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def attach(name, callback)
|
||||
|
||||
|
|
|
@ -614,4 +614,42 @@ raise FooError, "I like foos"
|
|||
|
||||
assert_equal expected_string_flags, TestPlatform.public_flags_to_strings(flags)
|
||||
end
|
||||
|
||||
def test_can_dispose_context
|
||||
context = MiniRacer::Context.new(timeout: 5)
|
||||
context.dispose
|
||||
assert_raises(MiniRacer::ContextDisposedError) do
|
||||
context.eval("a")
|
||||
end
|
||||
end
|
||||
|
||||
def test_estimated_size
|
||||
context = MiniRacer::Context.new(timeout: 5)
|
||||
context.eval("let a='testing';")
|
||||
|
||||
stats = context.heap_stats
|
||||
# eg: {:total_physical_size=>1280640, :total_heap_size_executable=>4194304, :total_heap_size=>3100672, :used_heap_size=>1205376, :heap_size_limit=>1501560832}
|
||||
assert_equal(
|
||||
[:total_physical_size, :total_heap_size_executable, :total_heap_size, :used_heap_size, :heap_size_limit].sort,
|
||||
stats.keys.sort
|
||||
)
|
||||
|
||||
assert(stats.values.all?{|v| v > 0}, "expecting the isolate to have values for all the vals")
|
||||
end
|
||||
|
||||
def test_can_dispose
|
||||
skip "takes too long"
|
||||
#
|
||||
# junk_it_up
|
||||
# 3.times do
|
||||
# GC.start(full_mark: true, immediate_sweep: true)
|
||||
# end
|
||||
end
|
||||
|
||||
def junk_it_up
|
||||
1000.times do
|
||||
context = MiniRacer::Context.new(timeout: 5)
|
||||
context.dispose
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue