blob: a828cc84e4fef18a43346df132263b73bd5e4227 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <atomic>
#include "base/compiler_specific.h"
#include "base/containers/span.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/persistent_histogram_allocator.h"
#include "base/metrics/statistics_recorder.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/threading/platform_thread.h"
#include "base/threading/simple_thread.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace base {
namespace {
constexpr TimeDelta kTestRunningTime = Seconds(5);
constexpr char kHistogramNamePrefix[] = "SRStarvationTest.";
class BaseThread : public SimpleThread {
public:
explicit BaseThread(const std::string& thread_name)
: SimpleThread(thread_name, Options()), thread_name_(thread_name) {}
BaseThread(const BaseThread&) = delete;
BaseThread& operator=(const BaseThread&) = delete;
~BaseThread() override = default;
const std::string& thread_name() const LIFETIME_BOUND { return thread_name_; }
void SetStartTime() { start_time_ = Time::Now(); }
bool ShouldStop() { return stop_.load(std::memory_order_relaxed); }
void Stop() {
stop_.store(true, std::memory_order_relaxed);
end_time_ = Time::Now();
}
void IncrementIterCount() { ++iter_count_; }
size_t iter_count() { return iter_count_; }
TimeDelta running_time() { return end_time_ - start_time_; }
TimeDelta average_time_per_iter() { return running_time() / iter_count_; }
private:
std::string thread_name_;
std::atomic<bool> stop_ = false;
size_t iter_count_ = 0;
Time start_time_;
Time end_time_;
};
class ReadThread : public BaseThread {
public:
explicit ReadThread(size_t id)
: BaseThread(/*thread_name=*/StrCat({"ReadThread", NumberToString(id)})) {
}
void Run() override {
SetStartTime();
const std::string histogram_name =
StrCat({kHistogramNamePrefix, "ReadThreadHistogram"});
while (!ShouldStop()) {
// Continuously emit to the same histogram. Because this histogram should
// already exist within the StatisticsRecorder's internal map (except the
// first time this is called), it should not cause any modifications to
// it, only a lookup. In other words, it will call
// StatisticsRecorder::FindHistogram().
UmaHistogramBoolean(histogram_name, false);
IncrementIterCount();
}
}
};
class WriteThread : public BaseThread {
public:
explicit WriteThread(size_t id)
: BaseThread(
/*thread_name=*/StrCat({"WriteThread", NumberToString(id)})) {}
void Run() override {
SetStartTime();
const std::string base_name =
StrCat({kHistogramNamePrefix, thread_name(), ".Iteration"});
while (!ShouldStop()) {
// Continuously emit to a new histogram. Because this histogram should not
// exist within the StatisticsRecorder's internal map, it will cause an
// insertion to it every time. In other words, it will call
// StatisticsRecorder::RegisterOrDeleteDuplicate().
UmaHistogramBoolean(StrCat({base_name, NumberToString(iter_count())}),
false);
IncrementIterCount();
}
}
};
} // namespace
// Determines the number of reader and writer threads to run in the test.
struct StarvationTestParams {
size_t num_read_threads;
size_t num_write_threads;
};
// Determines which threads should start running first.
enum class FirstThreadsToStart {
kReaders,
kWriters,
};
class StatisticsRecorderStarvationTest
: public testing::TestWithParam<
std::tuple<StarvationTestParams, FirstThreadsToStart>> {
public:
StatisticsRecorderStarvationTest() = default;
StatisticsRecorderStarvationTest(const StatisticsRecorderStarvationTest&) =
delete;
StatisticsRecorderStarvationTest& operator=(
const StatisticsRecorderStarvationTest&) = delete;
~StatisticsRecorderStarvationTest() override = default;
void SetUp() override {
// Create a new StatisticsRecorder so that this test run will not affect
// future ones. In particular, this test relies on creating new histograms
// and adding them to the SR.
sr_ = StatisticsRecorder::CreateTemporaryForTesting();
// Emit a bunch of histograms, which will add them to the SR's internal
// histogram map. This is so that lookups and insertions (FindHistogram()
// and RegisterOrDeleteDuplicate()) during the test don't complete (pretty
// much) instantly.
for (size_t i = 0; i < 10000; ++i) {
UmaHistogramBoolean(
StrCat({kHistogramNamePrefix, "Dummy", NumberToString(i)}), false);
}
}
void TearDown() override {
// Clean up histograms that were allocated during this test. Note that the
// histogram objects are deleted after releasing the temporary `sr_`.
// Otherwise, for a brief moment, the temporary `sr_` would be holding
// dangling pointers.
auto histograms = sr_->GetHistograms();
sr_.reset();
for (auto* histogram : histograms) {
if (StartsWith(histogram->histogram_name(), kHistogramNamePrefix)) {
delete histogram;
}
}
}
// Starts reader and writer threads.
void StartThreads() {
for (size_t i = 0; i < num_read_threads(); ++i) {
read_threads_.emplace_back(std::make_unique<ReadThread>(/*id=*/i));
}
for (size_t i = 0; i < num_write_threads(); ++i) {
write_threads_.emplace_back(std::make_unique<WriteThread>(/*id=*/i));
}
// Depending on the value of GetFirstThreadsToStart(), either start the
// readers or the writers first. Because some implementations will give
// priority to whatever managed to get the lock first, do this to have
// coverage.
span<std::unique_ptr<BaseThread>> start_first_threads;
span<std::unique_ptr<BaseThread>> start_second_threads;
switch (GetFirstThreadsToStart()) {
case FirstThreadsToStart::kReaders:
start_first_threads = read_threads_;
start_second_threads = write_threads_;
break;
case FirstThreadsToStart::kWriters:
start_first_threads = write_threads_;
start_second_threads = read_threads_;
break;
}
for (auto& thread : start_first_threads) {
thread->Start();
}
PlatformThread::Sleep(Milliseconds(100));
for (auto& thread : start_second_threads) {
thread->Start();
}
}
// Stops reader and writer threads.
void StopThreads() {
for (auto* thread : GetAllThreads()) {
thread->Stop();
thread->Join();
}
}
std::vector<BaseThread*> GetAllThreads() {
std::vector<BaseThread*> threads;
threads.reserve(num_read_threads() + num_write_threads());
for (auto& read_thread : read_threads_) {
threads.push_back(read_thread.get());
}
for (auto& write_thread : write_threads_) {
threads.push_back(write_thread.get());
}
return threads;
}
size_t num_read_threads() { return std::get<0>(GetParam()).num_read_threads; }
size_t num_write_threads() {
return std::get<0>(GetParam()).num_write_threads;
}
FirstThreadsToStart GetFirstThreadsToStart() {
return std::get<1>(GetParam());
}
protected:
std::vector<std::unique_ptr<BaseThread>> read_threads_;
std::vector<std::unique_ptr<BaseThread>> write_threads_;
private:
std::unique_ptr<StatisticsRecorder> sr_;
};
// Verifies that there are no starvation issues when emitting histograms (since
// it may be done from any thread). In particular, emitting a histogram requires
// a lock to look up (and sometimes write to) an internal map in the
// StatisticsRecorder. When switching to a Read/Write lock (see crbug/1123627),
// we encountered such a starvation issue, where a thread trying to write to the
// internal map was starved out for 10+ seconds by readers on iOS.
// TODO(crbug.com/41489801): StatisticsRecorderNoStarvation continuously emits a
// new histogram which can cause the app memory footprint to grow unbounded and
// watchdog kill the unit test on iOS devices.
TEST_P(StatisticsRecorderStarvationTest, StatisticsRecorderNoStarvation) {
// Make sure there is no GlobalHistogramAllocator so that histograms emitted
// during this test are all allocated on the heap, which makes it a lot easier
// to clean them up at the end.
ASSERT_FALSE(GlobalHistogramAllocator::Get());
// Start reader and writer threads.
StartThreads();
// Let the test run for |kTestRunningTime|.
PlatformThread::Sleep(kTestRunningTime);
// Stop reader and writer threads. This waits for them to complete by joining
// the threads.
StopThreads();
// Verify that on average, on each thread, performing a read or write took
// less than 1ms. There is no meaning to 50ms -- this is just to ensure that
// there is no egregious starvation effect (for example, we've seen crash
// reports where readers were starving out writers for >10 seconds). Note that
// the average time it took to perform one iteration is:
// average_time_per_iteration = running_time / iteration_count.
static constexpr TimeDelta kStarvationThreshold = Milliseconds(50);
std::vector<BaseThread*> threads = GetAllThreads();
for (auto* thread : threads) {
EXPECT_LT(thread->average_time_per_iter(), kStarvationThreshold);
}
// Print some useful information that could come in handy if this test fails
// (or for diagnostic purposes).
LOG(INFO) << "Params: num_read_threads=" << num_read_threads()
<< ", num_write_threads=" << num_write_threads()
<< ", FirstThreadsToStart="
<< static_cast<int>(GetFirstThreadsToStart());
for (auto* thread : threads) {
LOG(INFO) << thread->thread_name() << " iter_count=" << thread->iter_count()
<< ", running_time=" << thread->running_time()
<< ", average_time_per_iter=" << thread->average_time_per_iter();
}
}
INSTANTIATE_TEST_SUITE_P(
All,
StatisticsRecorderStarvationTest,
testing::Combine(
testing::Values(StarvationTestParams{.num_read_threads = 10,
.num_write_threads = 1},
StarvationTestParams{.num_read_threads = 1,
.num_write_threads = 10},
StarvationTestParams{.num_read_threads = 1,
.num_write_threads = 1},
StarvationTestParams{.num_read_threads = 5,
.num_write_threads = 5}),
testing::Values(FirstThreadsToStart::kReaders,
FirstThreadsToStart::kWriters)));
} // namespace base