diff --git a/v8.go b/v8.go index dd8ee8d..f5e81b4 100644 --- a/v8.go +++ b/v8.go @@ -154,8 +154,11 @@ type Isolate struct { // NewIsolate creates a new V8 Isolate. func NewIsolate() *Isolate { v8_init_once.Do(func() { C.v8_init() }) - iso := &Isolate{ptr: C.v8_Isolate_New(C.StartupData{ptr: nil, len: 0})} + iso := &Isolate{ptr: C.v8_Isolate_New(C.StartupData{ptr: nil, len: 0}, nil)} runtime.SetFinalizer(iso, (*Isolate).release) + + addIsolate(iso) + setOOMErrorHandler(iso.ptr) return iso } @@ -163,11 +166,45 @@ func NewIsolate() *Isolate { // to initialize all Contexts created from this Isolate. func NewIsolateWithSnapshot(s *Snapshot) *Isolate { v8_init_once.Do(func() { C.v8_init() }) - iso := &Isolate{ptr: C.v8_Isolate_New(s.data), s: s} + iso := &Isolate{ptr: C.v8_Isolate_New(s.data, nil), s: s} runtime.SetFinalizer(iso, (*Isolate).release) + + addIsolate(iso) + setOOMErrorHandler(iso.ptr) return iso } +type IsolateOptions struct { + Snapshot *Snapshot + // MaxOldSpaceSize sets the maximum size of the old object heap in MiB. + MaxOldSpaceSize int +} + +// NewIsolateWithOptions creates a new V8 Isolate applying additional options +// like resource constraints or using the supplied Snapshot to initialize all +// Contexts created from this Isolate. +func NewIsolateWithOptions(opts IsolateOptions) (*Isolate, error) { + var startupData = C.StartupData{ptr: nil, len: 0} + var resourceConstraints C.ResourceConstraints + + v8_init_once.Do(func() { C.v8_init() }) + if opts.Snapshot != nil { + startupData = opts.Snapshot.data + } + if opts.MaxOldSpaceSize < 3 { + return nil, errors.New("MaxOldSpaceSize is too small to initialize v8") + } + if opts.MaxOldSpaceSize > 0 { + resourceConstraints = C.ResourceConstraints{max_old_space_size: C.int(opts.MaxOldSpaceSize)} + } + iso := &Isolate{ptr: C.v8_Isolate_New(startupData, &resourceConstraints)} + runtime.SetFinalizer(iso, (*Isolate).release) + + addIsolate(iso) + setOOMErrorHandler(iso.ptr) + return iso, nil +} + // NewContext creates a new, clean V8 Context within this Isolate. func (i *Isolate) NewContext() *Context { ctx := &Context{ @@ -205,6 +242,42 @@ func (i *Isolate) convertErrorMsg(error_msg C.Error) error { return err } +type OOMErrorCallback func(location string, isHeapOOM bool) + +var oomErrorCallbackMutex sync.RWMutex +var oomErrorHandler OOMErrorCallback + +//export go_oom_error_handler +func go_oom_error_handler(location C.String, heapOOM C.int) { + if oomErrorHandler != nil { + var isHeapOOM bool + b := C.int(heapOOM) + if b == 1 { + isHeapOOM = true + } + oomErrorCallbackMutex.RLock() + oomErrorHandler(C.GoString(location.ptr), isHeapOOM) + oomErrorCallbackMutex.RUnlock() + } +} + +func SetOOMErrorHandler(fn OOMErrorCallback) { + oomErrorCallbackMutex.Lock() + oomErrorHandler = fn + oomErrorCallbackMutex.Unlock() + for ptr := range isolates { + C.v8_Isolate_SetOOMErrorHandler(ptr) + } +} + +func setOOMErrorHandler(ptr C.IsolatePtr) { + oomErrorCallbackMutex.RLock() + if oomErrorHandler != nil { + C.v8_Isolate_SetOOMErrorHandler(ptr) + } + oomErrorCallbackMutex.RUnlock() +} + // Context is a sandboxed js environment with its own set of built-in objects // and functions. Values and javascript operations within a context are visible // only within that context unless the Go code explicitly moves values from one @@ -523,6 +596,11 @@ var contexts = map[int]*refCount{} var contextsMutex sync.RWMutex var nextContextId int +var ( + isolatesMutex sync.RWMutex + isolates = map[C.IsolatePtr]*Isolate{} +) + type refCount struct { ptr *Context count int @@ -532,7 +610,7 @@ func addRef(ctx *Context) { contextsMutex.Lock() ref := contexts[ctx.id] if ref == nil { - ref = &refCount{ctx, 0} + ref = &refCount{ptr: ctx, count: 0} contexts[ctx.id] = ref } ref.count++ @@ -549,6 +627,12 @@ func decRef(ctx *Context) { contextsMutex.Unlock() } +func addIsolate(iso *Isolate) { + isolatesMutex.Lock() + isolates[iso.ptr] = iso + isolatesMutex.Unlock() +} + //export go_callback_handler func go_callback_handler( cbIdStr C.String, diff --git a/v8_c_bridge.cc b/v8_c_bridge.cc index 69f93f3..5832acb 100644 --- a/v8_c_bridge.cc +++ b/v8_c_bridge.cc @@ -1,4 +1,5 @@ #include "v8_c_bridge.h" +#include "_cgo_export.h" #include "libplatform/libplatform.h" #include "v8.h" @@ -9,6 +10,8 @@ #include #include +#include + #define ISOLATE_SCOPE(iso) \ v8::Isolate* isolate = (iso); \ v8::Locker locker(isolate); /* Lock to current thread. */ \ @@ -24,6 +27,8 @@ extern "C" ValueTuple go_callback_handler( String id, CallerInfo info, int argc, ValueTuple* argv); +extern "C" void go_oom_error_handler(const char *location, bool is_heap_oom); + // We only need one, it's stateless. auto allocator = v8::ArrayBuffer::Allocator::NewDefaultAllocator(); @@ -182,7 +187,12 @@ StartupData v8_CreateSnapshotDataBlob(const char* js) { return StartupData{data.data, data.raw_size}; } -IsolatePtr v8_Isolate_New(StartupData startup_data) { +void v8_Isolate_SetOOMErrorHandler(IsolatePtr isolate_ptr) { + v8::Isolate* isolate = static_cast(isolate_ptr); + isolate->SetOOMErrorHandler(go_oom_error_handler); +} + +IsolatePtr v8_Isolate_New(StartupData startup_data, ResourceConstraints* resource_constraints) { v8::Isolate::CreateParams create_params; create_params.array_buffer_allocator = allocator; if (startup_data.len > 0 && startup_data.ptr != nullptr) { @@ -191,6 +201,10 @@ IsolatePtr v8_Isolate_New(StartupData startup_data) { data->raw_size = startup_data.len; create_params.snapshot_blob = data; } + if (resource_constraints != nullptr) { + create_params.constraints = v8::ResourceConstraints(); + create_params.constraints.set_max_old_space_size(resource_constraints->max_old_space_size); + } return static_cast(v8::Isolate::New(create_params)); } ContextPtr v8_Isolate_NewContext(IsolatePtr isolate_ptr) { diff --git a/v8_c_bridge.h b/v8_c_bridge.h index ab91aca..5f89c48 100644 --- a/v8_c_bridge.h +++ b/v8_c_bridge.h @@ -33,6 +33,10 @@ typedef struct { size_t does_zap_garbage; } HeapStatistics; +typedef struct { + int max_old_space_size; +} ResourceConstraints; + // NOTE! These values must exactly match the values in kinds.go. Any mismatch // will cause kinds to be misreported. typedef enum { @@ -114,10 +118,11 @@ extern void v8_init(); extern StartupData v8_CreateSnapshotDataBlob(const char* js); -extern IsolatePtr v8_Isolate_New(StartupData data); +extern IsolatePtr v8_Isolate_New(StartupData data, ResourceConstraints* resource_constraints); extern ContextPtr v8_Isolate_NewContext(IsolatePtr isolate); extern void v8_Isolate_Terminate(IsolatePtr isolate); extern void v8_Isolate_Release(IsolatePtr isolate); +extern void v8_Isolate_SetOOMErrorHandler(IsolatePtr isolate); extern HeapStatistics v8_Isolate_GetHeapStatistics(IsolatePtr isolate); extern void v8_Isolate_LowMemoryNotification(IsolatePtr isolate); diff --git a/v8_test.go b/v8_test.go index 03412e3..3f97e6a 100644 --- a/v8_test.go +++ b/v8_test.go @@ -1541,3 +1541,89 @@ func TestPanicHandling(t *testing.T) { _ = NewIsolate() _ = *f } + +func TestNewIsolateWithResourceConstraints(t *testing.T) { + // Creates a v8 runtime where the memory is limited to 3MB and memory is + // allocated with a small script until v8 runs out of memory. + t.Parallel() + isolate, err := NewIsolateWithOptions(IsolateOptions{MaxOldSpaceSize: 3}) + if err != nil { + t.Fatal(err) + } + SetOOMErrorHandler(func(location string, isHeapOOM bool) { + // Mark test as skipped, otherwise it would crash the process + t.SkipNow() + }) + ctx := isolate.NewContext() + for i := 0; i < 10; i++ { + _, err := ctx.Eval(fmt.Sprintf("var array%d = new Array(100000); array%d.fill(1);", i, i), "test.js") + if err != nil { + t.Fatalf("Error evaluating javascript, err: %v", err) + } + } +} + +func TestNewIsolateWithOptions(t *testing.T) { + var newIsolateWithOptionsTests = []struct { + name string + opts IsolateOptions + err string + }{ + { + name: "negative max old space size", + opts: IsolateOptions{ + MaxOldSpaceSize: -1, + }, + err: "MaxOldSpaceSize is too small to initialize v8", + }, + { + name: "too little memory to initialize v8", + opts: IsolateOptions{ + MaxOldSpaceSize: 2, + }, + err: "MaxOldSpaceSize is too small to initialize v8", + }, + } + for _, tt := range newIsolateWithOptionsTests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + _, err := NewIsolateWithOptions(tt.opts) + if tt.err != "" && err == nil { + t.Fatalf("expected error %s, actual %v", tt.err, err) + } + if err.Error() != tt.err { + t.Fatalf("expected error %s, actual %v", tt.err, err) + } + }) + } +} + +func TestNewIsolateWithResourceConstraintsMultipleIsolates(t *testing.T) { + // Creates a v8 runtime where the memory is limited to 3MB and memory is + // allocated with a small script until v8 runs out of memory. + t.Parallel() + _, err := NewIsolateWithOptions(IsolateOptions{MaxOldSpaceSize: 3}) + if err != nil { + t.Fatal(err) + } + SetOOMErrorHandler(func(location string, isHeapOOM bool) { + // Mark test as skipped, otherwise it would crash the process + t.SkipNow() + }) + _, err = NewIsolateWithOptions(IsolateOptions{MaxOldSpaceSize: 3}) + if err != nil { + t.Fatal(err) + } + isolate, err := NewIsolateWithOptions(IsolateOptions{MaxOldSpaceSize: 3}) + if err != nil { + t.Fatal(err) + } + ctx := isolate.NewContext() + for i := 0; i < 10; i++ { + _, err := ctx.Eval(fmt.Sprintf("var array%d = new Array(100000); array%d.fill(1);", i, i), "test.js") + if err != nil { + t.Fatalf("Error evaluating javascript, err: %v", err) + } + } +}