// 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 #include #include #include #include #include #include #include #include #include #include #include #include #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 #else #include #endif // BUILDFLAG(IS_WIN) namespace crashpad { namespace test { namespace { constexpr Annotation::Type kRingBufferLoadTestType = Annotation::UserDefinedType(0x0042); std::atomic g_should_exit = false; struct RingBufferAnnotationSnapshotParams final { enum class Mode { kUseScopedSpinGuard = 1, kDoNotUseSpinGuard = 2, }; Mode mode = Mode::kUseScopedSpinGuard; using Duration = std::chrono::duration; 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::max(); std::optional main_thread_run_duration = std::nullopt; }; template class RingBufferAnnotationSnapshot final { using RingBufferAnnotationType = RingBufferAnnotation; 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 thread_main) : thread_main_(std::move(thread_main)) {} private: void ThreadMain() override { thread_main_(); } const std::function 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 start_lock(mutex_); state_.ring_buffer_annotation.ResetForTesting(); state_.ring_buffer_ready = true; state_changed_condition_.notify_all(); } { std::unique_lock 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 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( params_.producer_thread_min_run_duration); auto max_run_duration_micros = std::chrono::duration_cast( params_.producer_thread_max_run_duration); std::uniform_int_distribution 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 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(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 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( params_.consumer_thread_min_run_duration); auto max_run_duration_micros = std::chrono::duration_cast( params_.consumer_thread_max_run_duration); std::uniform_int_distribution 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 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( std::chrono::duration_cast( params_.quiesce_timeout) .count()); uint8_t serialized_ring_buffer[sizeof(state_.ring_buffer_annotation)]; Annotation::ValueSizeType ring_buffer_size; { std::optional 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::max(); std::vector bytes; while (ring_buffer_reader.Pop(bytes)) { int next_value; base::StringPiece str(reinterpret_cast(&bytes[0]), bytes.size()); if (!base::HexStringToInt(str, &next_value)) { fprintf(stderr, "Couldn't parse value: [%.*s]\n", base::checked_cast(bytes.size()), bytes.data()); abort(); } if (value == std::numeric_limits::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