diff --git a/doc/changes/unreleased.md b/doc/changes/unreleased.md index f1e2cdb..f1d6cea 100644 --- a/doc/changes/unreleased.md +++ b/doc/changes/unreleased.md @@ -18,6 +18,7 @@ n/a ## Internal +* #57: Defined and enforced public v2 C++ coding style * #51: Added agent and contributor guidance for v1/v2 development, SLC workflows, CI testing, and PR conventions * #56: Restructured the developer guide and synchronized agent guidance * Updated Poetry dependencies and added developer guide and added .gitignore diff --git a/doc/developer_guide/v2/v2.md b/doc/developer_guide/v2/v2.md index 7a51f04..c6099f1 100644 --- a/doc/developer_guide/v2/v2.md +++ b/doc/developer_guide/v2/v2.md @@ -4,6 +4,7 @@ The v2 developer documentation is split into focused guides: - [Build and test](v2_build_and_test.md) — Bazel setup and test execution. - [Code quality](v2_code_quality.md) — clang-tidy and clang-format checks. +- [Coding style](v2_coding_style.md) — public C++ conventions for v2 code. - [Fuzzing](v2_fuzzing.md) — libFuzzer targets, Nox campaigns, and regression runs. - [Dependency policy](v2_dependency_policy.md) — third-party isolation and diff --git a/doc/developer_guide/v2/v2_code_quality.md b/doc/developer_guide/v2/v2_code_quality.md index 1507f37..864d574 100644 --- a/doc/developer_guide/v2/v2_code_quality.md +++ b/doc/developer_guide/v2/v2_code_quality.md @@ -13,6 +13,23 @@ bazel build --verbose_failures --config clang-tidy //... Run clang-tidy on changed `.cpp` files before submitting code for review to catch common issues early. +The Bazel configuration uses `clang-tidy-22` by default. To use another +installed executable, set `CLANG_TIDY` when invoking Bazel: + +```bash +CLANG_TIDY=clang-tidy bazel build --verbose_failures --config clang-tidy //... +``` + +`CLANG_TIDY` may also contain an absolute path to the executable. + +The wrapper removes `-fno-canonical-system-headers` from the compiler +arguments by default. To retain that argument, clear `CLANG_TIDY_REMOVED_ARG`: + +```bash +CLANG_TIDY=clang-tidy CLANG_TIDY_REMOVED_ARG= \ + bazel build --verbose_failures --config clang-tidy //... +``` + ### Apply clang-tidy fixes You can run `clang-apply-replacements` with: diff --git a/doc/developer_guide/v2/v2_coding_style.md b/doc/developer_guide/v2/v2_coding_style.md new file mode 100644 index 0000000..cfd7556 --- /dev/null +++ b/doc/developer_guide/v2/v2_coding_style.md @@ -0,0 +1,94 @@ +# v2 C++ Coding Style + +This guide defines the conventions for C++ code under +[`udf-runner-cpp/v2`](../../../udf-runner-cpp/v2). The checked-in +[`clang-format` configuration](../../../udf-runner-cpp/v2/tools/clang-format/.clang-format) +and [`clang-tidy` configuration](../../../udf-runner-cpp/v2/tools/clang-tidy/.clang-tidy) +are authoritative for automatically checked rules. + +## Files and includes + +- Use UTF-8 source files. +- Use `.cc` for implementation files and `.h` or `.hpp` for headers, matching + the convention already used by the v2 module. +- Keep `#include` directives at the top of the file. Do not include headers + inside functions unless there is a documented, compelling reason. +- Include every header required by a file directly; do not rely on transitive + includes. +- Put non-template function definitions in implementation files unless there + is a measured performance reason to keep them inline. +- Keep public headers independent of private implementation details and avoid + conditional compilation in headers unless it is required by the public API. + +## Names and namespaces + +- Use ASCII identifiers and `lower_case` for functions, variables, parameters, + and data members. +- Use `CamelCase` for classes and enum types. Use `CamelCase` for scoped enum + values as well. +- Name factory functions with a `create` prefix and getters/setters with + `get_`/`set_` prefixes, for example `get_value()` and `set_value()`. +- Put file-local functions and types in an unnamed namespace. +- Put project code in an appropriate `exasol::udf::v2` namespace rather than + importing a namespace with `using namespace`. +- Keep namespace aliases local and descriptive when they improve readability. + +The naming policy is enforced by clang-tidy’s +`readability-identifier-naming` check. Class members have no naming prefix or +suffix. When a member access would otherwise be ambiguous, qualify it with +`this->`, for example `this->value`. + +Names required by an external ABI or framework are exceptions. For example, +the libFuzzer entry point `LLVMFuzzerTestOneInput` keeps its required spelling. + +## Functions and classes + +- Prefer free functions for behavior that does not depend on object state. +- Avoid operator overloading unless the type has a clear value-like meaning + and the overload is required for natural use of the public API. +- Mark a class `final` when it is not designed for inheritance. +- Mark overriding methods with `override`. +- Keep class declarations ordered, where practical, as public, protected, then + private; within each section, place types before methods and data members. +- Avoid ambiguity between constructor parameters and members. Prefer the same + descriptive name and qualify member access with `this->`. +- Separate function definitions with a blank line. + +## Types, control flow, and errors + +- Prefer fixed-width integer types such as `std::int32_t` and `std::uint64_t` + when the width is part of the interface or serialized representation. +- Use `enum class` for new enumerations. +- For `std::optional`, use `has_value()` when testing presence and `value()` + when explicitly retrieving the contained value. Name the variable after its + value, not after the fact that it is optional. +- Follow the repository formatter for braces and indentation. Keep all code + belonging to a `case`, including its terminating `break`, `return`, or + fallthrough marker, inside the case body when braces are needed. +- Prefer safe, expressive casts. If a lower-level cast is required for a + measured hot path or ABI boundary, document why it is safe. +- Report failures caused by external input or environment through the public + error mechanism, normally an exception. Use assertions for programmer + contract violations and impossible internal states. + +## Documentation and cleanup + +- Document design decisions close to the code they constrain. +- Put API documentation in public headers and implementation details near the + implementation. +- Use Doxygen commands with `@`. Prefer `@returns`, `@throws`, and `@see`. + Omit `@brief` when the first sentence already provides the brief. +- Remove commented-out code and avoid `#if 0` or `#if 1` except when a clear, + documented temporary or compatibility purpose requires it. +- Keep comments factual and explain why non-obvious code exists, not what an + immediately readable statement does. + +## Tests and review + +- Add or update tests when changing behavior, public interfaces, parsing, + serialization, concurrency, or dependency boundaries. +- Prefer small, focused tests that make failures easy to diagnose. +- Run the v2 build and tests, then the `clang-format` and `clang-tidy` checks + described in the [code quality guide](v2_code_quality.md). +- Do not suppress a static-analysis warning without documenting the reason at + the suppression site. diff --git a/udf-runner-cpp/v2/.bazelrc b/udf-runner-cpp/v2/.bazelrc index ab5f37c..fe6bafa 100644 --- a/udf-runner-cpp/v2/.bazelrc +++ b/udf-runner-cpp/v2/.bazelrc @@ -31,6 +31,8 @@ build:asan-replay --@rules_fuzzing//fuzzing:cc_engine_sanitizer=asan build:clang-tidy --@rules_clang_tidy//:config=//tools/clang-tidy:config build:clang-tidy --@rules_clang_tidy//:clang-tidy=//tools/clang-tidy:wrapper +build:clang-tidy --action_env=CLANG_TIDY +build:clang-tidy --action_env=CLANG_TIDY_REMOVED_ARG build:clang-tidy --aspects=@rules_clang_tidy//:aspects.bzl%check build:clang-tidy --output_groups=report build:clang-tidy --remote_download_outputs=toplevel diff --git a/udf-runner-cpp/v2/arrow_c_data_demo.cc b/udf-runner-cpp/v2/arrow_c_data_demo.cc index 37777cd..61c7d59 100644 --- a/udf-runner-cpp/v2/arrow_c_data_demo.cc +++ b/udf-runner-cpp/v2/arrow_c_data_demo.cc @@ -18,12 +18,12 @@ namespace thread_local std::string g_last_error; -void SetLastError(const arrow::Status& status) +void set_last_error(const arrow::Status& status) { g_last_error = status.ToString(); } -arrow::Result> MakeDemoRecordBatch() +arrow::Result> make_demo_record_batch() { arrow::Int64Builder id_builder; arrow::StringBuilder name_builder; @@ -51,7 +51,7 @@ arrow::Result> MakeDemoRecordBatch() return arrow::RecordBatch::Make(schema, num_rows, {std::move(ids), std::move(names)}); } -arrow::Status ExportDemoRecordBatch(ArrowArray* out_array, ArrowSchema* out_schema) +arrow::Status export_demo_record_batch(ArrowArray* out_array, ArrowSchema* out_schema) { if (out_array == nullptr || out_schema == nullptr) { @@ -62,7 +62,7 @@ arrow::Status ExportDemoRecordBatch(ArrowArray* out_array, ArrowSchema* out_sche std::memset(out_array, 0, sizeof(*out_array)); std::memset(out_schema, 0, sizeof(*out_schema)); - auto maybe_batch = MakeDemoRecordBatch(); + auto maybe_batch = make_demo_record_batch(); if (!maybe_batch.ok()) { return maybe_batch.status(); @@ -71,10 +71,10 @@ arrow::Status ExportDemoRecordBatch(ArrowArray* out_array, ArrowSchema* out_sche return arrow::Status::OK(); } -arrow::Status ConsumeDemoRecordBatch(ArrowArray* array, - ArrowSchema* schema, - int64_t* out_row_count, - int64_t* out_id_sum) +arrow::Status consume_demo_record_batch(ArrowArray* array, + ArrowSchema* schema, + int64_t* out_row_count, + int64_t* out_id_sum) { if (array == nullptr || schema == nullptr || out_row_count == nullptr || out_id_sum == nullptr) { @@ -111,10 +111,10 @@ arrow::Status ConsumeDemoRecordBatch(ArrowArray* array, extern "C" UDF_RUNNER_CPP_V2_EXPORT int udf_runner_cpp_v2_demo_export_record_batch( ArrowArray* out_array, ArrowSchema* out_schema) { - const arrow::Status status = ExportDemoRecordBatch(out_array, out_schema); + const arrow::Status status = export_demo_record_batch(out_array, out_schema); if (!status.ok()) { - SetLastError(status); + set_last_error(status); return 1; } g_last_error.clear(); @@ -124,10 +124,11 @@ extern "C" UDF_RUNNER_CPP_V2_EXPORT int udf_runner_cpp_v2_demo_export_record_bat extern "C" UDF_RUNNER_CPP_V2_EXPORT int udf_runner_cpp_v2_demo_consume_record_batch( ArrowArray* array, ArrowSchema* schema, int64_t* out_row_count, int64_t* out_id_sum) { - const arrow::Status status = ConsumeDemoRecordBatch(array, schema, out_row_count, out_id_sum); + const arrow::Status status = + consume_demo_record_batch(array, schema, out_row_count, out_id_sum); if (!status.ok()) { - SetLastError(status); + set_last_error(status); return 1; } g_last_error.clear(); diff --git a/udf-runner-cpp/v2/arrow_c_data_demo_test.cc b/udf-runner-cpp/v2/arrow_c_data_demo_test.cc index d40584e..dbc62ca 100644 --- a/udf-runner-cpp/v2/arrow_c_data_demo_test.cc +++ b/udf-runner-cpp/v2/arrow_c_data_demo_test.cc @@ -25,8 +25,8 @@ using export_fn_t = int (*)(ArrowArray*, ArrowSchema*); using consume_fn_t = int (*)(ArrowArray*, ArrowSchema*, int64_t*, int64_t*); using error_fn_t = const char* (*)(); -constexpr std::string_view kArrowMangledPrefix = "_ZN5arrow"; -constexpr std::string_view kDemoExportedPrefix = "udf_runner_cpp_v2_demo_"; +constexpr std::string_view arrow_mangled_prefix = "_ZN5arrow"; +constexpr std::string_view demo_exported_prefix = "udf_runner_cpp_v2_demo_"; [[noreturn]] void fail(const std::string& message) { @@ -140,11 +140,11 @@ void verify_symbols(const std::string& library_path) const std::string name = read_string(file, string_table.sh_offset + symbol.st_name, string_table.sh_size - symbol.st_name); - if (name.compare(0, kArrowMangledPrefix.size(), kArrowMangledPrefix) == 0) + if (name.compare(0, arrow_mangled_prefix.size(), arrow_mangled_prefix) == 0) { fail("shared library exports an Arrow C++ symbol: " + name); } - if (name.compare(0, kDemoExportedPrefix.size(), kDemoExportedPrefix) == 0) + if (name.compare(0, demo_exported_prefix.size(), demo_exported_prefix) == 0) { found_demo_symbol = true; } @@ -227,6 +227,9 @@ int main(int argc, char** argv) assert(names->GetString(0) == "alpha"); assert(names->GetString(3) == "delta"); + assert(export_batch(nullptr, nullptr) != 0); + assert(std::string(last_error()).find("must not be null") != std::string::npos); + ArrowArray second_array{}; ArrowSchema second_schema{}; if (export_batch(&second_array, &second_schema) != 0) @@ -242,6 +245,9 @@ int main(int argc, char** argv) } assert(row_count == 4); assert(id_sum == 10); + + assert(consume_batch(nullptr, nullptr, nullptr, nullptr) != 0); + assert(std::string(last_error()).find("must not be null") != std::string::npos); } catch (const std::exception& error) { diff --git a/udf-runner-cpp/v2/call_metadata_fuzz_test.cc b/udf-runner-cpp/v2/call_metadata_fuzz_test.cc index 96b3a15..443758f 100644 --- a/udf-runner-cpp/v2/call_metadata_fuzz_test.cc +++ b/udf-runner-cpp/v2/call_metadata_fuzz_test.cc @@ -1,8 +1,7 @@ #include "test_utils/json_schema_fuzzing.hpp" -extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t *data, - std::size_t size) { - exasol::udf::v2::fuzzing::FuzzJsonSchema( - data, size, "json_schema/call_metadata.schema.json"); - return 0; +extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t* data, std::size_t size) +{ + exasol::udf::v2::fuzzing::fuzz_json_schema(data, size, "json_schema/call_metadata.schema.json"); + return 0; } diff --git a/udf-runner-cpp/v2/connection_information_fuzz_test.cc b/udf-runner-cpp/v2/connection_information_fuzz_test.cc index 6de5ffd..0de6437 100644 --- a/udf-runner-cpp/v2/connection_information_fuzz_test.cc +++ b/udf-runner-cpp/v2/connection_information_fuzz_test.cc @@ -1,8 +1,8 @@ #include "test_utils/json_schema_fuzzing.hpp" -extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t *data, - std::size_t size) { - exasol::udf::v2::fuzzing::FuzzJsonSchema( - data, size, "json_schema/connection_information.schema.json"); - return 0; +extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t* data, std::size_t size) +{ + exasol::udf::v2::fuzzing::fuzz_json_schema(data, size, + "json_schema/connection_information.schema.json"); + return 0; } diff --git a/udf-runner-cpp/v2/export_specification_fuzz_test.cc b/udf-runner-cpp/v2/export_specification_fuzz_test.cc index 18e4944..a25e52a 100644 --- a/udf-runner-cpp/v2/export_specification_fuzz_test.cc +++ b/udf-runner-cpp/v2/export_specification_fuzz_test.cc @@ -1,8 +1,8 @@ #include "test_utils/json_schema_fuzzing.hpp" -extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t *data, - std::size_t size) { - exasol::udf::v2::fuzzing::FuzzJsonSchema( - data, size, "json_schema/export_specification.schema.json"); - return 0; +extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t* data, std::size_t size) +{ + exasol::udf::v2::fuzzing::fuzz_json_schema(data, size, + "json_schema/export_specification.schema.json"); + return 0; } diff --git a/udf-runner-cpp/v2/frame_fuzz_test.cc b/udf-runner-cpp/v2/frame_fuzz_test.cc index d1e6419..d5f19d4 100644 --- a/udf-runner-cpp/v2/frame_fuzz_test.cc +++ b/udf-runner-cpp/v2/frame_fuzz_test.cc @@ -3,8 +3,8 @@ #include "udf_protocol.hpp" -extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t *data, - std::size_t size) { - exasol::udf::protocol::VerifyFrameBuffer(data, size); - return 0; +extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t* data, std::size_t size) +{ + exasol::udf::protocol::verify_frame_buffer(data, size); + return 0; } diff --git a/udf-runner-cpp/v2/import_specification_fuzz_test.cc b/udf-runner-cpp/v2/import_specification_fuzz_test.cc index 5efed0f..8fcbd56 100644 --- a/udf-runner-cpp/v2/import_specification_fuzz_test.cc +++ b/udf-runner-cpp/v2/import_specification_fuzz_test.cc @@ -1,8 +1,8 @@ #include "test_utils/json_schema_fuzzing.hpp" -extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t *data, - std::size_t size) { - exasol::udf::v2::fuzzing::FuzzJsonSchema( - data, size, "json_schema/import_specification.schema.json"); - return 0; +extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t* data, std::size_t size) +{ + exasol::udf::v2::fuzzing::fuzz_json_schema(data, size, + "json_schema/import_specification.schema.json"); + return 0; } diff --git a/udf-runner-cpp/v2/json_schema_symbol_leak_test.cc b/udf-runner-cpp/v2/json_schema_symbol_leak_test.cc index ab11565..927ffad 100644 --- a/udf-runner-cpp/v2/json_schema_symbol_leak_test.cc +++ b/udf-runner-cpp/v2/json_schema_symbol_leak_test.cc @@ -18,8 +18,8 @@ namespace isolated_nlohmann = exasol::udf::v2::third_party::nlohmann; namespace { -constexpr std::string_view kGlobalNamespacePrefix = "_ZN8nlohmann"; -constexpr std::string_view kIsolatedNamespacePrefix = "_ZN6exasol3udf2v211third_party8nlohmann"; +constexpr std::string_view global_namespace_prefix = "_ZN8nlohmann"; +constexpr std::string_view isolated_namespace_prefix = "_ZN6exasol3udf2v211third_party8nlohmann"; [[noreturn]] void fail(const std::string& message) { @@ -133,11 +133,11 @@ void verify_symbols(const std::string& library_path) const std::string name = read_string(file, string_table.sh_offset + symbol.st_name, string_table.sh_size - symbol.st_name); - if (name.compare(0, kGlobalNamespacePrefix.size(), kGlobalNamespacePrefix) == 0) + if (name.compare(0, global_namespace_prefix.size(), global_namespace_prefix) == 0) { fail("validator exports a global nlohmann symbol: " + name); } - if (name.compare(0, kIsolatedNamespacePrefix.size(), kIsolatedNamespacePrefix) == 0) + if (name.compare(0, isolated_namespace_prefix.size(), isolated_namespace_prefix) == 0) { found_isolated_symbol = true; } diff --git a/udf-runner-cpp/v2/moodycamel_symbol_leak_test.cc b/udf-runner-cpp/v2/moodycamel_symbol_leak_test.cc index c21e17c..b3e74d1 100644 --- a/udf-runner-cpp/v2/moodycamel_symbol_leak_test.cc +++ b/udf-runner-cpp/v2/moodycamel_symbol_leak_test.cc @@ -15,8 +15,8 @@ namespace { -constexpr std::string_view kGlobalNamespacePrefix = "_ZN10moodycamel"; -constexpr std::string_view kIsolatedNamespacePrefix = "_ZN6exasol3udf2v211third_party10moodycamel"; +constexpr std::string_view global_namespace_prefix = "_ZN10moodycamel"; +constexpr std::string_view isolated_namespace_prefix = "_ZN6exasol3udf2v211third_party10moodycamel"; [[noreturn]] void fail(const std::string& message) { @@ -97,11 +97,11 @@ void verify_symbols(const std::string& path) } const std::string name = read_string(file, string_table.sh_offset + symbol.st_name, string_table.sh_size - symbol.st_name); - if (name.starts_with(kGlobalNamespacePrefix)) + if (name.starts_with(global_namespace_prefix)) { fail("queue library exports a global moodycamel symbol: " + name); } - if (name.starts_with(kIsolatedNamespacePrefix)) + if (name.starts_with(isolated_namespace_prefix)) { found_isolated_symbol = true; } diff --git a/udf-runner-cpp/v2/queue_fuzz_test.cc b/udf-runner-cpp/v2/queue_fuzz_test.cc index 1a5989a..23936ea 100644 --- a/udf-runner-cpp/v2/queue_fuzz_test.cc +++ b/udf-runner-cpp/v2/queue_fuzz_test.cc @@ -14,145 +14,184 @@ #include #include -namespace { +namespace +{ constexpr std::size_t kMaxOperations = 32; -constexpr std::size_t kMaxBatchSize = 4; +constexpr std::size_t kMaxBatchSize = 4; -using Byte = std::byte; +using Byte = std::byte; using Value = std::uint64_t; -[[noreturn]] void fuzz_failure() { - // Queue corruption, loss, duplication, or reordering is a finding. Keep - // this independent from assert(), which may be disabled in fuzz builds. - std::abort(); +[[noreturn]] void fuzz_failure() +{ + // Queue corruption, loss, duplication, or reordering is a finding. Keep + // this independent from assert(), which may be disabled in fuzz builds. + std::abort(); } -class StartGate { +class StartGate +{ public: - explicit StartGate(const std::size_t participant_count) - : participant_count_(participant_count) {} + explicit StartGate(const std::size_t participant_count) : participant_count(participant_count) + { + } - void arrive_and_wait() { - ready_.fetch_add(1, std::memory_order::seq_cst); - while (!go_.load(std::memory_order::seq_cst)) { - std::this_thread::yield(); + void arrive_and_wait() + { + ready.fetch_add(1, std::memory_order::seq_cst); + while (!go.load(std::memory_order::seq_cst)) + { + std::this_thread::yield(); + } } - } - void release() { - while (ready_.load(std::memory_order::seq_cst) != participant_count_) { - std::this_thread::yield(); + void release() + { + while (ready.load(std::memory_order::seq_cst) != participant_count) + { + std::this_thread::yield(); + } + go.store(true, std::memory_order::seq_cst); } - go_.store(true, std::memory_order::seq_cst); - } private: - const std::size_t participant_count_; - std::atomic ready_{0}; - std::atomic go_{false}; + const std::size_t participant_count; + std::atomic ready{0}; + std::atomic go{false}; }; -Value make_value(const std::size_t producer, const std::size_t sequence) { - return (static_cast(producer) << 32U) | static_cast(sequence); +Value make_value(const std::size_t producer, const std::size_t sequence) +{ + return (static_cast(producer) << 32U) | static_cast(sequence); } -void verify_results(const std::vector> &produced, - const std::vector> &consumed, - const bool preserve_order) { - std::vector expected; - std::vector actual; - for (const auto &values : produced) { - expected.insert(expected.end(), values.begin(), values.end()); - } - for (const auto &values : consumed) { - actual.insert(actual.end(), values.begin(), values.end()); - } +void verify_results(const std::vector>& produced, + const std::vector>& consumed, + const bool preserve_order) +{ + std::vector expected; + std::vector actual; + for (const auto& values : produced) + { + expected.insert(expected.end(), values.begin(), values.end()); + } + for (const auto& values : consumed) + { + actual.insert(actual.end(), values.begin(), values.end()); + } - if (preserve_order) { - if (produced.size() != 1 || consumed.size() != 1 || expected != actual) { - fuzz_failure(); + if (preserve_order) + { + if (produced.size() != 1 || consumed.size() != 1 || expected != actual) + { + fuzz_failure(); + } + return; } - return; - } - std::ranges::sort(expected); - std::ranges::sort(actual); - if (expected != actual) { - fuzz_failure(); - } + std::ranges::sort(expected); + std::ranges::sort(actual); + if (expected != actual) + { + fuzz_failure(); + } } template -void produce_operation(Queue &queue, const Byte operation_byte, - const std::size_t producer, std::size_t &sequence, - std::vector &produced) { - const unsigned int operation = std::to_integer(operation_byte); - if constexpr (Waitable) { - if ((operation & 1U) != 0) { - std::array batch{}; - const std::size_t batch_size = 1 + ((operation >> 1U) % kMaxBatchSize); - for (std::size_t item = 0; item < batch_size; ++item) { - batch[item] = make_value(producer, sequence++); - } - const std::size_t enqueued = - queue.enqueue_batch(batch.begin(), batch.begin() + batch_size); - produced.insert(produced.end(), batch.begin(), batch.begin() + enqueued); - } else { - const Value value = make_value(producer, sequence++); - if (queue.enqueue(value)) { - produced.push_back(value); - } +void produce_operation(Queue& queue, + const Byte operation_byte, + const std::size_t producer, + std::size_t& sequence, + std::vector& produced) +{ + const unsigned int operation = std::to_integer(operation_byte); + if constexpr (Waitable) + { + if ((operation & 1U) != 0) + { + std::array batch{}; + const std::size_t batch_size = 1 + ((operation >> 1U) % kMaxBatchSize); + for (std::size_t item = 0; item < batch_size; ++item) + { + batch[item] = make_value(producer, sequence++); + } + const std::size_t enqueued = + queue.enqueue_batch(batch.begin(), batch.begin() + batch_size); + produced.insert(produced.end(), batch.begin(), batch.begin() + enqueued); + } + else + { + const Value value = make_value(producer, sequence++); + if (queue.enqueue(value)) + { + produced.push_back(value); + } + } } - - } else { - const Value value = make_value(producer, sequence++); - if (queue.enqueue(value)) { - produced.push_back(value); + else + { + const Value value = make_value(producer, sequence++); + if (queue.enqueue(value)) + { + produced.push_back(value); + } } - } } template -void produce(Queue &queue, StartGate &start_gate, - const std::span operations, const std::size_t producer, - std::vector &produced, - std::atomic &producers_remaining, - std::atomic &producers_done) { - start_gate.arrive_and_wait(); - std::size_t sequence = 0; - for (const Byte operation_byte : operations) { - produce_operation(queue, operation_byte, producer, - sequence, produced); - if ((std::to_integer(operation_byte) & 4U) != 0) { - std::this_thread::yield(); +void produce(Queue& queue, + StartGate& start_gate, + const std::span operations, + const std::size_t producer, + std::vector& produced, + std::atomic& producers_remaining, + std::atomic& producers_done) +{ + start_gate.arrive_and_wait(); + std::size_t sequence = 0; + for (const Byte operation_byte : operations) + { + produce_operation(queue, operation_byte, producer, sequence, produced); + if ((std::to_integer(operation_byte) & 4U) != 0) + { + std::this_thread::yield(); + } + } + if (producers_remaining.fetch_sub(1, std::memory_order::seq_cst) == 1) + { + producers_done.store(true, std::memory_order::seq_cst); } - } - if (producers_remaining.fetch_sub(1, std::memory_order::seq_cst) == 1) { - producers_done.store(true, std::memory_order::seq_cst); - } } template -bool consume_one(Queue &queue, std::vector &consumed, - const std::atomic &producers_done) { - if constexpr (Waitable) { - queue.drain_notifications(); - } - if (Value value = 0; queue.try_dequeue(value)) { - consumed.push_back(value); - return true; - } - return !producers_done.load(std::memory_order::seq_cst); +bool consume_one(Queue& queue, + std::vector& consumed, + const std::atomic& producers_done) +{ + if constexpr (Waitable) + { + queue.drain_notifications(); + } + if (Value value = 0; queue.try_dequeue(value)) + { + consumed.push_back(value); + return true; + } + return !producers_done.load(std::memory_order::seq_cst); } template -void consume(Queue &queue, StartGate &start_gate, std::vector &consumed, - const std::atomic &producers_done) { - start_gate.arrive_and_wait(); - while (consume_one(queue, consumed, producers_done)) { - std::this_thread::yield(); - } +void consume(Queue& queue, + StartGate& start_gate, + std::vector& consumed, + const std::atomic& producers_done) +{ + start_gate.arrive_and_wait(); + while (consume_one(queue, consumed, producers_done)) + { + std::this_thread::yield(); + } } template @@ -160,101 +199,110 @@ void run_queue(const std::type_identity, const std::bool_constant, const std::span operations, const std::size_t producer_count, - const std::size_t consumer_count, const bool preserve_order) { - Queue queue; - std::vector> produced(producer_count); - std::vector> consumed(consumer_count); - for (auto &values : produced) { - values.reserve(operations.size() * (Waitable ? kMaxBatchSize : 1)); - } - for (auto &values : consumed) { - values.reserve(operations.size() * producer_count * - (Waitable ? kMaxBatchSize : 1)); - } + const std::size_t consumer_count, + const bool preserve_order) +{ + Queue queue; + std::vector> produced(producer_count); + std::vector> consumed(consumer_count); + for (auto& values : produced) + { + values.reserve(operations.size() * (Waitable ? kMaxBatchSize : 1)); + } + for (auto& values : consumed) + { + values.reserve(operations.size() * producer_count * (Waitable ? kMaxBatchSize : 1)); + } - StartGate start_gate(producer_count + consumer_count); - std::atomic producers_done{false}; - std::atomic producers_remaining{producer_count}; - std::vector producers; - std::vector consumers; - producers.reserve(producer_count); - consumers.reserve(consumer_count); + StartGate start_gate(producer_count + consumer_count); + std::atomic producers_done{false}; + std::atomic producers_remaining{producer_count}; + std::vector producers; + std::vector consumers; + producers.reserve(producer_count); + consumers.reserve(consumer_count); - for (std::size_t producer = 0; producer < producer_count; ++producer) { - producers.emplace_back( - produce, std::ref(queue), std::ref(start_gate), - operations, producer, std::ref(produced[producer]), - std::ref(producers_remaining), std::ref(producers_done)); - } + for (std::size_t producer = 0; producer < producer_count; ++producer) + { + producers.emplace_back(produce, std::ref(queue), std::ref(start_gate), + operations, producer, std::ref(produced[producer]), + std::ref(producers_remaining), std::ref(producers_done)); + } - for (std::size_t consumer = 0; consumer < consumer_count; ++consumer) { - consumers.emplace_back(consume, std::ref(queue), - std::ref(start_gate), std::ref(consumed[consumer]), - std::cref(producers_done)); - } + for (std::size_t consumer = 0; consumer < consumer_count; ++consumer) + { + consumers.emplace_back(consume, std::ref(queue), std::ref(start_gate), + std::ref(consumed[consumer]), std::cref(producers_done)); + } - start_gate.release(); - for (auto &producer : producers) { - producer.join(); - } - for (auto &consumer : consumers) { - consumer.join(); - } + start_gate.release(); + for (auto& producer : producers) + { + producer.join(); + } + for (auto& consumer : consumers) + { + consumer.join(); + } - if constexpr (Waitable) { - // Notifications are hints rather than item counts. Drain any final - // counter value after all queue elements have been consumed. - queue.drain_notifications(); - } - // A consumer can observe an empty queue while another consumer is - // completing its final dequeue. Once all consumers have joined, there is no - // longer a race, so perform a final nonblocking drain before checking the - // accounting. This also makes termination independent of scheduling. - Value value = 0; - while (queue.try_dequeue(value)) { - consumed.front().push_back(value); - } - verify_results(produced, consumed, preserve_order); + if constexpr (Waitable) + { + // Notifications are hints rather than item counts. Drain any final + // counter value after all queue elements have been consumed. + queue.drain_notifications(); + } + // A consumer can observe an empty queue while another consumer is + // completing its final dequeue. Once all consumers have joined, there is no + // longer a race, so perform a final nonblocking drain before checking the + // accounting. This also makes termination independent of scheduling. + Value value = 0; + while (queue.try_dequeue(value)) + { + consumed.front().push_back(value); + } + verify_results(produced, consumed, preserve_order); } } // namespace -extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t *data, - const std::size_t size) { - if (size < 2) { - return 0; - } +extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t* data, const std::size_t size) +{ + if (size < 2) + { + return 0; + } - const std::size_t operation_count = std::min(kMaxOperations, size - 1); - const auto *input = reinterpret_cast(data); - const unsigned int input_byte = std::to_integer(input[0]); - const unsigned int mode = input_byte & 3U; - const unsigned int producer_bit = (input_byte >> 2U) & 1U; - const unsigned int consumer_bit = (input_byte >> 3U) & 1U; - const auto *operations = input + 1; - const std::span operation_span(operations, operation_count); + const std::size_t operation_count = std::min(kMaxOperations, size - 1); + const auto* input = reinterpret_cast(data); + const unsigned int input_byte = std::to_integer(input[0]); + const unsigned int mode = input_byte & 3U; + const unsigned int producer_bit = (input_byte >> 2U) & 1U; + const unsigned int consumer_bit = (input_byte >> 3U) & 1U; + const auto* operations = input + 1; + const std::span operation_span(operations, operation_count); - switch (mode) { - case 0: - run_queue(std::type_identity>{}, - std::bool_constant{}, operation_span, 1, 1, true); - break; - case 1: - run_queue(std::type_identity>{}, - std::bool_constant{}, operation_span, 2 + producer_bit, - 2 + consumer_bit, false); - break; - case 2: - run_queue(std::type_identity>{}, - std::bool_constant{}, operation_span, 1, 1, true); - break; - case 3: - run_queue(std::type_identity>{}, - std::bool_constant{}, operation_span, 2 + producer_bit, - 2 + consumer_bit, false); - break; - default: - fuzz_failure(); - } - return 0; + switch (mode) + { + case 0: + run_queue(std::type_identity>{}, + std::bool_constant{}, operation_span, 1, 1, true); + break; + case 1: + run_queue(std::type_identity>{}, + std::bool_constant{}, operation_span, 2 + producer_bit, + 2 + consumer_bit, false); + break; + case 2: + run_queue(std::type_identity>{}, + std::bool_constant{}, operation_span, 1, 1, true); + break; + case 3: + run_queue(std::type_identity>{}, + std::bool_constant{}, operation_span, 2 + producer_bit, + 2 + consumer_bit, false); + break; + default: + fuzz_failure(); + } + return 0; } diff --git a/udf-runner-cpp/v2/sonar-project.properties b/udf-runner-cpp/v2/sonar-project.properties index 12255e0..f1e7c43 100644 --- a/udf-runner-cpp/v2/sonar-project.properties +++ b/udf-runner-cpp/v2/sonar-project.properties @@ -5,7 +5,7 @@ sonar.projectKey=udf-runner-cpp # in v2 while excluding vendored third-party sources. sonar.sources=. sonar.exclusions=third_party/**,bazel-*/** -sonar.coverage.exclusions=**/*_test.cc,**/*_fuzz_test.cc,**/json_schema_fuzzing.hpp,**/event_fd.cc +sonar.coverage.exclusions=**/*_test.cc,**/*_fuzz_test.cc,**/json_schema_fuzzing.hpp,**/event_fd.cc,**/arrow_c_data_demo.cc # Bazel's SonarQube coverage generator writes this generic coverage report. sonar.coverageReportPaths=bazel-out/_coverage/_coverage_report.dat diff --git a/udf-runner-cpp/v2/test_utils/json_schema_fuzzing.hpp b/udf-runner-cpp/v2/test_utils/json_schema_fuzzing.hpp index bfe0bbd..1c17f5c 100644 --- a/udf-runner-cpp/v2/test_utils/json_schema_fuzzing.hpp +++ b/udf-runner-cpp/v2/test_utils/json_schema_fuzzing.hpp @@ -10,61 +10,70 @@ #include -namespace exasol::udf::v2::fuzzing { +namespace exasol::udf::v2::fuzzing +{ namespace isolated_nlohmann = exasol::udf::v2::third_party::nlohmann; -using Json = isolated_nlohmann::json; -using JsonValidator = isolated_nlohmann::json_schema::json_validator; +using Json = isolated_nlohmann::json; +using JsonValidator = isolated_nlohmann::json_schema::json_validator; -class SchemaReadError final : public std::runtime_error { +class SchemaReadError final : public std::runtime_error +{ public: - using std::runtime_error::runtime_error; + using std::runtime_error::runtime_error; }; -inline Json ReadSchema(const char *path) { - std::ifstream input(path); - if (!input.good()) { - throw SchemaReadError(std::string("Unable to read JSON schema: ") + path); - } - return Json::parse(input); +inline Json read_schema(const char* path) +{ + std::ifstream input(path); + if (!input.good()) + { + throw SchemaReadError(std::string("Unable to read JSON schema: ") + path); + } + return Json::parse(input); } -class SchemaValidator { +class SchemaValidator +{ public: - explicit SchemaValidator(const char *root_schema_path) { - validator_.set_root_schema(ReadSchema(root_schema_path)); - } - - void Validate(const uint8_t *data, std::size_t size) const noexcept { - if (size == 0) { - return; + explicit SchemaValidator(const char* root_schema_path) + { + this->validator.set_root_schema(read_schema(root_schema_path)); } - try { - const auto input = - Json::parse(std::string(reinterpret_cast(data), size)); - validator_.validate(input); - } catch (const std::exception &exception) { - // Parse and schema-validation failures are expected input - // outcomes. Sanitizer findings and other process failures still - // terminate the fuzz target. - static_cast(exception); + void validate(const uint8_t* data, std::size_t size) const noexcept + { + if (size == 0) + { + return; + } + + try + { + const auto input = Json::parse(std::string(reinterpret_cast(data), size)); + this->validator.validate(input); + } + catch (const std::exception& exception) + { + // Parse and schema-validation failures are expected input + // outcomes. Sanitizer findings and other process failures still + // terminate the fuzz target. + static_cast(exception); + } } - } private: - JsonValidator validator_{ - [](const isolated_nlohmann::json_uri &, Json &schema) { + JsonValidator validator{[](const isolated_nlohmann::json_uri&, Json& schema) { // The import and export schemas are the only schemas with an external // reference, and both reference this schema. - schema = ReadSchema("json_schema/connection_information.schema.json"); - }}; + schema = read_schema("json_schema/connection_information.schema.json"); + }}; }; -inline void FuzzJsonSchema(const uint8_t *data, std::size_t size, - const char *schema_path) { - static const auto validator = std::make_unique(schema_path); - validator->Validate(data, size); +inline void fuzz_json_schema(const uint8_t* data, std::size_t size, const char* schema_path) +{ + static const auto validator = std::make_unique(schema_path); + validator->validate(data, size); } } // namespace exasol::udf::v2::fuzzing diff --git a/udf-runner-cpp/v2/tools/clang-tidy/.clang-tidy b/udf-runner-cpp/v2/tools/clang-tidy/.clang-tidy index ccc41d9..aa62fa2 100644 --- a/udf-runner-cpp/v2/tools/clang-tidy/.clang-tidy +++ b/udf-runner-cpp/v2/tools/clang-tidy/.clang-tidy @@ -463,6 +463,7 @@ Checks: > # -hicpp-no-malloc, # cppcoreguidelines-no-malloc # -hicpp-noexcept-move, # performance-noexcept-move-constructor # -hicpp-special-member-functions, # cppcoreguidelines-special-member-functions + # -hicpp-static-assert, # misc-static-assert # -hicpp-undelegated-constructor, # bugprone-undelegated-constructor # -hicpp-uppercase-literal-suffix, # readability-uppercase-literal-suffix @@ -551,13 +552,36 @@ Checks: > # -openmp-exception-escape, # -openmp-use-default-none, # -zircon-temporary-objects, -HeaderFilterRegex: "" +HeaderFilterRegex: '(^|/)(udf-runner-cpp/v2|_main)/.*' +ExcludeHeaderFilterRegex: '(^|/)(external|third_party)/.*' WarningsAsErrors: "*" CheckOptions: - key: hicpp-signed-bitwise.IgnorePositiveIntegerLiterals value: 'true' - key: readability-magic-numbers.IgnoredIntegerValues value: '1;2;3;4;10' + - key: readability-identifier-naming.ClassCase + value: CamelCase + - key: readability-identifier-naming.EnumCase + value: CamelCase + - key: readability-identifier-naming.EnumConstantCase + value: CamelCase + - key: readability-identifier-naming.FunctionCase + value: lower_case + - key: readability-identifier-naming.FunctionIgnoredRegexp + value: '^(LLVMFuzzerTestOneInput)$' + - key: readability-identifier-naming.VariableCase + value: lower_case + - key: readability-identifier-naming.ParameterCase + value: lower_case + - key: readability-identifier-naming.ClassMemberCase + value: lower_case + - key: readability-identifier-naming.ClassMemberPrefix + value: '' + - key: readability-identifier-naming.ClassMemberSuffix + value: '' + - key: readability-identifier-naming.NamespaceCase + value: lower_case ExtraArgsBefore: - "-std=c++17" - "-xc++" diff --git a/udf-runner-cpp/v2/tools/clang-tidy/clang-tidy-wrapper.sh b/udf-runner-cpp/v2/tools/clang-tidy/clang-tidy-wrapper.sh index 3338257..f5897a4 100755 --- a/udf-runner-cpp/v2/tools/clang-tidy/clang-tidy-wrapper.sh +++ b/udf-runner-cpp/v2/tools/clang-tidy/clang-tidy-wrapper.sh @@ -1,2 +1,13 @@ #!/bin/bash -exec clang-tidy-22 --removed-arg=-fno-canonical-system-headers "$@" +clang_tidy="${CLANG_TIDY:-clang-tidy-22}" +removed_arg="${CLANG_TIDY_REMOVED_ARG--fno-canonical-system-headers}" +clang_tidy_args=() + +for arg in "$@"; do + if [[ -n "$removed_arg" && "$arg" == "$removed_arg" ]]; then + continue + fi + clang_tidy_args+=("$arg") +done + +exec "$clang_tidy" "${clang_tidy_args[@]}" diff --git a/udf-runner-cpp/v2/udf_protocol.cc b/udf-runner-cpp/v2/udf_protocol.cc index e122ae1..13d2882 100644 --- a/udf-runner-cpp/v2/udf_protocol.cc +++ b/udf-runner-cpp/v2/udf_protocol.cc @@ -3,7 +3,7 @@ namespace exasol::udf::protocol { -bool VerifyFrameBuffer(const void* data, std::size_t size) +bool verify_frame_buffer(const void* data, std::size_t size) { using IsolatedVerifier = exasol::udf::v2::third_party::flatbuffers::Verifier; IsolatedVerifier verifier(static_cast(data), size); diff --git a/udf-runner-cpp/v2/udf_protocol.hpp b/udf-runner-cpp/v2/udf_protocol.hpp index b98ed73..603a96f 100644 --- a/udf-runner-cpp/v2/udf_protocol.hpp +++ b/udf-runner-cpp/v2/udf_protocol.hpp @@ -13,7 +13,7 @@ namespace exasol::udf::protocol { -bool VerifyFrameBuffer(const void* data, std::size_t size); +bool verify_frame_buffer(const void* data, std::size_t size); } // namespace exasol::udf::protocol diff --git a/udf-runner-cpp/v2/udf_protocol_test.cc b/udf-runner-cpp/v2/udf_protocol_test.cc index 88b8d83..abcec30 100644 --- a/udf-runner-cpp/v2/udf_protocol_test.cc +++ b/udf-runner-cpp/v2/udf_protocol_test.cc @@ -12,7 +12,8 @@ int main() const auto frame = exasol::udf::protocol::CreateFrame(builder, 7, message); builder.Finish(frame); - assert(exasol::udf::protocol::VerifyFrameBuffer(builder.GetBufferPointer(), builder.GetSize())); + assert( + exasol::udf::protocol::verify_frame_buffer(builder.GetBufferPointer(), builder.GetSize())); const auto* decoded = exasol::udf::protocol::GetFrame(builder.GetBufferPointer()); assert(decoded->stream_id() == 7); assert(decoded->message()->open_call()->call_name()->str() == "example"); diff --git a/udf-runner-cpp/v2/waitable_queue_benchmark.cc b/udf-runner-cpp/v2/waitable_queue_benchmark.cc index bb74473..9d22d5c 100644 --- a/udf-runner-cpp/v2/waitable_queue_benchmark.cc +++ b/udf-runner-cpp/v2/waitable_queue_benchmark.cc @@ -40,7 +40,7 @@ struct TimedItem // Includes raw enqueue and dequeue only; this is the queue-operation baseline // for the waitable round-trip benchmark. -void BM_RawSpscRoundTrip(benchmark::State& state) +void bm_raw_spsc_round_trip(benchmark::State& state) { exasol::udf::v2::SpscQueue queue(1024); for (const auto iteration : state) @@ -56,7 +56,7 @@ void BM_RawSpscRoundTrip(benchmark::State& state) // Includes enqueue, eventfd notification draining, and dequeue. The eventfd // write is part of the measured round trip. -void BM_WaitableSpscRoundTrip(benchmark::State& state) +void bm_waitable_spsc_round_trip(benchmark::State& state) { exasol::udf::v2::WaitableSpscQueue queue(exasol::udf::v2::SpscQueue(1024)); for (const auto iteration : state) @@ -73,7 +73,7 @@ void BM_WaitableSpscRoundTrip(benchmark::State& state) // Includes wait_enqueue and dequeue on a non-full blocking SPSC queue. The // benchmark measures the uncontended fast path rather than intentional waits. -void BM_BlockingSpscRoundTrip(benchmark::State& state) +void bm_blocking_spsc_round_trip(benchmark::State& state) { exasol::udf::v2::SpscCircularBuffer queue(1024); for (const auto iteration : state) @@ -90,7 +90,7 @@ void BM_BlockingSpscRoundTrip(benchmark::State& state) // Enqueue-only benchmarks pause timing while removing the item so the queue // remains empty for the next iteration. The waitable case includes its // eventfd write; notification draining is cleanup and is not timed. -void BM_RawSpscEnqueueLatency(benchmark::State& state) +void bm_raw_spsc_enqueue_latency(benchmark::State& state) { exasol::udf::v2::SpscQueue queue(1024); for (const auto iteration : state) @@ -106,7 +106,7 @@ void BM_RawSpscEnqueueLatency(benchmark::State& state) state.SetItemsProcessed(state.iterations()); } -void BM_WaitableSpscEnqueueLatency(benchmark::State& state) +void bm_waitable_spsc_enqueue_latency(benchmark::State& state) { exasol::udf::v2::WaitableSpscQueue queue(exasol::udf::v2::SpscQueue(1024)); for (const auto iteration : state) @@ -123,7 +123,7 @@ void BM_WaitableSpscEnqueueLatency(benchmark::State& state) state.SetItemsProcessed(state.iterations()); } -void BM_BlockingSpscEnqueueLatency(benchmark::State& state) +void bm_blocking_spsc_enqueue_latency(benchmark::State& state) { exasol::udf::v2::SpscCircularBuffer queue(1024); for (const auto iteration : state) @@ -142,7 +142,7 @@ void BM_BlockingSpscEnqueueLatency(benchmark::State& state) // Raw and waitable batch benchmarks use the same batch sizes. The waitable // queue emits one eventfd notification after the entire batch, exposing how // batching amortizes notification overhead. -void BM_RawSpscBatch(benchmark::State& state) +void bm_raw_spsc_batch(benchmark::State& state) { const auto batch_size = static_cast(state.range(0)); const std::vector batch(batch_size, 1); @@ -165,7 +165,7 @@ void BM_RawSpscBatch(benchmark::State& state) state.SetItemsProcessed(state.iterations() * static_cast(batch_size)); } -void BM_WaitableSpscBatch(benchmark::State& state) +void bm_waitable_spsc_batch(benchmark::State& state) { const auto batch_size = static_cast(state.range(0)); const std::vector batch(batch_size, 1); @@ -190,7 +190,7 @@ void BM_WaitableSpscBatch(benchmark::State& state) // and dequeue. One item is outstanding at a time, so the result measures // wakeup latency rather than latency caused by queue backlog. The producer // handshake is outside the manually recorded interval. -void BM_WaitableSpscEpollLatency(benchmark::State& state) +void bm_waitable_spsc_epoll_latency(benchmark::State& state) { exasol::udf::v2::WaitableSpscQueue queue{exasol::udf::v2::SpscQueue(8)}; const int epoll_fd = ::epoll_create1(EPOLL_CLOEXEC); @@ -264,12 +264,12 @@ void BM_WaitableSpscEpollLatency(benchmark::State& state) } // namespace -BENCHMARK(BM_RawSpscRoundTrip); -BENCHMARK(BM_WaitableSpscRoundTrip); -BENCHMARK(BM_BlockingSpscRoundTrip); -BENCHMARK(BM_RawSpscEnqueueLatency); -BENCHMARK(BM_WaitableSpscEnqueueLatency); -BENCHMARK(BM_BlockingSpscEnqueueLatency); -BENCHMARK(BM_RawSpscBatch)->Args({1})->Args({8})->Args({64})->Args({256}); -BENCHMARK(BM_WaitableSpscBatch)->Args({1})->Args({8})->Args({64})->Args({256}); -BENCHMARK(BM_WaitableSpscEpollLatency)->UseManualTime(); +BENCHMARK(bm_raw_spsc_round_trip); +BENCHMARK(bm_waitable_spsc_round_trip); +BENCHMARK(bm_blocking_spsc_round_trip); +BENCHMARK(bm_raw_spsc_enqueue_latency); +BENCHMARK(bm_waitable_spsc_enqueue_latency); +BENCHMARK(bm_blocking_spsc_enqueue_latency); +BENCHMARK(bm_raw_spsc_batch)->Args({1})->Args({8})->Args({64})->Args({256}); +BENCHMARK(bm_waitable_spsc_batch)->Args({1})->Args({8})->Args({64})->Args({256}); +BENCHMARK(bm_waitable_spsc_epoll_latency)->UseManualTime();