diff --git a/binding.gyp b/binding.gyp index 71b0f215..b8af5ca4 100644 --- a/binding.gyp +++ b/binding.gyp @@ -18,7 +18,8 @@ "bindings/thread-cpu-clock.cc", "bindings/translate-heap-profile.cc", "bindings/translate-time-profile.cc", - "bindings/binding.cc" + "bindings/binding.cc", + "bindings/allocation-profile-node.cc" ], "include_dirs": [ "bindings", @@ -42,6 +43,7 @@ "bindings/translate-heap-profile.cc", "bindings/translate-time-profile.cc", "bindings/test/binding.cc", + "bindings/allocation-profile-node.cc" ], "include_dirs": [ "bindings", diff --git a/bindings/allocation-profile-node.cc b/bindings/allocation-profile-node.cc new file mode 100644 index 00000000..876d119a --- /dev/null +++ b/bindings/allocation-profile-node.cc @@ -0,0 +1,130 @@ +/* + * Copyright 2024 Datadog, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "allocation-profile-node.hh" +#include "per-isolate-data.hh" + +namespace dd { + +NAN_MODULE_INIT(ExternalAllocationNode::Init) { + v8::Local tpl = Nan::New(); + tpl->SetClassName(Nan::New("AllocationProfileNode").ToLocalChecked()); + tpl->InstanceTemplate()->SetInternalFieldCount(1); + + auto inst = tpl->InstanceTemplate(); + Nan::SetAccessor(inst, Nan::New("name").ToLocalChecked(), GetName); + Nan::SetAccessor( + inst, Nan::New("scriptName").ToLocalChecked(), GetScriptName); + Nan::SetAccessor(inst, Nan::New("scriptId").ToLocalChecked(), GetScriptId); + Nan::SetAccessor( + inst, Nan::New("lineNumber").ToLocalChecked(), GetLineNumber); + Nan::SetAccessor( + inst, Nan::New("columnNumber").ToLocalChecked(), GetColumnNumber); + Nan::SetAccessor( + inst, Nan::New("allocations").ToLocalChecked(), GetAllocations); + Nan::SetAccessor(inst, Nan::New("children").ToLocalChecked(), GetChildren); + + PerIsolateData::For(v8::Isolate::GetCurrent()) + ->AllocationNodeConstructor() + .Reset(Nan::GetFunction(tpl).ToLocalChecked()); +} + +v8::Local ExternalAllocationNode::New( + std::shared_ptr node) { + auto* isolate = v8::Isolate::GetCurrent(); + + v8::Local constructor = + Nan::New(PerIsolateData::For(isolate)->AllocationNodeConstructor()); + + v8::Local obj = Nan::NewInstance(constructor).ToLocalChecked(); + + auto* wrapper = new ExternalAllocationNode(node); + wrapper->Wrap(obj); + + return obj; +} + +NAN_GETTER(ExternalAllocationNode::GetName) { + auto* wrapper = + Nan::ObjectWrap::Unwrap(info.Holder()); + auto* isolate = v8::Isolate::GetCurrent(); + info.GetReturnValue().Set( + v8::Local::New(isolate, wrapper->node_->name)); +} + +NAN_GETTER(ExternalAllocationNode::GetScriptName) { + auto* wrapper = + Nan::ObjectWrap::Unwrap(info.Holder()); + auto* isolate = v8::Isolate::GetCurrent(); + info.GetReturnValue().Set( + v8::Local::New(isolate, wrapper->node_->script_name)); +} + +NAN_GETTER(ExternalAllocationNode::GetScriptId) { + auto* wrapper = + Nan::ObjectWrap::Unwrap(info.Holder()); + info.GetReturnValue().Set(Nan::New(wrapper->node_->script_id)); +} + +NAN_GETTER(ExternalAllocationNode::GetLineNumber) { + auto* wrapper = + Nan::ObjectWrap::Unwrap(info.Holder()); + info.GetReturnValue().Set(Nan::New(wrapper->node_->line_number)); +} + +NAN_GETTER(ExternalAllocationNode::GetColumnNumber) { + auto* wrapper = + Nan::ObjectWrap::Unwrap(info.Holder()); + info.GetReturnValue().Set(Nan::New(wrapper->node_->column_number)); +} + +NAN_GETTER(ExternalAllocationNode::GetAllocations) { + auto* wrapper = + Nan::ObjectWrap::Unwrap(info.Holder()); + auto* isolate = v8::Isolate::GetCurrent(); + auto context = isolate->GetCurrentContext(); + + const auto& allocations = wrapper->node_->allocations; + v8::Local arr = v8::Array::New(isolate, allocations.size()); + for (size_t i = 0; i < allocations.size(); i++) { + const auto& alloc = allocations[i]; + v8::Local alloc_obj = v8::Object::New(isolate); + Nan::Set(alloc_obj, + Nan::New("sizeBytes").ToLocalChecked(), + Nan::New(static_cast(alloc.size))); + Nan::Set(alloc_obj, + Nan::New("count").ToLocalChecked(), + Nan::New(static_cast(alloc.count))); + arr->Set(context, i, alloc_obj).Check(); + } + info.GetReturnValue().Set(arr); +} + +NAN_GETTER(ExternalAllocationNode::GetChildren) { + auto* wrapper = + Nan::ObjectWrap::Unwrap(info.Holder()); + auto* isolate = v8::Isolate::GetCurrent(); + auto context = isolate->GetCurrentContext(); + + const auto& children = wrapper->node_->children; + v8::Local arr = v8::Array::New(isolate, children.size()); + for (size_t i = 0; i < children.size(); i++) { + arr->Set(context, i, ExternalAllocationNode::New(children[i])).Check(); + } + info.GetReturnValue().Set(arr); +} + +} // namespace dd diff --git a/bindings/allocation-profile-node.hh b/bindings/allocation-profile-node.hh new file mode 100644 index 00000000..0db6bfb5 --- /dev/null +++ b/bindings/allocation-profile-node.hh @@ -0,0 +1,46 @@ +/* + * Copyright 2024 Datadog, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include "translate-heap-profile.hh" + +namespace dd { + +class ExternalAllocationNode : public Nan::ObjectWrap { + public: + static NAN_MODULE_INIT(Init); + + static v8::Local New(std::shared_ptr node); + + private: + ExternalAllocationNode(std::shared_ptr node) : node_(node) {} + + static NAN_GETTER(GetName); + static NAN_GETTER(GetScriptName); + static NAN_GETTER(GetScriptId); + static NAN_GETTER(GetLineNumber); + static NAN_GETTER(GetColumnNumber); + static NAN_GETTER(GetAllocations); + static NAN_GETTER(GetChildren); + + std::shared_ptr node_; +}; + +} // namespace dd diff --git a/bindings/binding.cc b/bindings/binding.cc index 57640194..b1935d28 100644 --- a/bindings/binding.cc +++ b/bindings/binding.cc @@ -18,6 +18,7 @@ #include #include +#include "allocation-profile-node.hh" #include "profilers/heap.hh" #include "profilers/wall.hh" @@ -47,6 +48,7 @@ NODE_MODULE_INIT(/* exports, module, context */) { #pragma GCC diagnostic pop #endif + dd::ExternalAllocationNode::Init(exports); dd::HeapProfiler::Init(exports); dd::WallProfiler::Init(exports); Nan::SetMethod(exports, "getNativeThreadId", GetNativeThreadId); diff --git a/bindings/per-isolate-data.cc b/bindings/per-isolate-data.cc index 6dfbd6d3..424f9c47 100644 --- a/bindings/per-isolate-data.cc +++ b/bindings/per-isolate-data.cc @@ -52,6 +52,10 @@ Nan::Global& PerIsolateData::WallProfilerConstructor() { return wall_profiler_constructor; } +Nan::Global& PerIsolateData::AllocationNodeConstructor() { + return allocation_node_constructor; +} + std::shared_ptr& PerIsolateData::GetHeapProfilerState() { return heap_profiler_state; } diff --git a/bindings/per-isolate-data.hh b/bindings/per-isolate-data.hh index f555c5e8..dba9d52a 100644 --- a/bindings/per-isolate-data.hh +++ b/bindings/per-isolate-data.hh @@ -28,6 +28,7 @@ struct HeapProfilerState; class PerIsolateData { private: Nan::Global wall_profiler_constructor; + Nan::Global allocation_node_constructor; std::shared_ptr heap_profiler_state; PerIsolateData() {} @@ -36,6 +37,7 @@ class PerIsolateData { static PerIsolateData* For(v8::Isolate* isolate); Nan::Global& WallProfilerConstructor(); + Nan::Global& AllocationNodeConstructor(); std::shared_ptr& GetHeapProfilerState(); }; diff --git a/bindings/profilers/heap.cc b/bindings/profilers/heap.cc index a577b7fb..4a4b54c9 100644 --- a/bindings/profilers/heap.cc +++ b/bindings/profilers/heap.cc @@ -28,6 +28,7 @@ #include #include +#include "allocation-profile-node.hh" namespace dd { @@ -55,17 +56,6 @@ static size_t NearHeapLimit(void* data, static void InterruptCallback(v8::Isolate* isolate, void* data); static void AsyncCallback(uv_async_t* handle); -struct Node { - using Allocation = v8::AllocationProfile::Allocation; - std::string name; - std::string script_name; - int line_number; - int column_number; - int script_id; - std::vector> children; - std::vector allocations; -}; - enum CallbackMode { kNoCallback = 0, kAsyncCallback = 1, @@ -139,73 +129,6 @@ struct HeapProfilerState { bool insideCallback = false; }; -std::shared_ptr TranslateAllocationProfileToCpp( - v8::AllocationProfile::Node* node) { - auto new_node = std::make_shared(); - new_node->line_number = node->line_number; - new_node->column_number = node->column_number; - new_node->script_id = node->script_id; - Nan::Utf8String name(node->name); - new_node->name.assign(*name, name.length()); - Nan::Utf8String script_name(node->script_name); - new_node->script_name.assign(*script_name, script_name.length()); - - new_node->children.reserve(node->children.size()); - for (auto& child : node->children) { - new_node->children.push_back(TranslateAllocationProfileToCpp(child)); - } - - new_node->allocations.reserve(node->allocations.size()); - for (auto& allocation : node->allocations) { - new_node->allocations.push_back(allocation); - } - return new_node; -} - -v8::Local TranslateAllocationProfile(Node* node) { - v8::Local js_node = Nan::New(); - - Nan::Set(js_node, - Nan::New("name").ToLocalChecked(), - Nan::New(node->name).ToLocalChecked()); - Nan::Set(js_node, - Nan::New("scriptName").ToLocalChecked(), - Nan::New(node->script_name).ToLocalChecked()); - Nan::Set(js_node, - Nan::New("scriptId").ToLocalChecked(), - Nan::New(node->script_id)); - Nan::Set(js_node, - Nan::New("lineNumber").ToLocalChecked(), - Nan::New(node->line_number)); - Nan::Set(js_node, - Nan::New("columnNumber").ToLocalChecked(), - Nan::New(node->column_number)); - - v8::Local children = Nan::New(node->children.size()); - for (size_t i = 0; i < node->children.size(); i++) { - Nan::Set(children, i, TranslateAllocationProfile(node->children[i].get())); - } - Nan::Set( - js_node, Nan::New("children").ToLocalChecked(), children); - v8::Local allocations = - Nan::New(node->allocations.size()); - for (size_t i = 0; i < node->allocations.size(); i++) { - v8::AllocationProfile::Allocation alloc = node->allocations[i]; - v8::Local js_alloc = Nan::New(); - Nan::Set(js_alloc, - Nan::New("sizeBytes").ToLocalChecked(), - Nan::New(alloc.size)); - Nan::Set(js_alloc, - Nan::New("count").ToLocalChecked(), - Nan::New(alloc.count)); - Nan::Set(allocations, i, js_alloc); - } - Nan::Set(js_node, - Nan::New("allocations").ToLocalChecked(), - allocations); - return js_node; -} - static void dumpAllocationProfile(FILE* file, Node* node, std::string& cur_stack) { @@ -582,6 +505,29 @@ NAN_METHOD(HeapProfiler::GetAllocationProfile) { info.GetReturnValue().Set(TranslateAllocationProfile(root)); } +// getAllocationProfileV2(): ExternalAllocationNode +NAN_METHOD(HeapProfiler::GetAllocationProfileV2) { + auto isolate = info.GetIsolate(); + + std::unique_ptr profile( + isolate->GetHeapProfiler()->GetAllocationProfile()); + + if (!profile) { + return Nan::ThrowError("Heap profiler is not enabled."); + } + + auto root_node = + TranslateAllocationProfileToExternal(isolate, profile->GetRootNode()); + + auto state = PerIsolateData::For(isolate)->GetHeapProfilerState(); + if (state) { + state->OnNewProfile(); + } + + auto root = ExternalAllocationNode::New(root_node); + info.GetReturnValue().Set(root); +} + NAN_METHOD(HeapProfiler::MonitorOutOfMemory) { if (info.Length() != 7) { return Nan::ThrowTypeError("MonitorOOMCondition must have 7 arguments."); @@ -645,6 +591,8 @@ NAN_MODULE_INIT(HeapProfiler::Init) { Nan::SetMethod( heapProfiler, "stopSamplingHeapProfiler", StopSamplingHeapProfiler); Nan::SetMethod(heapProfiler, "getAllocationProfile", GetAllocationProfile); + Nan::SetMethod( + heapProfiler, "getAllocationProfileV2", GetAllocationProfileV2); Nan::SetMethod(heapProfiler, "monitorOutOfMemory", MonitorOutOfMemory); Nan::Set(target, Nan::New("heapProfiler").ToLocalChecked(), diff --git a/bindings/profilers/heap.hh b/bindings/profilers/heap.hh index 5badc46c..34b8cb72 100644 --- a/bindings/profilers/heap.hh +++ b/bindings/profilers/heap.hh @@ -34,6 +34,10 @@ class HeapProfiler { // getAllocationProfile(): AllocationProfileNode static NAN_METHOD(GetAllocationProfile); + // Signature: + // getAllocationProfileV2(): ExternalAllocationNode + static NAN_METHOD(GetAllocationProfileV2); + static NAN_METHOD(MonitorOutOfMemory); static NAN_MODULE_INIT(Init); diff --git a/bindings/translate-heap-profile.cc b/bindings/translate-heap-profile.cc index a4331e09..5ef6902f 100644 --- a/bindings/translate-heap-profile.cc +++ b/bindings/translate-heap-profile.cc @@ -15,6 +15,7 @@ */ #include "translate-heap-profile.hh" +#include #include "profile-translator.hh" namespace dd { @@ -64,6 +65,29 @@ class HeapProfileTranslator : ProfileTranslator { allocations); } + v8::Local TranslateAllocationProfile(Node* node) { + v8::Local children = NewArray(node->children.size()); + for (size_t i = 0; i < node->children.size(); i++) { + Set(children, i, TranslateAllocationProfile(node->children[i].get())); + } + + v8::Local allocations = NewArray(node->allocations.size()); + for (size_t i = 0; i < node->allocations.size(); i++) { + auto alloc = node->allocations[i]; + Set(allocations, + i, + CreateAllocation(NewNumber(alloc.count), NewNumber(alloc.size))); + } + + return CreateNode(NewString(node->name.c_str()), + NewString(node->script_name.c_str()), + NewInteger(node->script_id), + NewInteger(node->line_number), + NewInteger(node->column_number), + children, + allocations); + } + private: v8::Local CreateNode(v8::Local name, v8::Local scriptName, @@ -95,9 +119,58 @@ class HeapProfileTranslator : ProfileTranslator { }; } // namespace +std::shared_ptr TranslateAllocationProfileToCpp( + v8::AllocationProfile::Node* node) { + auto new_node = std::make_shared(); + new_node->line_number = node->line_number; + new_node->column_number = node->column_number; + new_node->script_id = node->script_id; + Nan::Utf8String name(node->name); + new_node->name.assign(*name, name.length()); + Nan::Utf8String script_name(node->script_name); + new_node->script_name.assign(*script_name, script_name.length()); + + new_node->children.reserve(node->children.size()); + for (auto& child : node->children) { + new_node->children.push_back(TranslateAllocationProfileToCpp(child)); + } + + new_node->allocations.reserve(node->allocations.size()); + for (auto& allocation : node->allocations) { + new_node->allocations.push_back(allocation); + } + return new_node; +} + +std::shared_ptr TranslateAllocationProfileToExternal( + v8::Isolate* isolate, v8::AllocationProfile::Node* node) { + auto new_node = std::make_shared(); + new_node->line_number = node->line_number; + new_node->column_number = node->column_number; + new_node->script_id = node->script_id; + new_node->name.Reset(isolate, node->name); + new_node->script_name.Reset(isolate, node->script_name); + + new_node->children.reserve(node->children.size()); + for (auto& child : node->children) { + new_node->children.push_back( + TranslateAllocationProfileToExternal(isolate, child)); + } + + new_node->allocations.reserve(node->allocations.size()); + for (auto& allocation : node->allocations) { + new_node->allocations.push_back(allocation); + } + return new_node; +} + v8::Local TranslateAllocationProfile( v8::AllocationProfile::Node* node) { return HeapProfileTranslator().TranslateAllocationProfile(node); } +v8::Local TranslateAllocationProfile(Node* node) { + return HeapProfileTranslator().TranslateAllocationProfile(node); +} + } // namespace dd diff --git a/bindings/translate-heap-profile.hh b/bindings/translate-heap-profile.hh index dc5c7aa6..a8ccb279 100644 --- a/bindings/translate-heap-profile.hh +++ b/bindings/translate-heap-profile.hh @@ -17,9 +17,44 @@ #pragma once #include +#include +#include +#include +#include namespace dd { +struct Node { + using Allocation = v8::AllocationProfile::Allocation; + std::string name; + std::string script_name; + int line_number; + int column_number; + int script_id; + std::vector> children; + std::vector allocations; +}; + +// ExternalNode with v8::Global fields — used by +// ExternalAllocationNode. Promotion from Local to Global +struct ExternalNode { + using Allocation = v8::AllocationProfile::Allocation; + v8::Global name; + v8::Global script_name; + int line_number; + int column_number; + int script_id; + std::vector> children; + std::vector allocations; +}; + +std::shared_ptr TranslateAllocationProfileToCpp( + v8::AllocationProfile::Node* node); + +std::shared_ptr TranslateAllocationProfileToExternal( + v8::Isolate* isolate, v8::AllocationProfile::Node* node); + +v8::Local TranslateAllocationProfile(Node* node); v8::Local TranslateAllocationProfile( v8::AllocationProfile::Node* node); diff --git a/ts/src/heap-profiler-bindings.ts b/ts/src/heap-profiler-bindings.ts index 77522577..8506f9f7 100644 --- a/ts/src/heap-profiler-bindings.ts +++ b/ts/src/heap-profiler-bindings.ts @@ -41,6 +41,10 @@ export function getAllocationProfile(): AllocationProfileNode { return profiler.heapProfiler.getAllocationProfile(); } +export function getAllocationProfileV2(): AllocationProfileNode { + return profiler.heapProfiler.getAllocationProfileV2(); +} + export type NearHeapLimitCallback = (profile: AllocationProfileNode) => void; export function monitorOutOfMemory( diff --git a/ts/src/heap-profiler.ts b/ts/src/heap-profiler.ts index b6c64d0f..673bbc9b 100644 --- a/ts/src/heap-profiler.ts +++ b/ts/src/heap-profiler.ts @@ -18,6 +18,7 @@ import {Profile} from 'pprof-format'; import { getAllocationProfile, + getAllocationProfileV2, startSamplingHeapProfiler, stopSamplingHeapProfiler, monitorOutOfMemory as monitorOutOfMemoryImported, @@ -47,6 +48,13 @@ export function v8Profile(): AllocationProfileNode { return getAllocationProfile(); } +export function v8ProfileV2(): AllocationProfileNode { + if (!enabled) { + throw new Error('Heap profiler is not enabled.'); + } + return getAllocationProfileV2(); +} + /** * Collects a profile and returns it serialized in pprof format. * Throws if heap profiler is not enabled. @@ -79,6 +87,7 @@ export function convertProfile( // TODO: remove any once type definition is updated to include external. // eslint-disable-next-line @typescript-eslint/no-explicit-any const {external}: {external: number} = process.memoryUsage() as any; + let root: AllocationProfileNode; if (external > 0) { const externalNode: AllocationProfileNode = { name: '(external)', @@ -86,10 +95,12 @@ export function convertProfile( children: [], allocations: [{sizeBytes: external, count: 1}], }; - rootNode.children.push(externalNode); + root = {...rootNode, children: [...rootNode.children, externalNode]}; + } else { + root = rootNode; } return serializeHeapProfile( - rootNode, + root, startTimeNanos, heapIntervalBytes, ignoreSamplePath, @@ -98,6 +109,27 @@ export function convertProfile( ); } +/** + * Collects a profile and returns it serialized in pprof format using lazy V2 API. + * Throws if heap profiler is not enabled. + * + * @param ignoreSamplePath + * @param sourceMapper + * @param generateLabels + */ +export function profileV2( + ignoreSamplePath?: string, + sourceMapper?: SourceMapper, + generateLabels?: GenerateAllocationLabelsFunction +): Profile { + return convertProfile( + v8ProfileV2(), + ignoreSamplePath, + sourceMapper, + generateLabels + ); +} + /** * Starts heap profiling. If heap profiling has already been started with * the same parameters, this is a noop. If heap profiler has already been diff --git a/ts/src/index.ts b/ts/src/index.ts index 42454629..be2a4170 100644 --- a/ts/src/index.ts +++ b/ts/src/index.ts @@ -48,6 +48,7 @@ export const heap = { start: heapProfiler.start, stop: heapProfiler.stop, profile: heapProfiler.profile, + profileV2: heapProfiler.profileV2, convertProfile: heapProfiler.convertProfile, v8Profile: heapProfiler.v8Profile, monitorOutOfMemory: heapProfiler.monitorOutOfMemory, diff --git a/ts/src/v8-types.ts b/ts/src/v8-types.ts index 39178b19..1becf7d2 100644 --- a/ts/src/v8-types.ts +++ b/ts/src/v8-types.ts @@ -51,6 +51,7 @@ export interface TimeProfileNode extends ProfileNode { export interface AllocationProfileNode extends ProfileNode { allocations: Allocation[]; + children: AllocationProfileNode[]; } export interface Allocation { diff --git a/ts/test/heap-memory-worker.ts b/ts/test/heap-memory-worker.ts new file mode 100644 index 00000000..9d05b53f --- /dev/null +++ b/ts/test/heap-memory-worker.ts @@ -0,0 +1,73 @@ +import * as heapProfiler from '../src/heap-profiler'; +import * as v8HeapProfiler from '../src/heap-profiler-bindings'; +import {AllocationProfileNode} from '../src/v8-types'; + +const ALLOCATION_COUNT = 100_000; +const ALLOCATION_SIZE = 200; + +const gc = (global as NodeJS.Global & {gc?: () => void}).gc; +if (!gc) { + throw new Error('Run with --expose-gc flag'); +} + +let keepAlive: object[] = []; + +function generateAllocations(): object[] { + const result: object[] = []; + for (let i = 0; i < ALLOCATION_COUNT; i++) { + result.push({ + id: i, + data: new Array(ALLOCATION_SIZE).fill('Hello, world!'), + nested: {map: new Map([[i, new Array(ALLOCATION_SIZE).fill(i)]])}, + }); + } + return result; +} + +function traverseTree(root: AllocationProfileNode): void { + const stack: AllocationProfileNode[] = [root]; + while (stack.length > 0) { + const node = stack.pop()!; + if (node.children) { + for (const child of node.children) { + stack.push(child); + } + } + } +} + +function measureMemoryUsage(getProfile: () => AllocationProfileNode): { + initial: number; + afterTraversal: number; +} { + gc!(); + gc!(); + const baseline = process.memoryUsage().heapUsed; + + const profile = getProfile(); + const initial = process.memoryUsage().heapUsed - baseline; + + traverseTree(profile); + + return { + initial, + afterTraversal: process.memoryUsage().heapUsed - baseline, + }; +} + +process.on('message', (version: 'v1' | 'v2') => { + heapProfiler.start(128, 128); + keepAlive = generateAllocations(); + + const getProfile = + version === 'v1' + ? v8HeapProfiler.getAllocationProfile + : v8HeapProfiler.getAllocationProfileV2; + + const {initial, afterTraversal} = measureMemoryUsage(getProfile); + + heapProfiler.stop(); + keepAlive.length = 0; + + process.send!({initial, afterTraversal}); +}); diff --git a/ts/test/test-heap-profiler-v2.ts b/ts/test/test-heap-profiler-v2.ts new file mode 100644 index 00000000..3108e7b2 --- /dev/null +++ b/ts/test/test-heap-profiler-v2.ts @@ -0,0 +1,130 @@ +import {strict as assert} from 'assert'; +import {fork} from 'child_process'; +import * as heapProfiler from '../src/heap-profiler'; +import * as v8HeapProfiler from '../src/heap-profiler-bindings'; + +function generateAllocations(): object[] { + const allocations: object[] = []; + for (let i = 0; i < 1000; i++) { + allocations.push({data: new Array(100).fill(i)}); + } + return allocations; +} + +describe('HeapProfiler V2 API', () => { + let keepAlive: object[] = []; + + before(() => { + heapProfiler.start(512, 64); + keepAlive = generateAllocations(); + }); + + after(() => { + heapProfiler.stop(); + keepAlive.length = 0; + }); + + describe('v8ProfileV2', () => { + it('should return AllocationProfileNode', () => { + const root = heapProfiler.v8ProfileV2(); + + assert.equal(typeof root.name, 'string'); + assert.equal(typeof root.scriptName, 'string'); + assert.equal(typeof root.scriptId, 'number'); + assert.equal(typeof root.lineNumber, 'number'); + assert.equal(typeof root.columnNumber, 'number'); + assert.ok(Array.isArray(root.allocations)); + + assert.ok(Array.isArray(root.children)); + assert.equal(typeof root.children.length, 'number'); + + if (root.children.length > 0) { + const child = root.children[0]; + assert.equal(typeof child.name, 'string'); + assert.ok(Array.isArray(child.children)); + assert.ok(Array.isArray(child.allocations)); + } + }); + + it('should throw error when profiler not started', () => { + heapProfiler.stop(); + assert.throws( + () => { + heapProfiler.v8ProfileV2(); + }, + (err: Error) => { + return err.message === 'Heap profiler is not enabled.'; + } + ); + heapProfiler.start(512, 64); + }); + }); + + describe('getAllocationProfileV2', () => { + it('should return AllocationProfileNode directly', () => { + const root = v8HeapProfiler.getAllocationProfileV2(); + + assert.equal(typeof root.name, 'string'); + assert.equal(typeof root.scriptName, 'string'); + assert.ok(Array.isArray(root.children)); + assert.ok(Array.isArray(root.allocations)); + }); + }); + + describe('profileV2', () => { + it('should produce valid pprof Profile', () => { + const profile = heapProfiler.profileV2(); + + assert.ok(profile.sampleType); + assert.ok(profile.sample); + assert.ok(profile.location); + assert.ok(profile.function); + assert.ok(profile.stringTable); + }); + }); + + describe('Memory comparison', () => { + interface MemoryResult { + initial: number; + afterTraversal: number; + } + + function measureMemoryInWorker( + version: 'v1' | 'v2' + ): Promise { + return new Promise((resolve, reject) => { + const child = fork('./out/test/heap-memory-worker.js', [], { + execArgv: ['--expose-gc'], + }); + + child.on('message', (result: MemoryResult) => { + resolve(result); + child.kill(); + }); + + child.on('error', reject); + child.send(version); + }); + } + + it('getAllocationProfileV2 should use less initial memory than getAllocationProfile', async () => { + const v1MemoryUsage = await measureMemoryInWorker('v1'); + const v2MemoryUsage = await measureMemoryInWorker('v2'); + + console.log( + ` V1 initial: ${v1MemoryUsage.initial}, afterTraversal: ${v1MemoryUsage.afterTraversal} + | V2 initial: ${v2MemoryUsage.initial}, afterTraversal: ${v2MemoryUsage.afterTraversal}` + ); + + assert.ok( + v2MemoryUsage.initial < v1MemoryUsage.initial, + `V2 initial should be less: V1=${v1MemoryUsage.initial}, V2=${v2MemoryUsage.initial}` + ); + + assert.ok( + v2MemoryUsage.afterTraversal < v1MemoryUsage.afterTraversal, + `V2 afterTraversal should be less: V1=${v1MemoryUsage.afterTraversal}, V2=${v2MemoryUsage.afterTraversal}` + ); + }).timeout(30000); + }); +});