From 4de42593ebf3184d0a1ac60cd9e9783ad135d511 Mon Sep 17 00:00:00 2001 From: Genevieve L'Esperance Date: Fri, 10 Dec 2021 10:40:13 -0800 Subject: [PATCH] Add SnapshotCreator API --- context.go | 22 +++++ context_test.go | 16 ++++ isolate.go | 37 +++++++- snapshot_creator.go | 104 +++++++++++++++++++++ snapshot_creator_test.go | 191 +++++++++++++++++++++++++++++++++++++++ v8go.cc | 104 ++++++++++++++++++++- v8go.h | 33 ++++++- 7 files changed, 500 insertions(+), 7 deletions(-) create mode 100644 snapshot_creator.go create mode 100644 snapshot_creator_test.go diff --git a/context.go b/context.go index 71875706..aa3845b5 100644 --- a/context.go +++ b/context.go @@ -8,6 +8,7 @@ package v8go // #include "v8go.h" import "C" import ( + "errors" "runtime" "sync" "unsafe" @@ -76,6 +77,27 @@ func NewContext(opt ...ContextOption) *Context { return ctx } +func NewContextFromSnapShot(iso *Isolate, snapshot_index int) (*Context, error) { + ctxMutex.Lock() + ctxSeq++ + ref := ctxSeq + ctxMutex.Unlock() + + createParams := iso.createParams + if createParams == nil || createParams.startupData == nil { + return nil, errors.New("Must create an isolate from a snapshot blob") + } + + ctx := &Context{ + ref: ref, + ptr: C.NewContextFromSnapShot(iso.ptr, C.size_t(snapshot_index), C.int(ref)), + iso: iso, + } + + ctx.register() + return ctx, nil +} + // Isolate gets the current context's parent isolate.An error is returned // if the isolate has been terninated. func (c *Context) Isolate() *Isolate { diff --git a/context_test.go b/context_test.go index 10dad5d8..b297917a 100644 --- a/context_test.go +++ b/context_test.go @@ -39,6 +39,22 @@ func TestContextExec(t *testing.T) { } } +func TestNewContextFromSnapShotErrorWhenIsolateHasNoStartupData(t *testing.T) { + t.Parallel() + + iso := v8.NewIsolate() + defer iso.Dispose() + + ctx, err := v8.NewContextFromSnapShot(iso, 1) + + if ctx != nil { + t.Errorf("error expected nil context got: %+v", ctx) + } + if err == nil { + t.Error("error expected but was ") + } +} + func TestJSExceptions(t *testing.T) { t.Parallel() diff --git a/isolate.go b/isolate.go index 661fbec0..fb8861bb 100644 --- a/isolate.go +++ b/isolate.go @@ -25,8 +25,9 @@ type Isolate struct { cbSeq int cbs map[int]FunctionCallback - null *Value - undefined *Value + null *Value + undefined *Value + createParams *CreateParams } // HeapStatistics represents V8 isolate heap statistics @@ -44,6 +45,18 @@ type HeapStatistics struct { NumberOfDetachedContexts uint64 } +type createOptions func(*CreateParams) + +func WithStartupData(startupData *StartupData) createOptions { + return func(params *CreateParams) { + params.startupData = startupData + } +} + +type CreateParams struct { + startupData *StartupData +} + // NewIsolate creates a new V8 isolate. Only one thread may access // a given isolate at a time, but different threads may access // different isolates simultaneously. @@ -51,13 +64,26 @@ type HeapStatistics struct { // by calling iso.Dispose(). // An *Isolate can be used as a v8go.ContextOption to create a new // Context, rather than creating a new default Isolate. -func NewIsolate() *Isolate { +func NewIsolate(opts ...createOptions) *Isolate { v8once.Do(func() { C.Init() }) + params := &CreateParams{} + for _, opt := range opts { + opt(params) + } + + var cOptions C.IsolateOptions + + if params.startupData != nil { + cOptions.snapshot_blob_data = (*C.char)(unsafe.Pointer(¶ms.startupData.data[0])) + cOptions.snapshot_blob_raw_size = params.startupData.raw_size + } + iso := &Isolate{ - ptr: C.NewIsolate(), - cbs: make(map[int]FunctionCallback), + ptr: C.NewIsolate(cOptions), + cbs: make(map[int]FunctionCallback), + createParams: params, } iso.null = newValueNull(iso) iso.undefined = newValueUndefined(iso) @@ -146,6 +172,7 @@ func (i *Isolate) Dispose() { return } C.IsolateDispose(i.ptr) + i.createParams = nil i.ptr = nil } diff --git a/snapshot_creator.go b/snapshot_creator.go new file mode 100644 index 00000000..46ff5981 --- /dev/null +++ b/snapshot_creator.go @@ -0,0 +1,104 @@ +// Copyright 2021 the v8go contributors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package v8go + +// #include +// #include "v8go.h" +import "C" +import ( + "errors" + "unsafe" +) + +type FunctionCodeHandling int + +const ( + FunctionCodeHandlingKlear FunctionCodeHandling = iota + FunctionCodeHandlingKeep +) + +type StartupData struct { + data []byte + raw_size C.int +} + +type SnapshotCreator struct { + ptr C.SnapshotCreatorPtr + iso *Isolate + defaultContextAdded bool +} + +func NewSnapshotCreator() *SnapshotCreator { + v8once.Do(func() { + C.Init() + }) + + rtn := C.NewSnapshotCreator() + + return &SnapshotCreator{ + ptr: rtn.creator, + iso: &Isolate{ptr: rtn.iso}, + defaultContextAdded: false, + } +} + +func (s *SnapshotCreator) GetIsolate() (*Isolate, error) { + if s.ptr == nil { + return nil, errors.New("v8go: Cannot get Isolate after creating the blob") + } + + return s.iso, nil +} + +func (s *SnapshotCreator) SetDeafultContext(ctx *Context) error { + if s.defaultContextAdded { + return errors.New("v8go: Cannot set multiple default context for snapshot creator") + } + + C.SetDefaultContext(s.ptr, ctx.ptr) + s.defaultContextAdded = true + ctx.ptr = nil + + return nil +} + +func (s *SnapshotCreator) AddContext(ctx *Context) (int, error) { + if s.ptr == nil { + return 0, errors.New("v8go: Cannot add context to snapshot creator after creating the blob") + } + + index := C.AddContext(s.ptr, ctx.ptr) + ctx.ptr = nil + + return int(index), nil +} + +func (s *SnapshotCreator) Create(functionCode FunctionCodeHandling) (*StartupData, error) { + if s.ptr == nil { + return nil, errors.New("v8go: Cannot use snapshot creator after creating the blob") + } + + if !s.defaultContextAdded { + return nil, errors.New("v8go: Cannot create a snapshot without a default context") + } + + rtn := C.CreateBlob(s.ptr, C.int(functionCode)) + + s.ptr = nil + s.iso.ptr = nil + + raw_size := rtn.raw_size + data := C.GoBytes(unsafe.Pointer(rtn.data), raw_size) + + C.SnapshotBlobDelete(rtn) + + return &StartupData{data: data, raw_size: raw_size}, nil +} + +func (s *SnapshotCreator) Dispose() { + if s.ptr != nil { + C.DeleteSnapshotCreator(s.ptr) + } +} diff --git a/snapshot_creator_test.go b/snapshot_creator_test.go new file mode 100644 index 00000000..a9ea840d --- /dev/null +++ b/snapshot_creator_test.go @@ -0,0 +1,191 @@ +// Copyright 2021 the v8go contributors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package v8go_test + +import ( + "testing" + + v8 "rogchap.com/v8go" +) + +func TestCreateSnapshot(t *testing.T) { + snapshotCreator := v8.NewSnapshotCreator() + snapshotCreatorIso, err := snapshotCreator.GetIsolate() + fatalIf(t, err) + + snapshotCreatorCtx := v8.NewContext(snapshotCreatorIso) + defer snapshotCreatorCtx.Close() + + snapshotCreatorCtx.RunScript(`const add = (a, b) => a + b`, "add.js") + snapshotCreatorCtx.RunScript(`function run() { return add(3, 4); }`, "main.js") + err = snapshotCreator.SetDeafultContext(snapshotCreatorCtx) + fatalIf(t, err) + + data, err := snapshotCreator.Create(v8.FunctionCodeHandlingKlear) + fatalIf(t, err) + + iso := v8.NewIsolate(v8.WithStartupData(data)) + defer iso.Dispose() + + ctx := v8.NewContext(iso) + defer ctx.Close() + + runVal, err := ctx.Global().Get("run") + if err != nil { + panic(err) + } + + fn, err := runVal.AsFunction() + if err != nil { + panic(err) + } + val, err := fn.Call(v8.Undefined(iso)) + if err != nil { + panic(err) + } + if val.String() != "7" { + t.Fatal("invalid val") + } +} + +func TestCreateSnapshotAndAddExtraContext(t *testing.T) { + snapshotCreator := v8.NewSnapshotCreator() + snapshotCreatorIso, err := snapshotCreator.GetIsolate() + fatalIf(t, err) + + snapshotCreatorCtx := v8.NewContext(snapshotCreatorIso) + defer snapshotCreatorCtx.Close() + + snapshotCreatorCtx.RunScript(`const add = (a, b) => a + b`, "add.js") + snapshotCreatorCtx.RunScript(`function run() { return add(3, 4); }`, "main.js") + err = snapshotCreator.SetDeafultContext(snapshotCreatorCtx) + fatalIf(t, err) + + snapshotCreatorCtx2 := v8.NewContext(snapshotCreatorIso) + defer snapshotCreatorCtx2.Close() + + snapshotCreatorCtx2.RunScript(`const multiply = (a, b) => a * b`, "add.js") + snapshotCreatorCtx2.RunScript(`function run() { return multiply(3, 4); }`, "main.js") + index, err := snapshotCreator.AddContext(snapshotCreatorCtx2) + fatalIf(t, err) + + snapshotCreatorCtx3 := v8.NewContext(snapshotCreatorIso) + defer snapshotCreatorCtx3.Close() + + snapshotCreatorCtx3.RunScript(`const div = (a, b) => a / b`, "add.js") + snapshotCreatorCtx3.RunScript(`function run() { return div(6, 2); }`, "main.js") + index2, err := snapshotCreator.AddContext(snapshotCreatorCtx3) + fatalIf(t, err) + + data, err := snapshotCreator.Create(v8.FunctionCodeHandlingKlear) + fatalIf(t, err) + + iso := v8.NewIsolate(v8.WithStartupData(data)) + defer iso.Dispose() + + ctx, err := v8.NewContextFromSnapShot(iso, index) + fatalIf(t, err) + defer ctx.Close() + + runVal, err := ctx.Global().Get("run") + if err != nil { + panic(err) + } + + fn, err := runVal.AsFunction() + if err != nil { + panic(err) + } + val, err := fn.Call(v8.Undefined(iso)) + if err != nil { + panic(err) + } + if val.String() != "12" { + t.Fatal("invalid val") + } + + ctx, err = v8.NewContextFromSnapShot(iso, index2) + fatalIf(t, err) + defer ctx.Close() + + runVal, err = ctx.Global().Get("run") + if err != nil { + panic(err) + } + + fn, err = runVal.AsFunction() + if err != nil { + panic(err) + } + val, err = fn.Call(v8.Undefined(iso)) + if err != nil { + panic(err) + } + if val.String() != "3" { + t.Fatal("invalid val") + } +} + +func TestCreateSnapshotErrorAfterAddingMultipleDefaultContext(t *testing.T) { + snapshotCreator := v8.NewSnapshotCreator() + defer snapshotCreator.Dispose() + snapshotCreatorIso, err := snapshotCreator.GetIsolate() + fatalIf(t, err) + snapshotCreatorCtx := v8.NewContext(snapshotCreatorIso) + + snapshotCreatorCtx.RunScript(`const add = (a, b) => a + b`, "add.js") + snapshotCreatorCtx.RunScript(`function run() { return add(3, 4); }`, "main.js") + err = snapshotCreator.SetDeafultContext(snapshotCreatorCtx) + fatalIf(t, err) + + err = snapshotCreator.SetDeafultContext(snapshotCreatorCtx) + defer snapshotCreatorCtx.Close() + + if err == nil { + t.Error("Adding an extra default cointext show have fail") + } +} + +func TestCreateSnapshotErrorAfterSuccessfullCreate(t *testing.T) { + snapshotCreator := v8.NewSnapshotCreator() + snapshotCreatorIso, err := snapshotCreator.GetIsolate() + fatalIf(t, err) + snapshotCreatorCtx := v8.NewContext(snapshotCreatorIso) + defer snapshotCreatorCtx.Close() + + snapshotCreatorCtx.RunScript(`const add = (a, b) => a + b`, "add.js") + snapshotCreatorCtx.RunScript(`function run() { return add(3, 4); }`, "main.js") + err = snapshotCreator.SetDeafultContext(snapshotCreatorCtx) + fatalIf(t, err) + + _, err = snapshotCreator.Create(v8.FunctionCodeHandlingKlear) + fatalIf(t, err) + + _, err = snapshotCreator.GetIsolate() + if err == nil { + t.Error("Getting Isolate should have fail") + } + + _, err = snapshotCreator.AddContext(snapshotCreatorCtx) + if err == nil { + t.Error("Adding context should have fail") + } + + _, err = snapshotCreator.Create(v8.FunctionCodeHandlingKlear) + if err == nil { + t.Error("Creating snapshot should have fail") + } +} + +func TestCreateSnapshotErrorIfNodefaultContextIsAdded(t *testing.T) { + snapshotCreator := v8.NewSnapshotCreator() + defer snapshotCreator.Dispose() + + _, err := snapshotCreator.Create(v8.FunctionCodeHandlingKlear) + + if err == nil { + t.Error("Creating a snapshop should have fail") + } +} diff --git a/v8go.cc b/v8go.cc index 7284c0c3..ef4ca4e5 100644 --- a/v8go.cc +++ b/v8go.cc @@ -26,6 +26,7 @@ const int ScriptCompilerEagerCompile = ScriptCompiler::kEagerCompile; struct m_ctx { Isolate* iso; + StartupData* startup_data; std::vector vals; std::vector unboundScripts; Persistent ptr; @@ -152,9 +153,19 @@ void Init() { return; } -IsolatePtr NewIsolate() { +IsolatePtr NewIsolate(IsolateOptions options) { Isolate::CreateParams params; params.array_buffer_allocator = default_allocator; + + StartupData* startup_data; + if (options.snapshot_blob_data != nullptr) { + startup_data = new StartupData{options.snapshot_blob_data, + options.snapshot_blob_raw_size}; + params.snapshot_blob = startup_data; + } else { + startup_data = nullptr; + } + Isolate* iso = Isolate::New(params); Locker locker(iso); Isolate::Scope isolate_scope(iso); @@ -166,6 +177,7 @@ IsolatePtr NewIsolate() { m_ctx* ctx = new m_ctx; ctx->ptr.Reset(iso, Context::New(iso)); ctx->iso = iso; + ctx->startup_data = startup_data; iso->SetData(0, ctx); return iso; @@ -264,6 +276,67 @@ RtnUnboundScript IsolateCompileUnboundScript(IsolatePtr iso, return rtn; } +/********** SnapshotCreator **********/ + +RtnSnapshotCreator NewSnapshotCreator() { + RtnSnapshotCreator rtn = {}; + SnapshotCreator* creator = new SnapshotCreator; + Isolate* iso = creator->GetIsolate(); + rtn.creator = creator; + rtn.iso = iso; + + return rtn; +} + +void DeleteSnapshotCreator(SnapshotCreatorPtr snapshotCreator) { + delete snapshotCreator; +} + +void SetDefaultContext(SnapshotCreatorPtr snapshotCreator, ContextPtr ctx) { + Isolate* iso = ctx->iso; + Locker locker(iso); + Isolate::Scope isolate_scope(iso); + HandleScope handle_scope(iso); + Local local_ctx = ctx->ptr.Get(iso); + Context::Scope context_scope(local_ctx); + + ContextFree(ctx); + + snapshotCreator->SetDefaultContext(local_ctx); +} + +size_t AddContext(SnapshotCreatorPtr snapshotCreator, ContextPtr ctx) { + Isolate* iso = ctx->iso; + Locker locker(iso); + Isolate::Scope isolate_scope(iso); + HandleScope handle_scope(iso); + Local local_ctx = ctx->ptr.Get(iso); + Context::Scope context_scope(local_ctx); + + ContextFree(ctx); + + return snapshotCreator->AddContext(local_ctx); +} + +RtnSnapshotBlob* CreateBlob(SnapshotCreatorPtr snapshotCreator, + int function_code_handling) { + // kKeep - keeps any compiled functions + // kClear - does not keep any compiled functions + StartupData startup_data = snapshotCreator->CreateBlob( + SnapshotCreator::FunctionCodeHandling(function_code_handling)); + + RtnSnapshotBlob* rtn = new RtnSnapshotBlob; + rtn->data = startup_data.data; + rtn->raw_size = startup_data.raw_size; + delete snapshotCreator; + return rtn; +} + +void SnapshotBlobDelete(RtnSnapshotBlob* ptr) { + delete[] ptr->data; + delete ptr; +} + /********** Exceptions & Errors **********/ ValuePtr IsolateThrowException(IsolatePtr iso, ValuePtr value) { @@ -593,6 +666,31 @@ ContextPtr NewContext(IsolatePtr iso, m_ctx* ctx = new m_ctx; ctx->ptr.Reset(iso, local_ctx); ctx->iso = iso; + ctx->startup_data = nullptr; + return ctx; +} + +ContextPtr NewContextFromSnapShot(IsolatePtr iso, + size_t snapshot_blob_index, + int ref) { + Locker locker(iso); + Isolate::Scope isolate_scope(iso); + HandleScope handle_scope(iso); + + // For function callbacks we need a reference to the context, but because of + // the complexities of C -> Go function pointers, we store a reference to the + // context as a simple integer identifier; this can then be used on the Go + // side to lookup the context in the context registry. We use slot 1 as slot 0 + // has special meaning for the Chrome debugger. + + Local local_ctx = + Context::FromSnapshot(iso, snapshot_blob_index).ToLocalChecked(); + local_ctx->SetEmbedderData(1, Integer::New(iso, ref)); + + m_ctx* ctx = new m_ctx; + ctx->ptr.Reset(iso, local_ctx); + ctx->iso = iso; + ctx->startup_data = nullptr; return ctx; } @@ -612,6 +710,10 @@ void ContextFree(ContextPtr ctx) { delete us; } + if (ctx->startup_data) { + delete ctx->startup_data; + } + delete ctx; } diff --git a/v8go.h b/v8go.h index 051dc558..27f7c86b 100644 --- a/v8go.h +++ b/v8go.h @@ -15,6 +15,7 @@ typedef v8::CpuProfiler* CpuProfilerPtr; typedef v8::CpuProfile* CpuProfilePtr; typedef const v8::CpuProfileNode* CpuProfileNodePtr; typedef v8::ScriptCompiler::CachedData* ScriptCompilerCachedDataPtr; +typedef v8::SnapshotCreator* SnapshotCreatorPtr; extern "C" { #else @@ -22,6 +23,9 @@ extern "C" { typedef struct v8Isolate v8Isolate; typedef v8Isolate* IsolatePtr; +typedef struct v8SnapshotCreator v8SnapshotCreator; +typedef v8SnapshotCreator* SnapshotCreatorPtr; + typedef struct v8CpuProfiler v8CpuProfiler; typedef v8CpuProfiler* CpuProfilerPtr; @@ -72,11 +76,26 @@ typedef struct { int rejected; } ScriptCompilerCachedData; +typedef struct { + const char* data; + int raw_size; +} RtnSnapshotBlob; + +typedef struct { + SnapshotCreatorPtr creator; + IsolatePtr iso; +} RtnSnapshotCreator; + typedef struct { ScriptCompilerCachedData cachedData; int compileOption; } CompileOptions; +typedef struct { + char* snapshot_blob_data; + int snapshot_blob_raw_size; +} IsolateOptions; + typedef struct { CpuProfilerPtr ptr; IsolatePtr iso; @@ -131,13 +150,22 @@ typedef struct { } ValueBigInt; extern void Init(); -extern IsolatePtr NewIsolate(); +extern IsolatePtr NewIsolate(IsolateOptions opts); extern void IsolatePerformMicrotaskCheckpoint(IsolatePtr ptr); extern void IsolateDispose(IsolatePtr ptr); extern void IsolateTerminateExecution(IsolatePtr ptr); extern int IsolateIsExecutionTerminating(IsolatePtr ptr); extern IsolateHStatistics IsolationGetHeapStatistics(IsolatePtr ptr); +extern void SnapshotBlobDelete(RtnSnapshotBlob* ptr); +extern RtnSnapshotCreator NewSnapshotCreator(); +extern void DeleteSnapshotCreator(SnapshotCreatorPtr snapshotCreator); +extern void SetDefaultContext(SnapshotCreatorPtr snapshotCreator, + ContextPtr ctx); +extern size_t AddContext(SnapshotCreatorPtr snapshotCreator, ContextPtr ctx); +extern RtnSnapshotBlob* CreateBlob(SnapshotCreatorPtr snapshotCreator, + int function_code_handling); + extern ValuePtr IsolateThrowException(IsolatePtr iso, ValuePtr value); extern RtnUnboundScript IsolateCompileUnboundScript(IsolatePtr iso_ptr, @@ -161,6 +189,9 @@ extern void CPUProfileDelete(CPUProfile* ptr); extern ContextPtr NewContext(IsolatePtr iso_ptr, TemplatePtr global_template_ptr, int ref); +extern ContextPtr NewContextFromSnapShot(IsolatePtr iso, + size_t snapshot_blob_index, + int ref); extern void ContextFree(ContextPtr ptr); extern RtnValue RunScript(ContextPtr ctx_ptr, const char* source,