@@ -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