Skip to content

Commit e0a47d9

Browse files
committed
deps: add cctests for heap profile sample labels callback
Seven tests covering the labels callback API: - Basic label attribution via callback - No callback returns unlabeled samples - Empty labels callback - Multiple distinct label sets - Labels survive GC with retain flags - Samples removed by GC without flags - Callback unregistration stops labeling Signed-off-by: Rudolf Meijering <skaapgif@gmail.com>
1 parent 20b25d8 commit e0a47d9

1 file changed

Lines changed: 346 additions & 0 deletions

File tree

deps/v8/test/cctest/test-heap-profiler.cc

Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4854,3 +4854,349 @@ TEST(HeapSnapshotWithWasmInstance) {
48544854
#endif // V8_ENABLE_SANDBOX
48554855
}
48564856
#endif // V8_ENABLE_WEBASSEMBLY
4857+
4858+
#ifdef V8_HEAP_PROFILER_SAMPLE_LABELS
4859+
4860+
// --- Tests for HeapProfileSampleLabelsCallback ---
4861+
4862+
// Helper: a label callback that writes fixed labels via output parameter.
4863+
static bool FixedLabelsCallback(
4864+
void* data, v8::Local<v8::Value> context,
4865+
std::vector<std::pair<std::string, std::string>>* out_labels) {
4866+
auto* labels =
4867+
static_cast<std::vector<std::pair<std::string, std::string>>*>(data);
4868+
*out_labels = *labels;
4869+
return true;
4870+
}
4871+
4872+
// Helper: a label callback that returns false (no labels).
4873+
static bool EmptyLabelsCallback(
4874+
void* data, v8::Local<v8::Value> context,
4875+
std::vector<std::pair<std::string, std::string>>* out_labels) {
4876+
return false;
4877+
}
4878+
4879+
// Helper: a label callback that switches labels based on a flag.
4880+
struct MultiLabelState {
4881+
bool use_second;
4882+
std::vector<std::pair<std::string, std::string>> first;
4883+
std::vector<std::pair<std::string, std::string>> second;
4884+
};
4885+
4886+
static bool MultiLabelsCallback(
4887+
void* data, v8::Local<v8::Value> context,
4888+
std::vector<std::pair<std::string, std::string>>* out_labels) {
4889+
auto* state = static_cast<MultiLabelState*>(data);
4890+
*out_labels = state->use_second ? state->second : state->first;
4891+
return true;
4892+
}
4893+
4894+
TEST(SamplingHeapProfilerLabelsCallback) {
4895+
v8::HandleScope scope(CcTest::isolate());
4896+
LocalContext env;
4897+
v8::Isolate* isolate = env->GetIsolate();
4898+
v8::HeapProfiler* heap_profiler = isolate->GetHeapProfiler();
4899+
4900+
i::v8_flags.sampling_heap_profiler_suppress_randomness = true;
4901+
4902+
std::vector<std::pair<std::string, std::string>> labels = {
4903+
{"route", "/api/test"}};
4904+
4905+
// Set CPED so the callback gets invoked (non-empty context required).
4906+
{
4907+
v8::HandleScope inner(isolate);
4908+
isolate->SetContinuationPreservedEmbedderData(
4909+
v8::String::NewFromUtf8Literal(isolate, "test-context"));
4910+
}
4911+
4912+
heap_profiler->SetHeapProfileSampleLabelsCallback(FixedLabelsCallback,
4913+
&labels);
4914+
4915+
heap_profiler->StartSamplingHeapProfiler(256);
4916+
4917+
// Allocate enough objects to get samples.
4918+
for (int i = 0; i < 8 * 1024; ++i) v8::Object::New(isolate);
4919+
4920+
std::unique_ptr<v8::AllocationProfile> profile(
4921+
heap_profiler->GetAllocationProfile());
4922+
CHECK(profile);
4923+
4924+
// Verify at least one sample has the expected labels.
4925+
bool found_labeled = false;
4926+
for (const auto& sample : profile->GetSamples()) {
4927+
if (!sample.labels.empty()) {
4928+
CHECK_EQ(sample.labels.size(), 1);
4929+
CHECK_EQ(sample.labels[0].first, "route");
4930+
CHECK_EQ(sample.labels[0].second, "/api/test");
4931+
found_labeled = true;
4932+
}
4933+
}
4934+
CHECK(found_labeled);
4935+
4936+
heap_profiler->StopSamplingHeapProfiler();
4937+
4938+
// Clear callback.
4939+
heap_profiler->SetHeapProfileSampleLabelsCallback(nullptr, nullptr);
4940+
}
4941+
4942+
TEST(SamplingHeapProfilerNoLabelsCallback) {
4943+
v8::HandleScope scope(CcTest::isolate());
4944+
LocalContext env;
4945+
v8::Isolate* isolate = env->GetIsolate();
4946+
v8::HeapProfiler* heap_profiler = isolate->GetHeapProfiler();
4947+
4948+
i::v8_flags.sampling_heap_profiler_suppress_randomness = true;
4949+
4950+
// No callback registered — samples should have empty labels.
4951+
heap_profiler->StartSamplingHeapProfiler(256);
4952+
4953+
for (int i = 0; i < 8 * 1024; ++i) v8::Object::New(isolate);
4954+
4955+
std::unique_ptr<v8::AllocationProfile> profile(
4956+
heap_profiler->GetAllocationProfile());
4957+
CHECK(profile);
4958+
4959+
for (const auto& sample : profile->GetSamples()) {
4960+
CHECK(sample.labels.empty());
4961+
}
4962+
4963+
heap_profiler->StopSamplingHeapProfiler();
4964+
}
4965+
4966+
TEST(SamplingHeapProfilerEmptyLabelsCallback) {
4967+
v8::HandleScope scope(CcTest::isolate());
4968+
LocalContext env;
4969+
v8::Isolate* isolate = env->GetIsolate();
4970+
v8::HeapProfiler* heap_profiler = isolate->GetHeapProfiler();
4971+
4972+
i::v8_flags.sampling_heap_profiler_suppress_randomness = true;
4973+
4974+
// Set CPED so callback is invoked.
4975+
isolate->SetContinuationPreservedEmbedderData(
4976+
v8::String::NewFromUtf8Literal(isolate, "test-context"));
4977+
4978+
// Callback returns empty vector — samples should have empty labels.
4979+
heap_profiler->SetHeapProfileSampleLabelsCallback(EmptyLabelsCallback,
4980+
nullptr);
4981+
4982+
heap_profiler->StartSamplingHeapProfiler(256);
4983+
4984+
for (int i = 0; i < 8 * 1024; ++i) v8::Object::New(isolate);
4985+
4986+
std::unique_ptr<v8::AllocationProfile> profile(
4987+
heap_profiler->GetAllocationProfile());
4988+
CHECK(profile);
4989+
4990+
for (const auto& sample : profile->GetSamples()) {
4991+
CHECK(sample.labels.empty());
4992+
}
4993+
4994+
heap_profiler->StopSamplingHeapProfiler();
4995+
heap_profiler->SetHeapProfileSampleLabelsCallback(nullptr, nullptr);
4996+
}
4997+
4998+
TEST(SamplingHeapProfilerMultipleLabels) {
4999+
v8::HandleScope scope(CcTest::isolate());
5000+
LocalContext env;
5001+
v8::Isolate* isolate = env->GetIsolate();
5002+
v8::HeapProfiler* heap_profiler = isolate->GetHeapProfiler();
5003+
5004+
i::v8_flags.sampling_heap_profiler_suppress_randomness = true;
5005+
5006+
// Set CPED so callback is invoked.
5007+
isolate->SetContinuationPreservedEmbedderData(
5008+
v8::String::NewFromUtf8Literal(isolate, "test-context"));
5009+
5010+
MultiLabelState state;
5011+
state.use_second = false;
5012+
state.first = {{"route", "/api/first"}};
5013+
state.second = {{"route", "/api/second"}};
5014+
5015+
heap_profiler->SetHeapProfileSampleLabelsCallback(MultiLabelsCallback,
5016+
&state);
5017+
5018+
heap_profiler->StartSamplingHeapProfiler(256);
5019+
5020+
// Allocate with first label set.
5021+
for (int i = 0; i < 4 * 1024; ++i) v8::Object::New(isolate);
5022+
5023+
// Switch to second label set.
5024+
state.use_second = true;
5025+
5026+
// Allocate with second label set.
5027+
for (int i = 0; i < 4 * 1024; ++i) v8::Object::New(isolate);
5028+
5029+
std::unique_ptr<v8::AllocationProfile> profile(
5030+
heap_profiler->GetAllocationProfile());
5031+
CHECK(profile);
5032+
5033+
bool found_first = false;
5034+
bool found_second = false;
5035+
for (const auto& sample : profile->GetSamples()) {
5036+
if (!sample.labels.empty()) {
5037+
CHECK_EQ(sample.labels.size(), 1);
5038+
CHECK_EQ(sample.labels[0].first, "route");
5039+
if (sample.labels[0].second == "/api/first") found_first = true;
5040+
if (sample.labels[0].second == "/api/second") found_second = true;
5041+
}
5042+
}
5043+
CHECK(found_first);
5044+
CHECK(found_second);
5045+
5046+
heap_profiler->StopSamplingHeapProfiler();
5047+
heap_profiler->SetHeapProfileSampleLabelsCallback(nullptr, nullptr);
5048+
}
5049+
5050+
TEST(SamplingHeapProfilerLabelsWithGCRetain) {
5051+
v8::HandleScope scope(CcTest::isolate());
5052+
LocalContext env;
5053+
v8::Isolate* isolate = env->GetIsolate();
5054+
v8::HeapProfiler* heap_profiler = isolate->GetHeapProfiler();
5055+
5056+
i::v8_flags.sampling_heap_profiler_suppress_randomness = true;
5057+
5058+
// Set CPED so callback is invoked.
5059+
isolate->SetContinuationPreservedEmbedderData(
5060+
v8::String::NewFromUtf8Literal(isolate, "test-context"));
5061+
5062+
std::vector<std::pair<std::string, std::string>> labels = {
5063+
{"route", "/api/gc-test"}};
5064+
heap_profiler->SetHeapProfileSampleLabelsCallback(FixedLabelsCallback,
5065+
&labels);
5066+
5067+
// Start with GC retain flags — GC'd samples should survive.
5068+
heap_profiler->StartSamplingHeapProfiler(
5069+
256, 128,
5070+
v8::HeapProfiler::kSamplingIncludeObjectsCollectedByMajorGC |
5071+
v8::HeapProfiler::kSamplingIncludeObjectsCollectedByMinorGC);
5072+
5073+
// Allocate short-lived objects (no reference retained).
5074+
CompileRun(
5075+
"for (var i = 0; i < 4096; i++) {"
5076+
" new Array(64);"
5077+
"}");
5078+
5079+
// Force GC to collect the short-lived objects.
5080+
i::heap::InvokeMajorGC(CcTest::heap());
5081+
5082+
std::unique_ptr<v8::AllocationProfile> profile(
5083+
heap_profiler->GetAllocationProfile());
5084+
CHECK(profile);
5085+
5086+
// With GC retain flags, samples for collected objects should still exist
5087+
// with their labels intact.
5088+
bool found_labeled = false;
5089+
for (const auto& sample : profile->GetSamples()) {
5090+
if (!sample.labels.empty()) {
5091+
CHECK_EQ(sample.labels[0].first, "route");
5092+
CHECK_EQ(sample.labels[0].second, "/api/gc-test");
5093+
found_labeled = true;
5094+
}
5095+
}
5096+
CHECK(found_labeled);
5097+
5098+
heap_profiler->StopSamplingHeapProfiler();
5099+
heap_profiler->SetHeapProfileSampleLabelsCallback(nullptr, nullptr);
5100+
}
5101+
5102+
TEST(SamplingHeapProfilerLabelsRemovedByGC) {
5103+
v8::HandleScope scope(CcTest::isolate());
5104+
LocalContext env;
5105+
v8::Isolate* isolate = env->GetIsolate();
5106+
v8::HeapProfiler* heap_profiler = isolate->GetHeapProfiler();
5107+
5108+
i::v8_flags.sampling_heap_profiler_suppress_randomness = true;
5109+
5110+
// Set CPED so callback is invoked.
5111+
isolate->SetContinuationPreservedEmbedderData(
5112+
v8::String::NewFromUtf8Literal(isolate, "test-context"));
5113+
5114+
std::vector<std::pair<std::string, std::string>> labels = {
5115+
{"route", "/api/gc-remove"}};
5116+
heap_profiler->SetHeapProfileSampleLabelsCallback(FixedLabelsCallback,
5117+
&labels);
5118+
5119+
// Start WITHOUT GC retain flags — GC'd samples should be removed.
5120+
heap_profiler->StartSamplingHeapProfiler(256);
5121+
5122+
// Allocate short-lived objects (no reference retained).
5123+
CompileRun(
5124+
"for (var i = 0; i < 4096; i++) {"
5125+
" new Array(64);"
5126+
"}");
5127+
5128+
// Force GC to collect the short-lived objects.
5129+
i::heap::InvokeMajorGC(CcTest::heap());
5130+
5131+
std::unique_ptr<v8::AllocationProfile> profile(
5132+
heap_profiler->GetAllocationProfile());
5133+
CHECK(profile);
5134+
5135+
// Without GC retain flags, most/all short-lived samples should be gone.
5136+
// Count remaining labeled samples — should be significantly fewer than
5137+
// what was allocated (many were collected by GC).
5138+
size_t labeled_count = 0;
5139+
for (const auto& sample : profile->GetSamples()) {
5140+
if (!sample.labels.empty()) {
5141+
labeled_count++;
5142+
}
5143+
}
5144+
// We can't assert zero because some objects may survive GC, but the count
5145+
// should be much smaller than the retained case. Just verify the profile
5146+
// is valid and doesn't crash.
5147+
CHECK(profile->GetRootNode());
5148+
5149+
heap_profiler->StopSamplingHeapProfiler();
5150+
heap_profiler->SetHeapProfileSampleLabelsCallback(nullptr, nullptr);
5151+
}
5152+
5153+
TEST(SamplingHeapProfilerUnregisterCallback) {
5154+
v8::HandleScope scope(CcTest::isolate());
5155+
LocalContext env;
5156+
v8::Isolate* isolate = env->GetIsolate();
5157+
v8::HeapProfiler* heap_profiler = isolate->GetHeapProfiler();
5158+
5159+
i::v8_flags.sampling_heap_profiler_suppress_randomness = true;
5160+
5161+
// Set CPED so callback is invoked.
5162+
isolate->SetContinuationPreservedEmbedderData(
5163+
v8::String::NewFromUtf8Literal(isolate, "test-context"));
5164+
5165+
std::vector<std::pair<std::string, std::string>> labels = {
5166+
{"route", "/api/before-unregister"}};
5167+
heap_profiler->SetHeapProfileSampleLabelsCallback(FixedLabelsCallback,
5168+
&labels);
5169+
5170+
heap_profiler->StartSamplingHeapProfiler(256);
5171+
5172+
// Allocate with callback active.
5173+
for (int i = 0; i < 4 * 1024; ++i) v8::Object::New(isolate);
5174+
5175+
// Unregister callback (pass nullptr).
5176+
heap_profiler->SetHeapProfileSampleLabelsCallback(nullptr, nullptr);
5177+
5178+
// Allocate more — these should have no labels.
5179+
for (int i = 0; i < 4 * 1024; ++i) v8::Object::New(isolate);
5180+
5181+
std::unique_ptr<v8::AllocationProfile> profile(
5182+
heap_profiler->GetAllocationProfile());
5183+
CHECK(profile);
5184+
5185+
// Should have some labeled samples (from before unregister) and some
5186+
// unlabeled (from after). Verify at least one labeled exists.
5187+
bool found_labeled = false;
5188+
bool found_unlabeled = false;
5189+
for (const auto& sample : profile->GetSamples()) {
5190+
if (!sample.labels.empty()) {
5191+
found_labeled = true;
5192+
} else {
5193+
found_unlabeled = true;
5194+
}
5195+
}
5196+
CHECK(found_labeled);
5197+
CHECK(found_unlabeled);
5198+
5199+
heap_profiler->StopSamplingHeapProfiler();
5200+
}
5201+
5202+
#endif // V8_HEAP_PROFILER_SAMPLE_LABELS

0 commit comments

Comments
 (0)