diff --git a/tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml b/tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml
index c26904ebc5d5..c3f7aa3b23de 100644
--- a/tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml
+++ b/tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml
@@ -672,6 +672,7 @@
+
diff --git a/tracer/src/Datadog.Trace/Util/ValueStringBuilder.cs b/tracer/src/Datadog.Trace/Util/ValueStringBuilder.cs
new file mode 100644
index 000000000000..59357b9cbb98
--- /dev/null
+++ b/tracer/src/Datadog.Trace/Util/ValueStringBuilder.cs
@@ -0,0 +1,316 @@
+//
+// 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.
+//
+
+#if NET6_0_OR_GREATER
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Buffers;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+#nullable enable
+
+namespace Datadog.Trace.Util
+{
+ internal ref struct ValueStringBuilder
+ {
+ private char[]? _arrayToReturnToPool;
+ private Span _chars;
+ private int _pos;
+
+ public ValueStringBuilder(Span initialBuffer)
+ {
+ _arrayToReturnToPool = null;
+ _chars = initialBuffer;
+ _pos = 0;
+ }
+
+ public ValueStringBuilder(int initialCapacity)
+ {
+ _arrayToReturnToPool = ArrayPool.Shared.Rent(initialCapacity);
+ _chars = _arrayToReturnToPool;
+ _pos = 0;
+ }
+
+ /// Gets the underlying storage of the builder.
+ public Span RawChars => _chars;
+
+ public int Length
+ {
+ get => _pos;
+ set
+ {
+ _pos = value;
+ }
+ }
+
+ public int Capacity => _chars.Length;
+
+ public ref char this[int index]
+ {
+ get
+ {
+ return ref _chars[index];
+ }
+ }
+
+ public void EnsureCapacity(int capacity)
+ {
+ // This is not expected to be called this with negative capacity
+
+ // If the caller has a bug and calls this with negative capacity, make sure to call Grow to throw an exception.
+ if ((uint)capacity > (uint)_chars.Length)
+ {
+ Grow(capacity - _pos);
+ }
+ }
+
+ ///
+ /// Ensures that the builder is terminated with a NUL character.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void NullTerminate()
+ {
+ EnsureCapacity(_pos + 1);
+ _chars[_pos] = '\0';
+ }
+
+ ///
+ /// Get a pinnable reference to the builder.
+ /// Does not ensure there is a null char after
+ /// This overload is pattern matched in the C# 7.3+ compiler so you can omit
+ /// the explicit method call, and write eg "fixed (char* c = builder)"
+ ///
+ public ref char GetPinnableReference()
+ {
+ return ref MemoryMarshal.GetReference(_chars);
+ }
+
+ public override string ToString()
+ {
+ string s = _chars.Slice(0, _pos).ToString();
+ Dispose();
+ return s;
+ }
+
+ public ReadOnlySpan AsSpan() => _chars.Slice(0, _pos);
+
+ public ReadOnlySpan AsSpan(int start) => _chars.Slice(start, _pos - start);
+
+ public ReadOnlySpan AsSpan(int start, int length) => _chars.Slice(start, length);
+
+ public void Insert(int index, char value, int count)
+ {
+ if (_pos > _chars.Length - count)
+ {
+ Grow(count);
+ }
+
+ int remaining = _pos - index;
+ _chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count));
+ _chars.Slice(index, count).Fill(value);
+ _pos += count;
+ }
+
+ public void Insert(int index, string? s)
+ {
+ if (s == null)
+ {
+ return;
+ }
+
+ int count = s.Length;
+
+ if (_pos > (_chars.Length - count))
+ {
+ Grow(count);
+ }
+
+ int remaining = _pos - index;
+ _chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count));
+ s
+#if !NET
+ .AsSpan()
+#endif
+ .CopyTo(_chars.Slice(index));
+ _pos += count;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Append(char c)
+ {
+ int pos = _pos;
+ Span chars = _chars;
+ if ((uint)pos < (uint)chars.Length)
+ {
+ chars[pos] = c;
+ _pos = pos + 1;
+ }
+ else
+ {
+ GrowAndAppend(c);
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Append(string? s)
+ {
+ if (s == null)
+ {
+ return;
+ }
+
+ int pos = _pos;
+ // very common case, e.g. appending strings from NumberFormatInfo like separators, percent symbols, etc.
+ if (s.Length == 1 && (uint)pos < (uint)_chars.Length)
+ {
+ _chars[pos] = s[0];
+ _pos = pos + 1;
+ }
+ else
+ {
+ AppendSlow(s);
+ }
+ }
+
+ private void AppendSlow(string s)
+ {
+ int pos = _pos;
+ if (pos > _chars.Length - s.Length)
+ {
+ Grow(s.Length);
+ }
+
+ s
+#if !NET
+ .AsSpan()
+#endif
+ .CopyTo(_chars.Slice(pos));
+ _pos += s.Length;
+ }
+
+ public void Append(char c, int count)
+ {
+ if (_pos > _chars.Length - count)
+ {
+ Grow(count);
+ }
+
+ Span dst = _chars.Slice(_pos, count);
+ for (int i = 0; i < dst.Length; i++)
+ {
+ dst[i] = c;
+ }
+
+ _pos += count;
+ }
+
+ public void Append(scoped ReadOnlySpan value)
+ {
+ int pos = _pos;
+ if (pos > _chars.Length - value.Length)
+ {
+ Grow(value.Length);
+ }
+
+ value.CopyTo(_chars.Slice(_pos));
+ _pos += value.Length;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Span AppendSpan(int length)
+ {
+ int origPos = _pos;
+ if (origPos > _chars.Length - length)
+ {
+ Grow(length);
+ }
+
+ _pos = origPos + length;
+ return _chars.Slice(origPos, length);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AppendAsLowerInvariant(scoped ReadOnlySpan value)
+ {
+ int pos = _pos;
+ if (pos > _chars.Length - value.Length)
+ {
+ Grow(value.Length);
+ }
+
+ value.ToLowerInvariant(_chars.Slice(_pos));
+ _pos += value.Length;
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private void GrowAndAppend(char c)
+ {
+ Grow(1);
+ Append(c);
+ }
+
+ ///
+ /// Resize the internal buffer either by doubling current buffer size or
+ /// by adding to
+ /// whichever is greater.
+ ///
+ ///
+ /// Number of chars requested beyond current position.
+ ///
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private void Grow(int additionalCapacityBeyondPos)
+ {
+ const uint ArrayMaxLength = 0x7FFFFFC7; // same as Array.MaxLength
+
+ // Increase to at least the required size (_pos + additionalCapacityBeyondPos), but try
+ // to double the size if possible, bounding the doubling to not go beyond the max array length.
+ int newCapacity = (int)Math.Max(
+ (uint)(_pos + additionalCapacityBeyondPos),
+ Math.Min((uint)_chars.Length * 2, ArrayMaxLength));
+
+ // Make sure to let Rent throw an exception if the caller has a bug and the desired capacity is negative.
+ // This could also go negative if the actual required length wraps around.
+ char[] poolArray = ArrayPool.Shared.Rent(newCapacity);
+
+ _chars.Slice(0, _pos).CopyTo(poolArray);
+
+ char[]? toReturn = _arrayToReturnToPool;
+ _chars = _arrayToReturnToPool = poolArray;
+ if (toReturn != null)
+ {
+ ArrayPool.Shared.Return(toReturn);
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Dispose()
+ {
+ char[]? toReturn = _arrayToReturnToPool;
+ this = default; // for safety, to avoid using pooled array if this instance is erroneously appended to again
+ if (toReturn != null)
+ {
+ ArrayPool.Shared.Return(toReturn);
+ }
+ }
+
+ internal void AppendSpanFormattable(T value, string? format = null, IFormatProvider? provider = null)
+ where T : ISpanFormattable
+ {
+ if (value.TryFormat(_chars.Slice(_pos), out int charsWritten, format, provider))
+ {
+ _pos += charsWritten;
+ }
+ else
+ {
+ Append(value.ToString(format, provider));
+ }
+ }
+ }
+}
+#endif
diff --git a/tracer/test/Datadog.Trace.Tests/Util/ValueStringBuilderTests.cs b/tracer/test/Datadog.Trace.Tests/Util/ValueStringBuilderTests.cs
new file mode 100644
index 000000000000..149a5ddcf060
--- /dev/null
+++ b/tracer/test/Datadog.Trace.Tests/Util/ValueStringBuilderTests.cs
@@ -0,0 +1,288 @@
+//
+// 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.
+//
+
+#if NET6_0_OR_GREATER
+
+// Based on tests from https://github.com/dotnet/runtime/blob/b1e550cccc539b438a19f45816e8c5030ebb89db/src/libraries/Common/tests/Tests/System/Text/ValueStringBuilderTests.cs
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Text;
+using Datadog.Trace.Util;
+using FluentAssertions;
+
+namespace Datadog.Trace.Tests.Util;
+
+using Xunit;
+
+public class ValueStringBuilderTests
+{
+ [Fact]
+ public void Ctor_Default_CanAppend()
+ {
+ var vsb = default(ValueStringBuilder);
+ vsb.Length.Should().Be(0);
+
+ vsb.Append('a');
+ vsb.Length.Should().Be(1);
+ vsb.ToString().Should().Be("a");
+ }
+
+ [Fact]
+ public void Ctor_Span_CanAppend()
+ {
+ var vsb = new ValueStringBuilder(new char[1]);
+ vsb.Length.Should().Be(0);
+
+ vsb.Append('a');
+ vsb.Length.Should().Be(1);
+ vsb.ToString().Should().Be("a");
+ }
+
+ [Fact]
+ public void Ctor_InitialCapacity_CanAppend()
+ {
+ var vsb = new ValueStringBuilder(1);
+ vsb.Length.Should().Be(0);
+
+ vsb.Append('a');
+ vsb.Length.Should().Be(1);
+ vsb.ToString().Should().Be("a");
+ }
+
+ [Fact]
+ public void Append_Char_MatchesStringBuilder()
+ {
+ var sb = new StringBuilder();
+ var vsb = new ValueStringBuilder();
+ for (int i = 1; i <= 100; i++)
+ {
+ sb.Append((char)i);
+ vsb.Append((char)i);
+ }
+
+ vsb.Length.Should().Be(sb.Length);
+ vsb.ToString().Should().Be(sb.ToString());
+ }
+
+ [Fact]
+ public void Append_String_MatchesStringBuilder()
+ {
+ var sb = new StringBuilder();
+ var vsb = new ValueStringBuilder();
+ for (int i = 1; i <= 100; i++)
+ {
+ string s = i.ToString();
+ sb.Append(s);
+ vsb.Append(s);
+ }
+
+ vsb.Length.Should().Be(sb.Length);
+ vsb.ToString().Should().Be(sb.ToString());
+ }
+
+ [Theory]
+ [InlineData(0, 4 * 1024 * 1024)]
+ [InlineData(1025, 4 * 1024 * 1024)]
+ [InlineData(3 * 1024 * 1024, 6 * 1024 * 1024)]
+ public void Append_String_Large_MatchesStringBuilder(int initialLength, int stringLength)
+ {
+ var sb = new StringBuilder(initialLength);
+ var vsb = new ValueStringBuilder(new char[initialLength]);
+
+ string s = new string('a', stringLength);
+ sb.Append(s);
+ vsb.Append(s);
+
+ vsb.Length.Should().Be(sb.Length);
+ vsb.ToString().Should().Be(sb.ToString());
+ }
+
+ [Theory]
+ [InlineData(0, 4 * 1024 * 1024)]
+ [InlineData(1025, 4 * 1024 * 1024)]
+ [InlineData(3 * 1024 * 1024, 6 * 1024 * 1024)]
+ public void AppendLowerInvariant_String_Large_MatchesStringBuilder(int initialLength, int stringLength)
+ {
+ var sb = new StringBuilder(initialLength);
+ var vsb = new ValueStringBuilder(new char[initialLength]);
+
+ string s = new string('A', stringLength);
+ sb.Append(s);
+ vsb.AppendAsLowerInvariant(s);
+
+ vsb.Length.Should().Be(sb.Length);
+ vsb.ToString().Should().Be(sb.ToString().ToLowerInvariant());
+ }
+
+ [Fact]
+ public void Append_CharInt_MatchesStringBuilder()
+ {
+ var sb = new StringBuilder();
+ var vsb = new ValueStringBuilder();
+ for (int i = 1; i <= 100; i++)
+ {
+ sb.Append((char)i, i);
+ vsb.Append((char)i, i);
+ }
+
+ vsb.Length.Should().Be(sb.Length);
+ vsb.ToString().Should().Be(sb.ToString());
+ }
+
+ [Fact]
+ public void AppendSpan_DataAppendedCorrectly()
+ {
+ var sb = new StringBuilder();
+ var vsb = new ValueStringBuilder();
+
+ for (int i = 1; i <= 1000; i++)
+ {
+ string s = i.ToString();
+
+ sb.Append(s);
+
+ Span span = vsb.AppendSpan(s.Length);
+ vsb.Length.Should().Be(sb.Length);
+
+ s.AsSpan().CopyTo(span);
+ }
+
+ vsb.Length.Should().Be(sb.Length);
+ vsb.ToString().Should().Be(sb.ToString());
+ }
+
+ [Fact]
+ public void Insert_IntCharInt_MatchesStringBuilder()
+ {
+ var sb = new StringBuilder();
+ var vsb = new ValueStringBuilder();
+ var rand = new Random(42);
+
+ for (int i = 1; i <= 100; i++)
+ {
+ int index = rand.Next(sb.Length);
+ sb.Insert(index, new string((char)i, 1), i);
+ vsb.Insert(index, (char)i, i);
+ }
+
+ vsb.Length.Should().Be(sb.Length);
+ vsb.ToString().Should().Be(sb.ToString());
+ }
+
+ [Fact]
+ public void AsSpan_ReturnsCorrectValue_DoesntClearBuilder()
+ {
+ var sb = new StringBuilder();
+ var vsb = new ValueStringBuilder();
+
+ for (int i = 1; i <= 100; i++)
+ {
+ string s = i.ToString();
+ sb.Append(s);
+ vsb.Append(s);
+ }
+
+ var resultString = new string(vsb.AsSpan());
+ resultString.Should().Be(sb.ToString());
+
+ sb.Length.Should().NotBe(0);
+ vsb.Length.Should().Be(sb.Length);
+ vsb.ToString().Should().Be(sb.ToString());
+ }
+
+ [Fact]
+ public void ToString_ClearsBuilder_ThenReusable()
+ {
+ const string Text1 = "test";
+ var vsb = new ValueStringBuilder();
+
+ vsb.Append(Text1);
+ vsb.Length.Should().Be(Text1.Length);
+
+ string s = vsb.ToString();
+ s.Should().Be(Text1);
+
+ vsb.Length.Should().Be(0);
+ vsb.ToString().Should().BeEmpty();
+
+ const string Text2 = "another test";
+ vsb.Append(Text2);
+ vsb.Length.Should().Be(Text2.Length);
+ vsb.ToString().Should().Be(Text2);
+ }
+
+ [Fact]
+ public void Dispose_ClearsBuilder_ThenReusable()
+ {
+ const string Text1 = "test";
+ var vsb = new ValueStringBuilder();
+
+ vsb.Append(Text1);
+ vsb.Length.Should().Be(Text1.Length);
+
+ vsb.Dispose();
+
+ vsb.Length.Should().Be(0);
+ vsb.ToString().Should().BeEmpty();
+
+ const string Text2 = "another test";
+ vsb.Append(Text2);
+ vsb.Length.Should().Be(Text2.Length);
+ vsb.ToString().Should().Be(Text2);
+ }
+
+ [Fact]
+ public void Indexer()
+ {
+ const string Text1 = "foobar";
+ var vsb = new ValueStringBuilder();
+
+ vsb.Append(Text1);
+
+ vsb[3].Should().Be('b');
+ vsb[3] = 'c';
+ vsb[3].Should().Be('c');
+ vsb.Dispose();
+ }
+
+ [Fact]
+ public void EnsureCapacity_IfRequestedCapacityWins()
+ {
+ // Note: constants used here may be dependent on minimal buffer size
+ // the ArrayPool is able to return.
+ var builder = new ValueStringBuilder(stackalloc char[32]);
+
+ builder.EnsureCapacity(65);
+
+ builder.Capacity.Should().Be(128);
+ }
+
+ [Fact]
+ public void EnsureCapacity_IfBufferTimesTwoWins()
+ {
+ var builder = new ValueStringBuilder(stackalloc char[32]);
+
+ builder.EnsureCapacity(33);
+
+ builder.Capacity.Should().Be(64);
+ builder.Dispose();
+ }
+
+ [Fact]
+ public void EnsureCapacity_NoAllocIfNotNeeded()
+ {
+ // Note: constants used here may be dependent on minimal buffer size
+ // the ArrayPool is able to return.
+ var builder = new ValueStringBuilder(stackalloc char[64]);
+
+ builder.EnsureCapacity(16);
+
+ builder.Capacity.Should().Be(64);
+ builder.Dispose();
+ }
+}
+#endif