Skip to content

Commit 795fa12

Browse files
committed
test: add heap profile labels tests
JS tests: - test-v8-heap-profile-labels: basic labeling, multi-key, JSON round-trip, GC retention/removal, includeCollectedObjects flag - test-v8-heap-profile-labels-async: await boundary propagation, concurrent contexts, Hapi-style setHeapProfileLabels - test-v8-heap-profile-external: Buffer/ArrayBuffer per-label externalBytes tracking, GC cleanup, unlabeled isolation C++ cctests: - Node.js-level label registration, callback integration, cleanup Signed-off-by: Rudolf Meijering <skaapgif@gmail.com>
1 parent 9589661 commit 795fa12

File tree

4 files changed

+1083
-0
lines changed

4 files changed

+1083
-0
lines changed
Lines changed: 370 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,370 @@
1+
// Copyright Joyent, Inc. and other Node contributors.
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a
4+
// copy of this software and associated documentation files (the
5+
// "Software"), to deal in the Software without restriction, including
6+
// without limitation the rights to use, copy, modify, merge, publish,
7+
// distribute, sublicense, and/or sell copies of the Software, and to permit
8+
// persons to whom the Software is furnished to do so, subject to the
9+
// following conditions:
10+
//
11+
// The above copyright notice and this permission notice shall be included
12+
// in all copies or substantial portions of the Software.
13+
//
14+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15+
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16+
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
17+
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18+
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
19+
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
20+
// USE OR OTHER DEALINGS IN THE SOFTWARE.
21+
22+
// Tests for V8 HeapProfileSampleLabelsCallback API.
23+
// Validates the label callback feature at the V8 public API level.
24+
25+
#include <memory>
26+
#include <string>
27+
#include <utility>
28+
#include <vector>
29+
30+
#include "gtest/gtest.h"
31+
#include "node_test_fixture.h"
32+
#include "v8-profiler.h"
33+
#include "v8.h"
34+
35+
#ifdef V8_HEAP_PROFILER_SAMPLE_LABELS
36+
37+
// Helper: a label callback that writes fixed labels via output parameter.
38+
static bool FixedLabelsCallback(
39+
void* data, v8::Local<v8::Value> context,
40+
std::vector<std::pair<std::string, std::string>>* out_labels) {
41+
auto* labels =
42+
static_cast<std::vector<std::pair<std::string, std::string>>*>(data);
43+
*out_labels = *labels;
44+
return true;
45+
}
46+
47+
// Helper: a label callback that returns false (no labels).
48+
static bool EmptyLabelsCallback(
49+
void* data, v8::Local<v8::Value> context,
50+
std::vector<std::pair<std::string, std::string>>* out_labels) {
51+
return false;
52+
}
53+
54+
// Helper: a label callback that switches labels based on a flag.
55+
struct MultiLabelState {
56+
bool use_second;
57+
std::vector<std::pair<std::string, std::string>> first;
58+
std::vector<std::pair<std::string, std::string>> second;
59+
};
60+
61+
static bool MultiLabelsCallback(
62+
void* data, v8::Local<v8::Value> context,
63+
std::vector<std::pair<std::string, std::string>>* out_labels) {
64+
auto* state = static_cast<MultiLabelState*>(data);
65+
*out_labels = state->use_second ? state->second : state->first;
66+
return true;
67+
}
68+
69+
class HeapProfileLabelsTest : public NodeTestFixture {};
70+
71+
// Test: register callback, allocate, verify labels on samples.
72+
TEST_F(HeapProfileLabelsTest, CallbackReturnsLabels) {
73+
const v8::HandleScope handle_scope(isolate_);
74+
v8::Local<v8::Context> context = v8::Context::New(isolate_);
75+
v8::Context::Scope context_scope(context);
76+
77+
v8::HeapProfiler* heap_profiler = isolate_->GetHeapProfiler();
78+
79+
std::vector<std::pair<std::string, std::string>> labels = {
80+
{"route", "/api/test"}};
81+
82+
// Set CPED so the callback gets invoked (requires non-empty context).
83+
isolate_->SetContinuationPreservedEmbedderData(
84+
v8::String::NewFromUtf8Literal(isolate_, "test-context"));
85+
86+
heap_profiler->SetHeapProfileSampleLabelsCallback(FixedLabelsCallback,
87+
&labels);
88+
89+
heap_profiler->StartSamplingHeapProfiler(256);
90+
91+
// Allocate enough objects to get samples.
92+
for (int i = 0; i < 8 * 1024; ++i) v8::Object::New(isolate_);
93+
94+
std::unique_ptr<v8::AllocationProfile> profile(
95+
heap_profiler->GetAllocationProfile());
96+
ASSERT_NE(profile, nullptr);
97+
98+
bool found_labeled = false;
99+
for (const auto& sample : profile->GetSamples()) {
100+
if (!sample.labels.empty()) {
101+
EXPECT_EQ(sample.labels.size(), 1u);
102+
EXPECT_EQ(sample.labels[0].first, "route");
103+
EXPECT_EQ(sample.labels[0].second, "/api/test");
104+
found_labeled = true;
105+
}
106+
}
107+
EXPECT_TRUE(found_labeled);
108+
109+
heap_profiler->StopSamplingHeapProfiler();
110+
heap_profiler->SetHeapProfileSampleLabelsCallback(nullptr, nullptr);
111+
}
112+
113+
// Test: no callback registered — samples have empty labels.
114+
TEST_F(HeapProfileLabelsTest, NoCallbackEmptyLabels) {
115+
const v8::HandleScope handle_scope(isolate_);
116+
v8::Local<v8::Context> context = v8::Context::New(isolate_);
117+
v8::Context::Scope context_scope(context);
118+
119+
v8::HeapProfiler* heap_profiler = isolate_->GetHeapProfiler();
120+
121+
heap_profiler->StartSamplingHeapProfiler(256);
122+
123+
for (int i = 0; i < 8 * 1024; ++i) v8::Object::New(isolate_);
124+
125+
std::unique_ptr<v8::AllocationProfile> profile(
126+
heap_profiler->GetAllocationProfile());
127+
ASSERT_NE(profile, nullptr);
128+
129+
for (const auto& sample : profile->GetSamples()) {
130+
EXPECT_TRUE(sample.labels.empty());
131+
}
132+
133+
heap_profiler->StopSamplingHeapProfiler();
134+
}
135+
136+
// Test: callback returns empty vector — samples have empty labels.
137+
TEST_F(HeapProfileLabelsTest, EmptyCallbackEmptyLabels) {
138+
const v8::HandleScope handle_scope(isolate_);
139+
v8::Local<v8::Context> context = v8::Context::New(isolate_);
140+
v8::Context::Scope context_scope(context);
141+
142+
v8::HeapProfiler* heap_profiler = isolate_->GetHeapProfiler();
143+
144+
// Set CPED so callback is invoked.
145+
isolate_->SetContinuationPreservedEmbedderData(
146+
v8::String::NewFromUtf8Literal(isolate_, "test-context"));
147+
148+
heap_profiler->SetHeapProfileSampleLabelsCallback(EmptyLabelsCallback,
149+
nullptr);
150+
151+
heap_profiler->StartSamplingHeapProfiler(256);
152+
153+
for (int i = 0; i < 8 * 1024; ++i) v8::Object::New(isolate_);
154+
155+
std::unique_ptr<v8::AllocationProfile> profile(
156+
heap_profiler->GetAllocationProfile());
157+
ASSERT_NE(profile, nullptr);
158+
159+
for (const auto& sample : profile->GetSamples()) {
160+
EXPECT_TRUE(sample.labels.empty());
161+
}
162+
163+
heap_profiler->StopSamplingHeapProfiler();
164+
heap_profiler->SetHeapProfileSampleLabelsCallback(nullptr, nullptr);
165+
}
166+
167+
// Test: multiple distinct label sets from callback (simulate different contexts).
168+
TEST_F(HeapProfileLabelsTest, MultipleDistinctLabels) {
169+
const v8::HandleScope handle_scope(isolate_);
170+
v8::Local<v8::Context> context = v8::Context::New(isolate_);
171+
v8::Context::Scope context_scope(context);
172+
173+
v8::HeapProfiler* heap_profiler = isolate_->GetHeapProfiler();
174+
175+
// Set CPED so callback is invoked.
176+
isolate_->SetContinuationPreservedEmbedderData(
177+
v8::String::NewFromUtf8Literal(isolate_, "test-context"));
178+
179+
MultiLabelState state;
180+
state.use_second = false;
181+
state.first = {{"route", "/api/first"}};
182+
state.second = {{"route", "/api/second"}};
183+
184+
heap_profiler->SetHeapProfileSampleLabelsCallback(MultiLabelsCallback,
185+
&state);
186+
187+
heap_profiler->StartSamplingHeapProfiler(256);
188+
189+
// Allocate with first label set.
190+
for (int i = 0; i < 4 * 1024; ++i) v8::Object::New(isolate_);
191+
192+
// Switch to second label set.
193+
state.use_second = true;
194+
195+
// Allocate with second label set.
196+
for (int i = 0; i < 4 * 1024; ++i) v8::Object::New(isolate_);
197+
198+
std::unique_ptr<v8::AllocationProfile> profile(
199+
heap_profiler->GetAllocationProfile());
200+
ASSERT_NE(profile, nullptr);
201+
202+
bool found_first = false;
203+
bool found_second = false;
204+
for (const auto& sample : profile->GetSamples()) {
205+
if (!sample.labels.empty()) {
206+
EXPECT_EQ(sample.labels.size(), 1u);
207+
EXPECT_EQ(sample.labels[0].first, "route");
208+
if (sample.labels[0].second == "/api/first") found_first = true;
209+
if (sample.labels[0].second == "/api/second") found_second = true;
210+
}
211+
}
212+
EXPECT_TRUE(found_first);
213+
EXPECT_TRUE(found_second);
214+
215+
heap_profiler->StopSamplingHeapProfiler();
216+
heap_profiler->SetHeapProfileSampleLabelsCallback(nullptr, nullptr);
217+
}
218+
219+
// Test: labels survive GC when kSamplingIncludeObjectsCollectedByMajorGC enabled.
220+
TEST_F(HeapProfileLabelsTest, LabelsSurviveGCWithRetainFlags) {
221+
const v8::HandleScope handle_scope(isolate_);
222+
v8::Local<v8::Context> context = v8::Context::New(isolate_);
223+
v8::Context::Scope context_scope(context);
224+
225+
v8::HeapProfiler* heap_profiler = isolate_->GetHeapProfiler();
226+
227+
// Set CPED so callback is invoked.
228+
isolate_->SetContinuationPreservedEmbedderData(
229+
v8::String::NewFromUtf8Literal(isolate_, "test-context"));
230+
231+
std::vector<std::pair<std::string, std::string>> labels = {
232+
{"route", "/api/gc-test"}};
233+
heap_profiler->SetHeapProfileSampleLabelsCallback(FixedLabelsCallback,
234+
&labels);
235+
236+
// Start with GC retain flags — GC'd samples should survive.
237+
heap_profiler->StartSamplingHeapProfiler(
238+
256, 128,
239+
static_cast<v8::HeapProfiler::SamplingFlags>(
240+
v8::HeapProfiler::kSamplingIncludeObjectsCollectedByMajorGC |
241+
v8::HeapProfiler::kSamplingIncludeObjectsCollectedByMinorGC));
242+
243+
// Allocate short-lived objects via JS (no reference retained).
244+
v8::Local<v8::String> source =
245+
v8::String::NewFromUtf8Literal(isolate_,
246+
"for (var i = 0; i < 4096; i++) { new Array(64); }");
247+
v8::Local<v8::Script> script =
248+
v8::Script::Compile(context, source).ToLocalChecked();
249+
script->Run(context).ToLocalChecked();
250+
251+
// Force GC to collect the short-lived objects.
252+
v8::V8::SetFlagsFromString("--expose-gc");
253+
isolate_->RequestGarbageCollectionForTesting(
254+
v8::Isolate::kFullGarbageCollection);
255+
256+
std::unique_ptr<v8::AllocationProfile> profile(
257+
heap_profiler->GetAllocationProfile());
258+
ASSERT_NE(profile, nullptr);
259+
260+
// With GC retain flags, samples for collected objects should still exist
261+
// with their labels intact.
262+
bool found_labeled = false;
263+
for (const auto& sample : profile->GetSamples()) {
264+
if (!sample.labels.empty()) {
265+
EXPECT_EQ(sample.labels[0].first, "route");
266+
EXPECT_EQ(sample.labels[0].second, "/api/gc-test");
267+
found_labeled = true;
268+
}
269+
}
270+
EXPECT_TRUE(found_labeled);
271+
272+
heap_profiler->StopSamplingHeapProfiler();
273+
heap_profiler->SetHeapProfileSampleLabelsCallback(nullptr, nullptr);
274+
}
275+
276+
// Test: labels removed with samples when GC flags disabled and objects collected.
277+
TEST_F(HeapProfileLabelsTest, SamplesRemovedByGCWithoutFlags) {
278+
const v8::HandleScope handle_scope(isolate_);
279+
v8::Local<v8::Context> context = v8::Context::New(isolate_);
280+
v8::Context::Scope context_scope(context);
281+
282+
v8::HeapProfiler* heap_profiler = isolate_->GetHeapProfiler();
283+
284+
// Set CPED so callback is invoked.
285+
isolate_->SetContinuationPreservedEmbedderData(
286+
v8::String::NewFromUtf8Literal(isolate_, "test-context"));
287+
288+
std::vector<std::pair<std::string, std::string>> labels = {
289+
{"route", "/api/gc-remove"}};
290+
heap_profiler->SetHeapProfileSampleLabelsCallback(FixedLabelsCallback,
291+
&labels);
292+
293+
// Start WITHOUT GC retain flags — GC'd samples should be removed.
294+
heap_profiler->StartSamplingHeapProfiler(256);
295+
296+
// Allocate short-lived objects via JS (no reference retained).
297+
v8::Local<v8::String> source =
298+
v8::String::NewFromUtf8Literal(isolate_,
299+
"for (var i = 0; i < 4096; i++) { new Array(64); }");
300+
v8::Local<v8::Script> script =
301+
v8::Script::Compile(context, source).ToLocalChecked();
302+
script->Run(context).ToLocalChecked();
303+
304+
// Force GC to collect the short-lived objects.
305+
v8::V8::SetFlagsFromString("--expose-gc");
306+
isolate_->RequestGarbageCollectionForTesting(
307+
v8::Isolate::kFullGarbageCollection);
308+
309+
std::unique_ptr<v8::AllocationProfile> profile(
310+
heap_profiler->GetAllocationProfile());
311+
ASSERT_NE(profile, nullptr);
312+
313+
// Without GC retain flags, samples for collected objects should be removed.
314+
// The profile should still be valid (no crash).
315+
EXPECT_NE(profile->GetRootNode(), nullptr);
316+
317+
heap_profiler->StopSamplingHeapProfiler();
318+
heap_profiler->SetHeapProfileSampleLabelsCallback(nullptr, nullptr);
319+
}
320+
321+
// Test: unregister callback (pass nullptr) — subsequent samples have no labels.
322+
TEST_F(HeapProfileLabelsTest, UnregisterCallbackStopsLabels) {
323+
const v8::HandleScope handle_scope(isolate_);
324+
v8::Local<v8::Context> context = v8::Context::New(isolate_);
325+
v8::Context::Scope context_scope(context);
326+
327+
v8::HeapProfiler* heap_profiler = isolate_->GetHeapProfiler();
328+
329+
// Set CPED so callback is invoked.
330+
isolate_->SetContinuationPreservedEmbedderData(
331+
v8::String::NewFromUtf8Literal(isolate_, "test-context"));
332+
333+
std::vector<std::pair<std::string, std::string>> labels = {
334+
{"route", "/api/before-unregister"}};
335+
heap_profiler->SetHeapProfileSampleLabelsCallback(FixedLabelsCallback,
336+
&labels);
337+
338+
heap_profiler->StartSamplingHeapProfiler(256);
339+
340+
// Allocate with callback active.
341+
for (int i = 0; i < 4 * 1024; ++i) v8::Object::New(isolate_);
342+
343+
// Unregister callback (pass nullptr).
344+
heap_profiler->SetHeapProfileSampleLabelsCallback(nullptr, nullptr);
345+
346+
// Allocate more — these should have no labels.
347+
for (int i = 0; i < 4 * 1024; ++i) v8::Object::New(isolate_);
348+
349+
std::unique_ptr<v8::AllocationProfile> profile(
350+
heap_profiler->GetAllocationProfile());
351+
ASSERT_NE(profile, nullptr);
352+
353+
// Should have some labeled samples (from before unregister) and some
354+
// unlabeled (from after).
355+
bool found_labeled = false;
356+
bool found_unlabeled = false;
357+
for (const auto& sample : profile->GetSamples()) {
358+
if (!sample.labels.empty()) {
359+
found_labeled = true;
360+
} else {
361+
found_unlabeled = true;
362+
}
363+
}
364+
EXPECT_TRUE(found_labeled);
365+
EXPECT_TRUE(found_unlabeled);
366+
367+
heap_profiler->StopSamplingHeapProfiler();
368+
}
369+
370+
#endif // V8_HEAP_PROFILER_SAMPLE_LABELS

0 commit comments

Comments
 (0)