Skip to content

Commit c1934a5

Browse files
committed
test: add heap profile labels tests
Cover basic labeling, multi-key labels, async propagation, worker cleanup, and C++ callback tests. Signed-off-by: Rudolf Meijering <[email protected]>
1 parent a8ac88e commit c1934a5

4 files changed

Lines changed: 961 additions & 0 deletions

File tree

Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
1+
// Tests for V8 HeapProfileSampleLabelsCallback API.
2+
// Validates the label callback feature at the V8 public API level.
3+
4+
#include <memory>
5+
#include <string>
6+
#include <utility>
7+
#include <vector>
8+
9+
#include "gtest/gtest.h"
10+
#include "node_test_fixture.h"
11+
#include "v8-profiler.h"
12+
#include "v8.h"
13+
14+
#ifdef V8_HEAP_PROFILER_SAMPLE_LABELS
15+
16+
// Helper: a label callback that writes fixed labels via output parameter.
17+
static bool FixedLabelsCallback(
18+
void* data, v8::Local<v8::Value> context,
19+
std::vector<std::pair<std::string, std::string>>* out_labels) {
20+
auto* labels =
21+
static_cast<std::vector<std::pair<std::string, std::string>>*>(data);
22+
*out_labels = *labels;
23+
return true;
24+
}
25+
26+
// Helper: a label callback that returns false (no labels).
27+
static bool EmptyLabelsCallback(
28+
void* data, v8::Local<v8::Value> context,
29+
std::vector<std::pair<std::string, std::string>>* out_labels) {
30+
return false;
31+
}
32+
33+
// Helper: a label callback that resolves labels based on the CPED string value.
34+
// Used to verify that different CPED values produce different labels.
35+
struct ContextLabelState {
36+
v8::Isolate* isolate;
37+
};
38+
39+
static bool ContextBasedLabelsCallback(
40+
void* data, v8::Local<v8::Value> context,
41+
std::vector<std::pair<std::string, std::string>>* out_labels) {
42+
if (context.IsEmpty() || !context->IsString()) return false;
43+
auto* state = static_cast<ContextLabelState*>(data);
44+
v8::String::Utf8Value utf8(state->isolate, context);
45+
std::string cped_str(*utf8, utf8.length());
46+
if (cped_str == "first") {
47+
out_labels->push_back({"route", "/api/first"});
48+
} else if (cped_str == "second") {
49+
out_labels->push_back({"route", "/api/second"});
50+
}
51+
return !out_labels->empty();
52+
}
53+
54+
class HeapProfileLabelsTest : public NodeTestFixture {};
55+
56+
// Test: register callback, allocate, verify labels on samples.
57+
TEST_F(HeapProfileLabelsTest, CallbackReturnsLabels) {
58+
const v8::HandleScope handle_scope(isolate_);
59+
v8::Local<v8::Context> context = v8::Context::New(isolate_);
60+
v8::Context::Scope context_scope(context);
61+
62+
v8::HeapProfiler* heap_profiler = isolate_->GetHeapProfiler();
63+
64+
std::vector<std::pair<std::string, std::string>> labels = {
65+
{"route", "/api/test"}};
66+
67+
// Set CPED so the callback gets invoked (requires non-empty context).
68+
isolate_->SetContinuationPreservedEmbedderData(
69+
v8::String::NewFromUtf8Literal(isolate_, "test-context"));
70+
71+
heap_profiler->SetHeapProfileSampleLabelsCallback(FixedLabelsCallback,
72+
&labels);
73+
74+
heap_profiler->StartSamplingHeapProfiler(256);
75+
76+
// Allocate enough objects to get samples.
77+
for (int i = 0; i < 8 * 1024; ++i) v8::Object::New(isolate_);
78+
79+
std::unique_ptr<v8::AllocationProfile> profile(
80+
heap_profiler->GetAllocationProfile());
81+
ASSERT_NE(profile, nullptr);
82+
83+
bool found_labeled = false;
84+
for (const auto& sample : profile->GetSamples()) {
85+
if (!sample.labels.empty()) {
86+
EXPECT_EQ(sample.labels.size(), 1u);
87+
EXPECT_EQ(sample.labels[0].first, "route");
88+
EXPECT_EQ(sample.labels[0].second, "/api/test");
89+
found_labeled = true;
90+
}
91+
}
92+
EXPECT_TRUE(found_labeled);
93+
94+
heap_profiler->StopSamplingHeapProfiler();
95+
heap_profiler->SetHeapProfileSampleLabelsCallback(nullptr, nullptr);
96+
}
97+
98+
// Test: no callback registered — samples have empty labels.
99+
TEST_F(HeapProfileLabelsTest, NoCallbackEmptyLabels) {
100+
const v8::HandleScope handle_scope(isolate_);
101+
v8::Local<v8::Context> context = v8::Context::New(isolate_);
102+
v8::Context::Scope context_scope(context);
103+
104+
v8::HeapProfiler* heap_profiler = isolate_->GetHeapProfiler();
105+
106+
heap_profiler->StartSamplingHeapProfiler(256);
107+
108+
for (int i = 0; i < 8 * 1024; ++i) v8::Object::New(isolate_);
109+
110+
std::unique_ptr<v8::AllocationProfile> profile(
111+
heap_profiler->GetAllocationProfile());
112+
ASSERT_NE(profile, nullptr);
113+
114+
for (const auto& sample : profile->GetSamples()) {
115+
EXPECT_TRUE(sample.labels.empty());
116+
}
117+
118+
heap_profiler->StopSamplingHeapProfiler();
119+
}
120+
121+
// Test: callback returns empty vector — samples have empty labels.
122+
TEST_F(HeapProfileLabelsTest, EmptyCallbackEmptyLabels) {
123+
const v8::HandleScope handle_scope(isolate_);
124+
v8::Local<v8::Context> context = v8::Context::New(isolate_);
125+
v8::Context::Scope context_scope(context);
126+
127+
v8::HeapProfiler* heap_profiler = isolate_->GetHeapProfiler();
128+
129+
// Set CPED so callback is invoked.
130+
isolate_->SetContinuationPreservedEmbedderData(
131+
v8::String::NewFromUtf8Literal(isolate_, "test-context"));
132+
133+
heap_profiler->SetHeapProfileSampleLabelsCallback(EmptyLabelsCallback,
134+
nullptr);
135+
136+
heap_profiler->StartSamplingHeapProfiler(256);
137+
138+
for (int i = 0; i < 8 * 1024; ++i) v8::Object::New(isolate_);
139+
140+
std::unique_ptr<v8::AllocationProfile> profile(
141+
heap_profiler->GetAllocationProfile());
142+
ASSERT_NE(profile, nullptr);
143+
144+
for (const auto& sample : profile->GetSamples()) {
145+
EXPECT_TRUE(sample.labels.empty());
146+
}
147+
148+
heap_profiler->StopSamplingHeapProfiler();
149+
heap_profiler->SetHeapProfileSampleLabelsCallback(nullptr, nullptr);
150+
}
151+
152+
// Test: multiple distinct label sets resolved from different CPED values.
153+
// Labels are resolved at read time (BuildSamples) from stored CPED.
154+
TEST_F(HeapProfileLabelsTest, MultipleDistinctLabels) {
155+
const v8::HandleScope handle_scope(isolate_);
156+
v8::Local<v8::Context> context = v8::Context::New(isolate_);
157+
v8::Context::Scope context_scope(context);
158+
159+
v8::HeapProfiler* heap_profiler = isolate_->GetHeapProfiler();
160+
161+
ContextLabelState state{isolate_};
162+
heap_profiler->SetHeapProfileSampleLabelsCallback(ContextBasedLabelsCallback,
163+
&state);
164+
165+
heap_profiler->StartSamplingHeapProfiler(256);
166+
167+
// Allocate with first CPED value.
168+
isolate_->SetContinuationPreservedEmbedderData(
169+
v8::String::NewFromUtf8Literal(isolate_, "first"));
170+
for (int i = 0; i < 4 * 1024; ++i) v8::Object::New(isolate_);
171+
172+
// Switch to second CPED value.
173+
isolate_->SetContinuationPreservedEmbedderData(
174+
v8::String::NewFromUtf8Literal(isolate_, "second"));
175+
for (int i = 0; i < 4 * 1024; ++i) v8::Object::New(isolate_);
176+
177+
std::unique_ptr<v8::AllocationProfile> profile(
178+
heap_profiler->GetAllocationProfile());
179+
ASSERT_NE(profile, nullptr);
180+
181+
bool found_first = false;
182+
bool found_second = false;
183+
for (const auto& sample : profile->GetSamples()) {
184+
if (!sample.labels.empty()) {
185+
EXPECT_EQ(sample.labels.size(), 1u);
186+
EXPECT_EQ(sample.labels[0].first, "route");
187+
if (sample.labels[0].second == "/api/first") found_first = true;
188+
if (sample.labels[0].second == "/api/second") found_second = true;
189+
}
190+
}
191+
EXPECT_TRUE(found_first);
192+
EXPECT_TRUE(found_second);
193+
194+
heap_profiler->StopSamplingHeapProfiler();
195+
heap_profiler->SetHeapProfileSampleLabelsCallback(nullptr, nullptr);
196+
}
197+
198+
// Test: labels survive GC when kSamplingIncludeObjectsCollectedByMajorGC enabled.
199+
TEST_F(HeapProfileLabelsTest, LabelsSurviveGCWithRetainFlags) {
200+
const v8::HandleScope handle_scope(isolate_);
201+
v8::Local<v8::Context> context = v8::Context::New(isolate_);
202+
v8::Context::Scope context_scope(context);
203+
204+
v8::HeapProfiler* heap_profiler = isolate_->GetHeapProfiler();
205+
206+
// Set CPED so callback is invoked.
207+
isolate_->SetContinuationPreservedEmbedderData(
208+
v8::String::NewFromUtf8Literal(isolate_, "test-context"));
209+
210+
std::vector<std::pair<std::string, std::string>> labels = {
211+
{"route", "/api/gc-test"}};
212+
heap_profiler->SetHeapProfileSampleLabelsCallback(FixedLabelsCallback,
213+
&labels);
214+
215+
// Start with GC retain flags — GC'd samples should survive.
216+
heap_profiler->StartSamplingHeapProfiler(
217+
256, 128,
218+
static_cast<v8::HeapProfiler::SamplingFlags>(
219+
v8::HeapProfiler::kSamplingIncludeObjectsCollectedByMajorGC |
220+
v8::HeapProfiler::kSamplingIncludeObjectsCollectedByMinorGC));
221+
222+
// Allocate short-lived objects via JS (no reference retained).
223+
v8::Local<v8::String> source =
224+
v8::String::NewFromUtf8Literal(isolate_,
225+
"for (var i = 0; i < 4096; i++) { new Array(64); }");
226+
v8::Local<v8::Script> script =
227+
v8::Script::Compile(context, source).ToLocalChecked();
228+
script->Run(context).ToLocalChecked();
229+
230+
// Force GC to collect the short-lived objects.
231+
v8::V8::SetFlagsFromString("--expose-gc");
232+
isolate_->RequestGarbageCollectionForTesting(
233+
v8::Isolate::kFullGarbageCollection);
234+
235+
std::unique_ptr<v8::AllocationProfile> profile(
236+
heap_profiler->GetAllocationProfile());
237+
ASSERT_NE(profile, nullptr);
238+
239+
// With GC retain flags, samples for collected objects should still exist
240+
// with their labels intact.
241+
bool found_labeled = false;
242+
for (const auto& sample : profile->GetSamples()) {
243+
if (!sample.labels.empty()) {
244+
EXPECT_EQ(sample.labels[0].first, "route");
245+
EXPECT_EQ(sample.labels[0].second, "/api/gc-test");
246+
found_labeled = true;
247+
}
248+
}
249+
EXPECT_TRUE(found_labeled);
250+
251+
heap_profiler->StopSamplingHeapProfiler();
252+
heap_profiler->SetHeapProfileSampleLabelsCallback(nullptr, nullptr);
253+
}
254+
255+
// Test: labels removed with samples when GC flags disabled and objects collected.
256+
TEST_F(HeapProfileLabelsTest, SamplesRemovedByGCWithoutFlags) {
257+
const v8::HandleScope handle_scope(isolate_);
258+
v8::Local<v8::Context> context = v8::Context::New(isolate_);
259+
v8::Context::Scope context_scope(context);
260+
261+
v8::HeapProfiler* heap_profiler = isolate_->GetHeapProfiler();
262+
263+
// Set CPED so callback is invoked.
264+
isolate_->SetContinuationPreservedEmbedderData(
265+
v8::String::NewFromUtf8Literal(isolate_, "test-context"));
266+
267+
std::vector<std::pair<std::string, std::string>> labels = {
268+
{"route", "/api/gc-remove"}};
269+
heap_profiler->SetHeapProfileSampleLabelsCallback(FixedLabelsCallback,
270+
&labels);
271+
272+
// Start WITHOUT GC retain flags — GC'd samples should be removed.
273+
heap_profiler->StartSamplingHeapProfiler(256);
274+
275+
// Allocate short-lived objects via JS (no reference retained).
276+
v8::Local<v8::String> source =
277+
v8::String::NewFromUtf8Literal(isolate_,
278+
"for (var i = 0; i < 4096; i++) { new Array(64); }");
279+
v8::Local<v8::Script> script =
280+
v8::Script::Compile(context, source).ToLocalChecked();
281+
script->Run(context).ToLocalChecked();
282+
283+
// Force GC to collect the short-lived objects.
284+
v8::V8::SetFlagsFromString("--expose-gc");
285+
isolate_->RequestGarbageCollectionForTesting(
286+
v8::Isolate::kFullGarbageCollection);
287+
288+
std::unique_ptr<v8::AllocationProfile> profile(
289+
heap_profiler->GetAllocationProfile());
290+
ASSERT_NE(profile, nullptr);
291+
292+
// Without GC retain flags, samples for collected objects should be removed.
293+
// The profile should still be valid (no crash).
294+
EXPECT_NE(profile->GetRootNode(), nullptr);
295+
296+
heap_profiler->StopSamplingHeapProfiler();
297+
heap_profiler->SetHeapProfileSampleLabelsCallback(nullptr, nullptr);
298+
}
299+
300+
// Test: unregister callback — samples allocated after have no stored CPED.
301+
// In the new architecture, CPED is only captured when a callback is registered
302+
// at allocation time. Labels are resolved at read time. Samples without CPED
303+
// get no labels even when the callback is re-registered for reading.
304+
TEST_F(HeapProfileLabelsTest, UnregisterCallbackStopsLabels) {
305+
const v8::HandleScope handle_scope(isolate_);
306+
v8::Local<v8::Context> context = v8::Context::New(isolate_);
307+
v8::Context::Scope context_scope(context);
308+
309+
v8::HeapProfiler* heap_profiler = isolate_->GetHeapProfiler();
310+
311+
// Set CPED so it's available during allocation.
312+
isolate_->SetContinuationPreservedEmbedderData(
313+
v8::String::NewFromUtf8Literal(isolate_, "test-context"));
314+
315+
std::vector<std::pair<std::string, std::string>> labels = {
316+
{"route", "/api/before-unregister"}};
317+
heap_profiler->SetHeapProfileSampleLabelsCallback(FixedLabelsCallback,
318+
&labels);
319+
320+
heap_profiler->StartSamplingHeapProfiler(256);
321+
322+
// Allocate with callback active — CPED is captured on these samples.
323+
for (int i = 0; i < 4 * 1024; ++i) v8::Object::New(isolate_);
324+
325+
// Unregister callback — new samples won't have CPED captured.
326+
heap_profiler->SetHeapProfileSampleLabelsCallback(nullptr, nullptr);
327+
328+
// Allocate more — no CPED captured since callback is null at allocation time.
329+
for (int i = 0; i < 4 * 1024; ++i) v8::Object::New(isolate_);
330+
331+
// Re-register callback before reading profile. Labels are resolved at
332+
// read time, so only samples with stored CPED will get labels.
333+
heap_profiler->SetHeapProfileSampleLabelsCallback(FixedLabelsCallback,
334+
&labels);
335+
336+
std::unique_ptr<v8::AllocationProfile> profile(
337+
heap_profiler->GetAllocationProfile());
338+
ASSERT_NE(profile, nullptr);
339+
340+
// Should have labeled samples (CPED captured before unregister) and
341+
// unlabeled samples (no CPED captured after unregister).
342+
bool found_labeled = false;
343+
bool found_unlabeled = false;
344+
for (const auto& sample : profile->GetSamples()) {
345+
if (!sample.labels.empty()) {
346+
found_labeled = true;
347+
} else {
348+
found_unlabeled = true;
349+
}
350+
}
351+
EXPECT_TRUE(found_labeled);
352+
EXPECT_TRUE(found_unlabeled);
353+
354+
heap_profiler->StopSamplingHeapProfiler();
355+
heap_profiler->SetHeapProfileSampleLabelsCallback(nullptr, nullptr);
356+
}
357+
358+
#endif // V8_HEAP_PROFILER_SAMPLE_LABELS

0 commit comments

Comments
 (0)