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