diff --git a/src/OpenTelemetry/Trace/Sampler/SamplingResult.cs b/src/OpenTelemetry/Trace/Sampler/SamplingResult.cs index 21e5f92bee5..a65b3b67f92 100644 --- a/src/OpenTelemetry/Trace/Sampler/SamplingResult.cs +++ b/src/OpenTelemetry/Trace/Sampler/SamplingResult.cs @@ -8,6 +8,10 @@ namespace OpenTelemetry.Trace; /// public readonly struct SamplingResult : IEquatable { + // Null when no attributes were supplied; avoids a GetEnumerator() call (and enumerator boxing) + // on the hot path inside TracerProviderSdk when the sampler returns no attributes. + private readonly IEnumerable>? attributesField; + /// /// Initializes a new instance of the struct. /// @@ -61,7 +65,7 @@ public SamplingResult(SamplingDecision decision, IEnumerable /// Gets a map of attributes associated with the sampling decision. /// - public IEnumerable> Attributes { get; } + public IEnumerable> Attributes => this.attributesField ?? []; /// /// Gets the tracestate. /// public string? TraceStateString { get; } + // Internal accessor used by TracerProviderSdk to skip iteration entirely when null. + internal IEnumerable>? AttributesOrNull => this.attributesField; + /// /// Compare two for equality. /// diff --git a/src/OpenTelemetry/Trace/TracerProviderSdk.cs b/src/OpenTelemetry/Trace/TracerProviderSdk.cs index be47ed5cf58..1f689cb9c7f 100644 --- a/src/OpenTelemetry/Trace/TracerProviderSdk.cs +++ b/src/OpenTelemetry/Trace/TracerProviderSdk.cs @@ -222,7 +222,7 @@ internal TracerProviderSdk( if (this.Sampler is AlwaysOnSampler) { - activityListener.Sample = (ref options) => + activityListener.Sample = static (ref _) => !Sdk.SuppressInstrumentation ? ActivitySamplingResult.AllDataAndRecorded : ActivitySamplingResult.None; this.getRequestedDataAction = this.RunGetRequestedDataAlwaysOnSampler; } @@ -479,9 +479,12 @@ private static ActivitySamplingResult ComputeActivitySamplingResult( if (activitySamplingResult > ActivitySamplingResult.PropagationData) { - foreach (var att in samplingResult.Attributes) + if (samplingResult.AttributesOrNull is { } attributes) { - options.SamplingTags.Add(att.Key, att.Value); + foreach (var att in attributes) + { + options.SamplingTags.Add(att.Key, att.Value); + } } } @@ -575,9 +578,12 @@ private void RunGetRequestedDataOtherSampler(Activity activity) if (samplingResult.Decision != SamplingDecision.Drop) { - foreach (var att in samplingResult.Attributes) + if (samplingResult.AttributesOrNull is { } attributes) { - activity.SetTag(att.Key, att.Value); + foreach (var att in attributes) + { + activity.SetTag(att.Key, att.Value); + } } } diff --git a/test/Benchmarks/Trace/SamplingResultBenchmarks.cs b/test/Benchmarks/Trace/SamplingResultBenchmarks.cs new file mode 100644 index 00000000000..861198ad6e7 --- /dev/null +++ b/test/Benchmarks/Trace/SamplingResultBenchmarks.cs @@ -0,0 +1,134 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using BenchmarkDotNet.Attributes; +using OpenTelemetry; +using OpenTelemetry.Trace; + +namespace Benchmarks.Trace; + +#pragma warning disable CA1001 // Types that own disposable fields should be disposable - handled by GlobalCleanup +[MemoryDiagnoser] +public class SamplingResultBenchmarks +#pragma warning restore CA1001 // Types that own disposable fields should be disposable - handled by GlobalCleanup +{ + private static readonly KeyValuePair[] SamplingAttributes = + [ + new("sampling.priority", 1), + new("sampling.rule", "always"), + ]; + + private ActivitySource? sourceNoAttributes; + private ActivitySource? sourceWithAttributeArray; + private ActivitySource? sourceWithAttributeList; + private ActivitySource? sourceDrop; + private ActivitySource? sourceParentBased; + + private ActivityContext sampledRemoteParent; + + private TracerProvider? providerNoAttributes; + private TracerProvider? providerWithAttributeArray; + private TracerProvider? providerWithAttributeList; + private TracerProvider? providerDrop; + private TracerProvider? providerParentBased; + + [GlobalSetup] + public void Setup() + { + this.sourceNoAttributes = new ActivitySource("SamplingResult.NoAttributes"); + this.sourceWithAttributeArray = new ActivitySource("SamplingResult.WithAttributeArray"); + this.sourceWithAttributeList = new ActivitySource("SamplingResult.WithAttributeList"); + this.sourceDrop = new ActivitySource("SamplingResult.Drop"); + this.sourceParentBased = new ActivitySource("SamplingResult.ParentBased"); + + this.sampledRemoteParent = new ActivityContext( + ActivityTraceId.CreateRandom(), + ActivitySpanId.CreateRandom(), + ActivityTraceFlags.Recorded, + traceState: null, + isRemote: true); + + // Sampler returns RecordAndSample with no attributes - the common case. + this.providerNoAttributes = Sdk.CreateTracerProviderBuilder() + .AddSource(this.sourceNoAttributes.Name) + .SetSampler(new DelegateSampler(_ => new SamplingResult(SamplingDecision.RecordAndSample))) + .Build(); + + // Sampler returns attributes as a T[] - exercises the array fast-path. + this.providerWithAttributeArray = Sdk.CreateTracerProviderBuilder() + .AddSource(this.sourceWithAttributeArray.Name) + .SetSampler(new DelegateSampler(_ => new SamplingResult(SamplingDecision.RecordAndSample, SamplingAttributes))) + .Build(); + + // Sampler returns attributes as a List - exercises the IEnumerable fallback path. + this.providerWithAttributeList = Sdk.CreateTracerProviderBuilder() + .AddSource(this.sourceWithAttributeList.Name) + .SetSampler(new DelegateSampler(_ => new SamplingResult(SamplingDecision.RecordAndSample, [.. SamplingAttributes]))) + .Build(); + + // Sampler drops the span - attribute loop is never entered. + this.providerDrop = Sdk.CreateTracerProviderBuilder() + .AddSource(this.sourceDrop.Name) + .SetSampler(new DelegateSampler(_ => new SamplingResult(SamplingDecision.Drop))) + .Build(); + + // ParentBasedSampler with AlwaysOnSampler root - realistic production default. + this.providerParentBased = Sdk.CreateTracerProviderBuilder() + .AddSource(this.sourceParentBased.Name) + .SetSampler(new ParentBasedSampler(new AlwaysOnSampler())) + .Build(); + } + + [GlobalCleanup] + public void Cleanup() + { + this.sourceNoAttributes?.Dispose(); + this.sourceWithAttributeArray?.Dispose(); + this.sourceWithAttributeList?.Dispose(); + this.sourceDrop?.Dispose(); + this.sourceParentBased?.Dispose(); + + this.providerNoAttributes?.Dispose(); + this.providerWithAttributeArray?.Dispose(); + this.providerWithAttributeList?.Dispose(); + this.providerDrop?.Dispose(); + this.providerParentBased?.Dispose(); + } + + [Benchmark(Baseline = true)] + public void NoAttributes() + { + using var activity = this.sourceNoAttributes!.StartActivity("Benchmark", ActivityKind.Server, this.sampledRemoteParent); + } + + [Benchmark] + public void WithAttributeArray() + { + using var activity = this.sourceWithAttributeArray!.StartActivity("Benchmark", ActivityKind.Server, this.sampledRemoteParent); + } + + [Benchmark] + public void WithAttributeList() + { + using var activity = this.sourceWithAttributeList!.StartActivity("Benchmark", ActivityKind.Server, this.sampledRemoteParent); + } + + [Benchmark] + public void Drop() + { + using var activity = this.sourceDrop!.StartActivity("Benchmark", ActivityKind.Server, this.sampledRemoteParent); + } + + [Benchmark] + public void ParentBasedSampled() + { + using var activity = this.sourceParentBased!.StartActivity("Benchmark", ActivityKind.Server, this.sampledRemoteParent); + } + + private sealed class DelegateSampler(Func sample) : Sampler + { + public override SamplingResult ShouldSample(in SamplingParameters samplingParameters) + => sample(samplingParameters); + } +}