FEATURE: add support for Isolate#low_memory_notification

This is another mechanism we can use for releasing as much memory as
possible

Note: Isolate#idle_notification can be used as well, but it is much slower
to act.

low_memory_notification forces a full GC and will clear up large amounts of
space from the V8 heap.

This also adds support for `ensure_gc_after_idle` options for
MiniRacer::Context this allows you to automatically conserve memory on
contexts, and only runs the GC when the context was idle for a certain
amount of time.
This commit is contained in:
Sam Saffron 2020-05-15 12:05:14 +10:00
parent 2d4a760418
commit 5f29361ae9
No known key found for this signature in database
GPG Key ID: B9606168D2FFD9F5
6 changed files with 101 additions and 3 deletions

View File

@ -1,3 +1,10 @@
- 15-05-2020
- 0.2.12
- FEATURE: isolate.low_memory_notification which can force a full GC
- FEATURE: MiniRacer::Context.new(ensure_gc_after_idle: 2) - to force full GC 2 seconds after context is idle, this allows you to conserve memory on isolates
- 14-05-2020
- 0.2.11

View File

@ -230,12 +230,17 @@ context = MiniRacer::Context.new(isolate: isolate)
# give up to 100ms for V8 garbage collection
isolate.idle_notification(100)
# force V8 to perform a full GC
isolate.low_memory_notification
```
This can come in handy to force V8 GC runs for example in between requests if you use MiniRacer on a web application.
Note that this method maps directly to [`v8::Isolate::IdleNotification`](http://bespin.cz/~ondras/html/classv8_1_1Isolate.html#aea16cbb2e351de9a3ae7be2b7cb48297), and that in particular its return value is the same (true if there is no further garbage to collect, false otherwise) and the same caveats apply, in particular that `there is no guarantee that the [call will return] within the time limit.`
Additionally you may automate this process on a context by defining it with `MiniRacer::Content.new(ensure_gc_after_idle: 1)`. Using this will ensure V8 will run a full GC using `context.isolate.low_memory_notification` 1 second after the last eval on the context. Low memory notification is both slower and more aggressive than an idle_notification and will ensure long living isolates use minimal amounts of memory.
### V8 Runtime flags
It is possible to set V8 Runtime flags:

View File

@ -769,6 +769,16 @@ static VALUE rb_isolate_idle_notification(VALUE self, VALUE idle_time_in_ms) {
return isolate_info->isolate->IdleNotificationDeadline(now + duration) ? Qtrue : Qfalse;
}
static VALUE rb_isolate_low_memory_notification(VALUE self) {
IsolateInfo* isolate_info;
Data_Get_Struct(self, IsolateInfo, isolate_info);
if (current_platform == NULL) return Qfalse;
isolate_info->isolate->LowMemoryNotification();
return Qnil;
}
static VALUE rb_context_init_unsafe(VALUE self, VALUE isolate, VALUE snap) {
ContextInfo* context_info;
Data_Get_Struct(self, ContextInfo, context_info);
@ -1657,6 +1667,8 @@ extern "C" {
rb_define_private_method(rb_cSnapshot, "load", (VALUE(*)(...))&rb_snapshot_load, 1);
rb_define_method(rb_cIsolate, "idle_notification", (VALUE(*)(...))&rb_isolate_idle_notification, 1);
rb_define_method(rb_cIsolate, "low_memory_notification", (VALUE(*)(...))&rb_isolate_low_memory_notification, 0);
rb_define_private_method(rb_cIsolate, "init_with_snapshot",(VALUE(*)(...))&rb_isolate_init_with_snapshot, 1);
rb_define_singleton_method(rb_cPlatform, "set_flag_as_str!", (VALUE(*)(...))&rb_platform_set_flag_as_str, 1);

View File

@ -145,6 +145,15 @@ module MiniRacer
end
# false signals it should be fetched if requested
@isolate = options[:isolate] || false
@ensure_gc_after_idle = options[:ensure_gc_after_idle]
if @ensure_gc_after_idle
@last_eval = nil
@ensure_gc_thread = nil
@ensure_gc_mutex = Mutex.new
end
@disposed = false
@callback_mutex = Mutex.new
@ -203,6 +212,7 @@ module MiniRacer
end
ensure
@eval_thread = nil
ensure_gc_thread if @ensure_gc_after_idle
end
def call(function_name, *arguments)
@ -216,15 +226,17 @@ module MiniRacer
end
ensure
@eval_thread = nil
ensure_gc_thread if @ensure_gc_after_idle
end
def dispose
return if @disposed
isolate_mutex.synchronize do
return if @disposed
dispose_unsafe
@disposed = true
@isolate = nil # allow it to be garbage collected, if set
end
@disposed = true
@isolate = nil # allow it to be garbage collected, if set
end
@ -273,6 +285,36 @@ module MiniRacer
private
def ensure_gc_thread
@last_eval = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@ensure_gc_mutex.synchronize do
@ensure_gc_thread = nil if !@ensure_gc_thread&.alive?
@ensure_gc_thread ||= Thread.new do
done = false
while !done
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
if @disposed
@ensure_gc_thread = nil
break
end
if @ensure_gc_after_idle < now - @last_eval
@ensure_gc_mutex.synchronize do
isolate_mutex.synchronize do
# extra 50ms to make sure that we really have enough time
isolate.low_memory_notification if !@disposed
@ensure_gc_thread = nil
done = true
end
end
end
sleep @ensure_gc_after_idle if !done
end
end
end
end
def stop_attached
@callback_mutex.synchronize{
if @callback_running

View File

@ -1,3 +1,3 @@
module MiniRacer
VERSION = "0.2.11"
VERSION = "0.2.12"
end

View File

@ -549,6 +549,7 @@ raise FooError, "I like foos"
assert(isolate.idle_notification(1000))
end
def test_concurrent_access_over_the_same_isolate_1
isolate = MiniRacer::Isolate.new
context = MiniRacer::Context.new(isolate: isolate)
@ -705,6 +706,37 @@ raise FooError, "I like foos"
assert(stats.values.all?{|v| v > 0}, "expecting the isolate to have values for all the vals")
end
def test_releasing_memory
context = MiniRacer::Context.new
context.isolate.low_memory_notification
start_heap = context.heap_stats[:used_heap_size]
context.eval("'#{"x" * 1_000_000}'")
context.isolate.low_memory_notification
end_heap = context.heap_stats[:used_heap_size]
assert((end_heap - start_heap).abs < 1000, "expecting most of the 1_000_000 long string to be freed")
end
def test_ensure_gc
context = MiniRacer::Context.new(ensure_gc_after_idle: 0.001)
context.isolate.low_memory_notification
start_heap = context.heap_stats[:used_heap_size]
context.eval("'#{"x" * 10_000_000}'")
sleep 0.005
end_heap = context.heap_stats[:used_heap_size]
assert((end_heap - start_heap).abs < 1000, "expecting most of the 1_000_000 long string to be freed")
end
def test_eval_with_filename
context = MiniRacer::Context.new()
context.eval("var foo = function(){baz();}", filename: 'b/c/foo1.js')