Skip to content

Commit 4a6bcfb

Browse files
Genevievedylanahsmith
Genevieve
andauthored
Add functions to separate compilation from running to speed up start times on new isolates loading the same scripts. (#206)
* CompileScript + RunCompiledScript Co-authored-by: Dylan Thacker-Smith <[email protected]> * 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 <[email protected]>
1 parent 7df088a commit 4a6bcfb

10 files changed

+423
-14
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
- Support for setting and getting internal fields for template object instances
1717
- Support for CPU profiling
1818
- Add V8 build for Apple Silicon
19+
- Support for compiling a context-dependent UnboundScript which can be run in any context of the isolate it was compiled in.
20+
- Support for creating a code cache from an UnboundScript which can be used to create an UnboundScript in other isolates
21+
to run a pre-compiled script in new contexts.
1922

2023
### Changed
2124
- Removed error return value from NewIsolate which never fails

README.md

+22
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,28 @@ if err != nil {
8484
}
8585
```
8686

87+
### Pre-compile context-independent scripts to speed-up execution times
88+
89+
For scripts that are large or are repeatedly run in different contexts,
90+
it is beneficial to compile the script once and used the cached data from that
91+
compilation to avoid recompiling every time you want to run it.
92+
93+
```go
94+
source := "const multiply = (a, b) => a * b"
95+
iso1 := v8.NewIsolate() // creates a new JavaScript VM
96+
ctx1 := v8.NewContext() // new context within the VM
97+
script1, _ := iso1.CompileUnboundScript(source, "math.js", v8.CompileOptions{}) // compile script to get cached data
98+
val, _ := script1.Run(ctx1)
99+
100+
cachedData := script1.CreateCodeCache()
101+
102+
iso2 := v8.NewIsolate() // create a new JavaScript VM
103+
ctx2 := v8.NewContext(iso2) // new context within the VM
104+
105+
script2, _ := iso2.CompileUnboundScript(source, "math.js", v8.CompileOptions{CachedData: cachedData}) // compile script in new isolate with cached data
106+
val, _ = script2.Run(ctx2)
107+
```
108+
87109
### Terminate long running scripts
88110

89111
```go

context.go

+2-3
Original file line numberDiff line numberDiff line change
@@ -82,17 +82,16 @@ func (c *Context) Isolate() *Isolate {
8282
return c.iso
8383
}
8484

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

9494
rtn := C.RunScript(c.ptr, cSource, cOrigin)
95-
9695
return valueResult(c, rtn)
9796
}
9897

isolate.go

+46
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44

55
package v8go
66

7+
// #include <stdlib.h>
78
// #include "v8go.h"
89
import "C"
910

1011
import (
1112
"sync"
13+
"unsafe"
1214
)
1315

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

80+
type CompileOptions struct {
81+
CachedData *CompilerCachedData
82+
83+
Mode CompileMode
84+
}
85+
86+
// CompileUnboundScript will create an UnboundScript (i.e. context-indepdent)
87+
// using the provided source JavaScript, origin (a.k.a. filename), and options.
88+
// If options contain a non-null CachedData, compilation of the script will use
89+
// that code cache.
90+
// error will be of type `JSError` if not nil.
91+
func (i *Isolate) CompileUnboundScript(source, origin string, opts CompileOptions) (*UnboundScript, error) {
92+
cSource := C.CString(source)
93+
cOrigin := C.CString(origin)
94+
defer C.free(unsafe.Pointer(cSource))
95+
defer C.free(unsafe.Pointer(cOrigin))
96+
97+
var cOptions C.CompileOptions
98+
if opts.CachedData != nil {
99+
if opts.Mode != 0 {
100+
panic("On CompileOptions, Mode and CachedData can't both be set")
101+
}
102+
cOptions.compileOption = C.ScriptCompilerConsumeCodeCache
103+
cOptions.cachedData = C.ScriptCompilerCachedData{
104+
data: (*C.uchar)(unsafe.Pointer(&opts.CachedData.Bytes[0])),
105+
length: C.int(len(opts.CachedData.Bytes)),
106+
}
107+
} else {
108+
cOptions.compileOption = C.int(opts.Mode)
109+
}
110+
111+
rtn := C.IsolateCompileUnboundScript(i.ptr, cSource, cOrigin, cOptions)
112+
if rtn.ptr == nil {
113+
return nil, newJSError(rtn.error)
114+
}
115+
if opts.CachedData != nil {
116+
opts.CachedData.Rejected = int(rtn.cachedDataRejected) == 1
117+
}
118+
return &UnboundScript{
119+
ptr: rtn.ptr,
120+
iso: i,
121+
}, nil
122+
}
123+
78124
// GetHeapStatistics returns heap statistics for an isolate.
79125
func (i *Isolate) GetHeapStatistics() HeapStatistics {
80126
hs := C.IsolationGetHeapStatistics(i.ptr)

isolate_test.go

+88-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,94 @@ func TestIsolateTermination(t *testing.T) {
5757
}
5858
}
5959

60-
func TestGetHeapStatistics(t *testing.T) {
60+
func TestIsolateCompileUnboundScript(t *testing.T) {
61+
s := "function foo() { return 'bar'; }; foo()"
62+
63+
i1 := v8.NewIsolate()
64+
defer i1.Dispose()
65+
c1 := v8.NewContext(i1)
66+
defer c1.Close()
67+
68+
_, err := i1.CompileUnboundScript("invalid js", "filename", v8.CompileOptions{})
69+
if err == nil {
70+
t.Fatal("expected error")
71+
}
72+
73+
us, err := i1.CompileUnboundScript(s, "script.js", v8.CompileOptions{Mode: v8.CompileModeEager})
74+
fatalIf(t, err)
75+
76+
val, err := us.Run(c1)
77+
fatalIf(t, err)
78+
if val.String() != "bar" {
79+
t.Fatalf("invalid value returned, expected bar got %v", val)
80+
}
81+
82+
cachedData := us.CreateCodeCache()
83+
84+
i2 := v8.NewIsolate()
85+
defer i2.Dispose()
86+
c2 := v8.NewContext(i2)
87+
defer c2.Close()
88+
89+
opts := v8.CompileOptions{CachedData: cachedData}
90+
usWithCachedData, err := i2.CompileUnboundScript(s, "script.js", opts)
91+
fatalIf(t, err)
92+
if usWithCachedData == nil {
93+
t.Fatal("expected unbound script from cached data not to be nil")
94+
}
95+
if opts.CachedData.Rejected {
96+
t.Fatal("expected cached data to be used, not rejected")
97+
}
98+
99+
val, err = usWithCachedData.Run(c2)
100+
fatalIf(t, err)
101+
if val.String() != "bar" {
102+
t.Fatalf("invalid value returned, expected bar got %v", val)
103+
}
104+
}
105+
106+
func TestIsolateCompileUnboundScript_CachedDataRejected(t *testing.T) {
107+
s := "function foo() { return 'bar'; }; foo()"
108+
iso := v8.NewIsolate()
109+
defer iso.Dispose()
110+
111+
// Try to compile an unbound script using cached data that does not match this source
112+
opts := v8.CompileOptions{CachedData: &v8.CompilerCachedData{Bytes: []byte("Math.sqrt(4)")}}
113+
us, err := iso.CompileUnboundScript(s, "script.js", opts)
114+
fatalIf(t, err)
115+
if !opts.CachedData.Rejected {
116+
t.Error("expected cached data to be rejected")
117+
}
118+
119+
ctx := v8.NewContext(iso)
120+
defer ctx.Close()
121+
122+
// Verify that unbound script is still compiled and able to be used
123+
val, err := us.Run(ctx)
124+
fatalIf(t, err)
125+
if val.String() != "bar" {
126+
t.Errorf("invalid value returned, expected bar got %v", val)
127+
}
128+
}
129+
130+
func TestIsolateCompileUnboundScript_InvalidOptions(t *testing.T) {
131+
iso := v8.NewIsolate()
132+
defer iso.Dispose()
133+
134+
opts := v8.CompileOptions{
135+
CachedData: &v8.CompilerCachedData{Bytes: []byte("unused")},
136+
Mode: v8.CompileModeEager,
137+
}
138+
panicErr := recoverPanic(func() { iso.CompileUnboundScript("console.log(1)", "script.js", opts) })
139+
if panicErr == nil {
140+
t.Error("expected panic")
141+
}
142+
if panicErr != "On CompileOptions, Mode and CachedData can't both be set" {
143+
t.Errorf("unexpected panic: %v\n", panicErr)
144+
}
145+
}
146+
147+
func TestIsolateGetHeapStatistics(t *testing.T) {
61148
t.Parallel()
62149
iso := v8.NewIsolate()
63150
defer iso.Dispose()

script_compiler.go

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright 2021 the v8go contributors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package v8go
6+
7+
// #include "v8go.h"
8+
import "C"
9+
10+
type CompileMode C.int
11+
12+
var (
13+
CompileModeDefault = CompileMode(C.ScriptCompilerNoCompileOptions)
14+
CompileModeEager = CompileMode(C.ScriptCompilerEagerCompile)
15+
)
16+
17+
type CompilerCachedData struct {
18+
Bytes []byte
19+
Rejected bool
20+
}

unbound_script.go

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright 2021 the v8go contributors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package v8go
6+
7+
// #include <stdlib.h>
8+
// #include "v8go.h"
9+
import "C"
10+
import "unsafe"
11+
12+
type UnboundScript struct {
13+
ptr C.UnboundScriptPtr
14+
iso *Isolate
15+
}
16+
17+
// Run will bind the unbound script to the provided context and run it.
18+
// If the context provided does not belong to the same isolate that the script
19+
// was compiled in, Run will panic.
20+
// If an error occurs, it will be of type `JSError`.
21+
func (u *UnboundScript) Run(ctx *Context) (*Value, error) {
22+
if ctx.Isolate() != u.iso {
23+
panic("attempted to run unbound script in a context that belongs to a different isolate")
24+
}
25+
rtn := C.UnboundScriptRun(ctx.ptr, u.ptr)
26+
return valueResult(ctx, rtn)
27+
}
28+
29+
// Create a code cache from the unbound script.
30+
func (u *UnboundScript) CreateCodeCache() *CompilerCachedData {
31+
rtn := C.UnboundScriptCreateCodeCache(u.iso.ptr, u.ptr)
32+
33+
cachedData := &CompilerCachedData{
34+
Bytes: []byte(C.GoBytes(unsafe.Pointer(rtn.data), rtn.length)),
35+
Rejected: int(rtn.rejected) == 1,
36+
}
37+
C.ScriptCompilerCachedDataDelete(rtn)
38+
return cachedData
39+
}

unbound_script_test.go

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright 2021 the v8go contributors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package v8go_test
6+
7+
import (
8+
"testing"
9+
10+
v8 "rogchap.com/v8go"
11+
)
12+
13+
func TestUnboundScriptRun_OnlyInTheSameIsolate(t *testing.T) {
14+
str := "function foo() { return 'bar'; }; foo()"
15+
i1 := v8.NewIsolate()
16+
defer i1.Dispose()
17+
18+
us, err := i1.CompileUnboundScript(str, "script.js", v8.CompileOptions{})
19+
fatalIf(t, err)
20+
21+
c1 := v8.NewContext(i1)
22+
defer c1.Close()
23+
24+
val, err := us.Run(c1)
25+
fatalIf(t, err)
26+
if val.String() != "bar" {
27+
t.Fatalf("invalid value returned, expected bar got %v", val)
28+
}
29+
30+
c2 := v8.NewContext(i1)
31+
defer c2.Close()
32+
33+
val, err = us.Run(c2)
34+
fatalIf(t, err)
35+
if val.String() != "bar" {
36+
t.Fatalf("invalid value returned, expected bar got %v", val)
37+
}
38+
39+
i2 := v8.NewIsolate()
40+
defer i2.Dispose()
41+
i2c1 := v8.NewContext(i2)
42+
defer i2c1.Close()
43+
44+
if recoverPanic(func() { us.Run(i2c1) }) == nil {
45+
t.Error("expected panic running unbound script in a context belonging to a different isolate")
46+
}
47+
}

0 commit comments

Comments
 (0)