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