Skip to content

Commit e88303f

Browse files
authored
Improve heap profile memory usage by lazily loading js objects (#260)
Improve heap profile memory usage by lazily loading js objects
1 parent b3844dd commit e88303f

17 files changed

+593
-87
lines changed

binding.gyp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
"bindings/thread-cpu-clock.cc",
1919
"bindings/translate-heap-profile.cc",
2020
"bindings/translate-time-profile.cc",
21-
"bindings/binding.cc"
21+
"bindings/binding.cc",
22+
"bindings/allocation-profile-node.cc"
2223
],
2324
"include_dirs": [
2425
"bindings",
@@ -42,6 +43,7 @@
4243
"bindings/translate-heap-profile.cc",
4344
"bindings/translate-time-profile.cc",
4445
"bindings/test/binding.cc",
46+
"bindings/allocation-profile-node.cc"
4547
],
4648
"include_dirs": [
4749
"bindings",
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
* Copyright 2026 Datadog, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include "allocation-profile-node.hh"
18+
#include "per-isolate-data.hh"
19+
20+
using namespace v8;
21+
22+
namespace dd {
23+
24+
template <typename F>
25+
void AllocationProfileNodeView::mapAllocationProfileNode(
26+
const Nan::PropertyCallbackInfo<Value>& info, F&& mapper) {
27+
auto* node = static_cast<AllocationProfile::Node*>(
28+
Nan::GetInternalFieldPointer(info.Holder(), 0));
29+
info.GetReturnValue().Set(mapper(node));
30+
}
31+
32+
NAN_MODULE_INIT(AllocationProfileNodeView::Init) {
33+
Local<FunctionTemplate> tpl = Nan::New<FunctionTemplate>();
34+
tpl->SetClassName(Nan::New("AllocationProfileNode").ToLocalChecked());
35+
tpl->InstanceTemplate()->SetInternalFieldCount(1);
36+
37+
auto inst = tpl->InstanceTemplate();
38+
Nan::SetAccessor(inst, Nan::New("name").ToLocalChecked(), GetName);
39+
Nan::SetAccessor(
40+
inst, Nan::New("scriptName").ToLocalChecked(), GetScriptName);
41+
Nan::SetAccessor(inst, Nan::New("scriptId").ToLocalChecked(), GetScriptId);
42+
Nan::SetAccessor(
43+
inst, Nan::New("lineNumber").ToLocalChecked(), GetLineNumber);
44+
Nan::SetAccessor(
45+
inst, Nan::New("columnNumber").ToLocalChecked(), GetColumnNumber);
46+
Nan::SetAccessor(
47+
inst, Nan::New("allocations").ToLocalChecked(), GetAllocations);
48+
Nan::SetAccessor(inst, Nan::New("children").ToLocalChecked(), GetChildren);
49+
50+
PerIsolateData::For(Isolate::GetCurrent())
51+
->AllocationNodeConstructor()
52+
.Reset(Nan::GetFunction(tpl).ToLocalChecked());
53+
}
54+
55+
Local<Object> AllocationProfileNodeView::New(AllocationProfile::Node* node) {
56+
auto* isolate = Isolate::GetCurrent();
57+
58+
Local<Function> constructor =
59+
Nan::New(PerIsolateData::For(isolate)->AllocationNodeConstructor());
60+
61+
Local<Object> obj = Nan::NewInstance(constructor).ToLocalChecked();
62+
63+
Nan::SetInternalFieldPointer(obj, 0, node);
64+
65+
return obj;
66+
}
67+
68+
NAN_GETTER(AllocationProfileNodeView::GetName) {
69+
mapAllocationProfileNode(
70+
info, [](AllocationProfile::Node* node) { return node->name; });
71+
}
72+
73+
NAN_GETTER(AllocationProfileNodeView::GetScriptName) {
74+
mapAllocationProfileNode(
75+
info, [](AllocationProfile::Node* node) { return node->script_name; });
76+
}
77+
78+
NAN_GETTER(AllocationProfileNodeView::GetScriptId) {
79+
mapAllocationProfileNode(
80+
info, [](AllocationProfile::Node* node) { return node->script_id; });
81+
}
82+
83+
NAN_GETTER(AllocationProfileNodeView::GetLineNumber) {
84+
mapAllocationProfileNode(
85+
info, [](AllocationProfile::Node* node) { return node->line_number; });
86+
}
87+
88+
NAN_GETTER(AllocationProfileNodeView::GetColumnNumber) {
89+
mapAllocationProfileNode(
90+
info, [](AllocationProfile::Node* node) { return node->column_number; });
91+
}
92+
93+
NAN_GETTER(AllocationProfileNodeView::GetAllocations) {
94+
mapAllocationProfileNode(info, [](AllocationProfile::Node* node) {
95+
auto* isolate = Isolate::GetCurrent();
96+
auto context = isolate->GetCurrentContext();
97+
98+
const auto& allocations = node->allocations;
99+
Local<Array> arr = Array::New(isolate, allocations.size());
100+
auto sizeBytes = String::NewFromUtf8Literal(isolate, "sizeBytes");
101+
auto count = String::NewFromUtf8Literal(isolate, "count");
102+
103+
for (size_t i = 0; i < allocations.size(); i++) {
104+
const auto& alloc = allocations[i];
105+
Local<Object> alloc_obj = Object::New(isolate);
106+
Nan::Set(alloc_obj,
107+
sizeBytes,
108+
Number::New(isolate, static_cast<double>(alloc.size)));
109+
Nan::Set(alloc_obj,
110+
count,
111+
Number::New(isolate, static_cast<double>(alloc.count)));
112+
arr->Set(context, i, alloc_obj).Check();
113+
}
114+
return arr;
115+
});
116+
}
117+
118+
NAN_GETTER(AllocationProfileNodeView::GetChildren) {
119+
mapAllocationProfileNode(info, [](AllocationProfile::Node* node) {
120+
auto* isolate = Isolate::GetCurrent();
121+
auto context = isolate->GetCurrentContext();
122+
123+
const auto& children = node->children;
124+
Local<Array> arr = Array::New(isolate, children.size());
125+
for (size_t i = 0; i < children.size(); i++) {
126+
arr->Set(context, i, AllocationProfileNodeView::New(children[i])).Check();
127+
}
128+
return arr;
129+
});
130+
}
131+
132+
} // namespace dd
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2026 Datadog, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#pragma once
18+
19+
#include <nan.h>
20+
#include <v8-profiler.h>
21+
22+
namespace dd {
23+
24+
class AllocationProfileNodeView {
25+
public:
26+
static NAN_MODULE_INIT(Init);
27+
28+
static v8::Local<v8::Object> New(v8::AllocationProfile::Node* node);
29+
30+
private:
31+
template <typename F>
32+
static void mapAllocationProfileNode(
33+
const Nan::PropertyCallbackInfo<v8::Value>& info, F&& mapper);
34+
35+
static NAN_GETTER(GetName);
36+
static NAN_GETTER(GetScriptName);
37+
static NAN_GETTER(GetScriptId);
38+
static NAN_GETTER(GetLineNumber);
39+
static NAN_GETTER(GetColumnNumber);
40+
static NAN_GETTER(GetAllocations);
41+
static NAN_GETTER(GetChildren);
42+
};
43+
44+
} // namespace dd

bindings/binding.cc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include <node.h>
1919
#include <v8.h>
2020

21+
#include "allocation-profile-node.hh"
2122
#include "profilers/heap.hh"
2223
#include "profilers/wall.hh"
2324

@@ -47,6 +48,7 @@ NODE_MODULE_INIT(/* exports, module, context */) {
4748
#pragma GCC diagnostic pop
4849
#endif
4950

51+
dd::AllocationProfileNodeView::Init(exports);
5052
dd::HeapProfiler::Init(exports);
5153
dd::WallProfiler::Init(exports);
5254
Nan::SetMethod(exports, "getNativeThreadId", GetNativeThreadId);

bindings/per-isolate-data.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ Nan::Global<v8::Function>& PerIsolateData::WallProfilerConstructor() {
5252
return wall_profiler_constructor;
5353
}
5454

55+
Nan::Global<v8::Function>& PerIsolateData::AllocationNodeConstructor() {
56+
return allocation_node_constructor;
57+
}
58+
5559
std::shared_ptr<HeapProfilerState>& PerIsolateData::GetHeapProfilerState() {
5660
return heap_profiler_state;
5761
}

bindings/per-isolate-data.hh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ struct HeapProfilerState;
2828
class PerIsolateData {
2929
private:
3030
Nan::Global<v8::Function> wall_profiler_constructor;
31+
Nan::Global<v8::Function> allocation_node_constructor;
3132
std::shared_ptr<HeapProfilerState> heap_profiler_state;
3233

3334
PerIsolateData() {}
@@ -36,6 +37,7 @@ class PerIsolateData {
3637
static PerIsolateData* For(v8::Isolate* isolate);
3738

3839
Nan::Global<v8::Function>& WallProfilerConstructor();
40+
Nan::Global<v8::Function>& AllocationNodeConstructor();
3941
std::shared_ptr<HeapProfilerState>& GetHeapProfilerState();
4042
};
4143

bindings/profilers/heap.cc

Lines changed: 31 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
#include <node.h>
3030
#include <v8-profiler.h>
31+
#include "allocation-profile-node.hh"
3132

3233
namespace dd {
3334

@@ -55,17 +56,6 @@ static size_t NearHeapLimit(void* data,
5556
static void InterruptCallback(v8::Isolate* isolate, void* data);
5657
static void AsyncCallback(uv_async_t* handle);
5758

58-
struct Node {
59-
using Allocation = v8::AllocationProfile::Allocation;
60-
std::string name;
61-
std::string script_name;
62-
int line_number;
63-
int column_number;
64-
int script_id;
65-
std::vector<std::shared_ptr<Node>> children;
66-
std::vector<Allocation> allocations;
67-
};
68-
6959
enum CallbackMode {
7060
kNoCallback = 0,
7161
kAsyncCallback = 1,
@@ -139,73 +129,6 @@ struct HeapProfilerState {
139129
bool insideCallback = false;
140130
};
141131

142-
std::shared_ptr<Node> TranslateAllocationProfileToCpp(
143-
v8::AllocationProfile::Node* node) {
144-
auto new_node = std::make_shared<Node>();
145-
new_node->line_number = node->line_number;
146-
new_node->column_number = node->column_number;
147-
new_node->script_id = node->script_id;
148-
Nan::Utf8String name(node->name);
149-
new_node->name.assign(*name, name.length());
150-
Nan::Utf8String script_name(node->script_name);
151-
new_node->script_name.assign(*script_name, script_name.length());
152-
153-
new_node->children.reserve(node->children.size());
154-
for (auto& child : node->children) {
155-
new_node->children.push_back(TranslateAllocationProfileToCpp(child));
156-
}
157-
158-
new_node->allocations.reserve(node->allocations.size());
159-
for (auto& allocation : node->allocations) {
160-
new_node->allocations.push_back(allocation);
161-
}
162-
return new_node;
163-
}
164-
165-
v8::Local<v8::Value> TranslateAllocationProfile(Node* node) {
166-
v8::Local<v8::Object> js_node = Nan::New<v8::Object>();
167-
168-
Nan::Set(js_node,
169-
Nan::New<v8::String>("name").ToLocalChecked(),
170-
Nan::New(node->name).ToLocalChecked());
171-
Nan::Set(js_node,
172-
Nan::New<v8::String>("scriptName").ToLocalChecked(),
173-
Nan::New(node->script_name).ToLocalChecked());
174-
Nan::Set(js_node,
175-
Nan::New<v8::String>("scriptId").ToLocalChecked(),
176-
Nan::New<v8::Integer>(node->script_id));
177-
Nan::Set(js_node,
178-
Nan::New<v8::String>("lineNumber").ToLocalChecked(),
179-
Nan::New<v8::Integer>(node->line_number));
180-
Nan::Set(js_node,
181-
Nan::New<v8::String>("columnNumber").ToLocalChecked(),
182-
Nan::New<v8::Integer>(node->column_number));
183-
184-
v8::Local<v8::Array> children = Nan::New<v8::Array>(node->children.size());
185-
for (size_t i = 0; i < node->children.size(); i++) {
186-
Nan::Set(children, i, TranslateAllocationProfile(node->children[i].get()));
187-
}
188-
Nan::Set(
189-
js_node, Nan::New<v8::String>("children").ToLocalChecked(), children);
190-
v8::Local<v8::Array> allocations =
191-
Nan::New<v8::Array>(node->allocations.size());
192-
for (size_t i = 0; i < node->allocations.size(); i++) {
193-
v8::AllocationProfile::Allocation alloc = node->allocations[i];
194-
v8::Local<v8::Object> js_alloc = Nan::New<v8::Object>();
195-
Nan::Set(js_alloc,
196-
Nan::New<v8::String>("sizeBytes").ToLocalChecked(),
197-
Nan::New<v8::Number>(alloc.size));
198-
Nan::Set(js_alloc,
199-
Nan::New<v8::String>("count").ToLocalChecked(),
200-
Nan::New<v8::Number>(alloc.count));
201-
Nan::Set(allocations, i, js_alloc);
202-
}
203-
Nan::Set(js_node,
204-
Nan::New<v8::String>("allocations").ToLocalChecked(),
205-
allocations);
206-
return js_node;
207-
}
208-
209132
static void dumpAllocationProfile(FILE* file,
210133
Node* node,
211134
std::string& cur_stack) {
@@ -582,6 +505,35 @@ NAN_METHOD(HeapProfiler::GetAllocationProfile) {
582505
info.GetReturnValue().Set(TranslateAllocationProfile(root));
583506
}
584507

508+
// mapAllocationProfile(callback): callback result
509+
NAN_METHOD(HeapProfiler::MapAllocationProfile) {
510+
if (info.Length() < 1 || !info[0]->IsFunction()) {
511+
return Nan::ThrowTypeError("mapAllocationProfile requires a callback");
512+
}
513+
auto isolate = info.GetIsolate();
514+
auto callback = info[0].As<v8::Function>();
515+
516+
std::unique_ptr<v8::AllocationProfile> profile(
517+
isolate->GetHeapProfiler()->GetAllocationProfile());
518+
519+
if (!profile) {
520+
return Nan::ThrowError("Heap profiler is not enabled.");
521+
}
522+
523+
auto state = PerIsolateData::For(isolate)->GetHeapProfilerState();
524+
if (state) {
525+
state->OnNewProfile();
526+
}
527+
528+
auto root = AllocationProfileNodeView::New(profile->GetRootNode());
529+
v8::Local<v8::Value> argv[] = {root};
530+
auto result =
531+
Nan::Call(callback, isolate->GetCurrentContext()->Global(), 1, argv);
532+
if (!result.IsEmpty()) {
533+
info.GetReturnValue().Set(result.ToLocalChecked());
534+
}
535+
}
536+
585537
NAN_METHOD(HeapProfiler::MonitorOutOfMemory) {
586538
if (info.Length() != 7) {
587539
return Nan::ThrowTypeError("MonitorOOMCondition must have 7 arguments.");
@@ -645,6 +597,7 @@ NAN_MODULE_INIT(HeapProfiler::Init) {
645597
Nan::SetMethod(
646598
heapProfiler, "stopSamplingHeapProfiler", StopSamplingHeapProfiler);
647599
Nan::SetMethod(heapProfiler, "getAllocationProfile", GetAllocationProfile);
600+
Nan::SetMethod(heapProfiler, "mapAllocationProfile", MapAllocationProfile);
648601
Nan::SetMethod(heapProfiler, "monitorOutOfMemory", MonitorOutOfMemory);
649602
Nan::Set(target,
650603
Nan::New<v8::String>("heapProfiler").ToLocalChecked(),

bindings/profilers/heap.hh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ class HeapProfiler {
3434
// getAllocationProfile(): AllocationProfileNode
3535
static NAN_METHOD(GetAllocationProfile);
3636

37+
// Signature:
38+
// mapAllocationProfile(callback): callback result
39+
static NAN_METHOD(MapAllocationProfile);
40+
3741
static NAN_METHOD(MonitorOutOfMemory);
3842

3943
static NAN_MODULE_INIT(Init);

0 commit comments

Comments
 (0)