From 587daaf08b08abd351cb328af5ed4f326b8e0695 Mon Sep 17 00:00:00 2001 From: Andrew Lock Date: Fri, 21 Nov 2025 12:31:52 +0000 Subject: [PATCH 1/9] Update Jetbrains package to latest --- tracer/test/benchmarks/Benchmarks.Trace/Benchmarks.Trace.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tracer/test/benchmarks/Benchmarks.Trace/Benchmarks.Trace.csproj b/tracer/test/benchmarks/Benchmarks.Trace/Benchmarks.Trace.csproj index bbd352963c01..228fe0c41548 100644 --- a/tracer/test/benchmarks/Benchmarks.Trace/Benchmarks.Trace.csproj +++ b/tracer/test/benchmarks/Benchmarks.Trace/Benchmarks.Trace.csproj @@ -39,7 +39,7 @@ - + From 6a2f7b68a6b3d39202e22dc878a4d2fbe5525d83 Mon Sep 17 00:00:00 2001 From: Andrew Lock Date: Fri, 21 Nov 2025 12:43:18 +0000 Subject: [PATCH 2/9] Disable more services --- tracer/test/benchmarks/Benchmarks.Trace/SpanBenchmark.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tracer/test/benchmarks/Benchmarks.Trace/SpanBenchmark.cs b/tracer/test/benchmarks/Benchmarks.Trace/SpanBenchmark.cs index 765813b515a0..0ecc72e42e9f 100644 --- a/tracer/test/benchmarks/Benchmarks.Trace/SpanBenchmark.cs +++ b/tracer/test/benchmarks/Benchmarks.Trace/SpanBenchmark.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using BenchmarkDotNet.Attributes; using Datadog.Trace; +using Datadog.Trace.Agent.DiscoveryService; using Datadog.Trace.BenchmarkDotNet; using Datadog.Trace.ClrProfiler.AutoInstrumentation.ManualInstrumentation.Extensions; using Datadog.Trace.ClrProfiler.AutoInstrumentation.ManualInstrumentation.Proxies; @@ -10,6 +11,7 @@ using Datadog.Trace.Configuration; using Datadog.Trace.DuckTyping; using Datadog.Trace.ExtensionMethods; +using Datadog.Trace.Telemetry; using BindingFlags = System.Reflection.BindingFlags; using Tracer = Datadog.Trace.Tracer; using ManualTracer = DatadogTraceManual::Datadog.Trace.Tracer; @@ -35,9 +37,11 @@ static SpanBenchmark() { { ConfigurationKeys.StartupDiagnosticLogEnabled, false }, { ConfigurationKeys.TraceEnabled, false }, + { ConfigurationKeys.AgentFeaturePollingEnabled, false }, + { ConfigurationKeys.Telemetry.Enabled, false }, }); - Tracer = new Tracer(settings, new DummyAgentWriter(), null, null, null); + Tracer = new Tracer(settings, new DummyAgentWriter(), null, null, null, telemetry: NullTelemetryController.Instance, NullDiscoveryService.Instance); // Create the manual integration Dictionary manualSettings = new(); From e3dc18240f94fa682074f11c82e89ce1f179d23f Mon Sep 17 00:00:00 2001 From: Andrew Lock Date: Fri, 21 Nov 2025 12:43:27 +0000 Subject: [PATCH 3/9] Add benchmark --- .../benchmarks/Benchmarks.Trace/SpanBenchmark.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tracer/test/benchmarks/Benchmarks.Trace/SpanBenchmark.cs b/tracer/test/benchmarks/Benchmarks.Trace/SpanBenchmark.cs index 0ecc72e42e9f..b01487f138d0 100644 --- a/tracer/test/benchmarks/Benchmarks.Trace/SpanBenchmark.cs +++ b/tracer/test/benchmarks/Benchmarks.Trace/SpanBenchmark.cs @@ -93,5 +93,17 @@ public void StartFinishScope() scope.Span.SetTraceSamplingPriority(SamplingPriority.UserReject); } } + + /// + /// Starts and finishes two scopes in the same trace benchmark + /// + [Benchmark] + public void StartFinishTwoScopes() + { + using var scope1 = Tracer.StartActiveInternal("operation1"); + scope1.Span.SetTraceSamplingPriority(SamplingPriority.UserReject); + + using var scope2 = Tracer.StartActiveInternal("operation2"); + } } } From 5f173a734bea7e03719c8335a16d46e8c74f2d5a Mon Sep 17 00:00:00 2001 From: Andrew Lock Date: Fri, 21 Nov 2025 18:44:17 +0000 Subject: [PATCH 4/9] Add SpanCollection implementation similar to StringValues --- .../src/Datadog.Trace/Agent/SpanCollection.cs | 298 ++++++++++++++++++ 1 file changed, 298 insertions(+) create mode 100644 tracer/src/Datadog.Trace/Agent/SpanCollection.cs diff --git a/tracer/src/Datadog.Trace/Agent/SpanCollection.cs b/tracer/src/Datadog.Trace/Agent/SpanCollection.cs new file mode 100644 index 000000000000..9d8a6239548a --- /dev/null +++ b/tracer/src/Datadog.Trace/Agent/SpanCollection.cs @@ -0,0 +1,298 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#nullable enable + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Datadog.Trace.SourceGenerators; +#if NETFRAMEWORK +using Datadog.Trace.VendoredMicrosoftCode.System.Runtime.CompilerServices.Unsafe; +#endif + +namespace Datadog.Trace.Agent; + +/// +/// Represents zero/null, one, or many Spans in an efficient way. +/// +internal readonly struct SpanCollection : IEnumerable +{ + private readonly object? _values; + public readonly int Count; + + /// + /// Initializes a new instance of the structure using the specified Span. + /// + /// The span to include in the collection. + public SpanCollection(Span value) + { + _values = value; + Count = 1; + } + + /// + /// Initializes a new instance of the structure using the specified capacity, but no spans. + /// + /// The value to initializer + public SpanCollection(int arrayBuilderCapacity) + { + _values = new Span[arrayBuilderCapacity]; + Count = 0; + } + + /// + /// Initializes a new instance of the structure using the specified array of Spans. + /// + public SpanCollection(Span[] values, int count) + { + // We assume that the caller is "sensible" here, and doesn't set count > values.Length, + // but that will get hit "safely" elsewhere if it happens + _values = values; + Count = count; + } + + [TestingOnly] + internal SpanCollection(Span[] values) + : this(values, values.Length) + { + } + + /// + /// Gets the first span in the , or returns null if the collection is empty + /// + public Span? RootSpan + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + // Take local copy of _values so type checks remain valid even if the SpanCollection is overwritten in memory + var value = _values; + if (value is Span span) + { + return span; + } + + if (value is null) + { + return null; + } + + // Not Span, not null, can only be SpanArray + return Unsafe.As(value)[0]; + } + } + + /// + /// Gets the at the specified index. + /// + /// The Span at the specified index. + /// The zero-based index of the element to get. + public Span this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + // Take local copy of _values so type checks remain valid even if the SpanCollection is overwritten in memory + object? value = _values; + if (index < Count) + { + if (value is Span str) + { + return str; + } + else if (value != null) + { + // Not Span, not null, can only be Span[] + return Unsafe.As(value)[index]; + } + } + + return OutOfBounds(); // throws + } + } + + /// + /// Concatenates specified instance of with specified . + /// + /// The to add ti. + /// The to add. + /// The concatenation of and . + public static SpanCollection Append(in SpanCollection values, Span value) + { + // Take local copy of _values so type checks remain valid even if the SpanCollection is overwritten in memory + var current = values._values; + if (current is null) + { + return new SpanCollection(value); + } + + if (current is Span span) + { + // We use a default capacity of 4 spans + // 2 Spans would cover 25% not covered by single span case, 4 covers ~ 70%, 8 covers ~92% + return new SpanCollection([span, value, null!, null!], 2); + } + + // Not Span, not null, can only be Span[], so add the span + var array = Unsafe.As(current); + array = GrowIfNeeded(array, values.Count); + array[values.Count] = value; + return new SpanCollection(array, values.Count + 1); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static Span OutOfBounds() + { + return Array.Empty()[0]; // throws + } + + /// + /// Creates a Span array from the current object. + /// Note that this allocates if the does not have a count greater than 1 + /// + /// A Span array represented by this instance. + /// + /// If the contains a single Span internally, it is copied to a new array. + /// If the contains an array internally it returns that array instance. + /// + public ArraySegment ToArray() + { + // Take local copy of _values so type checks remain valid even if the SpanCollection is overwritten in memory + object? value = _values; + if (value is Span[] values) + { + return new ArraySegment(values, 0, Count); + } + else if (value != null) + { + // value not array, can only be Span + return new ArraySegment([Unsafe.As(value)], 0, 1); + } + else + { + return new ArraySegment(Array.Empty(), 0, 0); + } + } + + private static Span[] GrowIfNeeded(Span[] array, int currentCount) + { + if (currentCount < array.Length) + { + // The array is already big enough + return array; + } + + var newArray = new Span[array.Length * 2]; + + Array.Copy(array, 0, newArray, 0, array.Length); + + return newArray; + } + + /// Retrieves an object that can iterate through the individual Spans in this . + /// An enumerator that can be used to iterate through the . + public Enumerator GetEnumerator() + { + return new Enumerator(_values, Count); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Enumerates the Span values of a . + /// + public struct Enumerator : IEnumerator + { + private readonly Span[]? _values; + private readonly int _count; + private int _index; + private Span? _current; + + internal Enumerator(object? value, int count) + { + if (value is Span span) + { + _values = null; + _current = span; + _count = 1; + } + else + { + _current = null; + _values = Unsafe.As(value); + _count = count; + } + + _index = 0; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The to enumerate. + public Enumerator(ref SpanCollection values) + : this(values._values, values.Count) + { + } + + /// + /// Gets the element at the current position of the enumerator. + /// + public Span Current => _current!; + + object? IEnumerator.Current => _current; + + /// + /// Advances the enumerator to the next element of the . + /// + /// if the enumerator was successfully advanced to the next element; if the enumerator has passed the end of the . + public bool MoveNext() + { + int index = _index; + if (index < 0) + { + return false; + } + + var values = _values; + if (values != null) + { + if (index < _count) + { + _index = index + 1; + _current = values[index]; + return true; + } + + _index = -1; + return false; + } + + _index = -1; // sentinel value + return _current != null; + } + + void IEnumerator.Reset() => throw new NotSupportedException(); + + /// + /// Releases all resources used by the . + /// + public void Dispose() + { + } + } +} From 4be56a44217dc7d9e7029853a9a68968da1a7d8c Mon Sep 17 00:00:00 2001 From: Andrew Lock Date: Fri, 21 Nov 2025 18:39:49 +0000 Subject: [PATCH 5/9] Add unit tests for SpanCollection --- .../Agent/SpanCollectionTests.cs | 218 ++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 tracer/test/Datadog.Trace.Tests/Agent/SpanCollectionTests.cs diff --git a/tracer/test/Datadog.Trace.Tests/Agent/SpanCollectionTests.cs b/tracer/test/Datadog.Trace.Tests/Agent/SpanCollectionTests.cs new file mode 100644 index 000000000000..434bed6bd8c2 --- /dev/null +++ b/tracer/test/Datadog.Trace.Tests/Agent/SpanCollectionTests.cs @@ -0,0 +1,218 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Collections.Generic; +using Datadog.Trace.Agent; +using FluentAssertions; +using Xunit; + +namespace Datadog.Trace.Tests.Agent; + +public class SpanCollectionTests +{ + [Fact] + public void DefaultValue() + { + SpanCollection collection = default; + + collection.Count.Should().Be(0); + collection.RootSpan.Should().BeNull(); + collection.ToArray().Array.Should().BeEmpty(); + foreach (var span in collection) + { + Assert.Fail("We shouldn't have a span to enumerate: " + span); + } + + FluentActions.Invoking(() => collection[0]).Should().Throw(); + } + + [Fact] + public void SingleSpanConstructor() + { + var span = CreateSpan(); + var collection = new SpanCollection(span); + + collection.Count.Should().Be(1); + collection.RootSpan.Should().BeSameAs(span); + collection.ToArray().Array.Should().ContainSingle().Which.Should().BeSameAs(span); + var spans = new List(); + foreach (var x in collection) + { + spans.Add(x); + } + + spans.Should().ContainSingle().Which.Should().BeSameAs(span); + collection[0].Should().BeSameAs(span); + FluentActions.Invoking(() => collection[1]).Should().Throw(); + } + + [Fact] + public void ArrayCapacityConstructor() + { + var length = 10; + var collection = new SpanCollection(length); + + collection.Count.Should().Be(0); + collection.RootSpan.Should().BeNull(); + var array = collection.ToArray(); + array.Count.Should().Be(0); + array.Offset.Should().Be(0); + array.Array!.Length.Should().Be(length); + foreach (var span in collection) + { + Assert.Fail("We shouldn't have a span to enumerate: " + span); + } + + FluentActions.Invoking(() => collection[0]).Should().Throw(); + } + + [Fact] + public void ArrayConstructor() + { + var spans = new[] { CreateSpan("span1"), CreateSpan("span2"), null, null }; + var collection = new SpanCollection(spans, 2); + + collection.Count.Should().Be(2); + collection.RootSpan.Should().BeSameAs(spans[0]); + collection[0].Should().BeSameAs(spans[0]); + collection[1].Should().BeSameAs(spans[1]); + FluentActions.Invoking(() => collection[2]).Should().Throw(); + + var array = collection.ToArray(); + array.Count.Should().Be(2); + array.Offset.Should().Be(0); + array.Array.Should().BeSameAs(spans); + + var enumerated = new List(); + foreach (var span in collection) + { + enumerated.Add(span); + } + + enumerated.Should().HaveCount(2); + enumerated[0].Should().BeSameAs(spans[0]); + enumerated[1].Should().BeSameAs(spans[1]); + } + + [Fact] + public void ArrayConstructor_SetsCountToArrayLength() + { + var spans = new[] { CreateSpan("span1"), CreateSpan("span2") }; + var collection = new SpanCollection(spans); + + collection.Count.Should().Be(2); + collection.RootSpan.Should().BeSameAs(spans[0]); + collection[0].Should().BeSameAs(spans[0]); + collection[1].Should().BeSameAs(spans[1]); + } + + [Fact] + public void Append_ToEmptyCollection() + { + SpanCollection collection = default; + var span = CreateSpan(); + + var result = SpanCollection.Append(in collection, span); + + result.Count.Should().Be(1); + result.RootSpan.Should().BeSameAs(span); + result[0].Should().BeSameAs(span); + } + + [Fact] + public void Append_ToSingleSpanCollection() + { + var span1 = CreateSpan("span1"); + var collection = new SpanCollection(span1); + + var span2 = CreateSpan("span2"); + var result = SpanCollection.Append(in collection, span2); + + result.Count.Should().Be(2); + result.RootSpan.Should().BeSameAs(span1); + result[0].Should().BeSameAs(span1); + result[1].Should().BeSameAs(span2); + + // Default array size is 4 + var array = result.ToArray(); + array.Count.Should().Be(2); + array.Array.Length.Should().Be(4); + } + + [Fact] + public void Append_ToArrayWithCapacityCollection() + { + var spans = new[] { CreateSpan("span1"), CreateSpan("span2"), null, null }; + var collection = new SpanCollection(spans, 2); + var array = collection.ToArray(); + array.Count.Should().Be(2); + array.Array.Should().BeSameAs(spans); + + var span3 = CreateSpan("span3"); + var result = SpanCollection.Append(in collection, span3); + + result.Count.Should().Be(3); + result[0].Should().BeSameAs(spans[0]); + result[1].Should().BeSameAs(spans[1]); + result[2].Should().BeSameAs(span3); + + collection.ToArray().Array.Should().BeSameAs(spans); + } + + [Fact] + public void Append_ToFullArrayCollection() + { + // Create a collection with an array at capacity + var spans = new[] { CreateSpan("span1"), CreateSpan("span2") }; + var collection = new SpanCollection(spans); + var array = collection.ToArray(); + array.Count.Should().Be(2); + array.Array.Should().BeSameAs(spans); + + var span3 = CreateSpan("span3"); + var result = SpanCollection.Append(in collection, span3); + + result.Count.Should().Be(3); + result[0].Should().BeSameAs(spans[0]); + result[1].Should().BeSameAs(spans[1]); + result[2].Should().BeSameAs(span3); + + // Array should have grown from 2 to 4 + var array2 = result.ToArray(); + array2.Array.Should().HaveCount(4).And.NotBeSameAs(spans); + } + + [Fact] + public void Append_GrowsArrayExponentially() + { + SpanCollection collection = default; + + // Append 9 spans + for (var i = 0; i < 9; i++) + { + collection = SpanCollection.Append(in collection, CreateSpan($"span{i}")); + } + + collection.Count.Should().Be(9); + for (int i = 0; i < 9; i++) + { + collection[i].OperationName.Should().Be($"span{i}"); + } + + // Verify array grew: starts at 4, grows to 8, then needs to grow to 16 + var array = collection.ToArray(); + array.Array!.Length.Should().Be(16); + } + + private static Span CreateSpan(string operationName = "test-span") + { + var spanContext = new SpanContext(traceId: 1UL, spanId: 2, samplingPriority: SamplingPriority.AutoKeep); + return new Span(spanContext, DateTimeOffset.UtcNow) + { + OperationName = operationName + }; + } +} From 034a2b2a467c266ba35d4d86ccab934407f2b951 Mon Sep 17 00:00:00 2001 From: Andrew Lock Date: Fri, 21 Nov 2025 18:45:25 +0000 Subject: [PATCH 6/9] Replace ArraySegment with SpanCollection --- tracer/src/Datadog.Trace/Agent/AgentWriter.cs | 54 +++++++++---------- .../src/Datadog.Trace/Agent/IAgentWriter.cs | 2 +- .../Datadog.Trace/Agent/IStatsAggregator.cs | 8 +-- .../Agent/MessagePack/TraceChunkModel.cs | 42 ++++++++++----- .../Agent/NullStatsAggregator.cs | 6 +-- tracer/src/Datadog.Trace/Agent/SpanBuffer.cs | 4 +- .../Datadog.Trace/Agent/StatsAggregator.cs | 27 +++++----- .../TraceSamplers/AnalyticsEventsSampler.cs | 6 +-- .../Agent/TraceSamplers/ErrorSampler.cs | 6 +-- .../Agent/TraceSamplers/ITraceChunkSampler.cs | 2 +- .../Agent/TraceSamplers/PrioritySampler.cs | 4 +- .../Agent/TraceSamplers/RareSampler.cs | 18 +++---- .../Datadog.Trace/Ci/Agent/ApmAgentWriter.cs | 16 ++---- .../Ci/Agent/CIVisibilityProtocolWriter.cs | 10 ++-- .../Ci/Processors/OriginTagTraceProcessor.cs | 8 +-- .../TestSuiteVisibilityProcessor.cs | 48 ++++++++++++++--- tracer/src/Datadog.Trace/IDatadogTracer.cs | 3 +- .../Processors/ITraceProcessor.cs | 5 +- .../Processors/NormalizerTraceProcessor.cs | 12 ++--- .../Processors/ObfuscatorTraceProcessor.cs | 7 +-- .../Processors/TruncatorTraceProcessor.cs | 7 +-- tracer/src/Datadog.Trace/Span.cs | 6 +-- tracer/src/Datadog.Trace/TraceContext.cs | 39 +++++++------- tracer/src/Datadog.Trace/Tracer.cs | 4 +- tracer/src/Datadog.Trace/TracerManager.cs | 9 ++-- .../src/Datadog.Trace/Util/SamplingHelpers.cs | 5 +- 26 files changed, 199 insertions(+), 159 deletions(-) diff --git a/tracer/src/Datadog.Trace/Agent/AgentWriter.cs b/tracer/src/Datadog.Trace/Agent/AgentWriter.cs index 09c7281ecea5..9a7cf3ffaa38 100644 --- a/tracer/src/Datadog.Trace/Agent/AgentWriter.cs +++ b/tracer/src/Datadog.Trace/Agent/AgentWriter.cs @@ -115,7 +115,7 @@ internal AgentWriter(IApi api, IStatsAggregator statsAggregator, IDogStatsd stat public Task Ping() => _api.SendTracesAsync(EmptyPayload, 0, false, 0, 0); - public void WriteTrace(ArraySegment trace) + public void WriteTrace(in SpanCollection trace) { if (trace.Count == 0) { @@ -130,7 +130,7 @@ public void WriteTrace(ArraySegment trace) } else { - _pendingTraces.Enqueue(new WorkItem(trace)); + _pendingTraces.Enqueue(new WorkItem(in trace)); if (!_serializationMutex.IsSet) { @@ -374,7 +374,7 @@ async Task InternalBufferFlush() } } - private void SerializeTrace(ArraySegment spans) + private void SerializeTrace(in SpanCollection spans) { // Declaring as inline method because only safe to invoke in the context of SerializeTrace SpanBuffer SwapBuffers() @@ -406,26 +406,26 @@ SpanBuffer SwapBuffers() } int? chunkSamplingPriority = null; + var chunk = spans; if (CanComputeStats) { - spans = _statsAggregator?.ProcessTrace(spans) ?? spans; - bool shouldSendTrace = _statsAggregator?.ShouldKeepTrace(spans) ?? true; - _statsAggregator?.AddRange(spans); + chunk = _statsAggregator?.ProcessTrace(in chunk) ?? chunk; + bool shouldSendTrace = _statsAggregator?.ShouldKeepTrace(in chunk) ?? true; + _statsAggregator?.AddRange(in chunk); var singleSpanSamplingSpans = new List(); // TODO maybe we can store this from above? - for (var i = 0; i < spans.Count; i++) + foreach (var span in chunk) { - var index = i + spans.Offset; - if (spans.Array?[index].GetMetric(Metrics.SingleSpanSampling.SamplingMechanism) is not null) + if (span.GetMetric(Metrics.SingleSpanSampling.SamplingMechanism) is not null) { - singleSpanSamplingSpans.Add(spans.Array[index]); + singleSpanSamplingSpans.Add(span); } } if (shouldSendTrace) { TelemetryFactory.Metrics.RecordCountTraceChunkEnqueued(MetricTags.TraceChunkEnqueueReason.P0Keep); - TelemetryFactory.Metrics.RecordCountSpanEnqueuedForSerialization(MetricTags.SpanEnqueueReason.P0Keep, spans.Count); + TelemetryFactory.Metrics.RecordCountSpanEnqueuedForSerialization(MetricTags.SpanEnqueueReason.P0Keep, chunk.Count); } else { @@ -435,8 +435,8 @@ SpanBuffer SwapBuffers() if (singleSpanSamplingSpans.Count == 0) { Interlocked.Increment(ref _droppedP0Traces); - Interlocked.Add(ref _droppedP0Spans, spans.Count); - TelemetryFactory.Metrics.RecordCountSpanDropped(MetricTags.DropReason.P0Drop, spans.Count); + Interlocked.Add(ref _droppedP0Spans, chunk.Count); + TelemetryFactory.Metrics.RecordCountSpanDropped(MetricTags.DropReason.P0Drop, chunk.Count); return; } else @@ -445,11 +445,11 @@ SpanBuffer SwapBuffers() // this will override the TraceContext sampling priority when we do a SpanBuffer.TryWrite chunkSamplingPriority = SamplingPriorityValues.UserKeep; Interlocked.Increment(ref _droppedP0Traces); // increment since we are sampling out the entire trace - var spansDropped = spans.Count - singleSpanSamplingSpans.Count; + var spansDropped = chunk.Count - singleSpanSamplingSpans.Count; Interlocked.Add(ref _droppedP0Spans, spansDropped); - spans = new ArraySegment(singleSpanSamplingSpans.ToArray()); + chunk = new SpanCollection(singleSpanSamplingSpans.ToArray(), singleSpanSamplingSpans.Count); TelemetryFactory.Metrics.RecordCountSpanDropped(MetricTags.DropReason.P0Drop, spansDropped); - TelemetryFactory.Metrics.RecordCountSpanEnqueuedForSerialization(MetricTags.SpanEnqueueReason.SingleSpanSampling, spans.Count); + TelemetryFactory.Metrics.RecordCountSpanEnqueuedForSerialization(MetricTags.SpanEnqueueReason.SingleSpanSampling, chunk.Count); TelemetryFactory.Metrics.RecordCountTracePartialFlush(MetricTags.PartialFlushReason.SingleSpanIngestion); } } @@ -458,11 +458,11 @@ SpanBuffer SwapBuffers() { // not using stats, so trace always kept TelemetryFactory.Metrics.RecordCountTraceChunkEnqueued(MetricTags.TraceChunkEnqueueReason.Default); - TelemetryFactory.Metrics.RecordCountSpanEnqueuedForSerialization(MetricTags.SpanEnqueueReason.Default, spans.Count); + TelemetryFactory.Metrics.RecordCountSpanEnqueuedForSerialization(MetricTags.SpanEnqueueReason.Default, chunk.Count); } // Add the current keep rate to trace - if (spans.Array?[spans.Offset].Context.TraceContext is { } trace) + if (chunk.RootSpan?.Context.TraceContext is { } trace) { trace.TracesKeepRate = _traceKeepRateCalculator.GetKeepRate(); } @@ -471,7 +471,7 @@ SpanBuffer SwapBuffers() // This allows the serialization thread to keep doing its job while a buffer is being flushed var buffer = _activeBuffer; - var writeStatus = buffer.TryWrite(spans, ref _temporaryBuffer, chunkSamplingPriority); + var writeStatus = buffer.TryWrite(in chunk, ref _temporaryBuffer, chunkSamplingPriority); if (writeStatus == SpanBuffer.WriteStatus.Success) { @@ -482,7 +482,7 @@ SpanBuffer SwapBuffers() if (writeStatus == SpanBuffer.WriteStatus.Overflow) { // The trace is too big for the buffer, no point in trying again - DropTrace(spans); + DropTrace(chunk.Count); return; } @@ -494,7 +494,7 @@ SpanBuffer SwapBuffers() // One buffer is full, request an eager flush RequestFlush(); - if (buffer.TryWrite(spans, ref _temporaryBuffer, chunkSamplingPriority) == SpanBuffer.WriteStatus.Success) + if (buffer.TryWrite(in chunk, ref _temporaryBuffer, chunkSamplingPriority) == SpanBuffer.WriteStatus.Success) { // Serialization to the secondary buffer succeeded return; @@ -502,20 +502,20 @@ SpanBuffer SwapBuffers() } // All the buffers are full :( drop the trace - DropTrace(spans); + DropTrace(chunk.Count); } - private void DropTrace(ArraySegment spans) + private void DropTrace(int count) { Interlocked.Increment(ref _droppedTraces); _traceKeepRateCalculator.IncrementDrops(1); - TelemetryFactory.Metrics.RecordCountSpanDropped(MetricTags.DropReason.OverfullBuffer, spans.Count); + TelemetryFactory.Metrics.RecordCountSpanDropped(MetricTags.DropReason.OverfullBuffer, count); TelemetryFactory.Metrics.RecordCountTraceChunkDropped(MetricTags.DropReason.OverfullBuffer); if (_statsd != null) { _statsd.Increment(TracerMetricNames.Queue.DroppedTraces); - _statsd.Increment(TracerMetricNames.Queue.DroppedSpans, spans.Count); + _statsd.Increment(TracerMetricNames.Queue.DroppedSpans, count); } } @@ -577,10 +577,10 @@ private void SerializeTracesLoop() private readonly struct WorkItem { - public readonly ArraySegment Trace; + public readonly SpanCollection Trace; public readonly Action Callback; - public WorkItem(ArraySegment trace) + public WorkItem(in SpanCollection trace) { Trace = trace; Callback = null; diff --git a/tracer/src/Datadog.Trace/Agent/IAgentWriter.cs b/tracer/src/Datadog.Trace/Agent/IAgentWriter.cs index 2e3573ce6801..e2269d0c3c94 100644 --- a/tracer/src/Datadog.Trace/Agent/IAgentWriter.cs +++ b/tracer/src/Datadog.Trace/Agent/IAgentWriter.cs @@ -10,7 +10,7 @@ namespace Datadog.Trace.Agent { internal interface IAgentWriter { - void WriteTrace(ArraySegment trace); + void WriteTrace(in SpanCollection trace); Task Ping(); diff --git a/tracer/src/Datadog.Trace/Agent/IStatsAggregator.cs b/tracer/src/Datadog.Trace/Agent/IStatsAggregator.cs index 4412c0b8a583..67ae979f48a4 100644 --- a/tracer/src/Datadog.Trace/Agent/IStatsAggregator.cs +++ b/tracer/src/Datadog.Trace/Agent/IStatsAggregator.cs @@ -5,6 +5,7 @@ using System; using System.Threading.Tasks; +using Datadog.Trace.SourceGenerators; namespace Datadog.Trace.Agent { @@ -30,22 +31,23 @@ internal interface IStatsAggregator /// Receives an array of spans and computes stats points for them. /// /// The array of spans to process. + [TestingOnly] void Add(params Span[] spans); /// /// Receives an array of spans and computes stats points for them. /// /// The ArraySegment of spans to process. - void AddRange(ArraySegment spans); + void AddRange(in SpanCollection spans); /// /// Runs a series of samplers over the entire trace chunk /// /// The trace chunk to sample /// True if the trace chunk should be sampled, false otherwise. - bool ShouldKeepTrace(ArraySegment spans); + bool ShouldKeepTrace(in SpanCollection spans); - ArraySegment ProcessTrace(ArraySegment trace); + SpanCollection ProcessTrace(in SpanCollection trace); Task DisposeAsync(); } diff --git a/tracer/src/Datadog.Trace/Agent/MessagePack/TraceChunkModel.cs b/tracer/src/Datadog.Trace/Agent/MessagePack/TraceChunkModel.cs index 4e5dbb41540f..4e9afe4a4366 100644 --- a/tracer/src/Datadog.Trace/Agent/MessagePack/TraceChunkModel.cs +++ b/tracer/src/Datadog.Trace/Agent/MessagePack/TraceChunkModel.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Threading; using Datadog.Trace.Configuration; +using Datadog.Trace.SourceGenerators; using Datadog.Trace.Tagging; namespace Datadog.Trace.Agent.MessagePack; @@ -20,7 +21,7 @@ namespace Datadog.Trace.Agent.MessagePack; internal readonly struct TraceChunkModel { // for small trace chunks, use the ArraySegment copy directly, no heap allocations - private readonly ArraySegment _spans; + private readonly SpanCollection _spans; // for large trace chunks, use a HashSet instead of iterating the array. // there are 3 possible states: @@ -75,8 +76,8 @@ internal readonly struct TraceChunkModel /// The spans that will be within this . /// Optional sampling priority to override the sampling priority. /// Indicates if this is the first trace chunk being written to the output buffer. - public TraceChunkModel(in ArraySegment spans, int? samplingPriority = null, bool isFirstChunkInPayload = false) - : this(spans, TraceContext.GetTraceContext(spans), samplingPriority, isFirstChunkInPayload) + public TraceChunkModel(in SpanCollection spans, int? samplingPriority = null, bool isFirstChunkInPayload = false) + : this(in spans, TraceContext.GetTraceContext(in spans), samplingPriority, isFirstChunkInPayload) { // since all we have is an array of spans, use the trace context from the first span // to get the other values we need (sampling priority, origin, trace tags, etc) for now. @@ -85,8 +86,8 @@ public TraceChunkModel(in ArraySegment spans, int? samplingPriority = null } // used only to chain constructors - private TraceChunkModel(in ArraySegment spans, TraceContext? traceContext, int? samplingPriority, bool isFirstChunkInPayload) - : this(spans, traceContext?.RootSpan) + private TraceChunkModel(in SpanCollection spans, TraceContext? traceContext, int? samplingPriority, bool isFirstChunkInPayload) + : this(in spans, traceContext?.RootSpan) { // sampling decision override takes precedence over TraceContext.SamplingPriority SamplingPriority = samplingPriority; @@ -128,8 +129,8 @@ private TraceChunkModel(in ArraySegment spans, TraceContext? traceContext, } } - // used in tests - internal TraceChunkModel(in ArraySegment spans, Span? localRootSpan) + [TestingAndPrivateOnly] + internal TraceChunkModel(in SpanCollection spans, Span? localRootSpan) { _spans = spans; @@ -161,7 +162,7 @@ public SpanModel GetSpanModel(int spanIndex) ThrowHelper.ThrowArgumentOutOfRangeException(nameof(spanIndex)); } - var span = _spans.Array![_spans.Offset + spanIndex]; + var span = _spans[spanIndex]; var parentId = span.Context.ParentId ?? 0; bool isLocalRoot = parentId is 0 || span.SpanId == LocalRootSpanId; bool isFirstSpan = spanIndex == 0; @@ -225,9 +226,9 @@ private bool Contains(ulong spanId, int startIndex) if (hashSet.Count == 0) { - for (var i = 0; i < _spans.Count; i++) + foreach (var span in _spans) { - hashSet.Add(_spans.Array![_spans.Offset + i].SpanId); + hashSet.Add(span.SpanId); } } @@ -243,15 +244,28 @@ private bool Contains(ulong spanId, int startIndex) private int IndexOf(ulong spanId, int startIndex) { // wrap around the end of the array - if (startIndex >= _spans.Count) + var count = _spans.Count; + if (count == 0) + { + return -1; + } + + if (startIndex >= count) { startIndex = 0; } + if (count == 1) + { + return _spans[0].SpanId == spanId ? 0 : -1; + } + + var array = _spans.ToArray(); + // iterate over the span array starting at the specified index + 1 - for (var i = startIndex; i < _spans.Count; i++) + for (var i = startIndex; i < count; i++) { - if (spanId == _spans.Array![_spans.Offset + i].SpanId) + if (spanId == array.Array![array.Offset + i].SpanId) { return i; } @@ -260,7 +274,7 @@ private int IndexOf(ulong spanId, int startIndex) // if not found above, wrap around to the beginning to search the rest of the array for (var i = 0; i < startIndex; i++) { - if (spanId == _spans.Array![_spans.Offset + i].SpanId) + if (spanId == array.Array![array.Offset + i].SpanId) { return i; } diff --git a/tracer/src/Datadog.Trace/Agent/NullStatsAggregator.cs b/tracer/src/Datadog.Trace/Agent/NullStatsAggregator.cs index a547e2463b4b..811e0e27feda 100644 --- a/tracer/src/Datadog.Trace/Agent/NullStatsAggregator.cs +++ b/tracer/src/Datadog.Trace/Agent/NullStatsAggregator.cs @@ -16,13 +16,13 @@ public void Add(params Span[] spans) { } - public void AddRange(ArraySegment spans) + public void AddRange(in SpanCollection spans) { } - public bool ShouldKeepTrace(ArraySegment spans) => true; + public bool ShouldKeepTrace(in SpanCollection spans) => true; - public ArraySegment ProcessTrace(ArraySegment trace) => trace; + public SpanCollection ProcessTrace(in SpanCollection trace) => trace; public Task DisposeAsync() { diff --git a/tracer/src/Datadog.Trace/Agent/SpanBuffer.cs b/tracer/src/Datadog.Trace/Agent/SpanBuffer.cs index e0aa492931e8..249b764996c9 100644 --- a/tracer/src/Datadog.Trace/Agent/SpanBuffer.cs +++ b/tracer/src/Datadog.Trace/Agent/SpanBuffer.cs @@ -73,7 +73,7 @@ public ArraySegment Data // For tests only internal bool IsEmpty => !_locked && !IsFull && TraceCount == 0 && SpanCount == 0 && _offset == HeaderSize; - public WriteStatus TryWrite(ArraySegment spans, ref byte[] temporaryBuffer, int? samplingPriority = null) + public WriteStatus TryWrite(in SpanCollection spans, ref byte[] temporaryBuffer, int? samplingPriority = null) { bool lockTaken = false; @@ -91,7 +91,7 @@ public WriteStatus TryWrite(ArraySegment spans, ref byte[] temporaryBuffer // to get the other values we need (sampling priority, origin, trace tags, etc) for now. // the idea is that as we refactor further, we can pass more than just the spans, // and these values can come directly from the trace context. - var traceChunk = new TraceChunkModel(spans, samplingPriority, isFirstChunkInPayload: TraceCount == 0); + var traceChunk = new TraceChunkModel(in spans, samplingPriority, isFirstChunkInPayload: TraceCount == 0); // We don't know what the serialized size of the payload will be, // so we need to write to a temporary buffer first diff --git a/tracer/src/Datadog.Trace/Agent/StatsAggregator.cs b/tracer/src/Datadog.Trace/Agent/StatsAggregator.cs index dbe139bdd0f2..ca71a86f358f 100644 --- a/tracer/src/Datadog.Trace/Agent/StatsAggregator.cs +++ b/tracer/src/Datadog.Trace/Agent/StatsAggregator.cs @@ -14,6 +14,7 @@ using Datadog.Trace.Logging; using Datadog.Trace.PlatformHelpers; using Datadog.Trace.Processors; +using Datadog.Trace.SourceGenerators; using Datadog.Trace.Telemetry; using Datadog.Trace.Telemetry.Metrics; using Datadog.Trace.Util; @@ -104,12 +105,13 @@ public Task DisposeAsync() return _flushTask; } + [TestingOnly] public void Add(params Span[] spans) { - AddRange(new ArraySegment(spans, 0, spans.Length)); + AddRange(new(spans, spans.Length)); } - public void AddRange(ArraySegment spans) + public void AddRange(in SpanCollection spans) { // Contention around this lock is expected to be very small: // AddRange is called from the serialization thread, and concurrent serialization @@ -117,34 +119,35 @@ public void AddRange(ArraySegment spans) // The Flush thread only acquires the lock long enough to swap the metrics buffer. lock (_buffers) { - for (int i = 0; i < spans.Count; i++) + foreach (var span in spans) { - AddToBuffer(spans.Array[i + spans.Offset]); + AddToBuffer(span); } } } - public bool ShouldKeepTrace(ArraySegment trace) + public bool ShouldKeepTrace(in SpanCollection trace) { // Note: The RareSampler must be run before all other samplers so that // the first rare span in the trace chunk (if any) is marked with "_dd.rare". // The sampling decision is only used if no other samplers choose to keep the trace chunk. - bool rareSpanFound = _rareSampler.Sample(trace); + bool rareSpanFound = _rareSampler.Sample(in trace); return rareSpanFound - || _prioritySampler.Sample(trace) - || _errorSampler.Sample(trace) - || _analyticsEventSampler.Sample(trace); + || _prioritySampler.Sample(in trace) + || _errorSampler.Sample(in trace) + || _analyticsEventSampler.Sample(in trace); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ArraySegment ProcessTrace(ArraySegment trace) + public SpanCollection ProcessTrace(in SpanCollection trace) { + var spans = trace; foreach (var processor in _traceProcessors) { try { - trace = processor.Process(trace); + spans = processor.Process(in spans); } catch (Exception e) { @@ -152,7 +155,7 @@ public ArraySegment ProcessTrace(ArraySegment trace) } } - return trace; + return spans; } internal static StatsAggregationKey BuildKey(Span span) diff --git a/tracer/src/Datadog.Trace/Agent/TraceSamplers/AnalyticsEventsSampler.cs b/tracer/src/Datadog.Trace/Agent/TraceSamplers/AnalyticsEventsSampler.cs index 8032cabc5f0f..0d6c34fcc74b 100644 --- a/tracer/src/Datadog.Trace/Agent/TraceSamplers/AnalyticsEventsSampler.cs +++ b/tracer/src/Datadog.Trace/Agent/TraceSamplers/AnalyticsEventsSampler.cs @@ -10,12 +10,10 @@ namespace Datadog.Trace.Agent.TraceSamplers { internal class AnalyticsEventsSampler : ITraceChunkSampler { - public bool Sample(ArraySegment trace) + public bool Sample(in SpanCollection trace) { - for (int i = 0; i < trace.Count; i++) + foreach (var span in trace) { - var span = trace.Array![i + trace.Offset]; - if (span.GetMetric(Tags.Analytics) is { } rate) { return SamplingHelpers.SampleByRate(span.TraceId128, rate); diff --git a/tracer/src/Datadog.Trace/Agent/TraceSamplers/ErrorSampler.cs b/tracer/src/Datadog.Trace/Agent/TraceSamplers/ErrorSampler.cs index 6171aab7a0c4..92d3a6ea4390 100644 --- a/tracer/src/Datadog.Trace/Agent/TraceSamplers/ErrorSampler.cs +++ b/tracer/src/Datadog.Trace/Agent/TraceSamplers/ErrorSampler.cs @@ -9,11 +9,11 @@ namespace Datadog.Trace.Agent.TraceSamplers { internal class ErrorSampler : ITraceChunkSampler { - public bool Sample(ArraySegment trace) + public bool Sample(in SpanCollection trace) { - for (int i = 0; i < trace.Count; i++) + foreach (var span in trace) { - if (trace.Array[i + trace.Offset].Error) + if (span.Error) { return true; } diff --git a/tracer/src/Datadog.Trace/Agent/TraceSamplers/ITraceChunkSampler.cs b/tracer/src/Datadog.Trace/Agent/TraceSamplers/ITraceChunkSampler.cs index e4589e097423..9a8dbf898500 100644 --- a/tracer/src/Datadog.Trace/Agent/TraceSamplers/ITraceChunkSampler.cs +++ b/tracer/src/Datadog.Trace/Agent/TraceSamplers/ITraceChunkSampler.cs @@ -14,6 +14,6 @@ internal interface ITraceChunkSampler /// /// The trace chunk to sample /// True if the trace chunk should be sampled, false otherwise. - bool Sample(ArraySegment trace); + bool Sample(in SpanCollection trace); } } diff --git a/tracer/src/Datadog.Trace/Agent/TraceSamplers/PrioritySampler.cs b/tracer/src/Datadog.Trace/Agent/TraceSamplers/PrioritySampler.cs index b4ce029ff819..46c61d4fdf02 100644 --- a/tracer/src/Datadog.Trace/Agent/TraceSamplers/PrioritySampler.cs +++ b/tracer/src/Datadog.Trace/Agent/TraceSamplers/PrioritySampler.cs @@ -10,7 +10,7 @@ namespace Datadog.Trace.Agent.TraceSamplers { internal class PrioritySampler : ITraceChunkSampler { - public bool Sample(ArraySegment trace) => - SamplingHelpers.IsKeptBySamplingPriority(trace); + public bool Sample(in SpanCollection trace) => + SamplingHelpers.IsKeptBySamplingPriority(in trace); } } diff --git a/tracer/src/Datadog.Trace/Agent/TraceSamplers/RareSampler.cs b/tracer/src/Datadog.Trace/Agent/TraceSamplers/RareSampler.cs index 9f37f78d600f..63da46a9b2fe 100644 --- a/tracer/src/Datadog.Trace/Agent/TraceSamplers/RareSampler.cs +++ b/tracer/src/Datadog.Trace/Agent/TraceSamplers/RareSampler.cs @@ -34,27 +34,26 @@ public RareSampler(TracerSettings settings) /// /// The input trace chunk /// true when a rare span is found, false otherwise - public bool Sample(ArraySegment traceChunk) + public bool Sample(in SpanCollection traceChunk) { if (!IsEnabled) { return false; } - if (SamplingHelpers.IsKeptBySamplingPriority(traceChunk)) + if (SamplingHelpers.IsKeptBySamplingPriority(in traceChunk)) { - UpdateSeenSpans(traceChunk); + UpdateSeenSpans(in traceChunk); return false; } - return SampleSpansAndUpdateSeenSpansIfKept(traceChunk); + return SampleSpansAndUpdateSeenSpansIfKept(in traceChunk); } - private void UpdateSeenSpans(ArraySegment trace) + private void UpdateSeenSpans(in SpanCollection trace) { - for (int i = 0; i < trace.Count; i++) + foreach (var span in trace) { - var span = trace.Array![i + trace.Offset]; if (span.IsTopLevel || span.GetMetric(Tags.Measured) == 1.0 || span.GetMetric(Tags.PartialSnapshot) > 0) { UpdateSpan(span); @@ -62,13 +61,12 @@ private void UpdateSeenSpans(ArraySegment trace) } } - private bool SampleSpansAndUpdateSeenSpansIfKept(ArraySegment trace) + private bool SampleSpansAndUpdateSeenSpansIfKept(in SpanCollection trace) { bool rareSpanFound = false; - for (int i = 0; i < trace.Count; i++) + foreach (var span in trace) { - var span = trace.Array![i + trace.Offset]; if (span.IsTopLevel || span.GetMetric(Tags.Measured) == 1.0 || span.GetMetric(Tags.PartialSnapshot) > 0) { // Follow agent implementation to mark and exit on first sampled span diff --git a/tracer/src/Datadog.Trace/Ci/Agent/ApmAgentWriter.cs b/tracer/src/Datadog.Trace/Ci/Agent/ApmAgentWriter.cs index 95f8d4d9ce24..20ce3649e34b 100644 --- a/tracer/src/Datadog.Trace/Ci/Agent/ApmAgentWriter.cs +++ b/tracer/src/Datadog.Trace/Ci/Agent/ApmAgentWriter.cs @@ -21,8 +21,6 @@ internal class ApmAgentWriter : IEventWriter { private const int DefaultMaxBufferSize = 1024 * 1024 * 10; - [ThreadStatic] - private static Span[]? _spanArray; private readonly AgentWriter _agentWriter; public ApmAgentWriter(TracerSettings settings, Action> updateSampleRates, IDiscoveryService discoveryService, int maxBufferSize = DefaultMaxBufferSize) @@ -44,17 +42,9 @@ public void WriteEvent(IEvent @event) { // To keep compatibility with the agent version of the payload, any IEvent conversion to span // goes here. - - if (_spanArray is not { } spanArray) - { - spanArray = new Span[1]; - _spanArray = spanArray; - } - if (CIVisibilityEventsFactory.GetSpan(@event) is { } span) { - spanArray[0] = span; - WriteTrace(new ArraySegment(spanArray)); + WriteTrace(new SpanCollection(span)); } } @@ -73,8 +63,8 @@ public Task Ping() return _agentWriter.Ping(); } - public void WriteTrace(ArraySegment trace) + public void WriteTrace(in SpanCollection trace) { - _agentWriter.WriteTrace(trace); + _agentWriter.WriteTrace(in trace); } } diff --git a/tracer/src/Datadog.Trace/Ci/Agent/CIVisibilityProtocolWriter.cs b/tracer/src/Datadog.Trace/Ci/Agent/CIVisibilityProtocolWriter.cs index 95c28a5ce864..a35761eae194 100644 --- a/tracer/src/Datadog.Trace/Ci/Agent/CIVisibilityProtocolWriter.cs +++ b/tracer/src/Datadog.Trace/Ci/Agent/CIVisibilityProtocolWriter.cs @@ -9,6 +9,7 @@ using System.Diagnostics; using System.Threading; using System.Threading.Tasks; +using Datadog.Trace.Agent; using Datadog.Trace.Ci.Agent.Payloads; using Datadog.Trace.Ci.Configuration; using Datadog.Trace.Ci.EventModel; @@ -158,15 +159,12 @@ public Task Ping() return Task.FromResult(true); } - public void WriteTrace(ArraySegment trace) + public void WriteTrace(in SpanCollection trace) { // Transform spans to events - for (var i = trace.Offset; i < trace.Count; i++) + foreach (var span in trace) { - if (trace.Array is { } array) - { - WriteEvent(CIVisibilityEventsFactory.FromSpan(array[i])); - } + WriteEvent(CIVisibilityEventsFactory.FromSpan(span)); } } diff --git a/tracer/src/Datadog.Trace/Ci/Processors/OriginTagTraceProcessor.cs b/tracer/src/Datadog.Trace/Ci/Processors/OriginTagTraceProcessor.cs index 6925be6d4bf1..ec4ddffbc5c3 100644 --- a/tracer/src/Datadog.Trace/Ci/Processors/OriginTagTraceProcessor.cs +++ b/tracer/src/Datadog.Trace/Ci/Processors/OriginTagTraceProcessor.cs @@ -6,6 +6,7 @@ using System; using System.Threading; +using Datadog.Trace.Agent; using Datadog.Trace.Ci.Tags; using Datadog.Trace.Logging; using Datadog.Trace.Processors; @@ -29,7 +30,7 @@ public OriginTagTraceProcessor(bool isPartialFlushEnabled, bool isCiVisibilityPr Log.Information("OriginTraceProcessor initialized."); } - public ArraySegment Process(ArraySegment trace) + public SpanCollection Process(in SpanCollection trace) { // We ensure there's no trace (local root span) without a test tag. // And ensure all other spans have the origin tag. @@ -44,9 +45,8 @@ public ArraySegment Process(ArraySegment trace) if (!_isPartialFlushEnabled) { // Check if the root span is a test, benchmark or build span - for (var i = trace.Offset + trace.Count - 1; i >= trace.Offset; i--) + foreach (var span in trace) { - var span = trace.Array![i]; if (span.Context.Parent is null && span.Type != SpanTypes.Test && span.Type != SpanTypes.Browser && @@ -74,7 +74,7 @@ public ArraySegment Process(ArraySegment trace) if (!_isCiVisibilityProtocol) { // Sets the origin tag on the TraceContext to ensure the CI track. - var traceContext = trace.Array![trace.Offset].Context.TraceContext; + var traceContext = trace.RootSpan?.Context.TraceContext; if (traceContext is not null) { diff --git a/tracer/src/Datadog.Trace/Ci/Processors/TestSuiteVisibilityProcessor.cs b/tracer/src/Datadog.Trace/Ci/Processors/TestSuiteVisibilityProcessor.cs index 83565d4f1ce7..39442216ac79 100644 --- a/tracer/src/Datadog.Trace/Ci/Processors/TestSuiteVisibilityProcessor.cs +++ b/tracer/src/Datadog.Trace/Ci/Processors/TestSuiteVisibilityProcessor.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using Datadog.Trace.Agent; using Datadog.Trace.Logging; using Datadog.Trace.Processors; @@ -21,31 +22,62 @@ public TestSuiteVisibilityProcessor() Log.Information("TestSuiteVisibilityProcessor initialized."); } - public ArraySegment Process(ArraySegment trace) + public SpanCollection Process(in SpanCollection trace) { + var originalCount = trace.Count; + if (originalCount == 0) + { + return trace; + } + // Check if the trace has any span or Agentless is enabled - if (trace.Count == 0) + + // special case single span case + if (originalCount == 1) { + var span = trace[0]; + if (Process(span) is null) + { + Log.Warning("Span dropped because Test suite visibility is not supported without Agentless [Span.Type={Type}]", span.Type); + return default; + } + return trace; } + // we know we have multiple spans, so get the underlying array + var segment = trace.ToArray(); Span[]? spans = null; - var spIdx = 0; - for (var i = trace.Offset; i < trace.Count + trace.Offset; i++) + var copiedCount = 0; + var haveDrops = false; + // TODO: we could reuse the same underlying array rather than re-allocating when we need to recreate, but that can be a separate optimization + for (var i = segment.Offset; i < segment.Count + segment.Offset; i++) { - var span = trace.Array![i]; + var span = segment.Array![i]; if (Process(span) is { } processedSpan) { - spans ??= new Span[trace.Count]; - spans[spIdx++] = processedSpan; + if (haveDrops) + { + if (spans is null) + { + // first kept span after dropping some spans + spans = new Span[trace.Count]; + Array.Copy(segment.Array!, segment.Offset, spans, destinationIndex: 0, length: i - segment.Offset); + } + + spans[copiedCount++] = processedSpan; + } } else { + haveDrops = true; Log.Warning("Span dropped because Test suite visibility is not supported without Agentless [Span.Type={Type}]", span.Type); } } - return spans is null ? new ArraySegment([]) : new ArraySegment(spans, 0, spIdx); + return haveDrops + ? spans is null ? default : new SpanCollection(spans, copiedCount) + : trace; } public Span? Process(Span? span) diff --git a/tracer/src/Datadog.Trace/IDatadogTracer.cs b/tracer/src/Datadog.Trace/IDatadogTracer.cs index 52baa400338a..6eba5438171e 100644 --- a/tracer/src/Datadog.Trace/IDatadogTracer.cs +++ b/tracer/src/Datadog.Trace/IDatadogTracer.cs @@ -4,6 +4,7 @@ // using System; +using Datadog.Trace.Agent; using Datadog.Trace.Configuration; namespace Datadog.Trace @@ -22,6 +23,6 @@ internal interface IDatadogTracer PerTraceSettings PerTraceSettings { get; } - void Write(ArraySegment span); + void Write(in SpanCollection span); } } diff --git a/tracer/src/Datadog.Trace/Processors/ITraceProcessor.cs b/tracer/src/Datadog.Trace/Processors/ITraceProcessor.cs index c68ee9588d2a..e8de265a6dbc 100644 --- a/tracer/src/Datadog.Trace/Processors/ITraceProcessor.cs +++ b/tracer/src/Datadog.Trace/Processors/ITraceProcessor.cs @@ -5,14 +5,15 @@ #nullable enable using System; +using Datadog.Trace.Agent; namespace Datadog.Trace.Processors { internal interface ITraceProcessor { - ArraySegment Process(ArraySegment trace); + SpanCollection Process(in SpanCollection trace); - Span? Process(Span? span); + Span? Process(Span span); ITagProcessor? GetTagProcessor(); } diff --git a/tracer/src/Datadog.Trace/Processors/NormalizerTraceProcessor.cs b/tracer/src/Datadog.Trace/Processors/NormalizerTraceProcessor.cs index 85a7f4e9333f..b08470e082b9 100644 --- a/tracer/src/Datadog.Trace/Processors/NormalizerTraceProcessor.cs +++ b/tracer/src/Datadog.Trace/Processors/NormalizerTraceProcessor.cs @@ -4,6 +4,7 @@ // using System; +using Datadog.Trace.Agent; using Datadog.Trace.ExtensionMethods; using Datadog.Trace.Logging; using Datadog.Trace.Tagging; @@ -33,7 +34,7 @@ public NormalizerTraceProcessor() Log.Information("NormalizerTraceProcessor initialized."); } - public ArraySegment Process(ArraySegment trace) + public SpanCollection Process(in SpanCollection trace) { /* +----------+--------------------------------------------------------------------------------------------------------+ @@ -51,16 +52,15 @@ public ArraySegment Process(ArraySegment trace) | Meta | “http.status_code” key is deleted if it’s an invalid numeric value smaller than 100 or bigger than 600 | +----------+--------------------------------------------------------------------------------------------------------+ */ - - for (var i = trace.Offset; i < trace.Count + trace.Offset; i++) + foreach (var span in trace) { - trace.Array![i] = Process(trace.Array[i]); + Process(span); } // https://github.com/DataDog/datadog-agent/blob/eac2327c5574da7f225f9ef0f89eaeb05ed10382/pkg/trace/agent/normalizer.go#L133-L135 - var traceContext = trace.Array![trace.Offset].Context.TraceContext; + var traceContext = trace.RootSpan?.Context.TraceContext; - if (!string.IsNullOrEmpty(traceContext.Environment)) + if (!string.IsNullOrEmpty(traceContext?.Environment)) { traceContext.Environment = TraceUtil.NormalizeTag(traceContext.Environment); } diff --git a/tracer/src/Datadog.Trace/Processors/ObfuscatorTraceProcessor.cs b/tracer/src/Datadog.Trace/Processors/ObfuscatorTraceProcessor.cs index d9a0eb7d2c3a..e8abe2cbf662 100644 --- a/tracer/src/Datadog.Trace/Processors/ObfuscatorTraceProcessor.cs +++ b/tracer/src/Datadog.Trace/Processors/ObfuscatorTraceProcessor.cs @@ -5,6 +5,7 @@ using System; using System.Collections; +using Datadog.Trace.Agent; using Datadog.Trace.Logging; namespace Datadog.Trace.Processors @@ -48,11 +49,11 @@ public ObfuscatorTraceProcessor(bool redisTagObfuscationEnabled) Log.Information("ObfuscatorTraceProcessor initialized. Redis tag obfuscation enabled: {RedisObfuscation}", redisTagObfuscationEnabled); } - public ArraySegment Process(ArraySegment trace) + public SpanCollection Process(in SpanCollection trace) { - for (var i = trace.Offset; i < trace.Count + trace.Offset; i++) + foreach (var span in trace) { - trace.Array![i] = Process(trace.Array[i]); + Process(span); } return trace; diff --git a/tracer/src/Datadog.Trace/Processors/TruncatorTraceProcessor.cs b/tracer/src/Datadog.Trace/Processors/TruncatorTraceProcessor.cs index dcefcc3e39fb..d3c84d98fbf2 100644 --- a/tracer/src/Datadog.Trace/Processors/TruncatorTraceProcessor.cs +++ b/tracer/src/Datadog.Trace/Processors/TruncatorTraceProcessor.cs @@ -4,6 +4,7 @@ // using System; +using Datadog.Trace.Agent; using Datadog.Trace.Logging; namespace Datadog.Trace.Processors @@ -24,11 +25,11 @@ public TruncatorTraceProcessor() Log.Information("TruncatorTraceProcessor initialized."); } - public ArraySegment Process(ArraySegment trace) + public SpanCollection Process(in SpanCollection trace) { - for (var i = trace.Offset; i < trace.Count + trace.Offset; i++) + foreach (var span in trace) { - trace.Array![i] = Process(trace.Array[i]); + Process(span); } return trace; diff --git a/tracer/src/Datadog.Trace/Span.cs b/tracer/src/Datadog.Trace/Span.cs index 35a24c9b92f5..a937ebeae1b1 100644 --- a/tracer/src/Datadog.Trace/Span.cs +++ b/tracer/src/Datadog.Trace/Span.cs @@ -47,10 +47,8 @@ internal Span(SpanContext context, DateTimeOffset? start, ITags tags, IEnumerabl if (links is not null) { - foreach (var link in links) - { - AddLink(link); - } + // We know we're not finished and this allows optimizations vs using AddLink() + SpanLinks = [..links]; } if (IsLogLevelDebugEnabled) diff --git a/tracer/src/Datadog.Trace/TraceContext.cs b/tracer/src/Datadog.Trace/TraceContext.cs index 788b602b95b7..8a38599c4905 100644 --- a/tracer/src/Datadog.Trace/TraceContext.cs +++ b/tracer/src/Datadog.Trace/TraceContext.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading; +using Datadog.Trace.Agent; using Datadog.Trace.AppSec; using Datadog.Trace.Ci; using Datadog.Trace.ClrProfiler; @@ -18,6 +19,7 @@ using Datadog.Trace.Iast; using Datadog.Trace.Logging; using Datadog.Trace.Sampling; +using Datadog.Trace.SourceGenerators; using Datadog.Trace.Tagging; using Datadog.Trace.Telemetry; using Datadog.Trace.Telemetry.Metrics; @@ -29,7 +31,7 @@ internal class TraceContext { private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(); - private ArrayBuilder _spans; + private SpanCollection _spans; private int _openSpans; private IastRequestContext? _iastRequestContext; @@ -124,8 +126,8 @@ internal AppSecRequestContext AppSecRequestContext internal bool WafExecuted { get; set; } - internal static TraceContext? GetTraceContext(in ArraySegment spans) => - spans.Count > 0 ? spans.Array![spans.Offset].Context.TraceContext : null; + internal static TraceContext? GetTraceContext(in SpanCollection spans) + => spans.RootSpan?.Context.TraceContext; internal void EnableIastInRequest() { @@ -153,7 +155,7 @@ public void CloseSpan(Span span) { bool ShouldTriggerPartialFlush() => Tracer.Settings.PartialFlushEnabled && _spans.Count >= Tracer.Settings.PartialFlushMinSpans; - ArraySegment spansToWrite = default; + SpanCollection spansToWrite = default; // Propagate the resource name to the profiler for root web spans if (span.IsRootSpan) @@ -192,12 +194,12 @@ public void CloseSpan(Span span) lock (_rootSpan!) { - _spans.Add(span); + _spans = SpanCollection.Append(in _spans, span); _openSpans--; if (_openSpans == 0) { - spansToWrite = _spans.GetArray(); + spansToWrite = _spans; _spans = default; TelemetryFactory.Metrics.RecordCountTraceSegmentsClosed(); } @@ -207,7 +209,7 @@ public void CloseSpan(Span span) // all of them are known to be Root spans, so we can flush them as soon as they are closed // even if their children have not been closed yet. // An unclosed/unfinished child span should never block the report of a test. - spansToWrite = _spans.GetArray(); + spansToWrite = _spans; _spans = default; TelemetryFactory.Metrics.RecordCountTraceSegmentsClosed(); } @@ -219,12 +221,12 @@ public void CloseSpan(Span span) span.Context.RawTraceId, _spans.Count); - spansToWrite = _spans.GetArray(); + spansToWrite = _spans; // Making the assumption that, if the number of closed spans was big enough to trigger partial flush, // the number of remaining spans is probably big as well. // Therefore, we bypass the resize logic and immediately allocate the array to its maximum size - _spans = new ArrayBuilder(spansToWrite.Count); + _spans = new SpanCollection(spansToWrite.Count); TelemetryFactory.Metrics.RecordCountTracePartialFlush(MetricTags.PartialFlushReason.LargeTrace); } } @@ -232,27 +234,27 @@ public void CloseSpan(Span span) if (spansToWrite.Count > 0) { GetOrMakeSamplingDecision(); - RunSpanSampler(spansToWrite); - Tracer.Write(spansToWrite); + RunSpanSampler(in spansToWrite); + Tracer.Write(in spansToWrite); } } - // called from tests to force partial flush + [TestingOnly] internal void WriteClosedSpans() { - ArraySegment spansToWrite; + SpanCollection spansToWrite; lock (_rootSpan!) { - spansToWrite = _spans.GetArray(); + spansToWrite = _spans; _spans = default; } if (spansToWrite.Count > 0) { GetOrMakeSamplingDecision(); - RunSpanSampler(spansToWrite); - Tracer.Write(spansToWrite); + RunSpanSampler(in spansToWrite); + Tracer.Write(in spansToWrite); } } @@ -333,7 +335,7 @@ public void SetSamplingPriority( } } - private void RunSpanSampler(ArraySegment spans) + private void RunSpanSampler(in SpanCollection spans) { if (CurrentTraceSettings?.SpanSampler is null) { @@ -342,9 +344,8 @@ private void RunSpanSampler(ArraySegment spans) if (SamplingPriority is { } samplingPriority && SamplingPriorityValues.IsDrop(samplingPriority)) { - for (int i = 0; i < spans.Count; i++) + foreach (var span in spans) { - var span = spans.Array![i + spans.Offset]; CurrentTraceSettings.SpanSampler.MakeSamplingDecision(span); } } diff --git a/tracer/src/Datadog.Trace/Tracer.cs b/tracer/src/Datadog.Trace/Tracer.cs index 96aa01feba98..12ccd2445550 100644 --- a/tracer/src/Datadog.Trace/Tracer.cs +++ b/tracer/src/Datadog.Trace/Tracer.cs @@ -273,11 +273,11 @@ ISpan IDatadogOpenTracingTracer.StartSpan(string operationName, ISpanContext par /// Writes the specified collection to the agent writer. /// /// The collection to write. - void IDatadogTracer.Write(ArraySegment trace) + void IDatadogTracer.Write(in SpanCollection trace) { if (CurrentTraceSettings.Settings.TraceEnabled || Settings.AzureAppServiceMetadata?.CustomTracingEnabled is true) { - TracerManager.WriteTrace(trace); + TracerManager.WriteTrace(in trace); } } diff --git a/tracer/src/Datadog.Trace/TracerManager.cs b/tracer/src/Datadog.Trace/TracerManager.cs index 89d28ad425ed..0a06b798a7a4 100644 --- a/tracer/src/Datadog.Trace/TracerManager.cs +++ b/tracer/src/Datadog.Trace/TracerManager.cs @@ -758,15 +758,16 @@ private static void HeartbeatCallback(object state) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteTrace(ArraySegment trace) + public void WriteTrace(in SpanCollection trace) { + var chunk = trace; foreach (var processor in TraceProcessors) { if (processor is not null) { try { - trace = processor.Process(trace); + chunk = processor.Process(in chunk); } catch (Exception e) { @@ -775,9 +776,9 @@ public void WriteTrace(ArraySegment trace) } } - if (trace.Count > 0) + if (chunk.Count > 0) { - AgentWriter.WriteTrace(trace); + AgentWriter.WriteTrace(in chunk); } } } diff --git a/tracer/src/Datadog.Trace/Util/SamplingHelpers.cs b/tracer/src/Datadog.Trace/Util/SamplingHelpers.cs index d0fec8ba6ed6..fc2910ac4a0f 100644 --- a/tracer/src/Datadog.Trace/Util/SamplingHelpers.cs +++ b/tracer/src/Datadog.Trace/Util/SamplingHelpers.cs @@ -4,6 +4,7 @@ // using System; +using Datadog.Trace.Agent; namespace Datadog.Trace.Util { @@ -43,9 +44,9 @@ internal static bool SampleByRate(ulong id, double rate) return (id * KnuthFactor) <= (rate * ulong.MaxValue); } - internal static bool IsKeptBySamplingPriority(ArraySegment trace) + internal static bool IsKeptBySamplingPriority(in SpanCollection trace) { - if (TraceContext.GetTraceContext(trace)?.SamplingPriority is { } samplingPriority) + if (TraceContext.GetTraceContext(in trace)?.SamplingPriority is { } samplingPriority) { return SamplingPriorityValues.IsKeep(samplingPriority); } From a8b5479c5a5d3ef9aa89f94337befb7e0144cc26 Mon Sep 17 00:00:00 2001 From: Andrew Lock Date: Fri, 21 Nov 2025 18:45:47 +0000 Subject: [PATCH 7/9] Update unit tests to work with new APIs --- .../AspNetCoreDiagnosticObserverTests.cs | 4 +- .../EmptyDatadogTracer.cs | 8 +- .../Agent/AgentWriterTests.cs | 87 +++++++------------ .../Agent/MessagePack/SpanMetaStructTests.cs | 2 +- .../Agent/MessagePack/TraceChunkModelTests.cs | 27 ++---- .../Agent/SpanBufferTests.cs | 4 +- .../Configuration/MutableSettingsTests.cs | 2 +- tracer/test/Datadog.Trace.Tests/SpanTests.cs | 6 +- .../Datadog.Trace.Tests/TraceContextTests.cs | 69 ++++++--------- .../Util/StubDatadogTracer.cs | 14 ++- .../Benchmarks.Trace/AgentWriterBenchmark.cs | 4 +- .../Asm/EmptyDatadogTracer.cs | 10 +-- .../CIVisibilityProtocolWriterBenchmark.cs | 5 +- .../Benchmarks.Trace/DummyAgentWriter.cs | 2 +- .../TraceProcessorBenchmark.cs | 11 +-- 15 files changed, 101 insertions(+), 154 deletions(-) diff --git a/tracer/test/Datadog.Trace.IntegrationTests/DiagnosticListeners/AspNetCoreDiagnosticObserverTests.cs b/tracer/test/Datadog.Trace.IntegrationTests/DiagnosticListeners/AspNetCoreDiagnosticObserverTests.cs index 8269b65ce30e..630b7e687e0c 100644 --- a/tracer/test/Datadog.Trace.IntegrationTests/DiagnosticListeners/AspNetCoreDiagnosticObserverTests.cs +++ b/tracer/test/Datadog.Trace.IntegrationTests/DiagnosticListeners/AspNetCoreDiagnosticObserverTests.cs @@ -559,7 +559,7 @@ private static SpanCodeOrigin GetSpanCodeOrigin() private class AgentWriterStub : IAgentWriter { - public List> Traces { get; } = new(); + public List Traces { get; } = new(); public Task FlushAndCloseAsync() => Task.CompletedTask; @@ -567,7 +567,7 @@ private class AgentWriterStub : IAgentWriter public Task Ping() => Task.FromResult(true); - public void WriteTrace(ArraySegment trace) => Traces.Add(trace); + public void WriteTrace(in SpanCollection trace) => Traces.Add(trace); } } } diff --git a/tracer/test/Datadog.Trace.Security.Unit.Tests/EmptyDatadogTracer.cs b/tracer/test/Datadog.Trace.Security.Unit.Tests/EmptyDatadogTracer.cs index c43533eeb3a8..2b9984516fba 100644 --- a/tracer/test/Datadog.Trace.Security.Unit.Tests/EmptyDatadogTracer.cs +++ b/tracer/test/Datadog.Trace.Security.Unit.Tests/EmptyDatadogTracer.cs @@ -4,16 +4,12 @@ // using System; +using Datadog.Trace.Agent; using Datadog.Trace.Configuration; using Datadog.Trace.Configuration.Schema; using Datadog.Trace.Sampling; using Moq; -// -// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. -// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. -// - namespace Datadog.Trace.Security.Unit.Tests { internal class EmptyDatadogTracer : IDatadogTracer @@ -34,7 +30,7 @@ public EmptyDatadogTracer() public PerTraceSettings PerTraceSettings { get; } - void IDatadogTracer.Write(ArraySegment span) + void IDatadogTracer.Write(in SpanCollection span) { } } diff --git a/tracer/test/Datadog.Trace.Tests/Agent/AgentWriterTests.cs b/tracer/test/Datadog.Trace.Tests/Agent/AgentWriterTests.cs index e195acd0db87..20050e847bca 100644 --- a/tracer/test/Datadog.Trace.Tests/Agent/AgentWriterTests.cs +++ b/tracer/test/Datadog.Trace.Tests/Agent/AgentWriterTests.cs @@ -42,11 +42,8 @@ public async Task SpanSampling_CanComputeStats_ShouldNotSend_WhenSpanSamplingDoe { var api = new Mock(); var settings = SpanSamplingRule("*", "*", 0.0f); // don't sample any rule - var statsAggregator = new Mock(); - statsAggregator.Setup(x => x.CanComputeStats).Returns(true); - statsAggregator.Setup(x => x.ProcessTrace(It.IsAny>())).Returns>(x => x); - statsAggregator.Setup(x => x.ShouldKeepTrace(It.IsAny>())).Returns(false); - var agent = new AgentWriter(api.Object, statsAggregator.Object, statsd: null, automaticFlush: false); + var statsAggregator = new StubStatsAggregator(shouldKeepTrace: false, x => x); + var agent = new AgentWriter(api.Object, statsAggregator, statsd: null, automaticFlush: false); await using var tracer = TracerHelper.Create(settings, agent, sampler: null, scopeManager: null, statsd: null); @@ -56,7 +53,7 @@ public async Task SpanSampling_CanComputeStats_ShouldNotSend_WhenSpanSamplingDoe traceContext.AddSpan(span); traceContext.SetSamplingPriority(priority: SamplingPriorityValues.UserReject, mechanism: SamplingMechanism.Manual, rate: null, limiterRate: null); span.Finish(); // triggers the span sampler to run - var traceChunk = new ArraySegment([span]); + var traceChunk = new SpanCollection([span]); agent.WriteTrace(traceChunk); await agent.FlushTracesAsync(); // Force a flush to make sure the trace is written to the API @@ -70,12 +67,9 @@ public async Task SpanSampling_CanComputeStats_ShouldNotSend_WhenSpanSamplingDoe public async Task SpanSampling_ShouldSend_SingleMatchedSpan_WhenStatsDrops() { var api = new Mock(); - var statsAggregator = new Mock(); - statsAggregator.Setup(x => x.CanComputeStats).Returns(true); - statsAggregator.Setup(x => x.ProcessTrace(It.IsAny>())).Returns>(x => x); - statsAggregator.Setup(x => x.ShouldKeepTrace(It.IsAny>())).Returns(false); + var statsAggregator = new StubStatsAggregator(shouldKeepTrace: false, x => x); var settings = SpanSamplingRule("*", "*"); - var agent = new AgentWriter(api.Object, statsAggregator.Object, statsd: null, automaticFlush: false); + var agent = new AgentWriter(api.Object, statsAggregator, statsd: null, automaticFlush: false); await using var tracer = TracerHelper.Create(settings, agent, sampler: null, scopeManager: null, statsd: null); var traceContext = new TraceContext(tracer); @@ -84,7 +78,7 @@ public async Task SpanSampling_ShouldSend_SingleMatchedSpan_WhenStatsDrops() traceContext.AddSpan(span); traceContext.SetSamplingPriority(priority: SamplingPriorityValues.UserReject, mechanism: SamplingMechanism.Manual, rate: null, limiterRate: null); span.Finish(); - var traceChunk = new ArraySegment([span]); + var traceChunk = new SpanCollection([span]); var expectedData1 = Vendors.MessagePack.MessagePackSerializer.Serialize(new TraceChunkModel(traceChunk, SamplingPriorityValues.UserKeep), SpanFormatterResolver.Instance); await agent.FlushTracesAsync(); // Force a flush to make sure the trace is written to the API @@ -101,12 +95,9 @@ public async Task SpanSampling_ShouldSend_SingleMatchedSpan_WhenStatsDrops() public async Task SpanSampling_ShouldSend_MultipleMatchedSpans_WhenStatsDrops() { var api = new Mock(); - var statsAggregator = new Mock(); - statsAggregator.Setup(x => x.CanComputeStats).Returns(true); - statsAggregator.Setup(x => x.ProcessTrace(It.IsAny>())).Returns>(x => x); - statsAggregator.Setup(x => x.ShouldKeepTrace(It.IsAny>())).Returns(false); + var statsAggregator = new StubStatsAggregator(shouldKeepTrace: false, x => x); var settings = SpanSamplingRule("*", "*"); - var agent = new AgentWriter(api.Object, statsAggregator.Object, statsd: null, automaticFlush: false); + var agent = new AgentWriter(api.Object, statsAggregator, statsd: null, automaticFlush: false); await using var tracer = TracerHelper.Create(settings, agent, sampler: null, scopeManager: null, statsd: null); var traceContext = new TraceContext(tracer); @@ -120,7 +111,7 @@ public async Task SpanSampling_ShouldSend_MultipleMatchedSpans_WhenStatsDrops() rootSpan.Finish(); keptChildSpan.Finish(); - var expectedChunk = new ArraySegment([rootSpan, keptChildSpan]); + var expectedChunk = new SpanCollection([rootSpan, keptChildSpan]); // var size = ComputeSize(expectedChunk); var expectedData1 = Vendors.MessagePack.MessagePackSerializer.Serialize(new TraceChunkModel(expectedChunk, SamplingPriorityValues.UserKeep), SpanFormatterResolver.Instance); @@ -137,13 +128,10 @@ public async Task SpanSampling_ShouldSend_MultipleMatchedSpans_WhenStatsDrops() public async Task SpanSampling_ShouldSend_MultipleMatchedSpans_WhenStatsDropsOne() { var api = new MockApi(); - var statsAggregator = new Mock(); - statsAggregator.Setup(x => x.CanComputeStats).Returns(true); - statsAggregator.Setup(x => x.ProcessTrace(It.IsAny>())).Returns>(x => x); - statsAggregator.Setup(x => x.ShouldKeepTrace(It.IsAny>())).Returns(false); + var statsAggregator = new StubStatsAggregator(shouldKeepTrace: false, x => x); var settings = SpanSamplingRule("*", "operation"); - var agentWriter = new AgentWriter(api, statsAggregator.Object, statsd: null, automaticFlush: false); + var agentWriter = new AgentWriter(api, statsAggregator, statsd: null, automaticFlush: false); await using var tracer = TracerHelper.Create(settings, agentWriter, sampler: null, scopeManager: null, statsd: null); var traceContext = new TraceContext(tracer); @@ -162,14 +150,8 @@ public async Task SpanSampling_ShouldSend_MultipleMatchedSpans_WhenStatsDropsOne traceContext.CurrentTraceSettings.SpanSampler!.MakeSamplingDecision(rootSpan); traceContext.CurrentTraceSettings.SpanSampler!.MakeSamplingDecision(keptChildSpan); - // create a trace chunk so that our array has an offset - var unusedSpans = CreateTraceChunk(5, 10); - var spanList = new List(); - spanList.AddRange(unusedSpans.Array!); - spanList.AddRange(new[] { rootSpan, droppedChildSpan, droppedChildSpan2, keptChildSpan }); - var spans = spanList.ToArray(); - - var traceChunk = new ArraySegment(spans, 5, 4); + var spans = new[] { rootSpan, droppedChildSpan, droppedChildSpan2, keptChildSpan }; + var traceChunk = new SpanCollection(spans); agentWriter.WriteTrace(traceChunk); await agentWriter.FlushTracesAsync(); // Force a flush to make sure the trace is written to the API @@ -186,16 +168,13 @@ public async Task SpanSampling_ShouldSend_MultipleMatchedSpans_WhenStatsDropsOne [Fact] public void PushStats() { - var statsAggregator = new Mock(); var spans = CreateTraceChunk(1); - statsAggregator.Setup(x => x.ProcessTrace(spans)).Returns(spans); - statsAggregator.Setup(x => x.CanComputeStats).Returns(true); - - var agent = new AgentWriter(Mock.Of(), statsAggregator.Object, statsd: null, automaticFlush: false); + var statsAggregator = new StubStatsAggregator(shouldKeepTrace: false, x => spans); + var agent = new AgentWriter(Mock.Of(), statsAggregator, statsd: null, automaticFlush: false); agent.WriteTrace(spans); - statsAggregator.Verify(s => s.AddRange(It.Is>(x => x.Offset == 0 && x.Count == 1)), Times.Once); + statsAggregator.AddedSpans.Should().Contain(spans).Which.Count.Should().Be(1); } [Fact] @@ -433,7 +412,7 @@ public async Task AddsTraceKeepRateMetricToRootSpan() var childSpan = new Span(new SpanContext(rootSpanContext, traceContext, null), DateTimeOffset.UtcNow); traceContext.AddSpan(rootSpan); traceContext.AddSpan(childSpan); - var spans = new ArraySegment(new[] { rootSpan, childSpan }); + var spans = new SpanCollection([rootSpan, childSpan]); var sizeOfTrace = ComputeSize(spans); // Make the buffer size big enough for a single trace @@ -525,13 +504,13 @@ private static bool Equals(ArraySegment data, byte[] expectedData) return equals; } - private static int ComputeSize(ArraySegment spans) + private static int ComputeSize(SpanCollection spans) { var traceChunk = new TraceChunkModel(spans); return Vendors.MessagePack.MessagePackSerializer.Serialize(traceChunk, SpanFormatterResolver.Instance).Length; } - private static ArraySegment CreateTraceChunk(int spanCount, ulong startingId = 1) + private static SpanCollection CreateTraceChunk(int spanCount, ulong startingId = 1) { var spans = new Span[spanCount]; @@ -541,7 +520,7 @@ private static ArraySegment CreateTraceChunk(int spanCount, ulong starting spans[i] = new Span(spanContext, DateTimeOffset.UtcNow); } - return new ArraySegment(spans); + return new SpanCollection(spans); } private static TracerSettings SpanSamplingRule(string serviceName, string operationName, float sampleRate = 1.0f) @@ -559,24 +538,24 @@ private static TracerSettings SpanSamplingRule(string serviceName, string operat return TracerSettings.Create(new() { { ConfigurationKeys.SpanSamplingRules, JsonConvert.SerializeObject(rules) } }); } - private bool EqualSegments(ArraySegment expected, ArraySegment actual) + internal class StubStatsAggregator(bool shouldKeepTrace, Func processTrace) : IStatsAggregator { - if (expected.Count != actual.Count) - { - _output.WriteLine($"Segment count mismatch, Expected {expected.Count}, Actual {actual.Count}"); - return false; - } + public List AddedSpans { get; } = new(); + + public bool? CanComputeStats => true; - for (var ele = 0; ele < expected.Count; ele++) + public void Add(params Span[] spans) => AddRange(new SpanCollection(spans)); + + public void AddRange(in SpanCollection spans) { - if (expected.Array![expected.Offset + ele] != actual.Array![actual.Offset + ele]) - { - _output.WriteLine($"Element mismatch at entry {ele}, Expected {expected.Count}, Actual {actual.Count}"); - return false; - } + AddedSpans.Add(spans); } - return true; + public bool ShouldKeepTrace(in SpanCollection spans) => shouldKeepTrace; + + public SpanCollection ProcessTrace(in SpanCollection trace) => processTrace(trace); + + public Task DisposeAsync() => Task.CompletedTask; } } } diff --git a/tracer/test/Datadog.Trace.Tests/Agent/MessagePack/SpanMetaStructTests.cs b/tracer/test/Datadog.Trace.Tests/Agent/MessagePack/SpanMetaStructTests.cs index 8e0159570dcd..faf9774610cf 100644 --- a/tracer/test/Datadog.Trace.Tests/Agent/MessagePack/SpanMetaStructTests.cs +++ b/tracer/test/Datadog.Trace.Tests/Agent/MessagePack/SpanMetaStructTests.cs @@ -119,7 +119,7 @@ public static void GivenAEncodedSpanWithMetaStruct_WhenDecoding_ThenMetaStructIs var spanBytes = new byte[] { }; var spanBuffer = new SpanBuffer(10000, FormatterResolver); // We serialize the span - var serializationResult = spanBuffer.TryWrite(new ArraySegment(new[] { span }), ref spanBytes); + var serializationResult = spanBuffer.TryWrite(new([span]), ref spanBytes); serializationResult.Should().Be(SpanBuffer.WriteStatus.Success); // Find the offset of the header "meta_struct" in the byte array diff --git a/tracer/test/Datadog.Trace.Tests/Agent/MessagePack/TraceChunkModelTests.cs b/tracer/test/Datadog.Trace.Tests/Agent/MessagePack/TraceChunkModelTests.cs index dad9cb92f350..33278d87a7fd 100644 --- a/tracer/test/Datadog.Trace.Tests/Agent/MessagePack/TraceChunkModelTests.cs +++ b/tracer/test/Datadog.Trace.Tests/Agent/MessagePack/TraceChunkModelTests.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Datadog.Trace.Agent; using Datadog.Trace.Agent.MessagePack; using Datadog.Trace.Tests.Util; using FluentAssertions; @@ -46,30 +47,12 @@ public void DefaultTraceChunk() traceChunk.HashSetInitialized.Should().BeFalse(); } - [Fact] - public void EmptyArray() - { - // ArraySegment doesn't behave the same with "new ArraySegment" vs "default", - // so we're testing both to be sure - var spans = new ArraySegment(Array.Empty()); - var traceChunk = new TraceChunkModel(spans); - - traceChunk.SpanCount.Should().Be(0); - traceChunk.HashSetInitialized.Should().BeFalse(); - traceChunk.LocalRootSpanId.Should().BeNull(); - traceChunk.ContainsLocalRootSpan.Should().BeFalse(); - - traceChunk.Invoking(t => t.GetSpanModel(0)).Should().Throw(); - - traceChunk.HashSetInitialized.Should().BeFalse(); - } - [Fact] public void NewArraySegment() { // ArraySegment doesn't behave the same with "new ArraySegment" vs "default", // so we're testing both to be sure - var spans = new ArraySegment(); + var spans = new SpanCollection(); var traceChunk = new TraceChunkModel(spans); traceChunk.SpanCount.Should().Be(0); @@ -85,7 +68,7 @@ public void NewArraySegment() [Fact] public void DefaultArraySegment() { - ArraySegment spans = default; + SpanCollection spans = default; var traceChunk = new TraceChunkModel(spans); traceChunk.SpanCount.Should().Be(0); @@ -479,14 +462,14 @@ public void Override_SamplingPriority_WhenPresent(int samplingPriority) CreateSpan(traceId: 1, spanId: 10, parentId: 5), }; - var traceChunk = new TraceChunkModel(new ArraySegment(spans), samplingPriority); + var traceChunk = new TraceChunkModel(new SpanCollection(spans), samplingPriority); traceChunk.SamplingPriority.Should().Be(samplingPriority); } private static TraceChunkModel CreateTraceChunk(IEnumerable spans, Span root) { - var spansArray = new ArraySegment(spans.ToArray()); + var spansArray = new SpanCollection(spans.ToArray()); return new TraceChunkModel(spansArray, root); } diff --git a/tracer/test/Datadog.Trace.Tests/Agent/SpanBufferTests.cs b/tracer/test/Datadog.Trace.Tests/Agent/SpanBufferTests.cs index 723d868b7c22..ce8dd9244404 100644 --- a/tracer/test/Datadog.Trace.Tests/Agent/SpanBufferTests.cs +++ b/tracer/test/Datadog.Trace.Tests/Agent/SpanBufferTests.cs @@ -151,7 +151,7 @@ public void IsFirstChunkInBuffer_FirstChunkIsTrue_SubsequentChunksAreFalse() interceptedChunks[1].IsFirstChunkInPayload.Should().BeFalse(); } - private static ArraySegment CreateTraceChunk(int spanCount, ulong startingId = 1) + private static SpanCollection CreateTraceChunk(int spanCount, ulong startingId = 1) { var spans = new Span[spanCount]; @@ -161,7 +161,7 @@ private static ArraySegment CreateTraceChunk(int spanCount, ulong starting spans[i] = new Span(spanContext, DateTimeOffset.UtcNow); } - return new ArraySegment(spans); + return new SpanCollection(spans); } /// diff --git a/tracer/test/Datadog.Trace.Tests/Configuration/MutableSettingsTests.cs b/tracer/test/Datadog.Trace.Tests/Configuration/MutableSettingsTests.cs index 2bc1cc89e444..d99d7ce13666 100644 --- a/tracer/test/Datadog.Trace.Tests/Configuration/MutableSettingsTests.cs +++ b/tracer/test/Datadog.Trace.Tests/Configuration/MutableSettingsTests.cs @@ -183,7 +183,7 @@ public async Task TraceEnabled(string value, string otelValue, bool areTracesEna var assertion = areTracesEnabled ? Times.Once() : Times.Never(); - _writerMock.Verify(w => w.WriteTrace(It.IsAny>()), assertion); + _writerMock.Verify(w => w.WriteTrace(in It.Ref.IsAny), assertion); } [Theory] diff --git a/tracer/test/Datadog.Trace.Tests/SpanTests.cs b/tracer/test/Datadog.Trace.Tests/SpanTests.cs index 35cda5cd6903..61f5ffc94fc7 100644 --- a/tracer/test/Datadog.Trace.Tests/SpanTests.cs +++ b/tracer/test/Datadog.Trace.Tests/SpanTests.cs @@ -53,7 +53,7 @@ public void SetTag_KeyValue_KeyValueSet() span.SetTag(key, value); - _writerMock.Verify(x => x.WriteTrace(It.IsAny>()), Times.Never); + _writerMock.Verify(x => x.WriteTrace(It.IsAny()), Times.Never); span.GetTag(key).Should().Be(value); } @@ -65,7 +65,7 @@ public void SetPeerServiceTag_CallsRemapper() span.SetTag(Tags.PeerService, "a-peer-service"); - _writerMock.Verify(x => x.WriteTrace(It.IsAny>()), Times.Never); + _writerMock.Verify(x => x.WriteTrace(It.IsAny()), Times.Never); span.GetTag(Tags.PeerService).Should().Be("a-remmaped-peer-service"); span.GetTag(Tags.PeerServiceRemappedFrom).Should().Be("a-peer-service"); } @@ -121,7 +121,7 @@ public async Task Finish_NoEndTimeProvided_SpanWriten() await Task.Delay(TimeSpan.FromMilliseconds(1)); span.Finish(); - _writerMock.Verify(x => x.WriteTrace(It.IsAny>()), Times.Once); + _writerMock.Verify(x => x.WriteTrace(in It.Ref.IsAny), Times.Once); Assert.True(span.Duration > TimeSpan.Zero); } diff --git a/tracer/test/Datadog.Trace.Tests/TraceContextTests.cs b/tracer/test/Datadog.Trace.Tests/TraceContextTests.cs index 650ab28d25a3..b84db5d35ffd 100644 --- a/tracer/test/Datadog.Trace.Tests/TraceContextTests.cs +++ b/tracer/test/Datadog.Trace.Tests/TraceContextTests.cs @@ -56,16 +56,15 @@ public void UtcNow_IsMonotonic() [InlineData(false)] public void FlushPartialTraces(bool partialFlush) { - var tracer = new Mock(); + var settings = TracerSettings.Create( + new() + { + { ConfigurationKeys.PartialFlushEnabled, partialFlush }, + { ConfigurationKeys.PartialFlushMinSpans, 5 }, + }); + var tracer = new StubDatadogTracer(settings); - tracer.Setup(t => t.Settings).Returns(TracerSettings.Create(new() - { - { ConfigurationKeys.PartialFlushEnabled, partialFlush }, - { ConfigurationKeys.PartialFlushMinSpans, 5 }, - })); - tracer.Setup(x => x.PerTraceSettings).Returns(_tracerMock.PerTraceSettings); - - var traceContext = new TraceContext(tracer.Object); + var traceContext = new TraceContext(tracer); void AddAndCloseSpan() { @@ -85,18 +84,19 @@ void AddAndCloseSpan() } // At this point in time, we have 4 closed spans in the trace - tracer.Verify(t => t.Write(It.IsAny>()), Times.Never); + tracer.WrittenChunks.Should().BeEmpty(); AddAndCloseSpan(); // Now we have 5 closed spans, partial flush should kick-in if activated if (partialFlush) { - tracer.Verify(t => t.Write(It.Is>(s => s.Count == 5)), Times.Once); + tracer.WrittenChunks.Should().ContainSingle().Which.Count.Should().Be(5); + tracer.WrittenChunks.Clear(); } else { - tracer.Verify(t => t.Write(It.IsAny>()), Times.Never); + tracer.WrittenChunks.Should().BeEmpty(); } for (int i = 0; i < 5; i++) @@ -107,11 +107,12 @@ void AddAndCloseSpan() // We have 5 more closed spans, partial flush should kick-in a second time if activated if (partialFlush) { - tracer.Verify(t => t.Write(It.Is>(s => s.Count == 5)), Times.Exactly(2)); + tracer.WrittenChunks.Should().ContainSingle().Which.Count.Should().Be(5); + tracer.WrittenChunks.Clear(); } else { - tracer.Verify(t => t.Write(It.IsAny>()), Times.Never); + tracer.WrittenChunks.Should().BeEmpty(); } traceContext.CloseSpan(rootSpan); @@ -119,11 +120,11 @@ void AddAndCloseSpan() // Now the remaining spans are flushed if (partialFlush) { - tracer.Verify(t => t.Write(It.Is>(s => s.Count == 1)), Times.Once); + tracer.WrittenChunks.Should().ContainSingle().Which.Count.Should().Be(1); } else { - tracer.Verify(t => t.Write(It.Is>(s => s.Count == 11)), Times.Once); + tracer.WrittenChunks.Should().ContainSingle().Which.Count.Should().Be(11); } } @@ -134,21 +135,13 @@ public void FullFlushShouldNotPropagateSamplingPriority() Span CreateSpan() => new Span(new SpanContext(42, RandomIdGenerator.Shared.NextSpanId()), DateTimeOffset.UtcNow); - var tracer = new Mock(); - - tracer.Setup(t => t.Settings).Returns(TracerSettings.Create(new() + var tracer = new StubDatadogTracer(TracerSettings.Create(new() { { ConfigurationKeys.PartialFlushEnabled, true }, { ConfigurationKeys.PartialFlushMinSpans, partialFlushThreshold }, })); - tracer.Setup(x => x.PerTraceSettings).Returns(_tracerMock.PerTraceSettings); - - ArraySegment? spans = null; - tracer.Setup(t => t.Write(It.IsAny>())) - .Callback>((s) => spans = s); - - var traceContext = new TraceContext(tracer.Object); + var traceContext = new TraceContext(tracer); traceContext.SetSamplingPriority(SamplingPriorityValues.UserKeep); var rootSpan = CreateSpan(); @@ -163,18 +156,17 @@ public void FullFlushShouldNotPropagateSamplingPriority() } // At this point, only one span is missing to reach the threshold for partial flush - spans.Should().BeNull("partial flush should not have been triggered"); + tracer.WrittenChunks.Should().BeEmpty("partial flush should not have been triggered"); // Closing the root span brings the number of closed spans to the threshold // but a full flush should be triggered rather than a partial, because every span in the trace has been closed traceContext.CloseSpan(rootSpan); - spans.Should().NotBeNull("a full flush should have been triggered"); - spans!.Value.Should().NotBeNullOrEmpty("a full flush should have been triggered"); + tracer.WrittenChunks.Should().NotBeNullOrEmpty("a full flush should have been triggered"); rootSpan.GetMetric(Metrics.SamplingPriority).Should().BeNull("because sampling priority is not added until serialization"); - spans!.Value.Should().OnlyContain(s => s.GetMetric(Metrics.SamplingPriority) == null, "because sampling priority is not added until serialization"); + tracer.WrittenChunks.Should().ContainSingle().Which.Should().OnlyContain(s => s.GetMetric(Metrics.SamplingPriority) == null, "because sampling priority is not added until serialization"); } [Fact] @@ -184,21 +176,13 @@ public void PartialFlushShouldPropagateMetadata() Span CreateSpan() => new Span(new SpanContext(42, RandomIdGenerator.Shared.NextSpanId()), DateTimeOffset.UtcNow); - var tracer = new Mock(); - - tracer.Setup(t => t.Settings).Returns(TracerSettings.Create(new() + var tracer = new StubDatadogTracer(TracerSettings.Create(new() { { ConfigurationKeys.PartialFlushEnabled, true }, { ConfigurationKeys.PartialFlushMinSpans, partialFlushThreshold }, })); - ArraySegment? spans = null; - - tracer.Setup(t => t.Write(It.IsAny>())) - .Callback>((s) => spans = s); - tracer.Setup(x => x.PerTraceSettings).Returns(_tracerMock.PerTraceSettings); - - var traceContext = new TraceContext(tracer.Object); + var traceContext = new TraceContext(tracer); traceContext.SetSamplingPriority(SamplingPriorityValues.UserKeep); var rootSpan = CreateSpan(); @@ -214,9 +198,8 @@ public void PartialFlushShouldPropagateMetadata() traceContext.CloseSpan(span); } - spans.Should().NotBeNull("partial flush should have been triggered"); - spans!.Value.Should().NotBeNullOrEmpty("partial flush should have been triggered"); - spans!.Value.Should().OnlyContain(s => s.GetMetric(Metrics.SamplingPriority) == null, "because sampling priority is not added until serialization"); + tracer.WrittenChunks.Should().NotBeEmpty("partial flush should have been triggered"); + tracer.WrittenChunks.Should().ContainSingle().Which.Should().OnlyContain(s => s.GetMetric(Metrics.SamplingPriority) == null, "because sampling priority is not added until serialization"); } [Fact] diff --git a/tracer/test/Datadog.Trace.Tests/Util/StubDatadogTracer.cs b/tracer/test/Datadog.Trace.Tests/Util/StubDatadogTracer.cs index 595985f4c427..1665694703c6 100644 --- a/tracer/test/Datadog.Trace.Tests/Util/StubDatadogTracer.cs +++ b/tracer/test/Datadog.Trace.Tests/Util/StubDatadogTracer.cs @@ -4,6 +4,8 @@ // using System; +using System.Collections.Generic; +using Datadog.Trace.Agent; using Datadog.Trace.Configuration; using Datadog.Trace.Configuration.Schema; @@ -12,9 +14,14 @@ namespace Datadog.Trace.Tests.Util; internal class StubDatadogTracer : IDatadogTracer { public StubDatadogTracer() + : this(new TracerSettings(NullConfigurationSource.Instance)) + { + } + + public StubDatadogTracer(TracerSettings settings) { DefaultServiceName = "stub-service"; - Settings = new TracerSettings(NullConfigurationSource.Instance); + Settings = settings; var namingSchema = new NamingSchema(SchemaVersion.V0, false, false, DefaultServiceName, null, null); PerTraceSettings = new PerTraceSettings(null, null, namingSchema, MutableSettings.CreateWithoutDefaultSources(Settings)); } @@ -23,11 +30,14 @@ public StubDatadogTracer() public TracerSettings Settings { get; } + public List WrittenChunks { get; } = new(); + IGitMetadataTagsProvider IDatadogTracer.GitMetadataTagsProvider => new NullGitMetadataProvider(); public PerTraceSettings PerTraceSettings { get; } - void IDatadogTracer.Write(ArraySegment span) + void IDatadogTracer.Write(in SpanCollection span) { + WrittenChunks.Add(span); } } diff --git a/tracer/test/benchmarks/Benchmarks.Trace/AgentWriterBenchmark.cs b/tracer/test/benchmarks/Benchmarks.Trace/AgentWriterBenchmark.cs index 158b56489c51..f1c965d03bef 100644 --- a/tracer/test/benchmarks/Benchmarks.Trace/AgentWriterBenchmark.cs +++ b/tracer/test/benchmarks/Benchmarks.Trace/AgentWriterBenchmark.cs @@ -21,7 +21,7 @@ public class AgentWriterBenchmark private const int SpanCount = 1000; private static readonly IAgentWriter AgentWriter; - private static readonly ArraySegment EnrichedSpans; + private static readonly SpanCollection EnrichedSpans; static AgentWriterBenchmark() { var overrides = new NameValueConfigurationSource(new() @@ -46,7 +46,7 @@ static AgentWriterBenchmark() enrichedSpans[i].SetMetric(Metrics.SamplingRuleDecision, 1.0); } - EnrichedSpans = new ArraySegment(enrichedSpans); + EnrichedSpans = new SpanCollection(enrichedSpans, SpanCount); // Run benchmarks once to reduce noise new AgentWriterBenchmark().WriteAndFlushEnrichedTraces().GetAwaiter().GetResult(); diff --git a/tracer/test/benchmarks/Benchmarks.Trace/Asm/EmptyDatadogTracer.cs b/tracer/test/benchmarks/Benchmarks.Trace/Asm/EmptyDatadogTracer.cs index 9fa066ce7e47..ff49cf53744d 100644 --- a/tracer/test/benchmarks/Benchmarks.Trace/Asm/EmptyDatadogTracer.cs +++ b/tracer/test/benchmarks/Benchmarks.Trace/Asm/EmptyDatadogTracer.cs @@ -3,14 +3,8 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // -using System; - -// -// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. -// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. -// - using Datadog.Trace; +using Datadog.Trace.Agent; using Datadog.Trace.Configuration; namespace Benchmarks.Trace.Asm @@ -37,7 +31,7 @@ public EmptyDatadogTracer() PerTraceSettings IDatadogTracer.PerTraceSettings => _perTraceSettings; - void IDatadogTracer.Write(ArraySegment span) + void IDatadogTracer.Write(in SpanCollection span) { } diff --git a/tracer/test/benchmarks/Benchmarks.Trace/CIVisibilityProtocolWriterBenchmark.cs b/tracer/test/benchmarks/Benchmarks.Trace/CIVisibilityProtocolWriterBenchmark.cs index 8939d1e7c42d..6432308f13ff 100644 --- a/tracer/test/benchmarks/Benchmarks.Trace/CIVisibilityProtocolWriterBenchmark.cs +++ b/tracer/test/benchmarks/Benchmarks.Trace/CIVisibilityProtocolWriterBenchmark.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using Datadog.Trace; +using Datadog.Trace.Agent; using Datadog.Trace.Ci.Agent; using Datadog.Trace.Ci.Agent.Payloads; using Datadog.Trace.Ci.Configuration; @@ -18,7 +19,7 @@ public class CIVisibilityProtocolWriterBenchmark private const int SpanCount = 1000; private static readonly IEventWriter EventWriter; - private static readonly ArraySegment EnrichedSpans; + private static readonly SpanCollection EnrichedSpans; static CIVisibilityProtocolWriterBenchmark() { @@ -41,7 +42,7 @@ static CIVisibilityProtocolWriterBenchmark() enrichedSpans[i].SetMetric(Metrics.SamplingRuleDecision, 1.0); } - EnrichedSpans = new ArraySegment(enrichedSpans); + EnrichedSpans = new SpanCollection(enrichedSpans, SpanCount); // Run benchmarks once to reduce noise new CIVisibilityProtocolWriterBenchmark().WriteAndFlushEnrichedTraces().GetAwaiter().GetResult(); diff --git a/tracer/test/benchmarks/Benchmarks.Trace/DummyAgentWriter.cs b/tracer/test/benchmarks/Benchmarks.Trace/DummyAgentWriter.cs index 3d5fd0057833..ca9d981de06c 100644 --- a/tracer/test/benchmarks/Benchmarks.Trace/DummyAgentWriter.cs +++ b/tracer/test/benchmarks/Benchmarks.Trace/DummyAgentWriter.cs @@ -32,7 +32,7 @@ public Task Ping() return PingTask; } - public void WriteTrace(ArraySegment trace) + public void WriteTrace(in SpanCollection trace) { } } diff --git a/tracer/test/benchmarks/Benchmarks.Trace/TraceProcessorBenchmark.cs b/tracer/test/benchmarks/Benchmarks.Trace/TraceProcessorBenchmark.cs index c71254c385f2..b45969c125d9 100644 --- a/tracer/test/benchmarks/Benchmarks.Trace/TraceProcessorBenchmark.cs +++ b/tracer/test/benchmarks/Benchmarks.Trace/TraceProcessorBenchmark.cs @@ -2,6 +2,7 @@ using System.Linq; using BenchmarkDotNet.Attributes; using Datadog.Trace; +using Datadog.Trace.Agent; using Datadog.Trace.Processors; namespace Benchmarks.Trace @@ -13,7 +14,7 @@ public class TraceProcessorBenchmark private readonly ITraceProcessor _normalizerTraceProcessor; private readonly ITraceProcessor _trucantorTraceProcessor; private readonly ITraceProcessor _obfuscatorTraceProcessor; - private ArraySegment _spans; + private SpanCollection _spans; public TraceProcessorBenchmark() { @@ -26,25 +27,25 @@ public TraceProcessorBenchmark() var span = new Span(spanContext, DateTimeOffset.Now); span.ResourceName = "My Resource Name"; span.Type = "sql"; - _spans = new ArraySegment(Enumerable.Repeat(span, 100).ToArray()); + _spans = new SpanCollection(Enumerable.Repeat(span, 100).ToArray(), 100); } [Benchmark] public void NormalizerProcessor() { - _normalizerTraceProcessor.Process(_spans); + _normalizerTraceProcessor.Process(in _spans); } [Benchmark] public void TruncatorProcessor() { - _trucantorTraceProcessor.Process(_spans); + _trucantorTraceProcessor.Process(in _spans); } [Benchmark] public void ObfuscatorProcessor() { - _obfuscatorTraceProcessor.Process(_spans); + _obfuscatorTraceProcessor.Process(in _spans); } [Benchmark] From 685debf0029551249b7d04086d9112328664aa7d Mon Sep 17 00:00:00 2001 From: Andrew Lock Date: Fri, 21 Nov 2025 18:40:03 +0000 Subject: [PATCH 8/9] Try alternative SpanCollection implementation --- .../src/Datadog.Trace/Agent/SpanCollection.cs | 87 +++++++------------ 1 file changed, 32 insertions(+), 55 deletions(-) diff --git a/tracer/src/Datadog.Trace/Agent/SpanCollection.cs b/tracer/src/Datadog.Trace/Agent/SpanCollection.cs index 9d8a6239548a..0883fdddfee3 100644 --- a/tracer/src/Datadog.Trace/Agent/SpanCollection.cs +++ b/tracer/src/Datadog.Trace/Agent/SpanCollection.cs @@ -10,9 +10,6 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; using Datadog.Trace.SourceGenerators; -#if NETFRAMEWORK -using Datadog.Trace.VendoredMicrosoftCode.System.Runtime.CompilerServices.Unsafe; -#endif namespace Datadog.Trace.Agent; @@ -21,7 +18,8 @@ namespace Datadog.Trace.Agent; /// internal readonly struct SpanCollection : IEnumerable { - private readonly object? _values; + private readonly Span? _span; + private readonly Span[]? _spans; public readonly int Count; /// @@ -30,7 +28,7 @@ namespace Datadog.Trace.Agent; /// The span to include in the collection. public SpanCollection(Span value) { - _values = value; + _span = value; Count = 1; } @@ -40,7 +38,7 @@ public SpanCollection(Span value) /// The value to initializer public SpanCollection(int arrayBuilderCapacity) { - _values = new Span[arrayBuilderCapacity]; + _spans = new Span[arrayBuilderCapacity]; Count = 0; } @@ -51,7 +49,7 @@ public SpanCollection(Span[] values, int count) { // We assume that the caller is "sensible" here, and doesn't set count > values.Length, // but that will get hit "safely" elsewhere if it happens - _values = values; + _spans = values; Count = count; } @@ -69,20 +67,18 @@ public Span? RootSpan [MethodImpl(MethodImplOptions.AggressiveInlining)] get { - // Take local copy of _values so type checks remain valid even if the SpanCollection is overwritten in memory - var value = _values; - if (value is Span span) + if (_span is not null) { - return span; + return _span; } - if (value is null) + if (_spans is null) { return null; } // Not Span, not null, can only be SpanArray - return Unsafe.As(value)[0]; + return _spans[0]; } } @@ -96,20 +92,17 @@ public Span this[int index] [MethodImpl(MethodImplOptions.AggressiveInlining)] get { - // Take local copy of _values so type checks remain valid even if the SpanCollection is overwritten in memory - object? value = _values; - if (index < Count) + if (_span is not null) { - if (value is Span str) + if (index == 0) { - return str; - } - else if (value != null) - { - // Not Span, not null, can only be Span[] - return Unsafe.As(value)[index]; + return _span; } } + else if (_spans is not null && index < Count) + { + return _spans[index]; + } return OutOfBounds(); // throws } @@ -123,23 +116,19 @@ public Span this[int index] /// The concatenation of and . public static SpanCollection Append(in SpanCollection values, Span value) { - // Take local copy of _values so type checks remain valid even if the SpanCollection is overwritten in memory - var current = values._values; - if (current is null) + if (values._span is not null) { - return new SpanCollection(value); + // We use a default capacity of 4 spans + // 2 Spans would cover 25% not covered by single span case, 4 covers ~ 70%, 8 covers ~92% + return new SpanCollection([values._span, value, null!, null!], 2); } - if (current is Span span) + if (values._spans is null) { - // We use a default capacity of 4 spans - // 2 Spans would cover 25% not covered by single span case, 4 covers ~ 70%, 8 covers ~92% - return new SpanCollection([span, value, null!, null!], 2); + return new SpanCollection(value); } - // Not Span, not null, can only be Span[], so add the span - var array = Unsafe.As(current); - array = GrowIfNeeded(array, values.Count); + var array = GrowIfNeeded(values._spans, values.Count); array[values.Count] = value; return new SpanCollection(array, values.Count + 1); } @@ -161,20 +150,17 @@ private static Span OutOfBounds() /// public ArraySegment ToArray() { - // Take local copy of _values so type checks remain valid even if the SpanCollection is overwritten in memory - object? value = _values; - if (value is Span[] values) + if (_spans is not null) { - return new ArraySegment(values, 0, Count); + return new ArraySegment(_spans, 0, Count); } - else if (value != null) + else if (_span is not null) { - // value not array, can only be Span - return new ArraySegment([Unsafe.As(value)], 0, 1); + return new ArraySegment([_span], 0, 1); } else { - return new ArraySegment(Array.Empty(), 0, 0); + return new ArraySegment([], 0, 0); } } @@ -197,7 +183,7 @@ private static Span[] GrowIfNeeded(Span[] array, int currentCount) /// An enumerator that can be used to iterate through the . public Enumerator GetEnumerator() { - return new Enumerator(_values, Count); + return new Enumerator(_span, _spans, Count); } /// @@ -222,9 +208,9 @@ public struct Enumerator : IEnumerator private int _index; private Span? _current; - internal Enumerator(object? value, int count) + internal Enumerator(Span? span, Span[]? spans, int count) { - if (value is Span span) + if (span is not null) { _values = null; _current = span; @@ -233,22 +219,13 @@ internal Enumerator(object? value, int count) else { _current = null; - _values = Unsafe.As(value); + _values = spans; _count = count; } _index = 0; } - /// - /// Initializes a new instance of the struct. - /// - /// The to enumerate. - public Enumerator(ref SpanCollection values) - : this(values._values, values.Count) - { - } - /// /// Gets the element at the current position of the enumerator. /// From 49fc04695a047e46aca4d5746ade978ff9f65457 Mon Sep 17 00:00:00 2001 From: Andrew Lock Date: Fri, 21 Nov 2025 18:40:15 +0000 Subject: [PATCH 9/9] Revert "Try alternative SpanCollection implementation" This reverts commit 9b542fd6b89bfa0b31025d9587fd6f64df8aad03. --- .../src/Datadog.Trace/Agent/SpanCollection.cs | 87 ++++++++++++------- 1 file changed, 55 insertions(+), 32 deletions(-) diff --git a/tracer/src/Datadog.Trace/Agent/SpanCollection.cs b/tracer/src/Datadog.Trace/Agent/SpanCollection.cs index 0883fdddfee3..9d8a6239548a 100644 --- a/tracer/src/Datadog.Trace/Agent/SpanCollection.cs +++ b/tracer/src/Datadog.Trace/Agent/SpanCollection.cs @@ -10,6 +10,9 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; using Datadog.Trace.SourceGenerators; +#if NETFRAMEWORK +using Datadog.Trace.VendoredMicrosoftCode.System.Runtime.CompilerServices.Unsafe; +#endif namespace Datadog.Trace.Agent; @@ -18,8 +21,7 @@ namespace Datadog.Trace.Agent; /// internal readonly struct SpanCollection : IEnumerable { - private readonly Span? _span; - private readonly Span[]? _spans; + private readonly object? _values; public readonly int Count; /// @@ -28,7 +30,7 @@ namespace Datadog.Trace.Agent; /// The span to include in the collection. public SpanCollection(Span value) { - _span = value; + _values = value; Count = 1; } @@ -38,7 +40,7 @@ public SpanCollection(Span value) /// The value to initializer public SpanCollection(int arrayBuilderCapacity) { - _spans = new Span[arrayBuilderCapacity]; + _values = new Span[arrayBuilderCapacity]; Count = 0; } @@ -49,7 +51,7 @@ public SpanCollection(Span[] values, int count) { // We assume that the caller is "sensible" here, and doesn't set count > values.Length, // but that will get hit "safely" elsewhere if it happens - _spans = values; + _values = values; Count = count; } @@ -67,18 +69,20 @@ public Span? RootSpan [MethodImpl(MethodImplOptions.AggressiveInlining)] get { - if (_span is not null) + // Take local copy of _values so type checks remain valid even if the SpanCollection is overwritten in memory + var value = _values; + if (value is Span span) { - return _span; + return span; } - if (_spans is null) + if (value is null) { return null; } // Not Span, not null, can only be SpanArray - return _spans[0]; + return Unsafe.As(value)[0]; } } @@ -92,16 +96,19 @@ public Span this[int index] [MethodImpl(MethodImplOptions.AggressiveInlining)] get { - if (_span is not null) + // Take local copy of _values so type checks remain valid even if the SpanCollection is overwritten in memory + object? value = _values; + if (index < Count) { - if (index == 0) + if (value is Span str) { - return _span; + return str; + } + else if (value != null) + { + // Not Span, not null, can only be Span[] + return Unsafe.As(value)[index]; } - } - else if (_spans is not null && index < Count) - { - return _spans[index]; } return OutOfBounds(); // throws @@ -116,19 +123,23 @@ public Span this[int index] /// The concatenation of and . public static SpanCollection Append(in SpanCollection values, Span value) { - if (values._span is not null) + // Take local copy of _values so type checks remain valid even if the SpanCollection is overwritten in memory + var current = values._values; + if (current is null) { - // We use a default capacity of 4 spans - // 2 Spans would cover 25% not covered by single span case, 4 covers ~ 70%, 8 covers ~92% - return new SpanCollection([values._span, value, null!, null!], 2); + return new SpanCollection(value); } - if (values._spans is null) + if (current is Span span) { - return new SpanCollection(value); + // We use a default capacity of 4 spans + // 2 Spans would cover 25% not covered by single span case, 4 covers ~ 70%, 8 covers ~92% + return new SpanCollection([span, value, null!, null!], 2); } - var array = GrowIfNeeded(values._spans, values.Count); + // Not Span, not null, can only be Span[], so add the span + var array = Unsafe.As(current); + array = GrowIfNeeded(array, values.Count); array[values.Count] = value; return new SpanCollection(array, values.Count + 1); } @@ -150,17 +161,20 @@ private static Span OutOfBounds() /// public ArraySegment ToArray() { - if (_spans is not null) + // Take local copy of _values so type checks remain valid even if the SpanCollection is overwritten in memory + object? value = _values; + if (value is Span[] values) { - return new ArraySegment(_spans, 0, Count); + return new ArraySegment(values, 0, Count); } - else if (_span is not null) + else if (value != null) { - return new ArraySegment([_span], 0, 1); + // value not array, can only be Span + return new ArraySegment([Unsafe.As(value)], 0, 1); } else { - return new ArraySegment([], 0, 0); + return new ArraySegment(Array.Empty(), 0, 0); } } @@ -183,7 +197,7 @@ private static Span[] GrowIfNeeded(Span[] array, int currentCount) /// An enumerator that can be used to iterate through the . public Enumerator GetEnumerator() { - return new Enumerator(_span, _spans, Count); + return new Enumerator(_values, Count); } /// @@ -208,9 +222,9 @@ public struct Enumerator : IEnumerator private int _index; private Span? _current; - internal Enumerator(Span? span, Span[]? spans, int count) + internal Enumerator(object? value, int count) { - if (span is not null) + if (value is Span span) { _values = null; _current = span; @@ -219,13 +233,22 @@ internal Enumerator(Span? span, Span[]? spans, int count) else { _current = null; - _values = spans; + _values = Unsafe.As(value); _count = count; } _index = 0; } + /// + /// Initializes a new instance of the struct. + /// + /// The to enumerate. + public Enumerator(ref SpanCollection values) + : this(values._values, values.Count) + { + } + /// /// Gets the element at the current position of the enumerator. ///