Skip to content

Commit 6dff945

Browse files
Genevieveryanmorandylanahsmith
authored
Introduce CpuProfiler, CpuProfile, and CpuProfileNode (#167)
* Introduce CpuProfiler, CpuProfile, CpuProfileNode * Test no-op for subsequent disposes of cpu profiler * Verify is profiler or iso is nil within start/stop functions * Expand example in readme * Adjust script run time to ensure sampling * Remove unneeded unsafe pointer casts, fix time conversion, panic on nil pointers in StartProfiling/StopProfiling * Panic on nil ptr/iso in start/stop profiling * Add backports for timeUnixMicro, add comments to new objects fields * Getter methods for cpu profile + cpu profile node * Update Readme, add GetChildrenCount to node * Find start node on root, ignore program/garbage collector children * Specify timeout for script + loosen search on children * Fix reversing of order of profile start and end time * Expose profile duration instead of start and end time. Co-authored-by: Ryan Moran <[email protected]> Co-authored-by: Dylan Thacker-Smith <[email protected]>
1 parent 8d1f88b commit 6dff945

11 files changed

+689
-1
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
- Support for calling a method on an object.
1616
- Support for calling `IsExecutionTerminating` on isolate to check if execution is still terminating.
1717
- Support for setting and getting internal fields for template object instances
18+
- Support for CPU profiling
1819

1920
### Changed
2021
- Removed error return value from NewIsolate which never fails

README.md

+49-1
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ go func() {
101101

102102
select {
103103
case val := <- vals:
104-
// sucess
104+
// success
105105
case err := <- errs:
106106
// javascript error
107107
case <- time.After(200 * time.Milliseconds):
@@ -111,6 +111,54 @@ case <- time.After(200 * time.Milliseconds):
111111
}
112112
```
113113

114+
### CPU Profiler
115+
116+
```go
117+
func createProfile() {
118+
iso := v8.NewIsolate()
119+
ctx := v8.NewContext(iso)
120+
cpuProfiler := v8.NewCPUProfiler(iso)
121+
122+
cpuProfiler.StartProfiling("my-profile")
123+
124+
ctx.RunScript(profileScript, "script.js") # this script is defined in cpuprofiler_test.go
125+
val, _ := ctx.Global().Get("start")
126+
fn, _ := val.AsFunction()
127+
fn.Call(ctx.Global())
128+
129+
cpuProfile := cpuProfiler.StopProfiling("my-profile")
130+
131+
printTree("", cpuProfile.GetTopDownRoot()) # helper function to print the profile
132+
}
133+
134+
func printTree(nest string, node *v8.CPUProfileNode) {
135+
fmt.Printf("%s%s %s:%d:%d\n", nest, node.GetFunctionName(), node.GetScriptResourceName(), node.GetLineNumber(), node.GetColumnNumber())
136+
count := node.GetChildrenCount()
137+
if count == 0 {
138+
return
139+
}
140+
nest = fmt.Sprintf("%s ", nest)
141+
for i := 0; i < count; i++ {
142+
printTree(nest, node.GetChild(i))
143+
}
144+
}
145+
146+
// Output
147+
// (root) :0:0
148+
// (program) :0:0
149+
// start script.js:23:15
150+
// foo script.js:15:13
151+
// delay script.js:12:15
152+
// loop script.js:1:14
153+
// bar script.js:13:13
154+
// delay script.js:12:15
155+
// loop script.js:1:14
156+
// baz script.js:14:13
157+
// delay script.js:12:15
158+
// loop script.js:1:14
159+
// (garbage collector) :0:0
160+
```
161+
114162
## Documentation
115163

116164
Go Reference & more examples: https://pkg.go.dev/rogchap.com/v8go

backports.go

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package v8go
2+
3+
import "time"
4+
5+
// Backport time.UnixMicro from go 1.17 - https://pkg.go.dev/time#UnixMicro
6+
// timeUnixMicro accepts microseconds and converts to nanoseconds to be used
7+
// with time.Unix which returns the local Time corresponding to the given Unix time,
8+
// usec microseconds since January 1, 1970 UTC.
9+
func timeUnixMicro(usec int64) time.Time {
10+
return time.Unix(0, usec*1000)
11+
}

cpuprofile.go

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package v8go
2+
3+
/*
4+
#include "v8go.h"
5+
*/
6+
import "C"
7+
import "time"
8+
9+
type CPUProfile struct {
10+
p *C.CPUProfile
11+
12+
// The CPU profile title.
13+
title string
14+
15+
// root is the root node of the top down call tree.
16+
root *CPUProfileNode
17+
18+
// startTimeOffset is the time when the profile recording was started
19+
// since some unspecified starting point.
20+
startTimeOffset time.Duration
21+
22+
// endTimeOffset is the time when the profile recording was stopped
23+
// since some unspecified starting point.
24+
// The point is equal to the starting point used by startTimeOffset.
25+
endTimeOffset time.Duration
26+
}
27+
28+
// Returns CPU profile title.
29+
func (c *CPUProfile) GetTitle() string {
30+
return c.title
31+
}
32+
33+
// Returns the root node of the top down call tree.
34+
func (c *CPUProfile) GetTopDownRoot() *CPUProfileNode {
35+
return c.root
36+
}
37+
38+
// Returns the duration of the profile.
39+
func (c *CPUProfile) GetDuration() time.Duration {
40+
return c.endTimeOffset - c.startTimeOffset
41+
}
42+
43+
// Deletes the profile and removes it from CpuProfiler's list.
44+
// All pointers to nodes previously returned become invalid.
45+
func (c *CPUProfile) Delete() {
46+
if c.p == nil {
47+
return
48+
}
49+
C.CPUProfileDelete(c.p)
50+
c.p = nil
51+
}

cpuprofile_test.go

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package v8go_test
2+
3+
import (
4+
"testing"
5+
6+
v8 "rogchap.com/v8go"
7+
)
8+
9+
func TestCPUProfile(t *testing.T) {
10+
t.Parallel()
11+
12+
ctx := v8.NewContext(nil)
13+
iso := ctx.Isolate()
14+
defer iso.Dispose()
15+
defer ctx.Close()
16+
17+
cpuProfiler := v8.NewCPUProfiler(iso)
18+
defer cpuProfiler.Dispose()
19+
20+
title := "cpuprofiletest"
21+
cpuProfiler.StartProfiling(title)
22+
23+
_, err := ctx.RunScript(profileScript, "script.js")
24+
fatalIf(t, err)
25+
val, err := ctx.Global().Get("start")
26+
fatalIf(t, err)
27+
fn, err := val.AsFunction()
28+
fatalIf(t, err)
29+
_, err = fn.Call(ctx.Global())
30+
fatalIf(t, err)
31+
32+
cpuProfile := cpuProfiler.StopProfiling(title)
33+
defer cpuProfile.Delete()
34+
35+
if cpuProfile.GetTitle() != title {
36+
t.Fatalf("expected title %s, but got %s", title, cpuProfile.GetTitle())
37+
}
38+
39+
root := cpuProfile.GetTopDownRoot()
40+
if root == nil {
41+
t.Fatal("expected root not to be nil")
42+
}
43+
if root.GetFunctionName() != "(root)" {
44+
t.Errorf("expected (root), but got %v", root.GetFunctionName())
45+
}
46+
47+
if cpuProfile.GetDuration() <= 0 {
48+
t.Fatalf("expected positive profile duration (%s)", cpuProfile.GetDuration())
49+
}
50+
}
51+
52+
func TestCPUProfile_Delete(t *testing.T) {
53+
t.Parallel()
54+
55+
iso := v8.NewIsolate()
56+
defer iso.Dispose()
57+
58+
cpuProfiler := v8.NewCPUProfiler(iso)
59+
defer cpuProfiler.Dispose()
60+
61+
cpuProfiler.StartProfiling("cpuprofiletest")
62+
cpuProfile := cpuProfiler.StopProfiling("cpuprofiletest")
63+
cpuProfile.Delete()
64+
// noop when called multiple times
65+
cpuProfile.Delete()
66+
}

cpuprofilenode.go

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package v8go
2+
3+
type CPUProfileNode struct {
4+
// The resource name for script from where the function originates.
5+
scriptResourceName string
6+
7+
// The function name (empty string for anonymous functions.)
8+
functionName string
9+
10+
// The number of the line where the function originates.
11+
lineNumber int
12+
13+
// The number of the column where the function originates.
14+
columnNumber int
15+
16+
// The children node of this node.
17+
children []*CPUProfileNode
18+
19+
// The parent node of this node.
20+
parent *CPUProfileNode
21+
}
22+
23+
// Returns function name (empty string for anonymous functions.)
24+
func (c *CPUProfileNode) GetFunctionName() string {
25+
return c.functionName
26+
}
27+
28+
// Returns resource name for script from where the function originates.
29+
func (c *CPUProfileNode) GetScriptResourceName() string {
30+
return c.scriptResourceName
31+
}
32+
33+
// Returns number of the line where the function originates.
34+
func (c *CPUProfileNode) GetLineNumber() int {
35+
return c.lineNumber
36+
}
37+
38+
// Returns number of the column where the function originates.
39+
func (c *CPUProfileNode) GetColumnNumber() int {
40+
return c.columnNumber
41+
}
42+
43+
// Retrieves the ancestor node, or nil if the root.
44+
func (c *CPUProfileNode) GetParent() *CPUProfileNode {
45+
return c.parent
46+
}
47+
48+
func (c *CPUProfileNode) GetChildrenCount() int {
49+
return len(c.children)
50+
}
51+
52+
// Retrieves a child node by index.
53+
func (c *CPUProfileNode) GetChild(index int) *CPUProfileNode {
54+
return c.children[index]
55+
}

cpuprofilenode_test.go

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package v8go_test
2+
3+
import (
4+
"testing"
5+
6+
v8 "rogchap.com/v8go"
7+
)
8+
9+
func TestCPUProfileNode(t *testing.T) {
10+
t.Parallel()
11+
12+
ctx := v8.NewContext(nil)
13+
iso := ctx.Isolate()
14+
defer iso.Dispose()
15+
defer ctx.Close()
16+
17+
cpuProfiler := v8.NewCPUProfiler(iso)
18+
defer cpuProfiler.Dispose()
19+
20+
title := "cpuprofilenodetest"
21+
cpuProfiler.StartProfiling(title)
22+
23+
_, err := ctx.RunScript(profileScript, "script.js")
24+
fatalIf(t, err)
25+
val, err := ctx.Global().Get("start")
26+
fatalIf(t, err)
27+
fn, err := val.AsFunction()
28+
fatalIf(t, err)
29+
timeout, err := v8.NewValue(iso, int32(1000))
30+
fatalIf(t, err)
31+
_, err = fn.Call(ctx.Global(), timeout)
32+
fatalIf(t, err)
33+
34+
cpuProfile := cpuProfiler.StopProfiling(title)
35+
if cpuProfile == nil {
36+
t.Fatal("expected profile not to be nil")
37+
}
38+
defer cpuProfile.Delete()
39+
40+
rootNode := cpuProfile.GetTopDownRoot()
41+
if rootNode == nil {
42+
t.Fatal("expected top down root not to be nil")
43+
}
44+
count := rootNode.GetChildrenCount()
45+
var startNode *v8.CPUProfileNode
46+
for i := 0; i < count; i++ {
47+
if rootNode.GetChild(i).GetFunctionName() == "start" {
48+
startNode = rootNode.GetChild(i)
49+
}
50+
}
51+
if startNode == nil {
52+
t.Fatal("expected node not to be nil")
53+
}
54+
checkNode(t, startNode, "script.js", "start", 23, 15)
55+
56+
parentName := startNode.GetParent().GetFunctionName()
57+
if parentName != "(root)" {
58+
t.Fatalf("expected (root), but got %v", parentName)
59+
}
60+
61+
fooNode := findChild(t, startNode, "foo")
62+
checkNode(t, fooNode, "script.js", "foo", 15, 13)
63+
64+
delayNode := findChild(t, fooNode, "delay")
65+
checkNode(t, delayNode, "script.js", "delay", 12, 15)
66+
67+
barNode := findChild(t, fooNode, "bar")
68+
checkNode(t, barNode, "script.js", "bar", 13, 13)
69+
70+
loopNode := findChild(t, delayNode, "loop")
71+
checkNode(t, loopNode, "script.js", "loop", 1, 14)
72+
73+
bazNode := findChild(t, fooNode, "baz")
74+
checkNode(t, bazNode, "script.js", "baz", 14, 13)
75+
}
76+
77+
func findChild(t *testing.T, node *v8.CPUProfileNode, functionName string) *v8.CPUProfileNode {
78+
t.Helper()
79+
80+
var child *v8.CPUProfileNode
81+
count := node.GetChildrenCount()
82+
for i := 0; i < count; i++ {
83+
if node.GetChild(i).GetFunctionName() == functionName {
84+
child = node.GetChild(i)
85+
}
86+
}
87+
if child == nil {
88+
t.Fatal("failed to find child node")
89+
}
90+
return child
91+
}
92+
93+
func checkNode(t *testing.T, node *v8.CPUProfileNode, scriptResourceName string, functionName string, line, column int) {
94+
t.Helper()
95+
96+
if node.GetFunctionName() != functionName {
97+
t.Fatalf("expected node to have function name %s, but got %s", functionName, node.GetFunctionName())
98+
}
99+
if node.GetScriptResourceName() != scriptResourceName {
100+
t.Fatalf("expected node to have script resource name %s, but got %s", scriptResourceName, node.GetScriptResourceName())
101+
}
102+
if node.GetLineNumber() != line {
103+
t.Fatalf("expected node at line %d, but got %d", line, node.GetLineNumber())
104+
}
105+
if node.GetColumnNumber() != column {
106+
t.Fatalf("expected node at column %d, but got %d", column, node.GetColumnNumber())
107+
}
108+
}

0 commit comments

Comments
 (0)