Skip to content

Commit

Permalink
Add functions to separate compilation from running to speed up start …
Browse files Browse the repository at this point in the history
…times on new isolates loading the same scripts. (rogchap#206)

* CompileScript + RunCompiledScript

Co-authored-by: Dylan Thacker-Smith <dylan.smith@shopify.com>

* Use cached data pointer instead of copying data to a new byte slice

- Compile options support
- Add Bytes() on cached data, lazy load

* Revert "Use cached data pointer instead of copying data to a new byte slice"

This reverts commit 45a127e.

* RtnCachedData to handle error and accept option arg

* RunScript accepts cached data, CompileAndRun does not

- updated changelog
- updated readme

* Introduce UnboundScript

- Keep RunScript api

* Input rejected value can be ignored

* Fix warnnings around init

* Delete wrapping cached data obj instead of just internal ptr

* panic if Option and CachedData are used together in CompileOptions

* Use global variables to share C++ enum values with Go

* Rename CompilerOptions Option field to Mode

* Rename ScriptCompilerCachedData to CompilerCachedData

For brevity and because we aren't really exposing the concept of
a ScriptCompiler and there currently isn't another public compiler
type in the V8 API.

Co-authored-by: Dylan Thacker-Smith <dylan.smith@shopify.com>
  • Loading branch information
Genevieve and dylanahsmith authored Nov 10, 2021
1 parent 7df088a commit 4a6bcfb
Show file tree
Hide file tree
Showing 10 changed files with 423 additions and 14 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Support for setting and getting internal fields for template object instances
- Support for CPU profiling
- Add V8 build for Apple Silicon
- Support for compiling a context-dependent UnboundScript which can be run in any context of the isolate it was compiled in.
- Support for creating a code cache from an UnboundScript which can be used to create an UnboundScript in other isolates
to run a pre-compiled script in new contexts.

### Changed
- Removed error return value from NewIsolate which never fails
Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,28 @@ if err != nil {
}
```

### Pre-compile context-independent scripts to speed-up execution times

For scripts that are large or are repeatedly run in different contexts,
it is beneficial to compile the script once and used the cached data from that
compilation to avoid recompiling every time you want to run it.

```go
source := "const multiply = (a, b) => a * b"
iso1 := v8.NewIsolate() // creates a new JavaScript VM
ctx1 := v8.NewContext() // new context within the VM
script1, _ := iso1.CompileUnboundScript(source, "math.js", v8.CompileOptions{}) // compile script to get cached data
val, _ := script1.Run(ctx1)

cachedData := script1.CreateCodeCache()

iso2 := v8.NewIsolate() // create a new JavaScript VM
ctx2 := v8.NewContext(iso2) // new context within the VM

script2, _ := iso2.CompileUnboundScript(source, "math.js", v8.CompileOptions{CachedData: cachedData}) // compile script in new isolate with cached data
val, _ = script2.Run(ctx2)
```

### Terminate long running scripts

```go
Expand Down
5 changes: 2 additions & 3 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,17 +82,16 @@ func (c *Context) Isolate() *Isolate {
return c.iso
}

// RunScript executes the source JavaScript; origin or filename provides a
// RunScript executes the source JavaScript; origin (a.k.a. filename) provides a
// reference for the script and used in the stack trace if there is an error.
// error will be of type `JSError` of not nil.
// error will be of type `JSError` if not nil.
func (c *Context) RunScript(source string, origin string) (*Value, error) {
cSource := C.CString(source)
cOrigin := C.CString(origin)
defer C.free(unsafe.Pointer(cSource))
defer C.free(unsafe.Pointer(cOrigin))

rtn := C.RunScript(c.ptr, cSource, cOrigin)

return valueResult(c, rtn)
}

Expand Down
46 changes: 46 additions & 0 deletions isolate.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@

package v8go

// #include <stdlib.h>
// #include "v8go.h"
import "C"

import (
"sync"
"unsafe"
)

var v8once sync.Once
Expand Down Expand Up @@ -75,6 +77,50 @@ func (i *Isolate) IsExecutionTerminating() bool {
return C.IsolateIsExecutionTerminating(i.ptr) == 1
}

type CompileOptions struct {
CachedData *CompilerCachedData

Mode CompileMode
}

// CompileUnboundScript will create an UnboundScript (i.e. context-indepdent)
// using the provided source JavaScript, origin (a.k.a. filename), and options.
// If options contain a non-null CachedData, compilation of the script will use
// that code cache.
// error will be of type `JSError` if not nil.
func (i *Isolate) CompileUnboundScript(source, origin string, opts CompileOptions) (*UnboundScript, error) {
cSource := C.CString(source)
cOrigin := C.CString(origin)
defer C.free(unsafe.Pointer(cSource))
defer C.free(unsafe.Pointer(cOrigin))

var cOptions C.CompileOptions
if opts.CachedData != nil {
if opts.Mode != 0 {
panic("On CompileOptions, Mode and CachedData can't both be set")
}
cOptions.compileOption = C.ScriptCompilerConsumeCodeCache
cOptions.cachedData = C.ScriptCompilerCachedData{
data: (*C.uchar)(unsafe.Pointer(&opts.CachedData.Bytes[0])),
length: C.int(len(opts.CachedData.Bytes)),
}
} else {
cOptions.compileOption = C.int(opts.Mode)
}

rtn := C.IsolateCompileUnboundScript(i.ptr, cSource, cOrigin, cOptions)
if rtn.ptr == nil {
return nil, newJSError(rtn.error)
}
if opts.CachedData != nil {
opts.CachedData.Rejected = int(rtn.cachedDataRejected) == 1
}
return &UnboundScript{
ptr: rtn.ptr,
iso: i,
}, nil
}

// GetHeapStatistics returns heap statistics for an isolate.
func (i *Isolate) GetHeapStatistics() HeapStatistics {
hs := C.IsolationGetHeapStatistics(i.ptr)
Expand Down
89 changes: 88 additions & 1 deletion isolate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,94 @@ func TestIsolateTermination(t *testing.T) {
}
}

func TestGetHeapStatistics(t *testing.T) {
func TestIsolateCompileUnboundScript(t *testing.T) {
s := "function foo() { return 'bar'; }; foo()"

i1 := v8.NewIsolate()
defer i1.Dispose()
c1 := v8.NewContext(i1)
defer c1.Close()

_, err := i1.CompileUnboundScript("invalid js", "filename", v8.CompileOptions{})
if err == nil {
t.Fatal("expected error")
}

us, err := i1.CompileUnboundScript(s, "script.js", v8.CompileOptions{Mode: v8.CompileModeEager})
fatalIf(t, err)

val, err := us.Run(c1)
fatalIf(t, err)
if val.String() != "bar" {
t.Fatalf("invalid value returned, expected bar got %v", val)
}

cachedData := us.CreateCodeCache()

i2 := v8.NewIsolate()
defer i2.Dispose()
c2 := v8.NewContext(i2)
defer c2.Close()

opts := v8.CompileOptions{CachedData: cachedData}
usWithCachedData, err := i2.CompileUnboundScript(s, "script.js", opts)
fatalIf(t, err)
if usWithCachedData == nil {
t.Fatal("expected unbound script from cached data not to be nil")
}
if opts.CachedData.Rejected {
t.Fatal("expected cached data to be used, not rejected")
}

val, err = usWithCachedData.Run(c2)
fatalIf(t, err)
if val.String() != "bar" {
t.Fatalf("invalid value returned, expected bar got %v", val)
}
}

func TestIsolateCompileUnboundScript_CachedDataRejected(t *testing.T) {
s := "function foo() { return 'bar'; }; foo()"
iso := v8.NewIsolate()
defer iso.Dispose()

// Try to compile an unbound script using cached data that does not match this source
opts := v8.CompileOptions{CachedData: &v8.CompilerCachedData{Bytes: []byte("Math.sqrt(4)")}}
us, err := iso.CompileUnboundScript(s, "script.js", opts)
fatalIf(t, err)
if !opts.CachedData.Rejected {
t.Error("expected cached data to be rejected")
}

ctx := v8.NewContext(iso)
defer ctx.Close()

// Verify that unbound script is still compiled and able to be used
val, err := us.Run(ctx)
fatalIf(t, err)
if val.String() != "bar" {
t.Errorf("invalid value returned, expected bar got %v", val)
}
}

func TestIsolateCompileUnboundScript_InvalidOptions(t *testing.T) {
iso := v8.NewIsolate()
defer iso.Dispose()

opts := v8.CompileOptions{
CachedData: &v8.CompilerCachedData{Bytes: []byte("unused")},
Mode: v8.CompileModeEager,
}
panicErr := recoverPanic(func() { iso.CompileUnboundScript("console.log(1)", "script.js", opts) })
if panicErr == nil {
t.Error("expected panic")
}
if panicErr != "On CompileOptions, Mode and CachedData can't both be set" {
t.Errorf("unexpected panic: %v\n", panicErr)
}
}

func TestIsolateGetHeapStatistics(t *testing.T) {
t.Parallel()
iso := v8.NewIsolate()
defer iso.Dispose()
Expand Down
20 changes: 20 additions & 0 deletions script_compiler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// 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 "v8go.h"
import "C"

type CompileMode C.int

var (
CompileModeDefault = CompileMode(C.ScriptCompilerNoCompileOptions)
CompileModeEager = CompileMode(C.ScriptCompilerEagerCompile)
)

type CompilerCachedData struct {
Bytes []byte
Rejected bool
}
39 changes: 39 additions & 0 deletions unbound_script.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// 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 <stdlib.h>
// #include "v8go.h"
import "C"
import "unsafe"

type UnboundScript struct {
ptr C.UnboundScriptPtr
iso *Isolate
}

// Run will bind the unbound script to the provided context and run it.
// If the context provided does not belong to the same isolate that the script
// was compiled in, Run will panic.
// If an error occurs, it will be of type `JSError`.
func (u *UnboundScript) Run(ctx *Context) (*Value, error) {
if ctx.Isolate() != u.iso {
panic("attempted to run unbound script in a context that belongs to a different isolate")
}
rtn := C.UnboundScriptRun(ctx.ptr, u.ptr)
return valueResult(ctx, rtn)
}

// Create a code cache from the unbound script.
func (u *UnboundScript) CreateCodeCache() *CompilerCachedData {
rtn := C.UnboundScriptCreateCodeCache(u.iso.ptr, u.ptr)

cachedData := &CompilerCachedData{
Bytes: []byte(C.GoBytes(unsafe.Pointer(rtn.data), rtn.length)),
Rejected: int(rtn.rejected) == 1,
}
C.ScriptCompilerCachedDataDelete(rtn)
return cachedData
}
47 changes: 47 additions & 0 deletions unbound_script_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// 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 TestUnboundScriptRun_OnlyInTheSameIsolate(t *testing.T) {
str := "function foo() { return 'bar'; }; foo()"
i1 := v8.NewIsolate()
defer i1.Dispose()

us, err := i1.CompileUnboundScript(str, "script.js", v8.CompileOptions{})
fatalIf(t, err)

c1 := v8.NewContext(i1)
defer c1.Close()

val, err := us.Run(c1)
fatalIf(t, err)
if val.String() != "bar" {
t.Fatalf("invalid value returned, expected bar got %v", val)
}

c2 := v8.NewContext(i1)
defer c2.Close()

val, err = us.Run(c2)
fatalIf(t, err)
if val.String() != "bar" {
t.Fatalf("invalid value returned, expected bar got %v", val)
}

i2 := v8.NewIsolate()
defer i2.Dispose()
i2c1 := v8.NewContext(i2)
defer i2c1.Close()

if recoverPanic(func() { us.Run(i2c1) }) == nil {
t.Error("expected panic running unbound script in a context belonging to a different isolate")
}
}
Loading

0 comments on commit 4a6bcfb

Please sign in to comment.