blob: 2a767be58470a5095b498018f26bc5f1e879ee9b [file] [log] [blame]
// Copyright 2023 The Crashpad Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <getopt.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <chrono>
#include <condition_variable>
#include <functional>
#include <mutex>
#include <optional>
#include <random>
#include <ratio>
#include <string>
#include "base/notreached.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_piece.h"
#include "base/strings/stringprintf.h"
#include "build/build_config.h"
#include "client/annotation.h"
#include "client/length_delimited_ring_buffer.h"
#include "client/ring_buffer_annotation.h"
#include "tools/tool_support.h"
#include "util/stdlib/string_number_conversion.h"
#include "util/synchronization/scoped_spin_guard.h"
#include "util/thread/thread.h"
#if BUILDFLAG(IS_WIN)
#include <windows.h>
#else
#include <signal.h>
#endif // BUILDFLAG(IS_WIN)
namespace crashpad {
namespace test {
namespace {
constexpr Annotation::Type kRingBufferLoadTestType =
Annotation::UserDefinedType(0x0042);
std::atomic<bool> g_should_exit = false;
struct RingBufferAnnotationSnapshotParams final {
enum class Mode {
kUseScopedSpinGuard = 1,
kDoNotUseSpinGuard = 2,
};
Mode mode = Mode::kUseScopedSpinGuard;
using Duration = std::chrono::duration<uint64_t, std::nano>;
Duration producer_thread_min_run_duration = std::chrono::milliseconds(1);
Duration producer_thread_max_run_duration = std::chrono::milliseconds(10);
Duration producer_thread_sleep_duration = std::chrono::nanoseconds(10);
Duration consumer_thread_min_run_duration = std::chrono::milliseconds(5);
Duration consumer_thread_max_run_duration = std::chrono::milliseconds(100);
Duration quiesce_timeout = std::chrono::microseconds(500);
uint64_t num_loops = std::numeric_limits<uint64_t>::max();
std::optional<Duration> main_thread_run_duration = std::nullopt;
};
template <uint32_t RingBufferCapacity>
class RingBufferAnnotationSnapshot final {
using RingBufferAnnotationType = RingBufferAnnotation<RingBufferCapacity>;
struct State final {
State()
: ring_buffer_annotation(kRingBufferLoadTestType,
"ring-buffer-load-test"),
ring_buffer_ready(false),
producer_thread_running(false),
producer_thread_finished(false),
consumer_thread_finished(false),
should_exit(false) {}
State(const State&) = delete;
State& operator=(const State&) = delete;
RingBufferAnnotationType ring_buffer_annotation;
bool ring_buffer_ready;
bool producer_thread_running;
bool producer_thread_finished;
bool consumer_thread_finished;
bool should_exit;
};
class Thread final : public crashpad::Thread {
public:
Thread(std::function<void()> thread_main)
: thread_main_(std::move(thread_main)) {}
private:
void ThreadMain() override { thread_main_(); }
const std::function<void()> thread_main_;
};
public:
RingBufferAnnotationSnapshot(const RingBufferAnnotationSnapshotParams& params)
: params_(params),
main_loop_thread_([this]() { MainLoopThreadMain(); }),
producer_thread_([this]() { ProducerThreadMain(); }),
consumer_thread_([this]() { ConsumerThreadMain(); }),
mutex_(),
state_changed_condition_(),
state_() {}
RingBufferAnnotationSnapshot(const RingBufferAnnotationSnapshot&) = delete;
RingBufferAnnotationSnapshot& operator=(const RingBufferAnnotationSnapshot&) =
delete;
void Start() {
main_loop_thread_.Start();
producer_thread_.Start();
consumer_thread_.Start();
}
void Stop() {
consumer_thread_.Join();
producer_thread_.Join();
main_loop_thread_.Join();
}
private:
void MainLoopThreadMain() {
std::chrono::steady_clock::time_point main_thread_end_time;
if (params_.main_thread_run_duration) {
main_thread_end_time =
std::chrono::steady_clock::now() + *params_.main_thread_run_duration;
} else {
main_thread_end_time = std::chrono::steady_clock::time_point::max();
}
for (uint64_t i = 0;
i < params_.num_loops &&
std::chrono::steady_clock::now() < main_thread_end_time;
i++) {
{
std::unique_lock<std::mutex> start_lock(mutex_);
state_.ring_buffer_annotation.ResetForTesting();
state_.ring_buffer_ready = true;
state_changed_condition_.notify_all();
}
{
std::unique_lock<std::mutex> lock(mutex_);
state_changed_condition_.wait(lock, [this] {
return state_.producer_thread_finished &&
state_.consumer_thread_finished;
});
state_.ring_buffer_ready = false;
if (g_should_exit) {
printf("Exiting on Control-C.\n");
break;
}
printf(".");
fflush(stdout);
state_changed_condition_.notify_all();
}
}
state_.should_exit = true;
state_changed_condition_.notify_all();
}
void ProducerThreadMain() {
while (true) {
{
std::unique_lock<std::mutex> lock(mutex_);
state_changed_condition_.wait(lock, [this] {
return state_.should_exit || state_.ring_buffer_ready;
});
if (state_.should_exit) {
return;
}
state_.producer_thread_running = true;
state_.producer_thread_finished = false;
state_changed_condition_.notify_all();
}
auto min_run_duration_micros =
std::chrono::duration_cast<std::chrono::microseconds>(
params_.producer_thread_min_run_duration);
auto max_run_duration_micros =
std::chrono::duration_cast<std::chrono::microseconds>(
params_.producer_thread_max_run_duration);
std::uniform_int_distribution<std::chrono::microseconds::rep>
run_duration_distribution(min_run_duration_micros.count(),
max_run_duration_micros.count());
static thread_local std::mt19937 random_number_generator;
auto run_duration = std::chrono::microseconds(
run_duration_distribution(random_number_generator));
auto end_time = std::chrono::steady_clock::now() + run_duration;
uint64_t next_value = 0;
while (std::chrono::steady_clock::now() < end_time) {
if (!Produce(next_value++)) {
// The consumer thread interrupted this.
break;
}
}
{
std::unique_lock<std::mutex> lock(mutex_);
state_changed_condition_.wait(
lock, [this] { return state_.consumer_thread_finished; });
state_.producer_thread_running = false;
state_.producer_thread_finished = true;
state_changed_condition_.notify_all();
}
}
}
bool Produce(uint64_t value) {
std::string hex_value = base::StringPrintf("0x%08" PRIx64, value);
if (!state_.ring_buffer_annotation.Push(
hex_value.data(), static_cast<uint32_t>(hex_value.size()))) {
fprintf(stderr,
"Ignoring failed call to Push(0x%" PRIx64
") (ScopedSpinGuard was held by snapshot thread)\n",
value);
return false;
}
return true;
}
void ConsumerThreadMain() {
while (true) {
{
std::unique_lock<std::mutex> lock(mutex_);
state_changed_condition_.wait(lock, [this] {
return state_.should_exit ||
(state_.ring_buffer_ready && state_.producer_thread_running);
});
if (state_.should_exit) {
return;
}
state_.consumer_thread_finished = false;
state_changed_condition_.notify_all();
}
auto min_run_duration_micros =
std::chrono::duration_cast<std::chrono::microseconds>(
params_.consumer_thread_min_run_duration);
auto max_run_duration_micros =
std::chrono::duration_cast<std::chrono::microseconds>(
params_.consumer_thread_max_run_duration);
std::uniform_int_distribution<std::chrono::microseconds::rep>
run_duration_distribution(min_run_duration_micros.count(),
max_run_duration_micros.count());
static thread_local std::mt19937 random_number_generator;
auto run_duration = std::chrono::microseconds(
run_duration_distribution(random_number_generator));
auto end_time = std::chrono::steady_clock::now() + run_duration;
while (std::chrono::steady_clock::now() < end_time) {
constexpr uint64_t kSleepTimeNs = 10000; // 10 us
SleepNanoseconds(kSleepTimeNs);
}
Snapshot();
{
std::unique_lock<std::mutex> lock(mutex_);
state_.consumer_thread_finished = true;
state_.ring_buffer_ready = false;
state_changed_condition_.notify_all();
}
}
}
void Snapshot() {
int64_t timeout_ns = static_cast<int64_t>(
std::chrono::duration_cast<std::chrono::nanoseconds>(
params_.quiesce_timeout)
.count());
uint8_t serialized_ring_buffer[sizeof(state_.ring_buffer_annotation)];
Annotation::ValueSizeType ring_buffer_size;
{
std::optional<ScopedSpinGuard> scoped_spin_guard;
if (params_.mode ==
RingBufferAnnotationSnapshotParams::Mode::kUseScopedSpinGuard) {
scoped_spin_guard =
state_.ring_buffer_annotation.TryCreateScopedSpinGuard(timeout_ns);
}
if (params_.mode ==
RingBufferAnnotationSnapshotParams::Mode::kUseScopedSpinGuard &&
!scoped_spin_guard) {
fprintf(stderr,
"Could not quiesce writes within %" PRIi64 " ns\n",
timeout_ns);
abort();
}
ring_buffer_size = state_.ring_buffer_annotation.size();
memcpy(&serialized_ring_buffer[0],
state_.ring_buffer_annotation.value(),
ring_buffer_size);
}
RingBufferData ring_buffer;
if (!ring_buffer.DeserializeFromBuffer(serialized_ring_buffer,
ring_buffer_size)) {
fprintf(stderr, "Could not deserialize ring buffer\n");
abort();
}
LengthDelimitedRingBufferReader ring_buffer_reader(ring_buffer);
int value = std::numeric_limits<int>::max();
std::vector<uint8_t> bytes;
while (ring_buffer_reader.Pop(bytes)) {
int next_value;
base::StringPiece str(reinterpret_cast<const char*>(&bytes[0]),
bytes.size());
if (!base::HexStringToInt(str, &next_value)) {
fprintf(stderr,
"Couldn't parse value: [%.*s]\n",
base::checked_cast<int>(bytes.size()),
bytes.data());
abort();
}
if (value == std::numeric_limits<int>::max()) {
// First value in buffer.
} else if (value + 1 != next_value) {
fprintf(stderr,
"Expected value 0x%08x, got 0x%08x\n",
value + 1,
next_value);
abort();
}
value = next_value;
bytes.clear();
}
}
const RingBufferAnnotationSnapshotParams params_;
Thread main_loop_thread_;
Thread producer_thread_;
Thread consumer_thread_;
std::mutex mutex_;
// Fired whenever `state_` changes.
std::condition_variable state_changed_condition_;
// Protected by `mutex_`.
State state_;
};
void Usage(const base::FilePath& me) {
// clang-format off
fprintf(stderr,
"Usage: %" PRFilePath " [OPTION]...\n"
"Runs a load test for concurrent I/O to RingBufferAnnotation.\n"
"\n"
"By default, enables the annotation spin guard and runs indefinitely\n"
"until interrupted (e.g., with Control-C or SIGINT).\n"
"\n"
" -d,--disable-spin-guard Disables the annotation spin guard\n"
" (the test is expected to crash in this case)\n"
" -n,--num-loops=N Runs the test for N iterations, not indefinitely\n"
" -s,--duration-secs=SECS Runs the test for SECS seconds, not indefinitely\n",
me.value().c_str());
// clang-format on
ToolSupport::UsageTail(me);
}
int TestMain(int argc, char** argv) {
const base::FilePath argv0(
ToolSupport::CommandLineArgumentToFilePathStringType(argv[0]));
const base::FilePath me(argv0.BaseName());
#if BUILDFLAG(IS_WIN)
auto handler_routine = [](DWORD type) -> BOOL {
if (type == CTRL_C_EVENT) {
g_should_exit = true;
return TRUE;
}
return FALSE;
};
if (!SetConsoleCtrlHandler(handler_routine, /*Add=*/TRUE)) {
fprintf(stderr, "Couldn't set Control-C handler\n");
return EXIT_FAILURE;
}
#else
signal(SIGINT, [](int signal) { g_should_exit = true; });
#endif // BUILDFLAG(IS_WIN)
RingBufferAnnotationSnapshotParams params;
enum OptionFlags {
// "Short" (single-character) options.
kOptionDisableSpinGuard = 'd',
kOptionNumLoops = 'n',
kOptionDurationSecs = 's',
// Standard options.
kOptionHelp = -2,
kOptionVersion = -3,
};
static constexpr option long_options[] = {
{"disable-spin-guard", no_argument, nullptr, kOptionDisableSpinGuard},
{"num-loops", required_argument, nullptr, kOptionNumLoops},
{"duration-secs", required_argument, nullptr, kOptionDurationSecs},
{"help", no_argument, nullptr, kOptionHelp},
{"version", no_argument, nullptr, kOptionVersion},
{nullptr, 0, nullptr, 0},
};
int opt;
while ((opt = getopt_long(argc, argv, "dn:s:", long_options, nullptr)) !=
-1) {
switch (opt) {
case kOptionDisableSpinGuard:
printf("Disabling spin guard logic (this test will fail!)\n");
params.mode =
RingBufferAnnotationSnapshotParams::Mode::kDoNotUseSpinGuard;
break;
case kOptionNumLoops: {
std::string num_loops(optarg);
uint64_t num_loops_value;
if (!StringToNumber(num_loops, &num_loops_value)) {
ToolSupport::UsageHint(me, "--num-loops requires integer value");
return EXIT_FAILURE;
}
params.num_loops = num_loops_value;
break;
}
case kOptionDurationSecs: {
std::string duration_secs(optarg);
uint64_t duration_secs_value;
if (!StringToNumber(duration_secs, &duration_secs_value)) {
ToolSupport::UsageHint(me, "--duration-secs requires integer value");
return EXIT_FAILURE;
}
params.main_thread_run_duration =
std::chrono::seconds(duration_secs_value);
break;
}
case kOptionHelp:
Usage(me);
return EXIT_SUCCESS;
case kOptionVersion:
ToolSupport::Version(me);
return EXIT_SUCCESS;
default:
ToolSupport::UsageHint(me, nullptr);
return EXIT_FAILURE;
}
}
RingBufferAnnotationSnapshot<8192> test_producer_snapshot(params);
printf("Starting test (Control-C to exit)...\n");
test_producer_snapshot.Start();
test_producer_snapshot.Stop();
printf("Test finished.\n");
return EXIT_SUCCESS;
}
} // namespace
} // namespace test
} // namespace crashpad
#if BUILDFLAG(IS_POSIX)
int main(int argc, char** argv) {
return crashpad::test::TestMain(argc, argv);
}
#elif BUILDFLAG(IS_WIN)
int wmain(int argc, wchar_t* argv[]) {
return crashpad::ToolSupport::Wmain(argc, argv, crashpad::test::TestMain);
}
#endif