From 194aa620c9ab03b57eec6a846d81f74c8cb82d6f Mon Sep 17 00:00:00 2001 From: ishabi Date: Mon, 10 Aug 2026 17:43:21 +0200 Subject: [PATCH] deps: V8: cherry-pick 92ac9425fa52 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Original commit message: [profiler] Adjustable heap profiler sampling interval This change makes the V8 sampling heap profiler interval adjustable while heap sampling is running. Today, the sampling interval is fixed for the whole profiling session. Developers who want to adapt sampling under high allocation load have to stop the profiler and start it again with a new interval, which loses continuity. This PR adds a small API surface for adaptive heap sampling: - SetSamplingHeapProfilerInterval(sample_interval) updates the active sampling interval. - GetSamplingHeapProfilerSamples() returns the current retained samples without materializing the full allocation profile tree. - AllocationProfile::Sample now includes sample_interval, so consumers know which interval was used when the sample was generated. The new sample getter is intended for lightweight polling. It avoids the extra work done by GetAllocationProfile(), such as translating the allocation tree and resolving script metadata, when callers only need sample information. This keeps the adaptive policy outside V8: embedders can decide how to adjust the interval, while V8 exposes the mechanism needed to do it. Change-Id: I7cff46a13c02617d04149c8d5c273a8d9ef4bb9f Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8130742 Reviewed-by: Camillo Bruni Commit-Queue: Dominik Inführ Reviewed-by: Dominik Inführ Cr-Commit-Position: refs/heads/main@{#109934} Refs: https://github.com/v8/v8/commit/92ac9425fa5272ac480534dfac05f3fbf88495a5 Signed-off-by: ishabi --- common.gypi | 2 +- deps/v8/include/v8-profiler.h | 21 +++++ deps/v8/src/api/api.cc | 11 +++ deps/v8/src/profiler/heap-profiler.cc | 12 +++ deps/v8/src/profiler/heap-profiler.h | 2 + .../v8/src/profiler/sampling-heap-profiler.cc | 63 ++++++++++----- deps/v8/src/profiler/sampling-heap-profiler.h | 36 ++++++--- deps/v8/test/cctest/test-heap-profiler.cc | 79 +++++++++++++++++++ 8 files changed, 195 insertions(+), 31 deletions(-) diff --git a/common.gypi b/common.gypi index 52dc3ad96adf..24032442c097 100644 --- a/common.gypi +++ b/common.gypi @@ -44,7 +44,7 @@ # Reset this number to 0 on major V8 upgrades. # Increment by one for each non-official patch applied to deps/v8. - 'v8_embedder_string': '-node.22', + 'v8_embedder_string': '-node.24', ##### V8 defaults for Node.js ##### diff --git a/deps/v8/include/v8-profiler.h b/deps/v8/include/v8-profiler.h index 82769a8dfb4f..df60fd7f66d7 100644 --- a/deps/v8/include/v8-profiler.h +++ b/deps/v8/include/v8-profiler.h @@ -836,6 +836,11 @@ class V8_EXPORT AllocationProfile { * been collected by GC. */ bool is_live; + + /** + * Sample interval in bytes used when this sample was selected. + */ + uint64_t sample_interval; }; /** @@ -1257,6 +1262,22 @@ class V8_EXPORT HeapProfiler { */ void StopSamplingHeapProfiler(); + /** + * Updates the sampling interval for a currently running sampling heap + * profiler. The new interval is used for future sample scheduling. + * + * No-op if the sampling heap profiler is not running. + */ + void SetSamplingHeapProfilerInterval(uint64_t sample_interval); + + /** + * Returns the currently retained allocation samples without materializing + * the full allocation profile tree. + * + * Returns an empty vector if the sampling heap profiler is not running. + */ + std::vector GetSamplingHeapProfilerSamples(); + /** * Returns the sampled profile of allocations allocated (and still live) since * StartSamplingHeapProfiler was called. The ownership of the pointer is diff --git a/deps/v8/src/api/api.cc b/deps/v8/src/api/api.cc index f498fee34316..49e3139d432a 100644 --- a/deps/v8/src/api/api.cc +++ b/deps/v8/src/api/api.cc @@ -12098,6 +12098,17 @@ void HeapProfiler::StopSamplingHeapProfiler() { reinterpret_cast(this)->StopSamplingHeapProfiler(); } +void HeapProfiler::SetSamplingHeapProfilerInterval(uint64_t sample_interval) { + reinterpret_cast(this)->SetSamplingHeapProfilerInterval( + sample_interval); +} + +std::vector +HeapProfiler::GetSamplingHeapProfilerSamples() { + return reinterpret_cast(this) + ->GetSamplingHeapProfilerSamples(); +} + AllocationProfile* HeapProfiler::GetAllocationProfile() { return reinterpret_cast(this)->GetAllocationProfile(); } diff --git a/deps/v8/src/profiler/heap-profiler.cc b/deps/v8/src/profiler/heap-profiler.cc index 1bc589b71c04..5b7cb326a599 100644 --- a/deps/v8/src/profiler/heap-profiler.cc +++ b/deps/v8/src/profiler/heap-profiler.cc @@ -236,6 +236,18 @@ void HeapProfiler::StopSamplingHeapProfiler() { MaybeClearStringsStorage(); } +void HeapProfiler::SetSamplingHeapProfilerInterval(uint64_t sample_interval) { + if (sampling_heap_profiler_) { + sampling_heap_profiler_->SetSamplingInterval(sample_interval); + } +} + +std::vector +HeapProfiler::GetSamplingHeapProfilerSamples() { + if (!sampling_heap_profiler_) return {}; + return sampling_heap_profiler_->GetSamples(); +} + v8::AllocationProfile* HeapProfiler::GetAllocationProfile() { if (sampling_heap_profiler_) { return sampling_heap_profiler_->GetAllocationProfile(); diff --git a/deps/v8/src/profiler/heap-profiler.h b/deps/v8/src/profiler/heap-profiler.h index 9f82946bb317..031f32762fac 100644 --- a/deps/v8/src/profiler/heap-profiler.h +++ b/deps/v8/src/profiler/heap-profiler.h @@ -83,6 +83,8 @@ class HeapProfiler : public HeapObjectAllocationTracker { bool StartSamplingHeapProfiler(uint64_t sample_interval, int stack_depth, v8::HeapProfiler::SamplingFlags); void StopSamplingHeapProfiler(); + void SetSamplingHeapProfilerInterval(uint64_t sample_interval); + std::vector GetSamplingHeapProfilerSamples(); bool is_sampling_allocations() { return !!sampling_heap_profiler_; } AllocationProfile* GetAllocationProfile(); diff --git a/deps/v8/src/profiler/sampling-heap-profiler.cc b/deps/v8/src/profiler/sampling-heap-profiler.cc index 7ea26d4064f8..203854adcc22 100644 --- a/deps/v8/src/profiler/sampling-heap-profiler.cc +++ b/deps/v8/src/profiler/sampling-heap-profiler.cc @@ -41,13 +41,14 @@ intptr_t SamplingHeapProfiler::Observer::GetNextSampleInterval(uint64_t rate) { // Samples were collected according to a poisson process. Since we have not // recorded all allocations, we must approximate the shape of the underlying // space of allocations based on the samples we have collected. Given that -// we sample at rate R, the probability that an allocation of size S will be -// sampled is 1-exp(-S/R). This function uses the above probability to +// we sample at interval I, the probability that an allocation of size S will +// be sampled is 1-exp(-S/I). This function uses the above probability to // approximate the true number of allocations with size *size* given that // *count* samples were observed. v8::AllocationProfile::Allocation SamplingHeapProfiler::ScaleSample( - size_t size, unsigned int count) const { - double scale = 1.0 / (1.0 - std::exp(-static_cast(size) / rate_)); + size_t size, unsigned int count, uint64_t sample_interval) const { + double scale = + 1.0 / (1.0 - std::exp(-static_cast(size) / sample_interval)); // Round count instead of truncating. return {size, static_cast(count * scale + 0.5)}; } @@ -57,15 +58,15 @@ SamplingHeapProfiler::SamplingHeapProfiler( v8::HeapProfiler::SamplingFlags flags) : isolate_(Isolate::FromHeap(heap)), heap_(heap), - allocation_observer_(heap_, static_cast(rate), rate, this, + allocation_observer_(heap_, static_cast(rate), this, isolate_->random_number_generator()), names_(names), profile_root_(nullptr, "(root)", v8::UnboundScript::kNoScriptId, 0, next_node_id()), stack_depth_(stack_depth), - rate_(rate), + interval_(rate), flags_(flags) { - CHECK_GT(rate_, 0u); + CHECK_GT(interval_.load(std::memory_order_relaxed), 0u); heap_->AddAllocationObserversToAllSpaces(&allocation_observer_, &allocation_observer_); } @@ -94,10 +95,14 @@ void SamplingHeapProfiler::SampleObject(Address soon_object, size_t size) { TrustedHeapLayout::InTrustedSpace(heap_object) || !IsTheHole(*obj))); auto loc = Local::FromSlot(obj.location()); + // Record the current sampling interval so that this sample is scaled + // with the exact weight in effect at draw time. + const uint64_t interval_at_sample = + interval_.load(std::memory_order_relaxed); AllocationNode* node = AddStack(); - node->allocations_[size]++; - auto sample = - std::make_unique(size, node, loc, this, next_sample_id()); + node->allocations_[{size, interval_at_sample}]++; + auto sample = std::make_unique(size, node, loc, this, + next_sample_id(), interval_at_sample); sample->global.SetWeak(sample.get(), OnWeakCallback, WeakCallbackType::kParameter); samples_.emplace(sample.get(), std::move(sample)); @@ -120,10 +125,11 @@ void SamplingHeapProfiler::OnWeakCallback( return; } AllocationNode* node = sample->owner; - DCHECK_GT(node->allocations_[sample->size], 0); - node->allocations_[sample->size]--; - if (node->allocations_[sample->size] == 0) { - node->allocations_.erase(sample->size); + const auto key = std::make_pair(sample->size, sample->sample_interval); + DCHECK_GT(node->allocations_[key], 0u); + node->allocations_[key]--; + if (node->allocations_[key] == 0) { + node->allocations_.erase(key); while (node->allocations_.empty() && node->children_.empty() && node->parent_ && !node->parent_->pinned_) { AllocationNode* parent = node->parent_; @@ -263,8 +269,15 @@ v8::AllocationProfile::Node* SamplingHeapProfiler::TranslateAllocationNode( column = pos_info.column + 1; } } - for (auto alloc : node->allocations_) { - allocations.push_back(ScaleSample(alloc.first, alloc.second)); + // Entries are keyed by (size, interval). Scale each with its own interval, + // then merge by size for the public per-size Allocation output. + std::map per_size; + for (const auto& [key, count] : node->allocations_) { + const auto& [size, interval] = key; + per_size[size] += ScaleSample(size, count, interval).count; + } + for (const auto& [size, count] : per_size) { + allocations.push_back({size, count}); } profile->nodes_.push_back(v8::AllocationProfile::Node{ @@ -315,11 +328,25 @@ SamplingHeapProfiler::BuildSamples() const { const Sample* sample = it.second.get(); const bool is_live = !sample->global.IsEmpty(); samples.emplace_back(v8::AllocationProfile::Sample{ - sample->owner->id_, sample->size, ScaleSample(sample->size, 1).count, - sample->sample_id, is_live}); + sample->owner->id_, sample->size, + ScaleSample(sample->size, 1, sample->sample_interval).count, + sample->sample_id, is_live, sample->sample_interval}); } return samples; } +std::vector SamplingHeapProfiler::GetSamples() { + if (flags_ & v8::HeapProfiler::kSamplingForceGC) { + isolate_->heap()->CollectAllGarbage( + GCFlag::kNoFlags, GarbageCollectionReason::kSamplingProfiler); + } + return BuildSamples(); +} + +void SamplingHeapProfiler::SetSamplingInterval(uint64_t sample_interval) { + CHECK_GT(sample_interval, 0u); + interval_.store(sample_interval, std::memory_order_relaxed); +} + } // namespace internal } // namespace v8 diff --git a/deps/v8/src/profiler/sampling-heap-profiler.h b/deps/v8/src/profiler/sampling-heap-profiler.h index 6a1010b99931..6a4f7d861928 100644 --- a/deps/v8/src/profiler/sampling-heap-profiler.h +++ b/deps/v8/src/profiler/sampling-heap-profiler.h @@ -5,10 +5,12 @@ #ifndef V8_PROFILER_SAMPLING_HEAP_PROFILER_H_ #define V8_PROFILER_SAMPLING_HEAP_PROFILER_H_ +#include #include #include #include #include +#include #include "include/v8-profiler.h" #include "src/heap/heap.h" @@ -85,9 +87,12 @@ class SamplingHeapProfiler { } private: + // Keyed by (allocation size, sampling interval at draw time) so that + // samples drawn under different intervals are scaled with the correct + // weight when the tree is translated. // TODO(alph): make use of unordered_map's here. Pay attention to // iterator invalidation during TranslateAllocationNode. - std::map allocations_; + std::map, unsigned int> allocations_; std::map> children_; AllocationNode* const parent_; const int script_id_; @@ -101,12 +106,14 @@ class SamplingHeapProfiler { struct Sample { Sample(size_t size_, AllocationNode* owner_, Local local_, - SamplingHeapProfiler* profiler_, uint64_t sample_id) + SamplingHeapProfiler* profiler_, uint64_t sample_id_, + uint64_t sample_interval_) : size(size_), owner(owner_), global(reinterpret_cast(profiler_->isolate_), local_), profiler(profiler_), - sample_id(sample_id) {} + sample_id(sample_id_), + sample_interval(sample_interval_) {} Sample(const Sample&) = delete; Sample& operator=(const Sample&) = delete; const size_t size; @@ -114,6 +121,7 @@ class SamplingHeapProfiler { Global global; SamplingHeapProfiler* const profiler; const uint64_t sample_id; + const uint64_t sample_interval; }; SamplingHeapProfiler(Heap* heap, StringsStorage* names, uint64_t rate, @@ -123,19 +131,21 @@ class SamplingHeapProfiler { SamplingHeapProfiler& operator=(const SamplingHeapProfiler&) = delete; v8::AllocationProfile* GetAllocationProfile(); + + void SetSamplingInterval(uint64_t sample_interval); + std::vector GetSamples(); + StringsStorage* names() const { return names_; } private: class Observer : public AllocationObserver { public: - Observer(Heap* heap, intptr_t step_size, uint64_t rate, - SamplingHeapProfiler* profiler, + Observer(Heap* heap, intptr_t step_size, SamplingHeapProfiler* profiler, base::RandomNumberGenerator* random) : AllocationObserver(step_size), profiler_(profiler), heap_(heap), - random_(random), - rate_(rate) {} + random_(random) {} protected: void Step(int bytes_allocated, Address soon_object, size_t size) override { @@ -148,14 +158,16 @@ class SamplingHeapProfiler { } } - intptr_t GetNextStepSize() override { return GetNextSampleInterval(rate_); } + intptr_t GetNextStepSize() override { + return GetNextSampleInterval( + profiler_->interval_.load(std::memory_order_relaxed)); + } private: intptr_t GetNextSampleInterval(uint64_t rate); SamplingHeapProfiler* const profiler_; Heap* const heap_; base::RandomNumberGenerator* const random_; - uint64_t const rate_; }; void SampleObject(Address soon_object, size_t size); @@ -179,8 +191,8 @@ class SamplingHeapProfiler { v8::AllocationProfile::Node* TranslateAllocationNode( AllocationProfile* profile, SamplingHeapProfiler::AllocationNode* node, const std::map>& scripts); - v8::AllocationProfile::Allocation ScaleSample(size_t size, - unsigned int count) const; + v8::AllocationProfile::Allocation ScaleSample(size_t size, unsigned int count, + uint64_t sample_interval) const; AllocationNode* AddStack(); Isolate* const isolate_; @@ -192,7 +204,7 @@ class SamplingHeapProfiler { AllocationNode profile_root_; std::unordered_map> samples_; const int stack_depth_; - const uint64_t rate_; + std::atomic interval_; v8::HeapProfiler::SamplingFlags flags_; }; diff --git a/deps/v8/test/cctest/test-heap-profiler.cc b/deps/v8/test/cctest/test-heap-profiler.cc index 06a6d8e45c17..17c859e1f67d 100644 --- a/deps/v8/test/cctest/test-heap-profiler.cc +++ b/deps/v8/test/cctest/test-heap-profiler.cc @@ -4270,6 +4270,85 @@ TEST(SamplingHeapProfilerApiSamples) { heap_profiler->StopSamplingHeapProfiler(); } +TEST(SamplingHeapProfilerSetInterval) { + v8::HandleScope scope(CcTest::isolate()); + LocalContext env; + v8::HeapProfiler* heap_profiler = env.isolate()->GetHeapProfiler(); + + i::v8_flags.sampling_heap_profiler_suppress_randomness = true; + + const uint64_t initial_interval = 1024; + const uint64_t updated_interval = 4096; + heap_profiler->StartSamplingHeapProfiler(initial_interval); + + CompileRun("for (var i = 0; i < 1024; i++) new Array(64);"); + auto samples_before = heap_profiler->GetSamplingHeapProfilerSamples(); + CHECK_GT(samples_before.size(), 0u); + uint64_t max_id_before = 0; + for (const auto& s : samples_before) { + CHECK_EQ(initial_interval, s.sample_interval); + if (s.sample_id > max_id_before) max_id_before = s.sample_id; + } + + heap_profiler->SetSamplingHeapProfilerInterval(updated_interval); + CompileRun("for (var i = 0; i < 1024; i++) new Array(64);"); + + bool saw_new_sample = false; + for (const auto& s : heap_profiler->GetSamplingHeapProfilerSamples()) { + if (s.sample_id > max_id_before) { + CHECK_EQ(updated_interval, s.sample_interval); + saw_new_sample = true; + } else { + CHECK_EQ(initial_interval, s.sample_interval); + } + } + CHECK(saw_new_sample); + + heap_profiler->StopSamplingHeapProfiler(); +} + +TEST(SamplingHeapProfilerSampleIntervalSurvivesGC) { + v8::HandleScope scope(CcTest::isolate()); + LocalContext env; + v8::HeapProfiler* heap_profiler = env.isolate()->GetHeapProfiler(); + + i::v8_flags.sampling_heap_profiler_suppress_randomness = true; + + const uint64_t interval = 1024; + heap_profiler->StartSamplingHeapProfiler( + interval, 16, + static_cast( + v8::HeapProfiler::kSamplingIncludeObjectsCollectedByMajorGC)); + + CompileRun("for (var i = 0; i < 1024; i++) new Array(64);"); + i::heap::InvokeMajorGC(CcTest::heap()); + + bool saw_dead = false; + for (const auto& s : heap_profiler->GetSamplingHeapProfilerSamples()) { + CHECK_EQ(interval, s.sample_interval); + if (!s.is_live) saw_dead = true; + } + CHECK(saw_dead); + + heap_profiler->StopSamplingHeapProfiler(); +} + +TEST(SamplingHeapProfilerSetIntervalNoop) { + v8::HandleScope scope(CcTest::isolate()); + LocalContext env; + v8::HeapProfiler* heap_profiler = env.isolate()->GetHeapProfiler(); + + // Safe before Start. + heap_profiler->SetSamplingHeapProfilerInterval(2048); + CHECK_EQ(0u, heap_profiler->GetSamplingHeapProfilerSamples().size()); + + // Safe after Stop. + heap_profiler->StartSamplingHeapProfiler(1024); + heap_profiler->StopSamplingHeapProfiler(); + heap_profiler->SetSamplingHeapProfilerInterval(2048); + CHECK_EQ(0u, heap_profiler->GetSamplingHeapProfilerSamples().size()); +} + TEST(SamplingHeapProfilerLeftTrimming) { v8::HandleScope scope(CcTest::isolate()); LocalContext env;