Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend constructor to accept resource constraints #41

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
90 changes: 87 additions & 3 deletions v8.go
Expand Up @@ -154,20 +154,57 @@ 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
}

// NewIsolateWithSnapshot creates a new V8 Isolate using the supplied Snapshot
// 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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please doc the behavior when the resource limits are hit. That it crashed the process was a surprise! I'm happy that you're adding the OOM handler.

}

// 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{
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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++
Expand All @@ -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,
Expand Down
16 changes: 15 additions & 1 deletion v8_c_bridge.cc
@@ -1,4 +1,5 @@
#include "v8_c_bridge.h"
#include "_cgo_export.h"

#include "libplatform/libplatform.h"
#include "v8.h"
Expand All @@ -9,6 +10,8 @@
#include <sstream>
#include <stdio.h>

#include <functional>

#define ISOLATE_SCOPE(iso) \
v8::Isolate* isolate = (iso); \
v8::Locker locker(isolate); /* Lock to current thread. */ \
Expand All @@ -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();

Expand Down Expand Up @@ -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<v8::Isolate*>(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) {
Expand All @@ -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<IsolatePtr>(v8::Isolate::New(create_params));
}
ContextPtr v8_Isolate_NewContext(IsolatePtr isolate_ptr) {
Expand Down
7 changes: 6 additions & 1 deletion v8_c_bridge.h
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down
86 changes: 86 additions & 0 deletions v8_test.go
Expand Up @@ -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)
}
}
}