• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2023 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/metrics/statistics_recorder.h"
6 
7 #include <atomic>
8 
9 #include "base/containers/span.h"
10 #include "base/logging.h"
11 #include "base/metrics/histogram_functions.h"
12 #include "base/strings/strcat.h"
13 #include "base/strings/string_number_conversions.h"
14 #include "base/threading/platform_thread.h"
15 #include "base/threading/simple_thread.h"
16 #include "base/time/time.h"
17 #include "testing/gtest/include/gtest/gtest.h"
18 
19 namespace base {
20 
21 namespace {
22 
23 static constexpr TimeDelta kTestRunningTime = Seconds(5);
24 
25 class BaseThread : public SimpleThread {
26  public:
BaseThread(const std::string & thread_name)27   explicit BaseThread(const std::string& thread_name)
28       : SimpleThread(thread_name, Options()), thread_name_(thread_name) {}
29 
30   BaseThread(const BaseThread&) = delete;
31   BaseThread& operator=(const BaseThread&) = delete;
32 
33   ~BaseThread() override = default;
34 
thread_name()35   const std::string& thread_name() { return thread_name_; }
SetStartTime()36   void SetStartTime() { start_time_ = Time::Now(); }
ShouldStop()37   bool ShouldStop() { return stop_.load(std::memory_order_relaxed); }
Stop()38   void Stop() {
39     stop_.store(true, std::memory_order_relaxed);
40     end_time_ = Time::Now();
41   }
IncrementIterCount()42   void IncrementIterCount() { ++iter_count_; }
iter_count()43   size_t iter_count() { return iter_count_; }
running_time()44   TimeDelta running_time() { return end_time_ - start_time_; }
average_time_per_iter()45   TimeDelta average_time_per_iter() { return running_time() / iter_count_; }
46 
47  private:
48   std::string thread_name_;
49   std::atomic<bool> stop_ = false;
50   size_t iter_count_ = 0;
51   Time start_time_;
52   Time end_time_;
53 };
54 
55 class ReadThread : public BaseThread {
56  public:
ReadThread(size_t id)57   explicit ReadThread(size_t id)
58       : BaseThread(/*thread_name=*/StrCat({"ReadThread", NumberToString(id)})) {
59   }
60 
Run()61   void Run() override {
62     SetStartTime();
63 
64     while (!ShouldStop()) {
65       // Continuously emit to the same histogram. Because this histogram should
66       // already exist within the StatisticsRecorder's internal map (except the
67       // first time this is called), it should not cause any modifications to
68       // it, only a lookup. In other words, it will call
69       // StatisticsRecorder::FindHistogram().
70       static constexpr char kReadThreadHistogramName[] = "ReadThreadHistogram";
71       UmaHistogramBoolean(kReadThreadHistogramName, false);
72       IncrementIterCount();
73     }
74   }
75 };
76 
77 class WriteThread : public BaseThread {
78  public:
WriteThread(size_t id)79   explicit WriteThread(size_t id)
80       : BaseThread(
81             /*thread_name=*/StrCat({"WriteThread", NumberToString(id)})) {}
82 
Run()83   void Run() override {
84     SetStartTime();
85 
86     const std::string base_name = StrCat({thread_name(), ".Iteration"});
87     while (!ShouldStop()) {
88       // Continuously emit to a new histogram. Because this histogram should not
89       // exist within the StatisticsRecorder's internal map, it will cause an
90       // insertion to it every time. In other words, it will call
91       // StatisticsRecorder::RegisterOrDeleteDuplicate().
92       UmaHistogramBoolean(StrCat({base_name, NumberToString(iter_count())}),
93                           false);
94       IncrementIterCount();
95     }
96   }
97 };
98 
99 }  // namespace
100 
101 // Determines the number of reader and writer threads to run in the test.
102 struct StarvationTestParams {
103   size_t num_read_threads;
104   size_t num_write_threads;
105 };
106 
107 // Determines which threads should start running first.
108 enum class FirstThreadsToStart {
109   kReaders,
110   kWriters,
111 };
112 
113 class StatisticsRecorderStarvationTest
114     : public testing::TestWithParam<
115           std::tuple<StarvationTestParams, FirstThreadsToStart>> {
116  public:
117   StatisticsRecorderStarvationTest() = default;
118 
119   StatisticsRecorderStarvationTest(const StatisticsRecorderStarvationTest&) =
120       delete;
121   StatisticsRecorderStarvationTest& operator=(
122       const StatisticsRecorderStarvationTest&) = delete;
123 
124   ~StatisticsRecorderStarvationTest() override = default;
125 
SetUp()126   void SetUp() override {
127     // Create a new StatisticsRecorder so that this test run will not affect
128     // future ones. In particular, this test relies on creating new histograms
129     // and adding them to the SR.
130     sr_ = StatisticsRecorder::CreateTemporaryForTesting();
131 
132     // Emit a bunch of histograms, which will add them to the SR's internal
133     // histogram map. This is so that lookups and insertions (FindHistogram()
134     // and RegisterOrDeleteDuplicate()) during the test don't complete (pretty
135     // much) instantly.
136     for (size_t i = 0; i < 10000; ++i) {
137       UmaHistogramBoolean(StrCat({"Dummy", NumberToString(i)}), false);
138     }
139   }
140 
141   // Starts reader and writer threads.
StartThreads()142   void StartThreads() {
143     for (size_t i = 0; i < num_read_threads(); ++i) {
144       read_threads_.emplace_back(std::make_unique<ReadThread>(/*id=*/i));
145     }
146     for (size_t i = 0; i < num_write_threads(); ++i) {
147       write_threads_.emplace_back(std::make_unique<WriteThread>(/*id=*/i));
148     }
149 
150     // Depending on the value of GetFirstThreadsToStart(), either start the
151     // readers or the writers first. Because some implementations will give
152     // priority to whatever managed to get the lock first, do this to have
153     // coverage.
154     span<std::unique_ptr<BaseThread>> start_first_threads;
155     span<std::unique_ptr<BaseThread>> start_second_threads;
156     switch (GetFirstThreadsToStart()) {
157       case FirstThreadsToStart::kReaders:
158         start_first_threads = read_threads_;
159         start_second_threads = write_threads_;
160         break;
161       case FirstThreadsToStart::kWriters:
162         start_first_threads = write_threads_;
163         start_second_threads = read_threads_;
164         break;
165     }
166     for (auto& thread : start_first_threads) {
167       thread->Start();
168     }
169     PlatformThread::Sleep(Milliseconds(100));
170     for (auto& thread : start_second_threads) {
171       thread->Start();
172     }
173   }
174 
175   // Stops reader and writer threads.
StopThreads()176   void StopThreads() {
177     for (auto* thread : GetAllThreads()) {
178       thread->Stop();
179       thread->Join();
180     }
181   }
182 
GetAllThreads()183   std::vector<BaseThread*> GetAllThreads() {
184     std::vector<BaseThread*> threads;
185     threads.reserve(num_read_threads() + num_write_threads());
186     for (auto& read_thread : read_threads_) {
187       threads.push_back(read_thread.get());
188     }
189     for (auto& write_thread : write_threads_) {
190       threads.push_back(write_thread.get());
191     }
192     return threads;
193   }
194 
num_read_threads()195   size_t num_read_threads() { return std::get<0>(GetParam()).num_read_threads; }
196 
num_write_threads()197   size_t num_write_threads() {
198     return std::get<0>(GetParam()).num_write_threads;
199   }
200 
GetFirstThreadsToStart()201   FirstThreadsToStart GetFirstThreadsToStart() {
202     return std::get<1>(GetParam());
203   }
204 
205  protected:
206   std::vector<std::unique_ptr<BaseThread>> read_threads_;
207   std::vector<std::unique_ptr<BaseThread>> write_threads_;
208 
209  private:
210   std::unique_ptr<StatisticsRecorder> sr_;
211 };
212 
213 // Verifies that there are no starvation issues when emitting histograms (since
214 // it may be done from any thread). In particular, emitting a histogram requires
215 // a lock to look up (and sometimes write to) an internal map in the
216 // StatisticsRecorder. When switching to a Read/Write lock (see crbug/1123627),
217 // we encountered such a starvation issue, where a thread trying to write to the
218 // internal map was starved out for 10+ seconds by readers on iOS.
TEST_P(StatisticsRecorderStarvationTest,StatisticsRecorderNoStarvation)219 TEST_P(StatisticsRecorderStarvationTest, StatisticsRecorderNoStarvation) {
220   // Start reader and writer threads.
221   StartThreads();
222 
223   // Let the test run for |kTestRunningTime|.
224   PlatformThread::Sleep(kTestRunningTime);
225 
226   // Stop reader and writer threads. This waits for them to complete by joining
227   // the threads.
228   StopThreads();
229 
230   // Verify that on average, on each thread, performing a read or write took
231   // less than 1ms. There is no meaning to 50ms -- this is just to ensure that
232   // there is no egregious starvation effect (for example, we've seen crash
233   // reports where readers were starving out writers for >10 seconds). Note that
234   // the average time it took to perform one iteration is:
235   // average_time_per_iteration = running_time / iteration_count.
236   static constexpr TimeDelta kStarvationThreshold = Milliseconds(50);
237   std::vector<BaseThread*> threads = GetAllThreads();
238   for (auto* thread : threads) {
239     EXPECT_LT(thread->average_time_per_iter(), kStarvationThreshold);
240   }
241 
242   // Print some useful information that could come in handy if this test fails
243   // (or for diagnostic purposes).
244   LOG(INFO) << "Params: num_read_threads=" << num_read_threads()
245             << ", num_write_threads=" << num_write_threads()
246             << ", FirstThreadsToStart="
247             << static_cast<int>(GetFirstThreadsToStart());
248   for (auto* thread : threads) {
249     LOG(INFO) << thread->thread_name() << " iter_count=" << thread->iter_count()
250               << ", running_time=" << thread->running_time()
251               << ", average_time_per_iter=" << thread->average_time_per_iter();
252   }
253 }
254 
255 INSTANTIATE_TEST_SUITE_P(
256     All,
257     StatisticsRecorderStarvationTest,
258     testing::Combine(
259         testing::Values(StarvationTestParams{.num_read_threads = 10,
260                                              .num_write_threads = 1},
261                         StarvationTestParams{.num_read_threads = 1,
262                                              .num_write_threads = 10},
263                         StarvationTestParams{.num_read_threads = 1,
264                                              .num_write_threads = 1},
265                         StarvationTestParams{.num_read_threads = 5,
266                                              .num_write_threads = 5}),
267         testing::Values(FirstThreadsToStart::kReaders,
268                         FirstThreadsToStart::kWriters)));
269 
270 }  // namespace base
271