diff --git a/CHANGELOG.md b/CHANGELOG.md index 84830239f..2f67a6b09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support for calling a method on an object. - Support for calling `IsExecutionTerminating` on isolate to check if execution is still terminating. - Support for setting and getting internal fields for template object instances +- Support for CPU profiling ### Changed - Removed error return value from NewIsolate which never fails diff --git a/README.md b/README.md index 0f010a8d0..0030818b4 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ go func() { select { case val := <- vals: - // sucess + // success case err := <- errs: // javascript error case <- time.After(200 * time.Milliseconds): @@ -111,6 +111,54 @@ case <- time.After(200 * time.Milliseconds): } ``` +### CPU Profiler + +```go +func createProfile() { + iso := v8.NewIsolate() + ctx := v8.NewContext(iso) + cpuProfiler := v8.NewCPUProfiler(iso) + + cpuProfiler.StartProfiling("my-profile") + + ctx.RunScript(profileScript, "script.js") # this script is defined in cpuprofiler_test.go + val, _ := ctx.Global().Get("start") + fn, _ := val.AsFunction() + fn.Call(ctx.Global()) + + cpuProfile := cpuProfiler.StopProfiling("my-profile") + + printTree("", cpuProfile.GetTopDownRoot()) # helper function to print the profile +} + +func printTree(nest string, node *v8.CPUProfileNode) { + fmt.Printf("%s%s %s:%d:%d\n", nest, node.GetFunctionName(), node.GetScriptResourceName(), node.GetLineNumber(), node.GetColumnNumber()) + count := node.GetChildrenCount() + if count == 0 { + return + } + nest = fmt.Sprintf("%s ", nest) + for i := 0; i < count; i++ { + printTree(nest, node.GetChild(i)) + } +} + +// Output +// (root) :0:0 +// (program) :0:0 +// start script.js:23:15 +// foo script.js:15:13 +// delay script.js:12:15 +// loop script.js:1:14 +// bar script.js:13:13 +// delay script.js:12:15 +// loop script.js:1:14 +// baz script.js:14:13 +// delay script.js:12:15 +// loop script.js:1:14 +// (garbage collector) :0:0 +``` + ## Documentation Go Reference & more examples: https://pkg.go.dev/rogchap.com/v8go diff --git a/backports.go b/backports.go new file mode 100644 index 000000000..61089e1f4 --- /dev/null +++ b/backports.go @@ -0,0 +1,11 @@ +package v8go + +import "time" + +// Backport time.UnixMicro from go 1.17 - https://pkg.go.dev/time#UnixMicro +// timeUnixMicro accepts microseconds and converts to nanoseconds to be used +// with time.Unix which returns the local Time corresponding to the given Unix time, +// usec microseconds since January 1, 1970 UTC. +func timeUnixMicro(usec int64) time.Time { + return time.Unix(0, usec*1000) +} diff --git a/cpuprofile.go b/cpuprofile.go new file mode 100644 index 000000000..44c972514 --- /dev/null +++ b/cpuprofile.go @@ -0,0 +1,51 @@ +package v8go + +/* +#include "v8go.h" +*/ +import "C" +import "time" + +type CPUProfile struct { + p *C.CPUProfile + + // The CPU profile title. + title string + + // root is the root node of the top down call tree. + root *CPUProfileNode + + // startTimeOffset is the time when the profile recording was started + // since some unspecified starting point. + startTimeOffset time.Duration + + // endTimeOffset is the time when the profile recording was stopped + // since some unspecified starting point. + // The point is equal to the starting point used by startTimeOffset. + endTimeOffset time.Duration +} + +// Returns CPU profile title. +func (c *CPUProfile) GetTitle() string { + return c.title +} + +// Returns the root node of the top down call tree. +func (c *CPUProfile) GetTopDownRoot() *CPUProfileNode { + return c.root +} + +// Returns the duration of the profile. +func (c *CPUProfile) GetDuration() time.Duration { + return c.endTimeOffset - c.startTimeOffset +} + +// Deletes the profile and removes it from CpuProfiler's list. +// All pointers to nodes previously returned become invalid. +func (c *CPUProfile) Delete() { + if c.p == nil { + return + } + C.CPUProfileDelete(c.p) + c.p = nil +} diff --git a/cpuprofile_test.go b/cpuprofile_test.go new file mode 100644 index 000000000..e82add555 --- /dev/null +++ b/cpuprofile_test.go @@ -0,0 +1,66 @@ +package v8go_test + +import ( + "testing" + + v8 "rogchap.com/v8go" +) + +func TestCPUProfile(t *testing.T) { + t.Parallel() + + ctx := v8.NewContext(nil) + iso := ctx.Isolate() + defer iso.Dispose() + defer ctx.Close() + + cpuProfiler := v8.NewCPUProfiler(iso) + defer cpuProfiler.Dispose() + + title := "cpuprofiletest" + cpuProfiler.StartProfiling(title) + + _, err := ctx.RunScript(profileScript, "script.js") + fatalIf(t, err) + val, err := ctx.Global().Get("start") + fatalIf(t, err) + fn, err := val.AsFunction() + fatalIf(t, err) + _, err = fn.Call(ctx.Global()) + fatalIf(t, err) + + cpuProfile := cpuProfiler.StopProfiling(title) + defer cpuProfile.Delete() + + if cpuProfile.GetTitle() != title { + t.Fatalf("expected title %s, but got %s", title, cpuProfile.GetTitle()) + } + + root := cpuProfile.GetTopDownRoot() + if root == nil { + t.Fatal("expected root not to be nil") + } + if root.GetFunctionName() != "(root)" { + t.Errorf("expected (root), but got %v", root.GetFunctionName()) + } + + if cpuProfile.GetDuration() <= 0 { + t.Fatalf("expected positive profile duration (%s)", cpuProfile.GetDuration()) + } +} + +func TestCPUProfile_Delete(t *testing.T) { + t.Parallel() + + iso := v8.NewIsolate() + defer iso.Dispose() + + cpuProfiler := v8.NewCPUProfiler(iso) + defer cpuProfiler.Dispose() + + cpuProfiler.StartProfiling("cpuprofiletest") + cpuProfile := cpuProfiler.StopProfiling("cpuprofiletest") + cpuProfile.Delete() + // noop when called multiple times + cpuProfile.Delete() +} diff --git a/cpuprofilenode.go b/cpuprofilenode.go new file mode 100644 index 000000000..c560d35ce --- /dev/null +++ b/cpuprofilenode.go @@ -0,0 +1,55 @@ +package v8go + +type CPUProfileNode struct { + // The resource name for script from where the function originates. + scriptResourceName string + + // The function name (empty string for anonymous functions.) + functionName string + + // The number of the line where the function originates. + lineNumber int + + // The number of the column where the function originates. + columnNumber int + + // The children node of this node. + children []*CPUProfileNode + + // The parent node of this node. + parent *CPUProfileNode +} + +// Returns function name (empty string for anonymous functions.) +func (c *CPUProfileNode) GetFunctionName() string { + return c.functionName +} + +// Returns resource name for script from where the function originates. +func (c *CPUProfileNode) GetScriptResourceName() string { + return c.scriptResourceName +} + +// Returns number of the line where the function originates. +func (c *CPUProfileNode) GetLineNumber() int { + return c.lineNumber +} + +// Returns number of the column where the function originates. +func (c *CPUProfileNode) GetColumnNumber() int { + return c.columnNumber +} + +// Retrieves the ancestor node, or nil if the root. +func (c *CPUProfileNode) GetParent() *CPUProfileNode { + return c.parent +} + +func (c *CPUProfileNode) GetChildrenCount() int { + return len(c.children) +} + +// Retrieves a child node by index. +func (c *CPUProfileNode) GetChild(index int) *CPUProfileNode { + return c.children[index] +} diff --git a/cpuprofilenode_test.go b/cpuprofilenode_test.go new file mode 100644 index 000000000..bd74a0efe --- /dev/null +++ b/cpuprofilenode_test.go @@ -0,0 +1,108 @@ +package v8go_test + +import ( + "testing" + + v8 "rogchap.com/v8go" +) + +func TestCPUProfileNode(t *testing.T) { + t.Parallel() + + ctx := v8.NewContext(nil) + iso := ctx.Isolate() + defer iso.Dispose() + defer ctx.Close() + + cpuProfiler := v8.NewCPUProfiler(iso) + defer cpuProfiler.Dispose() + + title := "cpuprofilenodetest" + cpuProfiler.StartProfiling(title) + + _, err := ctx.RunScript(profileScript, "script.js") + fatalIf(t, err) + val, err := ctx.Global().Get("start") + fatalIf(t, err) + fn, err := val.AsFunction() + fatalIf(t, err) + timeout, err := v8.NewValue(iso, int32(1000)) + fatalIf(t, err) + _, err = fn.Call(ctx.Global(), timeout) + fatalIf(t, err) + + cpuProfile := cpuProfiler.StopProfiling(title) + if cpuProfile == nil { + t.Fatal("expected profile not to be nil") + } + defer cpuProfile.Delete() + + rootNode := cpuProfile.GetTopDownRoot() + if rootNode == nil { + t.Fatal("expected top down root not to be nil") + } + count := rootNode.GetChildrenCount() + var startNode *v8.CPUProfileNode + for i := 0; i < count; i++ { + if rootNode.GetChild(i).GetFunctionName() == "start" { + startNode = rootNode.GetChild(i) + } + } + if startNode == nil { + t.Fatal("expected node not to be nil") + } + checkNode(t, startNode, "script.js", "start", 23, 15) + + parentName := startNode.GetParent().GetFunctionName() + if parentName != "(root)" { + t.Fatalf("expected (root), but got %v", parentName) + } + + fooNode := findChild(t, startNode, "foo") + checkNode(t, fooNode, "script.js", "foo", 15, 13) + + delayNode := findChild(t, fooNode, "delay") + checkNode(t, delayNode, "script.js", "delay", 12, 15) + + barNode := findChild(t, fooNode, "bar") + checkNode(t, barNode, "script.js", "bar", 13, 13) + + loopNode := findChild(t, delayNode, "loop") + checkNode(t, loopNode, "script.js", "loop", 1, 14) + + bazNode := findChild(t, fooNode, "baz") + checkNode(t, bazNode, "script.js", "baz", 14, 13) +} + +func findChild(t *testing.T, node *v8.CPUProfileNode, functionName string) *v8.CPUProfileNode { + t.Helper() + + var child *v8.CPUProfileNode + count := node.GetChildrenCount() + for i := 0; i < count; i++ { + if node.GetChild(i).GetFunctionName() == functionName { + child = node.GetChild(i) + } + } + if child == nil { + t.Fatal("failed to find child node") + } + return child +} + +func checkNode(t *testing.T, node *v8.CPUProfileNode, scriptResourceName string, functionName string, line, column int) { + t.Helper() + + if node.GetFunctionName() != functionName { + t.Fatalf("expected node to have function name %s, but got %s", functionName, node.GetFunctionName()) + } + if node.GetScriptResourceName() != scriptResourceName { + t.Fatalf("expected node to have script resource name %s, but got %s", scriptResourceName, node.GetScriptResourceName()) + } + if node.GetLineNumber() != line { + t.Fatalf("expected node at line %d, but got %d", line, node.GetLineNumber()) + } + if node.GetColumnNumber() != column { + t.Fatalf("expected node at column %d, but got %d", column, node.GetColumnNumber()) + } +} diff --git a/cpuprofiler.go b/cpuprofiler.go new file mode 100644 index 000000000..bb34ff456 --- /dev/null +++ b/cpuprofiler.go @@ -0,0 +1,92 @@ +// Copyright 2021 Roger Chapman and 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 ( + "time" + "unsafe" +) + +type CPUProfiler struct { + p *C.CPUProfiler + iso *Isolate +} + +// CPUProfiler is used to control CPU profiling. +func NewCPUProfiler(iso *Isolate) *CPUProfiler { + profiler := C.NewCPUProfiler(iso.ptr) + return &CPUProfiler{ + p: profiler, + iso: iso, + } +} + +// Dispose will dispose the profiler. +func (c *CPUProfiler) Dispose() { + if c.p == nil { + return + } + + C.CPUProfilerDispose(c.p) + c.p = nil +} + +// StartProfiling starts collecting a CPU profile. Title may be an empty string. Several +// profiles may be collected at once. Attempts to start collecting several +// profiles with the same title are silently ignored. +func (c *CPUProfiler) StartProfiling(title string) { + if c.p == nil || c.iso.ptr == nil { + panic("profiler or isolate are nil") + } + + tstr := C.CString(title) + defer C.free(unsafe.Pointer(tstr)) + + C.CPUProfilerStartProfiling(c.p, tstr) +} + +// Stops collecting CPU profile with a given title and returns it. +// If the title given is empty, finishes the last profile started. +func (c *CPUProfiler) StopProfiling(title string) *CPUProfile { + if c.p == nil || c.iso.ptr == nil { + panic("profiler or isolate are nil") + } + + tstr := C.CString(title) + defer C.free(unsafe.Pointer(tstr)) + + profile := C.CPUProfilerStopProfiling(c.p, tstr) + + return &CPUProfile{ + p: profile, + title: C.GoString(profile.title), + root: newCPUProfileNode(profile.root, nil), + startTimeOffset: time.Duration(profile.startTime) * time.Millisecond, + endTimeOffset: time.Duration(profile.endTime) * time.Millisecond, + } +} + +func newCPUProfileNode(node *C.CPUProfileNode, parent *CPUProfileNode) *CPUProfileNode { + n := &CPUProfileNode{ + scriptResourceName: C.GoString(node.scriptResourceName), + functionName: C.GoString(node.functionName), + lineNumber: int(node.lineNumber), + columnNumber: int(node.columnNumber), + parent: parent, + } + + if node.childrenCount > 0 { + for _, child := range (*[1 << 28]*C.CPUProfileNode)(unsafe.Pointer(node.children))[:node.childrenCount:node.childrenCount] { + n.children = append(n.children, newCPUProfileNode(child, n)) + } + } + + return n +} diff --git a/cpuprofiler_test.go b/cpuprofiler_test.go new file mode 100644 index 000000000..646030a37 --- /dev/null +++ b/cpuprofiler_test.go @@ -0,0 +1,109 @@ +// Copyright 2021 Roger Chapman and 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 TestCPUProfiler_Dispose(t *testing.T) { + t.Parallel() + + iso := v8.NewIsolate() + defer iso.Dispose() + cpuProfiler := v8.NewCPUProfiler(iso) + + cpuProfiler.Dispose() + // noop when called multiple times + cpuProfiler.Dispose() + + // verify panics when profiler disposed + if recoverPanic(func() { cpuProfiler.StartProfiling("") }) == nil { + t.Error("expected panic") + } + + if recoverPanic(func() { cpuProfiler.StopProfiling("") }) == nil { + t.Error("expected panic") + } + + cpuProfiler = v8.NewCPUProfiler(iso) + defer cpuProfiler.Dispose() + iso.Dispose() + + // verify panics when isolate disposed + if recoverPanic(func() { cpuProfiler.StartProfiling("") }) == nil { + t.Error("expected panic") + } + + if recoverPanic(func() { cpuProfiler.StopProfiling("") }) == nil { + t.Error("expected panic") + } +} + +func TestCPUProfiler(t *testing.T) { + t.Parallel() + + ctx := v8.NewContext(nil) + iso := ctx.Isolate() + defer iso.Dispose() + defer ctx.Close() + + cpuProfiler := v8.NewCPUProfiler(iso) + defer cpuProfiler.Dispose() + + title := "cpuprofilertest" + cpuProfiler.StartProfiling(title) + + _, err := ctx.RunScript(profileScript, "script.js") + fatalIf(t, err) + val, err := ctx.Global().Get("start") + fatalIf(t, err) + fn, err := val.AsFunction() + fatalIf(t, err) + timeout, err := v8.NewValue(iso, int32(0)) + fatalIf(t, err) + _, err = fn.Call(ctx.Global(), timeout) + fatalIf(t, err) + + cpuProfile := cpuProfiler.StopProfiling(title) + defer cpuProfile.Delete() + + if cpuProfile.GetTitle() != title { + t.Errorf("expected %s, but got %s", title, cpuProfile.GetTitle()) + } +} + +const profileScript = `function loop(timeout) { + this.mmm = 0; + var start = Date.now(); + while (Date.now() - start < timeout) { + var n = 10; + while(n > 1) { + n--; + this.mmm += n * n * n; + } + } +} +function delay() { try { loop(10); } catch(e) { } } +function bar() { delay(); } +function baz() { delay(); } +function foo() { + try { + delay(); + bar(); + delay(); + baz(); + } catch (e) { } +} +function start(timeout) { + var start = Date.now(); + do { + foo(); + var duration = Date.now() - start; + } while (duration < timeout); + return duration; +};` diff --git a/v8go.cc b/v8go.cc index 8fda5de99..b0f80b32a 100644 --- a/v8go.cc +++ b/v8go.cc @@ -20,6 +20,7 @@ struct _EXCEPTION_POINTERS; #include "libplatform/libplatform.h" #include "v8.h" +#include "v8-profiler.h" #include "_cgo_export.h" using namespace v8; @@ -205,6 +206,108 @@ IsolateHStatistics IsolationGetHeapStatistics(IsolatePtr iso) { hs.number_of_detached_contexts()}; } +/********** CpuProfiler **********/ + +CPUProfiler* NewCPUProfiler(IsolatePtr iso_ptr) { + Isolate* iso = static_cast(iso_ptr); + Locker locker(iso); + Isolate::Scope isolate_scope(iso); + HandleScope handle_scope(iso); + + CPUProfiler* c = new CPUProfiler; + c->iso = iso; + c->ptr = CpuProfiler::New(iso); + return c; +} + +void CPUProfilerDispose(CPUProfiler* profiler) { + if (profiler->ptr == nullptr) { + return; + } + profiler->ptr->Dispose(); + + delete profiler; +} + +void CPUProfilerStartProfiling(CPUProfiler* profiler, const char* title) { + if (profiler->iso == nullptr) { + return; + } + + Locker locker(profiler->iso); + Isolate::Scope isolate_scope(profiler->iso); + HandleScope handle_scope(profiler->iso); + + Local title_str = String::NewFromUtf8(profiler->iso, title, NewStringType::kNormal).ToLocalChecked(); + profiler->ptr->StartProfiling(title_str); +} + +CPUProfileNode* NewCPUProfileNode(const CpuProfileNode* ptr_) { + int count = ptr_->GetChildrenCount(); + CPUProfileNode** children = new CPUProfileNode*[count]; + for (int i = 0; i < count; ++i) { + children[i] = NewCPUProfileNode(ptr_->GetChild(i)); + } + + CPUProfileNode* root = new CPUProfileNode{ + ptr_, + ptr_->GetScriptResourceNameStr(), + ptr_->GetFunctionNameStr(), + ptr_->GetLineNumber(), + ptr_->GetColumnNumber(), + count, + children, + }; + return root; +} + +CPUProfile* CPUProfilerStopProfiling(CPUProfiler* profiler, const char* title) { + if (profiler->iso == nullptr) { + return nullptr; + } + + Locker locker(profiler->iso); + Isolate::Scope isolate_scope(profiler->iso); + HandleScope handle_scope(profiler->iso); + + Local title_str = + String::NewFromUtf8(profiler->iso, title, NewStringType::kNormal).ToLocalChecked(); + + CPUProfile* profile = new CPUProfile; + profile->ptr = profiler->ptr->StopProfiling(title_str); + + Local str = profile->ptr->GetTitle(); + String::Utf8Value t(profiler->iso, str); + profile->title = CopyString(t); + + CPUProfileNode* root = NewCPUProfileNode(profile->ptr->GetTopDownRoot()); + profile->root = root; + + profile->startTime = profile->ptr->GetStartTime(); + profile->endTime = profile->ptr->GetEndTime(); + + return profile; +} + +void CPUProfileNodeDelete(CPUProfileNode* node) { + for (int i = 0; i < node->childrenCount; ++i) { + CPUProfileNodeDelete(node->children[i]); + } + + delete node; +} + +void CPUProfileDelete(CPUProfile* profile) { + if (profile->ptr == nullptr) { + return; + } + profile->ptr->Delete(); + + CPUProfileNodeDelete(profile->root); + + delete profile; +} + /********** Template **********/ #define LOCAL_TEMPLATE(tmpl_ptr) \ diff --git a/v8go.h b/v8go.h index 371ec10ee..001912861 100644 --- a/v8go.h +++ b/v8go.h @@ -8,15 +8,30 @@ namespace v8 { class Isolate; +class CpuProfiler; +class CpuProfile; +class CpuProfileNode; } typedef v8::Isolate* IsolatePtr; +typedef v8::CpuProfiler* CpuProfilerPtr; +typedef v8::CpuProfile* CpuProfilePtr; +typedef const v8::CpuProfileNode* CpuProfileNodePtr; extern "C" { #else // Opaque to cgo, but useful to treat it as a pointer to a distinct type typedef struct v8Isolate v8Isolate; typedef v8Isolate* IsolatePtr; + +typedef struct v8CpuProfiler v8CpuProfiler; +typedef v8CpuProfiler* CpuProfilerPtr; + +typedef struct v8CpuProfile v8CpuProfile; +typedef v8CpuProfile* CpuProfilePtr; + +typedef struct v8CpuProfileNode v8CpuProfileNode; +typedef const v8CpuProfileNode* CpuProfileNodePtr; #endif #include @@ -30,6 +45,29 @@ typedef m_ctx* ContextPtr; typedef m_value* ValuePtr; typedef m_template* TemplatePtr; +typedef struct { + CpuProfilerPtr ptr; + IsolatePtr iso; +} CPUProfiler; + +typedef struct CPUProfileNode { + CpuProfileNodePtr ptr; + const char* scriptResourceName; + const char* functionName; + int lineNumber; + int columnNumber; + int childrenCount; + struct CPUProfileNode** children; +} CPUProfileNode; + +typedef struct { + CpuProfilePtr ptr; + const char* title; + CPUProfileNode* root; + int64_t startTime; + int64_t endTime; +} CPUProfile; + typedef struct { const char* msg; const char* location; @@ -74,6 +112,12 @@ extern void IsolateTerminateExecution(IsolatePtr ptr); extern int IsolateIsExecutionTerminating(IsolatePtr ptr); extern IsolateHStatistics IsolationGetHeapStatistics(IsolatePtr ptr); +extern CPUProfiler* NewCPUProfiler(IsolatePtr iso_ptr); +extern void CPUProfilerDispose(CPUProfiler* ptr); +extern void CPUProfilerStartProfiling(CPUProfiler* ptr, const char* title); +extern CPUProfile* CPUProfilerStopProfiling(CPUProfiler* ptr, const char* title); +extern void CPUProfileDelete(CPUProfile* ptr); + extern ContextPtr NewContext(IsolatePtr iso_ptr, TemplatePtr global_template_ptr, int ref);