diff --git a/README.md b/README.md index 2b10506f..b8940be6 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,27 @@ func printTree(nest string, node *v8.CPUProfileNode) { // (garbage collector) :0:0 ``` +### Heap Snapshots + +```go +func createHeapSnapshot() { + iso := v8.NewIsolate() + heapProfiler := v8.NewHeapProfiler(iso) + defer iso.Dispose() + printfn := v8.NewFunctionTemplate(iso, func(info *v8.FunctionCallbackInfo) *v8.Value { + fmt.Printf("%v", info.Args()) + return nil + }) + global := v8.NewObjectTemplate(iso) + global.Set("print", printfn) + ctx := v8.NewContext(iso, global) + ctx.RunScript("print('foo')", "print.js") + + // This snapshot can be loaded in Chrome dev tools for debugging and inspection + str, err := heapProfiler.TakeHeapSnapshot() + ioutil.WriteFile("isolate.heapsnapshot", []byte(str), 0755) +} +``` ## Documentation Go Reference & more examples: https://pkg.go.dev/rogchap.com/v8go diff --git a/heap_profiler.go b/heap_profiler.go new file mode 100644 index 00000000..b4a46cc1 --- /dev/null +++ b/heap_profiler.go @@ -0,0 +1,31 @@ +package v8go + +/* +#include +#include "v8go.h" +*/ +import "C" +import "unsafe" + +type HeapProfiler struct { + p *C.V8HeapProfiler + iso *Isolate +} + +func NewHeapProfiler(iso *Isolate) *HeapProfiler { + profiler := C.NewHeapProfiler(iso.ptr) + return &HeapProfiler{ + p: profiler, + iso: iso, + } +} + +func (c *HeapProfiler) TakeHeapSnapshot() (string, error) { + if c.p == nil || c.iso.ptr == nil { + panic("heap profiler or isolate is nil") + } + + str := C.TakeHeapSnapshot(c.p) + defer C.free(unsafe.Pointer(str)) + return C.GoString(str), nil +} diff --git a/heap_profiler_test.go b/heap_profiler_test.go new file mode 100644 index 00000000..a3b18c53 --- /dev/null +++ b/heap_profiler_test.go @@ -0,0 +1,35 @@ +package v8go_test + +import ( + "encoding/json" + "fmt" + "testing" + + v8 "rogchap.com/v8go" +) + +func TestHeapSnapshot(t *testing.T) { + t.Parallel() + iso := v8.NewIsolate() + heapProfiler := v8.NewHeapProfiler(iso) + defer iso.Dispose() + printfn := v8.NewFunctionTemplate(iso, func(info *v8.FunctionCallbackInfo) *v8.Value { + fmt.Printf("%v", info.Args()) + return nil + }) + global := v8.NewObjectTemplate(iso) + global.Set("print", printfn) + ctx := v8.NewContext(iso, global) + ctx.RunScript("print('foo')", "print.js") + + str, err := heapProfiler.TakeHeapSnapshot() + if err != nil { + t.Errorf("expected nil but got error: %v", err) + } + + var snapshot map[string]interface{} + err = json.Unmarshal([]byte(str), &snapshot) + if err != nil { + t.Fatal(err) + } +} diff --git a/v8go.cc b/v8go.cc index 86cdc874..488998f4 100644 --- a/v8go.cc +++ b/v8go.cc @@ -278,6 +278,67 @@ ValuePtr IsolateThrowException(IsolatePtr iso, ValuePtr value) { return tracked_value(ctx, new_val); } +/********** HeapProfiler **********/ + +V8HeapProfiler* NewHeapProfiler(IsolatePtr iso_ptr) { + Isolate* iso = static_cast(iso_ptr); + Locker locker(iso); + Isolate::Scope isolate_scope(iso); + HandleScope handle_scope(iso); + + V8HeapProfiler* c = new V8HeapProfiler; + c->iso = iso; + c->ptr = iso->GetHeapProfiler(); + return c; +} + +// v8::OutputStream is required for snapshot serialization +class BufferOutputStream : public v8::OutputStream { + public: + BufferOutputStream() : buffer(new ExternalStringResource()) {} + + void EndOfStream() override {} + int GetChunkSize() override { return 1024 * 1024; } + WriteResult WriteAsciiChunk(char* data, int size) override { + buffer->Append(data, size); + return kContinue; + } + + Local ToString(Isolate* isolate) { + return String::NewExternalOneByte(isolate, buffer.release()) + .ToLocalChecked(); + } + + private: + class ExternalStringResource : public String::ExternalOneByteStringResource { + public: + void Append(char* data, size_t count) { store.append(data, count); } + + const char* data() const override { return store.data(); } + size_t length() const override { return store.size(); } + + private: + std::string store; + }; + + std::unique_ptr buffer; +}; + +const char* TakeHeapSnapshot(V8HeapProfiler* profiler) { + if (profiler->iso == nullptr) { + return nullptr; + } + Locker locker(profiler->iso); + Isolate::Scope isolate_scope(profiler->iso); + HandleScope handle_scope(profiler->iso); + const HeapSnapshot* snapshot = profiler->ptr->TakeHeapSnapshot(); + BufferOutputStream stream; + snapshot->Serialize(&stream); + const_cast(snapshot)->Delete(); + String::Utf8Value json(profiler->iso, stream.ToString(profiler->iso)); + return CopyString(json); +} + /********** CpuProfiler **********/ CPUProfiler* NewCPUProfiler(IsolatePtr iso_ptr) { diff --git a/v8go.h b/v8go.h index b20daca4..c9afaa57 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::HeapProfiler* HeapProfilerPtr; extern "C" { #else @@ -22,6 +23,9 @@ extern "C" { typedef struct v8Isolate v8Isolate; typedef v8Isolate* IsolatePtr; +typedef struct v8HeapProfiler v8HeapProfiler; +typedef v8HeapProfiler* HeapProfilerPtr; + typedef struct v8CpuProfiler v8CpuProfiler; typedef v8CpuProfiler* CpuProfilerPtr; @@ -77,6 +81,11 @@ typedef struct { int compileOption; } CompileOptions; +typedef struct { + HeapProfilerPtr ptr; + IsolatePtr iso; +} V8HeapProfiler; + typedef struct { CpuProfilerPtr ptr; IsolatePtr iso; @@ -156,6 +165,9 @@ extern void ScriptCompilerCachedDataDelete( ScriptCompilerCachedData* cached_data); extern RtnValue UnboundScriptRun(ContextPtr ctx_ptr, UnboundScriptPtr us_ptr); +extern V8HeapProfiler* NewHeapProfiler(IsolatePtr iso_ptr); +extern const char* TakeHeapSnapshot(V8HeapProfiler* ptr); + extern CPUProfiler* NewCPUProfiler(IsolatePtr iso_ptr); extern void CPUProfilerDispose(CPUProfiler* ptr); extern void CPUProfilerStartProfiling(CPUProfiler* ptr, const char* title);