1 // Copyright 2018 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "base/sampling_heap_profiler/sampling_heap_profiler.h"
6
7 #include <stdlib.h>
8 #include <cinttypes>
9
10 #include "base/allocator/dispatcher/dispatcher.h"
11 #include "base/allocator/dispatcher/notification_data.h"
12 #include "base/allocator/dispatcher/subsystem.h"
13 #include "base/debug/alias.h"
14 #include "base/memory/raw_ptr.h"
15 #include "base/rand_util.h"
16 #include "base/sampling_heap_profiler/poisson_allocation_sampler.h"
17 #include "base/synchronization/waitable_event.h"
18 #include "base/threading/simple_thread.h"
19 #include "build/build_config.h"
20 #include "partition_alloc/shim/allocator_shim.h"
21 #include "testing/gtest/include/gtest/gtest.h"
22
23 namespace base {
24
25 using ScopedSuppressRandomnessForTesting =
26 PoissonAllocationSampler::ScopedSuppressRandomnessForTesting;
27 using base::allocator::dispatcher::AllocationNotificationData;
28 using base::allocator::dispatcher::AllocationSubsystem;
29 using base::allocator::dispatcher::FreeNotificationData;
30
31 class SamplingHeapProfilerTest : public ::testing::Test {
32 public:
SetUp()33 void SetUp() override {
34 #if BUILDFLAG(IS_APPLE)
35 allocator_shim::InitializeAllocatorShim();
36 #endif
37 SamplingHeapProfiler::Init();
38
39 // Ensure the PoissonAllocationSampler starts in the default state.
40 ASSERT_FALSE(PoissonAllocationSampler::AreHookedSamplesMuted());
41 ASSERT_FALSE(PoissonAllocationSampler::ScopedMuteThreadSamples::IsMuted());
42 ASSERT_FALSE(ScopedSuppressRandomnessForTesting::IsSuppressed());
43
44 allocator::dispatcher::Dispatcher::GetInstance().InitializeForTesting(
45 PoissonAllocationSampler::Get());
46 }
47
TearDown()48 void TearDown() override {
49 allocator::dispatcher::Dispatcher::GetInstance().ResetForTesting();
50 }
51
GetNextSample(size_t mean_interval)52 size_t GetNextSample(size_t mean_interval) {
53 return PoissonAllocationSampler::GetNextSampleInterval(mean_interval);
54 }
55
GetRunningSessionsCount()56 static int GetRunningSessionsCount() {
57 SamplingHeapProfiler* p = SamplingHeapProfiler::Get();
58 AutoLock lock(p->start_stop_mutex_);
59 return p->running_sessions_;
60 }
61
RunStartStopLoop(SamplingHeapProfiler * profiler)62 static void RunStartStopLoop(SamplingHeapProfiler* profiler) {
63 for (int i = 0; i < 100000; ++i) {
64 profiler->Start();
65 EXPECT_LE(1, GetRunningSessionsCount());
66 profiler->Stop();
67 }
68 }
69 };
70
71 class SamplesCollector : public PoissonAllocationSampler::SamplesObserver {
72 public:
SamplesCollector(size_t watch_size)73 explicit SamplesCollector(size_t watch_size) : watch_size_(watch_size) {}
74
SampleAdded(void * address,size_t size,size_t,AllocationSubsystem,const char *)75 void SampleAdded(void* address,
76 size_t size,
77 size_t,
78 AllocationSubsystem,
79 const char*) override {
80 if (sample_added || size != watch_size_)
81 return;
82 sample_address_ = address;
83 sample_added = true;
84 }
85
SampleRemoved(void * address)86 void SampleRemoved(void* address) override {
87 if (address == sample_address_)
88 sample_removed = true;
89 }
90
91 bool sample_added = false;
92 bool sample_removed = false;
93
94 private:
95 size_t watch_size_;
96 raw_ptr<void, DanglingUntriaged> sample_address_ = nullptr;
97 };
98
TEST_F(SamplingHeapProfilerTest,SampleObserver)99 TEST_F(SamplingHeapProfilerTest, SampleObserver) {
100 ScopedSuppressRandomnessForTesting suppress;
101 SamplesCollector collector(10000);
102 auto* sampler = PoissonAllocationSampler::Get();
103 sampler->SetSamplingInterval(1024);
104 sampler->AddSamplesObserver(&collector);
105 void* volatile p = malloc(10000);
106 free(p);
107 sampler->RemoveSamplesObserver(&collector);
108 EXPECT_TRUE(collector.sample_added);
109 EXPECT_TRUE(collector.sample_removed);
110 }
111
TEST_F(SamplingHeapProfilerTest,SampleObserverMuted)112 TEST_F(SamplingHeapProfilerTest, SampleObserverMuted) {
113 ScopedSuppressRandomnessForTesting suppress;
114 SamplesCollector collector(10000);
115 auto* sampler = PoissonAllocationSampler::Get();
116 sampler->SetSamplingInterval(1024);
117 sampler->AddSamplesObserver(&collector);
118 {
119 PoissonAllocationSampler::ScopedMuteThreadSamples muted_scope;
120 void* volatile p = malloc(10000);
121 free(p);
122 }
123 sampler->RemoveSamplesObserver(&collector);
124 EXPECT_FALSE(collector.sample_added);
125 EXPECT_FALSE(collector.sample_removed);
126 }
127
TEST_F(SamplingHeapProfilerTest,IntervalRandomizationSanity)128 TEST_F(SamplingHeapProfilerTest, IntervalRandomizationSanity) {
129 ASSERT_FALSE(ScopedSuppressRandomnessForTesting::IsSuppressed());
130 constexpr int iterations = 50;
131 constexpr size_t target = 10000000;
132 int sum = 0;
133 for (int i = 0; i < iterations; ++i) {
134 int samples = 0;
135 for (size_t value = 0; value < target; value += GetNextSample(10000))
136 ++samples;
137 // There are should be ~ target/10000 = 1000 samples.
138 sum += samples;
139 }
140 int mean_samples = sum / iterations;
141 EXPECT_NEAR(1000, mean_samples, 100); // 10% tolerance.
142 }
143
144 #if BUILDFLAG(IS_IOS)
145 // iOS devices generally have ~4GB of RAM with no swap and therefore need a
146 // lower allocation limit here.
147 const int kNumberOfAllocations = 1000;
148 #else
149 const int kNumberOfAllocations = 10000;
150 #endif
151
Allocate1()152 NOINLINE void Allocate1() {
153 void* p = malloc(400);
154 base::debug::Alias(&p);
155 }
156
Allocate2()157 NOINLINE void Allocate2() {
158 void* p = malloc(700);
159 base::debug::Alias(&p);
160 }
161
Allocate3()162 NOINLINE void Allocate3() {
163 void* p = malloc(20480);
164 base::debug::Alias(&p);
165 }
166
167 class MyThread1 : public SimpleThread {
168 public:
MyThread1()169 MyThread1() : SimpleThread("MyThread1") {}
Run()170 void Run() override {
171 for (int i = 0; i < kNumberOfAllocations; ++i)
172 Allocate1();
173 }
174 };
175
176 class MyThread2 : public SimpleThread {
177 public:
MyThread2()178 MyThread2() : SimpleThread("MyThread2") {}
Run()179 void Run() override {
180 for (int i = 0; i < kNumberOfAllocations; ++i)
181 Allocate2();
182 }
183 };
184
CheckAllocationPattern(void (* allocate_callback)())185 void CheckAllocationPattern(void (*allocate_callback)()) {
186 ASSERT_FALSE(ScopedSuppressRandomnessForTesting::IsSuppressed());
187 auto* profiler = SamplingHeapProfiler::Get();
188 profiler->SetSamplingInterval(10240);
189 base::TimeTicks t0 = base::TimeTicks::Now();
190 std::map<size_t, size_t> sums;
191 const int iterations = 40;
192 for (int i = 0; i < iterations; ++i) {
193 uint32_t id = profiler->Start();
194 allocate_callback();
195 std::vector<SamplingHeapProfiler::Sample> samples =
196 profiler->GetSamples(id);
197 profiler->Stop();
198 std::map<size_t, size_t> buckets;
199 for (auto& sample : samples) {
200 buckets[sample.size] += sample.total;
201 }
202 for (auto& it : buckets) {
203 if (it.first != 400 && it.first != 700 && it.first != 20480)
204 continue;
205 sums[it.first] += it.second;
206 printf("%zu,", it.second);
207 }
208 printf("\n");
209 }
210
211 printf("Time taken %" PRIu64 "ms\n",
212 (base::TimeTicks::Now() - t0).InMilliseconds());
213
214 for (auto sum : sums) {
215 intptr_t expected = sum.first * kNumberOfAllocations;
216 intptr_t actual = sum.second / iterations;
217 printf("%zu:\tmean: %zu\trelative error: %.2f%%\n", sum.first, actual,
218 100. * (actual - expected) / expected);
219 }
220 }
221
222 // Manual tests to check precision of the sampling profiler.
223 // Yes, they do leak lots of memory.
224
TEST_F(SamplingHeapProfilerTest,DISABLED_ParallelLargeSmallStats)225 TEST_F(SamplingHeapProfilerTest, DISABLED_ParallelLargeSmallStats) {
226 CheckAllocationPattern([] {
227 MyThread1 t1;
228 MyThread1 t2;
229 t1.Start();
230 t2.Start();
231 for (int i = 0; i < kNumberOfAllocations; ++i)
232 Allocate3();
233 t1.Join();
234 t2.Join();
235 });
236 }
237
TEST_F(SamplingHeapProfilerTest,DISABLED_SequentialLargeSmallStats)238 TEST_F(SamplingHeapProfilerTest, DISABLED_SequentialLargeSmallStats) {
239 CheckAllocationPattern([] {
240 for (int i = 0; i < kNumberOfAllocations; ++i) {
241 Allocate1();
242 Allocate2();
243 Allocate3();
244 }
245 });
246 }
247
248 // Platform TLS: alloc+free[ns]: 22.184 alloc[ns]: 8.910 free[ns]: 13.274
249 // thread_local: alloc+free[ns]: 18.353 alloc[ns]: 5.021 free[ns]: 13.331
250 // TODO(crbug.com/40145097) Disabled on Mac
251 #if BUILDFLAG(IS_MAC)
252 #define MAYBE_MANUAL_SamplerMicroBenchmark DISABLED_MANUAL_SamplerMicroBenchmark
253 #else
254 #define MAYBE_MANUAL_SamplerMicroBenchmark MANUAL_SamplerMicroBenchmark
255 #endif
TEST_F(SamplingHeapProfilerTest,MAYBE_MANUAL_SamplerMicroBenchmark)256 TEST_F(SamplingHeapProfilerTest, MAYBE_MANUAL_SamplerMicroBenchmark) {
257 // With the sampling interval of 100KB it happens to record ~ every 450th
258 // allocation in the browser process. We model this pattern here.
259 constexpr size_t sampling_interval = 100000;
260 constexpr size_t allocation_size = sampling_interval / 450;
261 SamplesCollector collector(0);
262 auto* sampler = PoissonAllocationSampler::Get();
263 sampler->SetSamplingInterval(sampling_interval);
264 sampler->AddSamplesObserver(&collector);
265 int kNumAllocations = 50000000;
266
267 base::TimeTicks t0 = base::TimeTicks::Now();
268 for (int i = 1; i <= kNumAllocations; ++i) {
269 sampler->OnAllocation(AllocationNotificationData(
270 reinterpret_cast<void*>(static_cast<intptr_t>(i)), allocation_size,
271 nullptr, AllocationSubsystem::kAllocatorShim));
272 }
273 base::TimeTicks t1 = base::TimeTicks::Now();
274 for (int i = 1; i <= kNumAllocations; ++i)
275 sampler->OnFree(
276 FreeNotificationData(reinterpret_cast<void*>(static_cast<intptr_t>(i)),
277 AllocationSubsystem::kAllocatorShim));
278 base::TimeTicks t2 = base::TimeTicks::Now();
279
280 printf(
281 "alloc+free[ns]: %.3f alloc[ns]: %.3f free[ns]: %.3f "
282 "alloc+free[mln/s]: %.1f total[ms]: %.1f\n",
283 (t2 - t0).InNanoseconds() * 1. / kNumAllocations,
284 (t1 - t0).InNanoseconds() * 1. / kNumAllocations,
285 (t2 - t1).InNanoseconds() * 1. / kNumAllocations,
286 kNumAllocations / (t2 - t0).InMicrosecondsF(),
287 (t2 - t0).InMillisecondsF());
288
289 sampler->RemoveSamplesObserver(&collector);
290 }
291
292 class StartStopThread : public SimpleThread {
293 public:
StartStopThread(WaitableEvent * event)294 StartStopThread(WaitableEvent* event)
295 : SimpleThread("MyThread2"), event_(event) {}
Run()296 void Run() override {
297 auto* profiler = SamplingHeapProfiler::Get();
298 event_->Signal();
299 SamplingHeapProfilerTest::RunStartStopLoop(profiler);
300 }
301
302 private:
303 raw_ptr<WaitableEvent> event_;
304 };
305
TEST_F(SamplingHeapProfilerTest,StartStop)306 TEST_F(SamplingHeapProfilerTest, StartStop) {
307 auto* profiler = SamplingHeapProfiler::Get();
308 EXPECT_EQ(0, GetRunningSessionsCount());
309 profiler->Start();
310 EXPECT_EQ(1, GetRunningSessionsCount());
311 profiler->Start();
312 EXPECT_EQ(2, GetRunningSessionsCount());
313 profiler->Stop();
314 EXPECT_EQ(1, GetRunningSessionsCount());
315 profiler->Stop();
316 EXPECT_EQ(0, GetRunningSessionsCount());
317 }
318
319 // TODO(crbug.com/40711998): Test is crashing on Mac.
320 #if BUILDFLAG(IS_MAC)
321 #define MAYBE_ConcurrentStartStop DISABLED_ConcurrentStartStop
322 #else
323 #define MAYBE_ConcurrentStartStop ConcurrentStartStop
324 #endif
TEST_F(SamplingHeapProfilerTest,MAYBE_ConcurrentStartStop)325 TEST_F(SamplingHeapProfilerTest, MAYBE_ConcurrentStartStop) {
326 auto* profiler = SamplingHeapProfiler::Get();
327 WaitableEvent event;
328 StartStopThread thread(&event);
329 thread.Start();
330 event.Wait();
331 RunStartStopLoop(profiler);
332 thread.Join();
333 EXPECT_EQ(0, GetRunningSessionsCount());
334 }
335
TEST_F(SamplingHeapProfilerTest,HookedAllocatorMuted)336 TEST_F(SamplingHeapProfilerTest, HookedAllocatorMuted) {
337 ScopedSuppressRandomnessForTesting suppress;
338 EXPECT_FALSE(PoissonAllocationSampler::AreHookedSamplesMuted());
339
340 auto* sampler = PoissonAllocationSampler::Get();
341 sampler->SetSamplingInterval(1024);
342
343 {
344 PoissonAllocationSampler::ScopedMuteHookedSamplesForTesting mute_hooks;
345 EXPECT_TRUE(PoissonAllocationSampler::AreHookedSamplesMuted());
346
347 SamplesCollector collector(10000);
348
349 // A ScopedMuteHookedSamplesForTesting exists so hooked allocations should
350 // be ignored.
351 sampler->AddSamplesObserver(&collector);
352 void* volatile p = malloc(10000);
353 free(p);
354 sampler->RemoveSamplesObserver(&collector);
355 EXPECT_FALSE(collector.sample_added);
356 EXPECT_FALSE(collector.sample_removed);
357
358 // Manual allocations should be captured.
359 sampler->AddSamplesObserver(&collector);
360 void* const kAddress = reinterpret_cast<void*>(0x1234);
361 sampler->OnAllocation(AllocationNotificationData(
362 kAddress, 10000, nullptr, AllocationSubsystem::kManualForTesting));
363 sampler->OnFree(
364 FreeNotificationData(kAddress, AllocationSubsystem::kManualForTesting));
365 sampler->RemoveSamplesObserver(&collector);
366 EXPECT_TRUE(collector.sample_added);
367 EXPECT_TRUE(collector.sample_removed);
368 }
369
370 EXPECT_FALSE(PoissonAllocationSampler::AreHookedSamplesMuted());
371
372 // Hooked allocations should be captured again.
373 SamplesCollector collector(10000);
374 sampler->AddSamplesObserver(&collector);
375 void* volatile p = malloc(10000);
376 free(p);
377 sampler->RemoveSamplesObserver(&collector);
378 EXPECT_TRUE(collector.sample_added);
379 EXPECT_TRUE(collector.sample_removed);
380 }
381
382 } // namespace base
383