diff --git a/exporters/otlp/BUILD b/exporters/otlp/BUILD index 60311dc949..826d0987e0 100644 --- a/exporters/otlp/BUILD +++ b/exporters/otlp/BUILD @@ -1005,3 +1005,20 @@ otel_cc_benchmark( "//sdk/src/metrics", ], ) + +otel_cc_benchmark( + name = "otlp_recordable_benchmark", + srcs = ["test/otlp_recordable_benchmark.cc"], + tags = [ + "benchmark", + "otlp", + "test", + ], + deps = [ + ":otlp_recordable", + "//sdk/src/metrics", + "//sdk/src/resource", + "//sdk/src/trace", + "@com_github_opentelemetry_proto//:trace_service_proto_cc", + ], +) diff --git a/exporters/otlp/CMakeLists.txt b/exporters/otlp/CMakeLists.txt index 1d2852dc73..0e1ea1791b 100644 --- a/exporters/otlp/CMakeLists.txt +++ b/exporters/otlp/CMakeLists.txt @@ -1201,3 +1201,13 @@ if(WITH_BENCHMARK AND WITH_OTLP_HTTP) opentelemetry_resources benchmark::benchmark ${CMAKE_THREAD_LIBS_INIT}) endif() + +if(WITH_BENCHMARK) + add_executable(otlp_recordable_benchmark test/otlp_recordable_benchmark.cc) + + target_link_libraries( + otlp_recordable_benchmark + PRIVATE opentelemetry_otlp_recordable opentelemetry_trace + opentelemetry_metrics opentelemetry_resources benchmark::benchmark + ${CMAKE_THREAD_LIBS_INIT}) +endif() diff --git a/exporters/otlp/test/otlp_recordable_benchmark.cc b/exporters/otlp/test/otlp_recordable_benchmark.cc new file mode 100644 index 0000000000..6c672c9217 --- /dev/null +++ b/exporters/otlp/test/otlp_recordable_benchmark.cc @@ -0,0 +1,343 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// clang-format off +// +// 2026-06-18T21:05:11+00:00 +// Running /home/devuser/build/exporters/otlp/otlp_recordable_benchmark +// Run on (32 X 5175.97 MHz CPU s) +// CPU Caches: +// L1 Data 48 KiB (x16) +// L1 Instruction 32 KiB (x16) +// L2 Unified 2048 KiB (x16) +// L3 Unified 36864 KiB (x1) +// Load Average: 0.61, 1.30, 1.33 +// ----------------------------------------------------------------------------------- +// Benchmark Time CPU Iterations +// ----------------------------------------------------------------------------------- +// OtlpSpanFixture/Minimal_mean 355 ns 357 ns 5 +// OtlpSpanFixture/Minimal_median 355 ns 358 ns 5 +// OtlpSpanFixture/Minimal_stddev 0.489 ns 0.790 ns 5 +// OtlpSpanFixture/Minimal_cv 0.14 % 0.22 % 5 +// OtlpSpanFixture/Nominal_mean 617 ns 619 ns 5 +// OtlpSpanFixture/Nominal_median 617 ns 620 ns 5 +// OtlpSpanFixture/Nominal_stddev 0.960 ns 1.25 ns 5 +// OtlpSpanFixture/Nominal_cv 0.16 % 0.20 % 5 +// OtlpSpanFixture/MaxAttributes_mean 8014 ns 8016 ns 5 items_per_second=16.0018M/s +// OtlpSpanFixture/MaxAttributes_median 7737 ns 7739 ns 5 items_per_second=16.5389M/s +// OtlpSpanFixture/MaxAttributes_stddev 419 ns 418 ns 5 items_per_second=819.876k/s +// OtlpSpanFixture/MaxAttributes_cv 5.23 % 5.22 % 5 items_per_second=5.12% +// BM_PopulateRequest/span_count:1_mean 0.398 us 0.398 us 5 items_per_second=2.51517M/s +// BM_PopulateRequest/span_count:1_median 0.397 us 0.397 us 5 items_per_second=2.52167M/s +// BM_PopulateRequest/span_count:1_stddev 0.002 us 0.002 us 5 items_per_second=12.1068k/s +// BM_PopulateRequest/span_count:1_cv 0.48 % 0.48 % 5 items_per_second=0.48% +// BM_PopulateRequest/span_count:512_mean 26.3 us 26.3 us 5 items_per_second=19.4714M/s +// BM_PopulateRequest/span_count:512_median 26.3 us 26.3 us 5 items_per_second=19.4746M/s +// BM_PopulateRequest/span_count:512_stddev 0.078 us 0.077 us 5 items_per_second=56.7323k/s +// BM_PopulateRequest/span_count:512_cv 0.30 % 0.29 % 5 items_per_second=0.29% +// +// clang-format on + +#include + +#include +#include +#include +#include +#include +#include + +#include "opentelemetry/common/attribute_value.h" +#include "opentelemetry/common/timestamp.h" +#include "opentelemetry/exporters/otlp/otlp_recordable.h" +#include "opentelemetry/exporters/otlp/otlp_recordable_utils.h" +#include "opentelemetry/exporters/otlp/protobuf_include_prefix.h" +#include "opentelemetry/exporters/otlp/protobuf_include_suffix.h" +#include "opentelemetry/nostd/shared_ptr.h" +#include "opentelemetry/nostd/span.h" +#include "opentelemetry/nostd/string_view.h" +#include "opentelemetry/nostd/utility.h" +#include "opentelemetry/sdk/common/exporter_utils.h" +#include "opentelemetry/sdk/instrumentationscope/instrumentation_scope.h" +#include "opentelemetry/sdk/resource/resource.h" +#include "opentelemetry/sdk/trace/exporter.h" +#include "opentelemetry/sdk/trace/processor.h" +#include "opentelemetry/sdk/trace/recordable.h" +#include "opentelemetry/sdk/trace/tracer_provider.h" +#include "opentelemetry/trace/span.h" +#include "opentelemetry/trace/span_context.h" +#include "opentelemetry/trace/span_id.h" +#include "opentelemetry/trace/span_metadata.h" +#include "opentelemetry/trace/trace_flags.h" +#include "opentelemetry/trace/trace_id.h" +#include "opentelemetry/trace/tracer.h" + +// clang-format off +#include "opentelemetry/exporters/otlp/protobuf_include_prefix.h" // IWYU pragma: keep +#include "google/protobuf/arena.h" +#include "opentelemetry/proto/collector/trace/v1/trace_service.pb.h" +#include "opentelemetry/exporters/otlp/protobuf_include_suffix.h" // IWYU pragma: keep +// clang-format on + +namespace otlp = opentelemetry::exporter::otlp; +namespace trace_sdk = opentelemetry::sdk::trace; +namespace trace_api = opentelemetry::trace; + +namespace +{ + +// A no-op SpanExporter whose MakeRecordable creates an OtlpRecordable. +// This exercises the full OTLP recordable population path via the public API. +class NullSpanExporter final : public trace_sdk::SpanExporter +{ +public: + std::unique_ptr MakeRecordable() noexcept override + { + return std::make_unique(); + } + + opentelemetry::sdk::common::ExportResult Export( + const opentelemetry::nostd::span> &) noexcept override + { + return opentelemetry::sdk::common::ExportResult::kSuccess; + } + + bool ForceFlush(std::chrono::microseconds) noexcept override { return true; } + + bool Shutdown(std::chrono::microseconds) noexcept override { return true; } +}; + +// A span processor that queues completed recordables into an internal buffer +// without serialising or destroying them mirroring the batch processor. +class BufferingProcessor final : public trace_sdk::SpanProcessor +{ +public: + using Buffer = std::vector>; + + explicit BufferingProcessor(std::unique_ptr exporter) + : exporter_(std::move(exporter)) + {} + + std::unique_ptr MakeRecordable() noexcept override + { + return exporter_->MakeRecordable(); + } + + void OnStart(trace_sdk::Recordable &, const opentelemetry::trace::SpanContext &) noexcept override + {} + + void OnEnd(std::unique_ptr &&span) noexcept override + { + buffer_.push_back(std::move(span)); + } + + bool ForceFlush(std::chrono::microseconds) noexcept override + { + buffer_.clear(); + return true; + } + bool Shutdown(std::chrono::microseconds) noexcept override { return true; } + +private: + std::unique_ptr exporter_; + Buffer buffer_; +}; + +const opentelemetry::sdk::resource::Resource &TestResource() +{ + static const auto resource = opentelemetry::sdk::resource::Resource::Create( + {{"service.name", opentelemetry::nostd::string_view{"benchmark_service"}}, + {"service.version", opentelemetry::nostd::string_view{"1.0.0"}}}); + return resource; +} + +const opentelemetry::sdk::instrumentationscope::InstrumentationScope &TestScope() +{ + static auto scope = opentelemetry::sdk::instrumentationscope::InstrumentationScope::Create( + "benchmark_scope", "1.0.0", "https://opentelemetry.io/schemas/1.24.0", + {{"scope.source", "benchmark"}}); + return *scope; +} + +// Benchmark fixture: each benchmark function gets its own TracerProvider backed +// by a BufferingProcessor with a NullSpanExporter. +class OtlpSpanFixture : public benchmark::Fixture +{ +public: + void SetUp(benchmark::State &) override + { + auto processor = std::make_unique(std::make_unique()); + provider_ = std::make_shared(std::move(processor)); + tracer_ = provider_->GetTracer(TestScope().GetName(), TestScope().GetVersion(), + TestScope().GetSchemaURL()); + } + +protected: + std::shared_ptr provider_; + opentelemetry::nostd::shared_ptr tracer_; +}; + +// Benchmark argument values. +constexpr int kMaxAttributes = 128; // default attribute count limit +constexpr int kBatchSmall = 1; // single span +constexpr int kBatchLarge = 512; // default batch size + +// Pre-built keys for the max-attributes benchmark (matches SDK default attribute limit). +const std::vector &MaxAttributeKeys() +{ + static const std::vector keys = []() { + std::vector result; + result.reserve(kMaxAttributes); + for (int key_idx = 0; key_idx < kMaxAttributes; ++key_idx) + { + result.push_back("attribute." + std::to_string(key_idx)); + } + return result; + }(); + return keys; +} + +// Build a batch of N fully-populated OtlpRecordable spans for PopulateRequest benchmarks. +std::vector> MakeSpanBatch( + std::size_t span_count) +{ + constexpr uint8_t kTraceIdBytes[opentelemetry::trace::TraceId::kSize] = {1, 2, 3, 4, 5, 6, 7, 8, + 1, 2, 3, 4, 5, 6, 7, 8}; + constexpr uint8_t kSpanIdBytes[opentelemetry::trace::SpanId::kSize] = {1, 2, 3, 4, 5, 6, 7, 8}; + constexpr uint8_t kParentIdBytes[opentelemetry::trace::SpanId::kSize] = {8, 7, 6, 5, 4, 3, 2, 1}; + const opentelemetry::trace::TraceId trace_id{kTraceIdBytes}; + const opentelemetry::trace::SpanId span_id{kSpanIdBytes}; + const opentelemetry::trace::SpanId parent_id{kParentIdBytes}; + const opentelemetry::trace::SpanContext ctx{ + trace_id, span_id, + opentelemetry::trace::TraceFlags{opentelemetry::trace::TraceFlags::kIsSampled}, true}; + const auto now = opentelemetry::common::SystemTimestamp(std::chrono::system_clock::now()); + + std::vector> batch; + batch.reserve(span_count); + for (std::size_t span_idx = 0; span_idx < span_count; ++span_idx) + { + auto recordable = std::make_unique(); + recordable->SetIdentity(ctx, parent_id); + recordable->SetName("benchmark_span"); + recordable->SetSpanKind(trace_api::SpanKind::kServer); + recordable->SetStartTime(now); + recordable->SetDuration(std::chrono::milliseconds(5)); + recordable->SetResource(TestResource()); + recordable->SetInstrumentationScope(TestScope()); + batch.push_back(std::move(recordable)); + } + return batch; +} + +std::unique_ptr CreateArena() +{ + google::protobuf::ArenaOptions options; + options.initial_block_size = 1024; + options.max_block_size = 65536; + return std::make_unique(options); +} + +} // namespace + +// Baseline: start a span and end it immediately with no attributes. +BENCHMARK_DEFINE_F(OtlpSpanFixture, Minimal)(benchmark::State &state) +{ + for (auto _ : state) + { + auto span = tracer_->StartSpan("benchmark_span"); + span->End(); + state.PauseTiming(); + provider_->ForceFlush(); + state.ResumeTiming(); + } +} +BENCHMARK_REGISTER_F(OtlpSpanFixture, Minimal)->Unit(benchmark::kNanosecond); + +// Nominal span: representative mix of attribute types passed at creation. +BENCHMARK_DEFINE_F(OtlpSpanFixture, Nominal)(benchmark::State &state) +{ + using SpanAttribute = + std::pair; + static const std::vector kAttributes = { + {"benchmark.string.short", opentelemetry::nostd::string_view{"abcdefgh"}}, + {"benchmark.string.medium", + opentelemetry::nostd::string_view{"abcdefghijklmnopqrstuvwxyz012345"}}, + {"benchmark.string.long", + opentelemetry::nostd::string_view{ + "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz01"}}, + {"benchmark.int", opentelemetry::common::AttributeValue{static_cast(1)}}, + {"benchmark.double", opentelemetry::common::AttributeValue{3.14159265358979}}, + {"benchmark.bool", opentelemetry::common::AttributeValue{true}}, + }; + for (auto _ : state) + { + auto span = tracer_->StartSpan("benchmark_span", kAttributes); + span->End(); + state.PauseTiming(); + provider_->ForceFlush(); + state.ResumeTiming(); + } +} +BENCHMARK_REGISTER_F(OtlpSpanFixture, Nominal)->Unit(benchmark::kNanosecond); + +// Max-attributes span: fills the recordable to its attribute limit (128) with string values. +BENCHMARK_DEFINE_F(OtlpSpanFixture, MaxAttributes)(benchmark::State &state) +{ + using SpanAttribute = + std::pair; + static const std::vector kAttributes = []() { + const auto &keys = MaxAttributeKeys(); + std::vector attributes; + attributes.reserve(kMaxAttributes); + for (const auto &key : keys) + { + attributes.emplace_back(key, opentelemetry::nostd::string_view{"value-string-attribute"}); + } + return attributes; + }(); + for (auto _ : state) + { + auto span = tracer_->StartSpan("benchmark_span", kAttributes); + span->End(); + state.PauseTiming(); + provider_->ForceFlush(); + state.ResumeTiming(); + } + state.SetItemsProcessed(state.iterations() * kMaxAttributes); +} +BENCHMARK_REGISTER_F(OtlpSpanFixture, MaxAttributes)->Unit(benchmark::kNanosecond); + +// Benchmark the arena allocation + OtlpRecordableUtils::PopulateRequest for a +// batch of N spans. This mirrors the path in OtlpGrpcExporter::Export. +static void BM_PopulateRequest(benchmark::State &state) +{ + const std::size_t span_count = static_cast(state.range(0)); + auto batch = MakeSpanBatch(span_count); + for (auto _ : state) + { + auto arena = CreateArena(); + auto *request = google::protobuf::Arena::Create< + opentelemetry::proto::collector::trace::v1::ExportTraceServiceRequest>(arena.get()); + + opentelemetry::exporter::otlp::OtlpRecordableUtils::PopulateRequest( + opentelemetry::nostd::span>{ + batch.data(), batch.size()}, + request); + benchmark::DoNotOptimize(request); + } + state.SetItemsProcessed(state.iterations() * state.range(0)); +} +BENCHMARK(BM_PopulateRequest) + ->ArgName("span_count") + ->Arg(kBatchSmall) + ->Arg(kBatchLarge) + ->Unit(benchmark::kMicrosecond); + +int main(int argc, char **argv) +{ + benchmark::Initialize(&argc, argv); + benchmark::RunSpecifiedBenchmarks(); + benchmark::Shutdown(); + return 0; +}