From 247a2ef96880eb796d6d2813108b7efe1e4b6850 Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Sat, 9 Aug 2025 10:26:00 -0700 Subject: [PATCH 1/6] Implement PooledSpanBuilder directly from PooledArrayBuilder --- .../PooledObjects/PooledArrayBuilder.cs | 10 +- .../PooledObjects/PooledArrayBuilder`1.cs | 702 +++--------------- 2 files changed, 100 insertions(+), 612 deletions(-) diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects/PooledArrayBuilder.cs b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects/PooledArrayBuilder.cs index 292283f54ea..aa3abc06085 100644 --- a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects/PooledArrayBuilder.cs +++ b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects/PooledArrayBuilder.cs @@ -5,12 +5,12 @@ namespace Microsoft.AspNetCore.Razor.PooledObjects; -internal static class PooledArrayBuilder +internal static class PooledSpanBuilder { - public static PooledArrayBuilder Create(ReadOnlySpan source) + public static PooledSpanBuilder Create(ReadOnlySpan source) { - var pooledArray = new PooledArrayBuilder(source.Length); - pooledArray.AddRange(source); - return pooledArray; + var spanBuilder = new PooledSpanBuilder(source.Length); + spanBuilder.AddRange(source); + return spanBuilder; } } diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects/PooledArrayBuilder`1.cs b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects/PooledArrayBuilder`1.cs index 5ac77c7dfe7..bb895a62042 100644 --- a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects/PooledArrayBuilder`1.cs +++ b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects/PooledArrayBuilder`1.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Buffers; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; @@ -9,188 +10,75 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Microsoft.AspNetCore.Razor.Utilities; -using Microsoft.Extensions.ObjectPool; namespace Microsoft.AspNetCore.Razor.PooledObjects; /// -/// Wraps a pooled but doesn't allocate it until -/// it's needed. Note: Dispose this to ensure that the pooled array builder is returned -/// to the pool. -/// -/// There is significant effort to avoid retrieving the . -/// For very small arrays of length 4 or less, the elements will be stored on the stack. If the array -/// grows larger than 4 elements, a builder will be employed. Afterward, the build will -/// continue to be used, even if the arrays shrinks and has fewer than 4 elements. +/// Wraps a pooled array but doesn't allocate it until it's needed. Provides +/// access to the backing array as a . +/// Note: Disposal ensures the pooled array is returned to the pool. /// [NonCopyable] -[CollectionBuilder(typeof(PooledArrayBuilder), nameof(PooledArrayBuilder.Create))] -internal partial struct PooledArrayBuilder : IDisposable +[CollectionBuilder(typeof(PooledSpanBuilder), nameof(PooledSpanBuilder.Create))] +internal partial struct PooledSpanBuilder(int capacity) : IDisposable { - public static PooledArrayBuilder Empty => default; + public static PooledSpanBuilder Empty => new(); /// - /// The number of items that can be stored inline. + /// An array to be used as storage. /// - private const int InlineCapacity = 4; - - private ObjectPool.Builder>? _builderPool; + private T[] _array = capacity > 0 ? ArrayPool.Shared.Rent(capacity) : []; /// - /// A builder to be used as storage after the first time that the number - /// of items exceeds . Once the builder is used, - /// it is still used even if the number of items shrinks below . - /// Essentially, if this field is non-null, it will be used as storage. + /// Number of items in this collection. /// - private ImmutableArray.Builder? _builder; - - /// - /// An optional initial capacity for the builder. - /// - private int? _capacity; - - private T _element0; - private T _element1; - private T _element2; - private T _element3; - - /// - /// The number of inline elements. Note that this value is only used when is . - /// - private int _inlineCount; - - public PooledArrayBuilder(int? capacity = null, ObjectPool.Builder>? builderPool = null) - { - _capacity = capacity is > InlineCapacity ? capacity : null; - _builderPool = builderPool; - _element0 = default!; - _element1 = default!; - _element2 = default!; - _element3 = default!; - _inlineCount = 0; - } + private int _count = 0; - private PooledArrayBuilder(in PooledArrayBuilder builder) + public PooledSpanBuilder() + : this(capacity: 0) { - // This is an intentional copy used to create an Enumerator. -#pragma warning disable RS0042 - this = builder; -#pragma warning restore RS0042 } public void Dispose() { - // Return _builder to the pool if necessary. Note that we don't need to clear the inline elements here - // because this type is intended to be allocated on the stack and the GC can reclaim objects from the - // stack after the last use of a reference to them. - if (_builder is { } innerBuilder) - { - _builderPool?.Return(innerBuilder); - _builder = null; - } - } - - /// - /// Retrieves the inner , moving any inline elements to it if necessary. - /// - private ImmutableArray.Builder GetBuilder() - { - if (!TryGetBuilder(out var builder)) + // Return _array to the pool if necessary. + if (_array.Length > 0) { - MoveInlineItemsToBuilder(); - builder = _builder; + ArrayPool.Shared.Return(_array, clearArray: true); + _array = []; + _count = 0; } - - return builder; - } - - /// - /// Retrieves the inner . - /// - /// - /// Returns if is available; otherwise - /// - /// - /// This should only be used by methods that will not add to the inner . - /// - private readonly bool TryGetBuilder([NotNullWhen(true)] out ImmutableArray.Builder? builder) - { - builder = _builder; - return builder is not null; } /// - /// Retrieves the inner and resets its capacity if necessary. + /// Ensures the inner 's capacity is at least the specified value. /// - /// - /// Returns if is available; otherwise - /// /// - /// This should only be used by methods that will add to the inner . + /// This should only be used by methods that will add to the inner . /// - private readonly bool TryGetBuilderAndEnsureCapacity([NotNullWhen(true)] out ImmutableArray.Builder? builder) + private void EnsureCapacity(int capacity) { - if (TryGetBuilder(out builder)) + if (_array.Length < capacity) { - if (builder.Capacity == 0 && _capacity is int capacity) - { - builder.Capacity = capacity; - } + var newArray = ArrayPool.Shared.Rent(capacity); + Array.Copy(_array, 0, newArray, 0, Count); + ArrayPool.Shared.Return(_array); + _array = newArray; } - - return builder is not null; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ClearInlineElement(int index) - { - Debug.Assert(_inlineCount <= InlineCapacity); - - // Clearing out an item makes it potentially available for garbage collection. - // Note: On .NET Core, we can be a bit more judicious and only zero-out - // fields that contain references to heap-allocated objects. - -#if NETCOREAPP - if (RuntimeHelpers.IsReferenceOrContainsReferences()) -#endif - { - SetInlineElement(index, default!); - } - } - - public T this[int index] + public readonly T this[int index] { [MethodImpl(MethodImplOptions.AggressiveInlining)] - readonly get + get { - if (TryGetBuilder(out var builder)) - { - return builder[index]; - } - - if (index >= _inlineCount) - { - ThrowIndexOutOfRangeException(); - } - - return GetInlineElement(index); + return _array[index]; } [MethodImpl(MethodImplOptions.AggressiveInlining)] set { - if (TryGetBuilder(out var builder)) - { - builder[index] = value; - return; - } - - if (index >= _inlineCount) - { - ThrowIndexOutOfRangeException(); - } - - SetInlineElement(index, value); + _array[index] = value; } } @@ -200,55 +88,11 @@ public T this[Index index] set => this[index.GetOffset(Count)] = value; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private readonly T GetInlineElement(int index) - { - Debug.Assert(_inlineCount <= InlineCapacity); - - return index switch - { - 0 => _element0, - 1 => _element1, - 2 => _element2, - 3 => _element3, - _ => Assumed.Unreachable() - }; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void SetInlineElement(int index, T value) - { - Debug.Assert(_inlineCount <= InlineCapacity); - - switch (index) - { - case 0: - _element0 = value; - break; - - case 1: - _element1 = value; - break; - - case 2: - _element2 = value; - break; - - case 3: - _element3 = value; - break; - - default: - Assumed.Unreachable(); - break; - } - } - public readonly int Count - => _builder?.Count ?? _inlineCount; + => _count; public readonly int Capacity - => _builder?.Capacity ?? _capacity ?? InlineCapacity; + => _array.Length; public void Add(T item) => Insert(Count, item); @@ -257,195 +101,58 @@ public void AddRange(ImmutableArray items) => InsertRange(Count, items); public void AddRange(ReadOnlySpan items) - { - // Note: We don't delegate this overload to InsertRange(ReadOnlySpan) because - // ImmutableArray.Builder supports an AddRange overload that takes a ReadOnlySpan. - // Delegating to InsertRange(ReadOnlySpan) could unnecessarily introduce an - // array allocation and copy. - - if (items.IsEmpty) - { - return; - } - - if (TryGetBuilderAndEnsureCapacity(out var builder)) - { - builder.AddRange(items); - } - else if (_inlineCount + items.Length <= InlineCapacity) - { - foreach (var item in items) - { - SetInlineElement(_inlineCount, item); - _inlineCount++; - } - } - else - { - MoveInlineItemsToBuilder(); - _builder.AddRange(items); - } - } + => InsertRange(Count, items); public void AddRange(TList list) where TList : struct, IReadOnlyList - => AddRange(list, startIndex: 0, list.Count); + => InsertRange(Count, list); public void AddRange(TList list, int startIndex, int count) where TList : struct, IReadOnlyList - { - // Note: We don't delegate this overload to InsertRange(TList, int, int) because - // it requires extra allocations that aren't necessary for AddRange(...). - - if (count == 0) - { - return; - } - - var (start, end) = (startIndex, startIndex + count - 1); - - if (TryGetBuilderAndEnsureCapacity(out var builder)) - { - for (var i = start; i <= end; i++) - { - builder.Add(list[i]); - } - - return; - } - - if (_inlineCount + count <= InlineCapacity) - { - for (var i = start; i <= end; i++) - { - SetInlineElement(_inlineCount, list[i]); - _inlineCount++; - } - } - else - { - MoveInlineItemsToBuilder(); - - for (var i = start; i <= end; i++) - { - _builder.Add(list[i]); - } - } - } + => InsertRange(Count, list, startIndex, count); public void AddRange(IEnumerable items) => InsertRange(Count, items); public void Clear() { - if (TryGetBuilder(out var builder)) - { - // Keep using a real builder to avoid churn in the object pool. - builder.Clear(); - } - else - { - var oldCapacity = _capacity; - this = Empty; - _capacity = oldCapacity; - } + // Keep the array to avoid churn in the object pool. + Array.Clear(_array, 0, Count); + _count = 0; } - public readonly Enumerator GetEnumerator() - => new(in this); + public readonly Span.Enumerator GetEnumerator() + => AsSpan().GetEnumerator(); public void Insert(int index, T item) - { - Debug.Assert(index >= 0 && index <= Count); - - if (TryGetBuilderAndEnsureCapacity(out var builder)) - { - builder.Insert(index, item); - } - else if (_inlineCount < InlineCapacity) - { - // Shift elements if not inserting at the end. - if (index < _inlineCount) - { - ShiftInlineItemsByOffset(index, offset: 1); - } - - SetInlineElement(index, item); - _inlineCount++; - } - else - { - MoveInlineItemsToBuilder(); - _builder.Insert(index, item); - } - } + => InsertSpan(index, [item]); public void InsertRange(int index, ImmutableArray items) - { - Debug.Assert(index >= 0 && index <= Count); - - if (items.IsEmpty) - { - return; - } - - if (TryGetBuilderAndEnsureCapacity(out var builder)) - { - builder.InsertRange(index, items); - } - else if (_inlineCount + items.Length <= InlineCapacity) - { - // Shift elements if not inserting at the end. - if (index < _inlineCount) - { - ShiftInlineItemsByOffset(index, offset: items.Length); - } - - foreach (var item in items) - { - SetInlineElement(index++, item); - _inlineCount++; - } - } - else - { - MoveInlineItemsToBuilder(); - _builder.InsertRange(index, items); - } - } + => InsertSpan(index, ImmutableCollectionsMarshal.AsArray(items).AsSpan()); public void InsertRange(int index, ReadOnlySpan items) + => InsertSpan(index, items); + + private void InsertSpan(int index, ReadOnlySpan items) { Debug.Assert(index >= 0 && index <= Count); - if (items.IsEmpty) + var count = items.Length; + if (count == 0) { return; } - if (TryGetBuilderAndEnsureCapacity(out var builder)) - { - builder.InsertRange(index, items); - } - else if (_inlineCount + items.Length <= InlineCapacity) - { - // Shift elements if not inserting at the end. - if (index < _inlineCount) - { - ShiftInlineItemsByOffset(index, offset: items.Length); - } + var newCount = Count + count; + EnsureCapacity(newCount); - foreach (var item in items) - { - SetInlineElement(index++, item); - _inlineCount++; - } - } - else + if (index != Count) { - MoveInlineItemsToBuilder(); - _builder.InsertRange(index, items); + Array.Copy(_array, index, _array, index + count, Count - index); } + + items.CopyTo(_array.AsSpan(index)); + _count = newCount; } public void InsertRange(int index, TList list) @@ -455,71 +162,26 @@ public void InsertRange(int index, TList list) public void InsertRange(int index, TList list, int startIndex, int count) where TList : struct, IReadOnlyList { - if (index == Count) - { - // AddRange doesn't delegate to this method. Instead, this delegates to AddRange for - // insertions at the end. - AddRange(list, startIndex, count); - return; - } - if (count == 0) { return; } - var (start, end) = (startIndex, startIndex + count - 1); - - if (TryGetBuilderAndEnsureCapacity(out var builder)) - { - var spacer = ImmutableCollectionsMarshal.AsImmutableArray(new T[count]); - builder.InsertRange(index, spacer); - - for (var i = start; i <= end; i++) - { - builder[index++] = list[i]; - } - - return; - } + var newCount = Count + count; + EnsureCapacity(newCount); - if (_inlineCount + count <= InlineCapacity) + if (startIndex != Count) { - // Shift elements if not inserting at the end. - if (index < _inlineCount) - { - ShiftInlineItemsByOffset(index, offset: count); - } - - for (var i = start; i <= end; i++) - { - SetInlineElement(index++, list[i]); - _inlineCount++; - } + Array.Copy(_array, index, _array, index + count, Count - index); } - else - { - MoveInlineItemsToBuilder(); - - var spacer = ImmutableCollectionsMarshal.AsImmutableArray(new T[count]); - _builder.InsertRange(index, spacer); - for (var i = start; i <= end; i++) - { - _builder[index++] = list[i]; - } - } + list.CopyTo(_array.AsSpan(index)); + _count = newCount; } public void InsertRange(int index, IEnumerable items) { - if (TryGetBuilderAndEnsureCapacity(out var builder)) - { - builder.InsertRange(index, items); - return; - } - - if (!items.TryGetCount(out var itemCount)) + if (!items.TryGetCount(out var count)) { // We couldn't retrieve a count, so we have to enumerate the elements. foreach (var item in items) @@ -530,179 +192,73 @@ public void InsertRange(int index, IEnumerable items) return; } - if (itemCount == 0) + if (count == 0) { // No items, so there's nothing to do. return; } - if (_inlineCount + itemCount <= InlineCapacity) - { - // Shift elements if not inserting at the end. - if (index < _inlineCount) - { - ShiftInlineItemsByOffset(index, offset: itemCount); - } + var newCount = Count + count; + EnsureCapacity(newCount); - // The items can fit into our inline elements. - foreach (var item in items) - { - SetInlineElement(index++, item); - _inlineCount++; - } - } - else + if (index != Count) { - // The items can't fit into our inline elements, so we switch to a builder. - MoveInlineItemsToBuilder(); - _builder.InsertRange(index, items); + Array.Copy(_array, index, _array, index + count, Count - index); } + + items.CopyTo(_array.AsSpan(index)); + _count = newCount; } public void RemoveAt(int index) { - if (TryGetBuilderAndEnsureCapacity(out var builder)) - { - builder.RemoveAt(index); - return; - } - - if (index < 0 || index >= _inlineCount) - { - ThrowIndexOutOfRangeException(); - } - - // Copy inline elements depending on the index to be removed. - switch (index) - { - case 0: - _element0 = _element1; - - if (_inlineCount > 1) - { - _element1 = _element2; - } - - if (_inlineCount > 2) - { - _element2 = _element3; - } - - break; - - case 1: - _element1 = _element2; - - if (_inlineCount > 2) - { - _element2 = _element3; - } - - break; - - case 2: - _element2 = _element3; - break; - } - - // Clear the last element if necessary. - if (_inlineCount > 3) - { - ClearInlineElement(3); - } - - // Decrement the count. - _inlineCount--; + Array.Copy(_array, index + 1, _array, index, _array.Length - index - 1); + _count--; } public void RemoveAt(Index index) => RemoveAt(index.GetOffset(Count)); /// - /// Returns the current contents as an and sets - /// the collection to a zero length array. + /// Returns the current contents as an and changes + /// the collection to zero length. /// - /// - /// If equals , the - /// internal array will be extracted as an without copying - /// the contents. Otherwise, the contents will be copied into a new array. The collection - /// will then be set to a zero-length array. - /// - /// An immutable array. public ImmutableArray ToImmutableAndClear() { - if (TryGetBuilder(out var builder)) - { - return builder.ToImmutableAndClear(); - } - - var inlineArray = InlineItemsToImmutableArray(); + var newArray = ToImmutable(); - var oldCapacity = _capacity; - this = Empty; - _capacity = oldCapacity; + Clear(); - return inlineArray; + return newArray; } public readonly ImmutableArray ToImmutable() - { - if (TryGetBuilder(out var builder)) - { - return builder.ToImmutable(); - } - - return InlineItemsToImmutableArray(); - } + => AsSpan().ToImmutableArray(); - private readonly ImmutableArray InlineItemsToImmutableArray() - { - Debug.Assert(_inlineCount <= InlineCapacity); - - return _inlineCount switch - { - 0 => [], - 1 => [_element0], - 2 => [_element0, _element1], - 3 => [_element0, _element1, _element2], - _ => [_element0, _element1, _element2, _element3] - }; - } + /// + /// Returns a span representing the active portion of the underlying array. + /// + /// The returned span should not be used after disposal. + public readonly Span AsSpan() + => _array.AsSpan(0, Count); public readonly T[] ToArray() - { - if (TryGetBuilder(out var builder)) - { - return builder.ToArray(); - } - - return _inlineCount switch - { - 0 => [], - 1 => [_element0], - 2 => [_element0, _element1], - 3 => [_element0, _element1, _element2], - _ => [_element0, _element1, _element2, _element3] - }; - } + => AsSpan().ToArray(); public T[] ToArrayAndClear() { var result = ToArray(); + Clear(); return result; } public void Push(T item) - { - Add(item); - } + => Add(item); public readonly T Peek() - { - return this[^1]; - } + => this[^1]; public T Pop() { @@ -1554,13 +1110,6 @@ public readonly T SingleOrDefault(TArg arg, Func predicate, return result; } - /// - /// This is present to help the JIT inline methods that need to throw an . - /// - [DoesNotReturn] - private static void ThrowIndexOutOfRangeException() - => throw new IndexOutOfRangeException(); - /// /// This is present to help the JIT inline methods that need to throw an . /// @@ -1568,75 +1117,23 @@ private static void ThrowIndexOutOfRangeException() private static T ThrowInvalidOperation(string message) => ThrowHelper.ThrowInvalidOperationException(message); - [MemberNotNull(nameof(_builder))] - private void MoveInlineItemsToBuilder() - { - Debug.Assert(_builder is null); - - _builderPool ??= ArrayBuilderPool.Default; - var builder = _builderPool.Get(); - - if (_capacity is int capacity) - { - builder.Capacity = capacity; - } - else if (builder.Capacity < InlineCapacity) - { - builder.Capacity = InlineCapacity; - } - - _builder = builder; - - // Add the inline items and clear their field values. - for (var i = 0; i < _inlineCount; i++) - { - builder.Add(GetInlineElement(i)); - ClearInlineElement(i); - } - - // Since _inlineCount tracks the number of inline items used, we zero it out here. - _inlineCount = 0; - } - - private void ShiftInlineItemsByOffset(int index, int offset) - { - Debug.Assert(_builder is null); - Debug.Assert(index >= 0 && index < _inlineCount); - Debug.Assert(offset > 0); - Debug.Assert(offset + _inlineCount <= InlineCapacity); - - for (var i = _inlineCount - 1; i >= index; i--) - { - SetInlineElement(i + offset, GetInlineElement(i)); - } - } - /// /// Sorts the contents of this builder. /// - public void Sort() - { - var builder = GetBuilder(); - builder.Sort(); - } + public readonly void Sort() + => Array.Sort(_array); /// - /// Sorts the contents of this builder using the provided . + /// Sorts the contents of this array using the provided . /// - public void Sort(IComparer comparer) - { - var builder = GetBuilder(); - builder.Sort(comparer); - } + public readonly void Sort(IComparer comparer) + => Array.Sort(_array, comparer); /// - /// Sorts the contents of this builder using the provided . + /// Sorts the contents of this array using the provided . /// - public void Sort(Comparison comparison) - { - var builder = GetBuilder(); - builder.Sort(comparison); - } + public readonly void Sort(Comparison comparison) + => Array.Sort(_array, comparison); public readonly ImmutableArray ToImmutableOrdered() { @@ -1845,13 +1342,4 @@ public ImmutableArray ToImmutableReversedAndClear() return result; } - - internal readonly TestAccessor GetTestAccessor() => new(in this); - - internal readonly struct TestAccessor(ref readonly PooledArrayBuilder builder) - { - public ImmutableArray.Builder? InnerArrayBuilder { get; } = builder._builder; - public int? Capacity { get; } = builder._capacity; - public int InlineItemCount { get; } = builder._inlineCount; - } } From 928e13944d5d67b1738da98bacab296cb84d33aa Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Sat, 9 Aug 2025 10:26:54 -0700 Subject: [PATCH 2/6] Move to appropriate file --- .../PooledObjects/{PooledArrayBuilder.cs => PooledSpanBuilder.cs} | 0 .../{PooledArrayBuilder`1.cs => PooledSpanBuilder`1.cs} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects/{PooledArrayBuilder.cs => PooledSpanBuilder.cs} (100%) rename src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects/{PooledArrayBuilder`1.cs => PooledSpanBuilder`1.cs} (100%) diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects/PooledArrayBuilder.cs b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects/PooledSpanBuilder.cs similarity index 100% rename from src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects/PooledArrayBuilder.cs rename to src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects/PooledSpanBuilder.cs diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects/PooledArrayBuilder`1.cs b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects/PooledSpanBuilder`1.cs similarity index 100% rename from src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects/PooledArrayBuilder`1.cs rename to src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects/PooledSpanBuilder`1.cs From 008da56c5162e9c7ec7bd67f8f2f0113050a8535 Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Sat, 9 Aug 2025 10:27:53 -0700 Subject: [PATCH 3/6] Restore PooledArrayBuilder classes --- .../PooledObjects/PooledArrayBuilder.cs | 16 + .../PooledObjects/PooledArrayBuilder`1.cs | 1857 +++++++++++++++++ 2 files changed, 1873 insertions(+) create mode 100644 src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects/PooledArrayBuilder.cs create mode 100644 src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects/PooledArrayBuilder`1.cs diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects/PooledArrayBuilder.cs b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects/PooledArrayBuilder.cs new file mode 100644 index 00000000000..292283f54ea --- /dev/null +++ b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects/PooledArrayBuilder.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.AspNetCore.Razor.PooledObjects; + +internal static class PooledArrayBuilder +{ + public static PooledArrayBuilder Create(ReadOnlySpan source) + { + var pooledArray = new PooledArrayBuilder(source.Length); + pooledArray.AddRange(source); + return pooledArray; + } +} diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects/PooledArrayBuilder`1.cs b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects/PooledArrayBuilder`1.cs new file mode 100644 index 00000000000..5ac77c7dfe7 --- /dev/null +++ b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects/PooledArrayBuilder`1.cs @@ -0,0 +1,1857 @@ +// 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.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Microsoft.AspNetCore.Razor.Utilities; +using Microsoft.Extensions.ObjectPool; + +namespace Microsoft.AspNetCore.Razor.PooledObjects; + +/// +/// Wraps a pooled but doesn't allocate it until +/// it's needed. Note: Dispose this to ensure that the pooled array builder is returned +/// to the pool. +/// +/// There is significant effort to avoid retrieving the . +/// For very small arrays of length 4 or less, the elements will be stored on the stack. If the array +/// grows larger than 4 elements, a builder will be employed. Afterward, the build will +/// continue to be used, even if the arrays shrinks and has fewer than 4 elements. +/// +[NonCopyable] +[CollectionBuilder(typeof(PooledArrayBuilder), nameof(PooledArrayBuilder.Create))] +internal partial struct PooledArrayBuilder : IDisposable +{ + public static PooledArrayBuilder Empty => default; + + /// + /// The number of items that can be stored inline. + /// + private const int InlineCapacity = 4; + + private ObjectPool.Builder>? _builderPool; + + /// + /// A builder to be used as storage after the first time that the number + /// of items exceeds . Once the builder is used, + /// it is still used even if the number of items shrinks below . + /// Essentially, if this field is non-null, it will be used as storage. + /// + private ImmutableArray.Builder? _builder; + + /// + /// An optional initial capacity for the builder. + /// + private int? _capacity; + + private T _element0; + private T _element1; + private T _element2; + private T _element3; + + /// + /// The number of inline elements. Note that this value is only used when is . + /// + private int _inlineCount; + + public PooledArrayBuilder(int? capacity = null, ObjectPool.Builder>? builderPool = null) + { + _capacity = capacity is > InlineCapacity ? capacity : null; + _builderPool = builderPool; + _element0 = default!; + _element1 = default!; + _element2 = default!; + _element3 = default!; + _inlineCount = 0; + } + + private PooledArrayBuilder(in PooledArrayBuilder builder) + { + // This is an intentional copy used to create an Enumerator. +#pragma warning disable RS0042 + this = builder; +#pragma warning restore RS0042 + } + + public void Dispose() + { + // Return _builder to the pool if necessary. Note that we don't need to clear the inline elements here + // because this type is intended to be allocated on the stack and the GC can reclaim objects from the + // stack after the last use of a reference to them. + if (_builder is { } innerBuilder) + { + _builderPool?.Return(innerBuilder); + _builder = null; + } + } + + /// + /// Retrieves the inner , moving any inline elements to it if necessary. + /// + private ImmutableArray.Builder GetBuilder() + { + if (!TryGetBuilder(out var builder)) + { + MoveInlineItemsToBuilder(); + builder = _builder; + } + + return builder; + } + + /// + /// Retrieves the inner . + /// + /// + /// Returns if is available; otherwise + /// + /// + /// This should only be used by methods that will not add to the inner . + /// + private readonly bool TryGetBuilder([NotNullWhen(true)] out ImmutableArray.Builder? builder) + { + builder = _builder; + return builder is not null; + } + + /// + /// Retrieves the inner and resets its capacity if necessary. + /// + /// + /// Returns if is available; otherwise + /// + /// + /// This should only be used by methods that will add to the inner . + /// + private readonly bool TryGetBuilderAndEnsureCapacity([NotNullWhen(true)] out ImmutableArray.Builder? builder) + { + if (TryGetBuilder(out builder)) + { + if (builder.Capacity == 0 && _capacity is int capacity) + { + builder.Capacity = capacity; + } + } + + return builder is not null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ClearInlineElement(int index) + { + Debug.Assert(_inlineCount <= InlineCapacity); + + // Clearing out an item makes it potentially available for garbage collection. + // Note: On .NET Core, we can be a bit more judicious and only zero-out + // fields that contain references to heap-allocated objects. + +#if NETCOREAPP + if (RuntimeHelpers.IsReferenceOrContainsReferences()) +#endif + { + SetInlineElement(index, default!); + } + } + + public T this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + readonly get + { + if (TryGetBuilder(out var builder)) + { + return builder[index]; + } + + if (index >= _inlineCount) + { + ThrowIndexOutOfRangeException(); + } + + return GetInlineElement(index); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + if (TryGetBuilder(out var builder)) + { + builder[index] = value; + return; + } + + if (index >= _inlineCount) + { + ThrowIndexOutOfRangeException(); + } + + SetInlineElement(index, value); + } + } + + public T this[Index index] + { + readonly get => this[index.GetOffset(Count)]; + set => this[index.GetOffset(Count)] = value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private readonly T GetInlineElement(int index) + { + Debug.Assert(_inlineCount <= InlineCapacity); + + return index switch + { + 0 => _element0, + 1 => _element1, + 2 => _element2, + 3 => _element3, + _ => Assumed.Unreachable() + }; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SetInlineElement(int index, T value) + { + Debug.Assert(_inlineCount <= InlineCapacity); + + switch (index) + { + case 0: + _element0 = value; + break; + + case 1: + _element1 = value; + break; + + case 2: + _element2 = value; + break; + + case 3: + _element3 = value; + break; + + default: + Assumed.Unreachable(); + break; + } + } + + public readonly int Count + => _builder?.Count ?? _inlineCount; + + public readonly int Capacity + => _builder?.Capacity ?? _capacity ?? InlineCapacity; + + public void Add(T item) + => Insert(Count, item); + + public void AddRange(ImmutableArray items) + => InsertRange(Count, items); + + public void AddRange(ReadOnlySpan items) + { + // Note: We don't delegate this overload to InsertRange(ReadOnlySpan) because + // ImmutableArray.Builder supports an AddRange overload that takes a ReadOnlySpan. + // Delegating to InsertRange(ReadOnlySpan) could unnecessarily introduce an + // array allocation and copy. + + if (items.IsEmpty) + { + return; + } + + if (TryGetBuilderAndEnsureCapacity(out var builder)) + { + builder.AddRange(items); + } + else if (_inlineCount + items.Length <= InlineCapacity) + { + foreach (var item in items) + { + SetInlineElement(_inlineCount, item); + _inlineCount++; + } + } + else + { + MoveInlineItemsToBuilder(); + _builder.AddRange(items); + } + } + + public void AddRange(TList list) + where TList : struct, IReadOnlyList + => AddRange(list, startIndex: 0, list.Count); + + public void AddRange(TList list, int startIndex, int count) + where TList : struct, IReadOnlyList + { + // Note: We don't delegate this overload to InsertRange(TList, int, int) because + // it requires extra allocations that aren't necessary for AddRange(...). + + if (count == 0) + { + return; + } + + var (start, end) = (startIndex, startIndex + count - 1); + + if (TryGetBuilderAndEnsureCapacity(out var builder)) + { + for (var i = start; i <= end; i++) + { + builder.Add(list[i]); + } + + return; + } + + if (_inlineCount + count <= InlineCapacity) + { + for (var i = start; i <= end; i++) + { + SetInlineElement(_inlineCount, list[i]); + _inlineCount++; + } + } + else + { + MoveInlineItemsToBuilder(); + + for (var i = start; i <= end; i++) + { + _builder.Add(list[i]); + } + } + } + + public void AddRange(IEnumerable items) + => InsertRange(Count, items); + + public void Clear() + { + if (TryGetBuilder(out var builder)) + { + // Keep using a real builder to avoid churn in the object pool. + builder.Clear(); + } + else + { + var oldCapacity = _capacity; + this = Empty; + _capacity = oldCapacity; + } + } + + public readonly Enumerator GetEnumerator() + => new(in this); + + public void Insert(int index, T item) + { + Debug.Assert(index >= 0 && index <= Count); + + if (TryGetBuilderAndEnsureCapacity(out var builder)) + { + builder.Insert(index, item); + } + else if (_inlineCount < InlineCapacity) + { + // Shift elements if not inserting at the end. + if (index < _inlineCount) + { + ShiftInlineItemsByOffset(index, offset: 1); + } + + SetInlineElement(index, item); + _inlineCount++; + } + else + { + MoveInlineItemsToBuilder(); + _builder.Insert(index, item); + } + } + + public void InsertRange(int index, ImmutableArray items) + { + Debug.Assert(index >= 0 && index <= Count); + + if (items.IsEmpty) + { + return; + } + + if (TryGetBuilderAndEnsureCapacity(out var builder)) + { + builder.InsertRange(index, items); + } + else if (_inlineCount + items.Length <= InlineCapacity) + { + // Shift elements if not inserting at the end. + if (index < _inlineCount) + { + ShiftInlineItemsByOffset(index, offset: items.Length); + } + + foreach (var item in items) + { + SetInlineElement(index++, item); + _inlineCount++; + } + } + else + { + MoveInlineItemsToBuilder(); + _builder.InsertRange(index, items); + } + } + + public void InsertRange(int index, ReadOnlySpan items) + { + Debug.Assert(index >= 0 && index <= Count); + + if (items.IsEmpty) + { + return; + } + + if (TryGetBuilderAndEnsureCapacity(out var builder)) + { + builder.InsertRange(index, items); + } + else if (_inlineCount + items.Length <= InlineCapacity) + { + // Shift elements if not inserting at the end. + if (index < _inlineCount) + { + ShiftInlineItemsByOffset(index, offset: items.Length); + } + + foreach (var item in items) + { + SetInlineElement(index++, item); + _inlineCount++; + } + } + else + { + MoveInlineItemsToBuilder(); + _builder.InsertRange(index, items); + } + } + + public void InsertRange(int index, TList list) + where TList : struct, IReadOnlyList + => InsertRange(index, list, startIndex: 0, list.Count); + + public void InsertRange(int index, TList list, int startIndex, int count) + where TList : struct, IReadOnlyList + { + if (index == Count) + { + // AddRange doesn't delegate to this method. Instead, this delegates to AddRange for + // insertions at the end. + AddRange(list, startIndex, count); + return; + } + + if (count == 0) + { + return; + } + + var (start, end) = (startIndex, startIndex + count - 1); + + if (TryGetBuilderAndEnsureCapacity(out var builder)) + { + var spacer = ImmutableCollectionsMarshal.AsImmutableArray(new T[count]); + builder.InsertRange(index, spacer); + + for (var i = start; i <= end; i++) + { + builder[index++] = list[i]; + } + + return; + } + + if (_inlineCount + count <= InlineCapacity) + { + // Shift elements if not inserting at the end. + if (index < _inlineCount) + { + ShiftInlineItemsByOffset(index, offset: count); + } + + for (var i = start; i <= end; i++) + { + SetInlineElement(index++, list[i]); + _inlineCount++; + } + } + else + { + MoveInlineItemsToBuilder(); + + var spacer = ImmutableCollectionsMarshal.AsImmutableArray(new T[count]); + _builder.InsertRange(index, spacer); + + for (var i = start; i <= end; i++) + { + _builder[index++] = list[i]; + } + } + } + + public void InsertRange(int index, IEnumerable items) + { + if (TryGetBuilderAndEnsureCapacity(out var builder)) + { + builder.InsertRange(index, items); + return; + } + + if (!items.TryGetCount(out var itemCount)) + { + // We couldn't retrieve a count, so we have to enumerate the elements. + foreach (var item in items) + { + Insert(index++, item); + } + + return; + } + + if (itemCount == 0) + { + // No items, so there's nothing to do. + return; + } + + if (_inlineCount + itemCount <= InlineCapacity) + { + // Shift elements if not inserting at the end. + if (index < _inlineCount) + { + ShiftInlineItemsByOffset(index, offset: itemCount); + } + + // The items can fit into our inline elements. + foreach (var item in items) + { + SetInlineElement(index++, item); + _inlineCount++; + } + } + else + { + // The items can't fit into our inline elements, so we switch to a builder. + MoveInlineItemsToBuilder(); + _builder.InsertRange(index, items); + } + } + + public void RemoveAt(int index) + { + if (TryGetBuilderAndEnsureCapacity(out var builder)) + { + builder.RemoveAt(index); + return; + } + + if (index < 0 || index >= _inlineCount) + { + ThrowIndexOutOfRangeException(); + } + + // Copy inline elements depending on the index to be removed. + switch (index) + { + case 0: + _element0 = _element1; + + if (_inlineCount > 1) + { + _element1 = _element2; + } + + if (_inlineCount > 2) + { + _element2 = _element3; + } + + break; + + case 1: + _element1 = _element2; + + if (_inlineCount > 2) + { + _element2 = _element3; + } + + break; + + case 2: + _element2 = _element3; + break; + } + + // Clear the last element if necessary. + if (_inlineCount > 3) + { + ClearInlineElement(3); + } + + // Decrement the count. + _inlineCount--; + } + + public void RemoveAt(Index index) + => RemoveAt(index.GetOffset(Count)); + + /// + /// Returns the current contents as an and sets + /// the collection to a zero length array. + /// + /// + /// If equals , the + /// internal array will be extracted as an without copying + /// the contents. Otherwise, the contents will be copied into a new array. The collection + /// will then be set to a zero-length array. + /// + /// An immutable array. + public ImmutableArray ToImmutableAndClear() + { + if (TryGetBuilder(out var builder)) + { + return builder.ToImmutableAndClear(); + } + + var inlineArray = InlineItemsToImmutableArray(); + + var oldCapacity = _capacity; + this = Empty; + _capacity = oldCapacity; + + return inlineArray; + } + + public readonly ImmutableArray ToImmutable() + { + if (TryGetBuilder(out var builder)) + { + return builder.ToImmutable(); + } + + return InlineItemsToImmutableArray(); + } + + private readonly ImmutableArray InlineItemsToImmutableArray() + { + Debug.Assert(_inlineCount <= InlineCapacity); + + return _inlineCount switch + { + 0 => [], + 1 => [_element0], + 2 => [_element0, _element1], + 3 => [_element0, _element1, _element2], + _ => [_element0, _element1, _element2, _element3] + }; + } + + public readonly T[] ToArray() + { + if (TryGetBuilder(out var builder)) + { + return builder.ToArray(); + } + + return _inlineCount switch + { + 0 => [], + 1 => [_element0], + 2 => [_element0, _element1], + 3 => [_element0, _element1, _element2], + _ => [_element0, _element1, _element2, _element3] + }; + } + + public T[] ToArrayAndClear() + { + var result = ToArray(); + Clear(); + + return result; + } + + public void Push(T item) + { + Add(item); + } + + public readonly T Peek() + { + return this[^1]; + } + + public T Pop() + { + var index = ^1; + var item = this[index]; + RemoveAt(index); + + return item; + } + + public bool TryPop([MaybeNullWhen(false)] out T item) + { + if (Count == 0) + { + item = default; + return false; + } + + item = Pop(); + return true; + } + + /// + /// Determines whether this builder contains any elements. + /// + /// + /// if this builder contains any elements; otherwise, . + /// + public readonly bool Any() + => Count > 0; + + /// + /// Determines whether any element in this builder satisfies a condition. + /// + /// + /// A function to test each element for a condition. + /// + /// + /// if this builder is not empty and at least one of its elements passes + /// the test in the specified predicate; otherwise, . + /// + public readonly bool Any(Func predicate) + { + foreach (var item in this) + { + if (predicate(item)) + { + return true; + } + } + + return false; + } + + /// + /// Determines whether any element in this builder satisfies a condition. + /// + /// + /// An argument to pass to . + /// + /// + /// A function to test each element for a condition. + /// + /// + /// if this builder is not empty and at least one of its elements passes + /// the test in the specified predicate; otherwise, . + /// + public readonly bool Any(TArg arg, Func predicate) + { + foreach (var item in this) + { + if (predicate(item, arg)) + { + return true; + } + } + + return false; + } + + /// + /// Determines whether all elements in this builder satisfy a condition. + /// + /// + /// A function to test each element for a condition. + /// + /// + /// if every element in this builder passes the test + /// in the specified predicate, or if the builder is empty; otherwise, + /// . + public readonly bool All(Func predicate) + { + foreach (var item in this) + { + if (!predicate(item)) + { + return false; + } + } + + return true; + } + + /// + /// Determines whether all elements in this builder satisfy a condition. + /// + /// + /// An argument to pass to . + /// + /// + /// A function to test each element for a condition. + /// + /// + /// if every element in this builder passes the test + /// in the specified predicate, or if the builder is empty; otherwise, + /// . + public readonly bool All(TArg arg, Func predicate) + { + foreach (var item in this) + { + if (!predicate(item, arg)) + { + return false; + } + } + + return true; + } + + /// + /// Returns the first element in this builder. + /// + /// + /// The first element in this builder. + /// + /// + /// The builder is empty. + /// + public readonly T First() + => Count > 0 ? this[0] : ThrowInvalidOperation(SR.Contains_no_elements); + + /// + /// Returns the first element in this builder that satisfies a specified condition. + /// + /// + /// A function to test each element for a condition. + /// + /// + /// The first element in this builder that passes the test in the specified predicate function. + /// + /// + /// No element satisfies the condition in . + /// + public readonly T First(Func predicate) + { + foreach (var item in this) + { + if (predicate(item)) + { + return item; + } + } + + return ThrowInvalidOperation(SR.Contains_no_matching_elements); + } + + /// + /// Returns the first element in this builder that satisfies a specified condition. + /// + /// + /// An argument to pass to . + /// + /// + /// A function to test each element for a condition. + /// + /// + /// The first element in this builder that passes the test in the specified predicate function. + /// + /// + /// No element satisfies the condition in . + /// + public readonly T First(TArg arg, Func predicate) + { + foreach (var item in this) + { + if (predicate(item, arg)) + { + return item; + } + } + + return ThrowInvalidOperation(SR.Contains_no_matching_elements); + } + + /// + /// Returns the first element in this builder, or a default value if the builder is empty. + /// + /// + /// () if this builder is empty; otherwise, + /// the first element in this builder. + /// + public readonly T? FirstOrDefault() + => Count > 0 ? this[0] : default; + + /// + /// Returns the first element in this builder, or a specified default value if the builder is empty. + /// + /// + /// The default value to return if this builder is empty. + /// + /// + /// if this builder is empty; otherwise, + /// the first element in this builder. + /// + public readonly T FirstOrDefault(T defaultValue) + => Count > 0 ? this[0] : defaultValue; + + /// + /// Returns the first element in this builder that satisfies a condition, or a default value + /// if no such element is found. + /// + /// + /// A function to test each element for a condition. + /// + /// + /// () if this builder is empty or if no element + /// passes the test specified by ; otherwise, the first element in this + /// builder that passes the test specified by . + /// + public readonly T? FirstOrDefault(Func predicate) + { + foreach (var item in this) + { + if (predicate(item)) + { + return item; + } + } + + return default; + } + + /// + /// Returns the first element in this builder that satisfies a condition, or a specified default value + /// if no such element is found. + /// + /// + /// A function to test each element for a condition. + /// + /// + /// The default value to return if this builder is empty. + /// + /// + /// if this builder is empty or if no element + /// passes the test specified by ; otherwise, the first element in this + /// builder that passes the test specified by . + /// + public readonly T FirstOrDefault(Func predicate, T defaultValue) + { + foreach (var item in this) + { + if (predicate(item)) + { + return item; + } + } + + return defaultValue; + } + + /// + /// Returns the first element in this builder that satisfies a condition, or a default value + /// if no such element is found. + /// + /// + /// An argument to pass to . + /// + /// + /// A function to test each element for a condition. + /// + /// + /// () if this builder is empty or if no element + /// passes the test specified by ; otherwise, the first element in this + /// builder that passes the test specified by . + /// + public readonly T? FirstOrDefault(TArg arg, Func predicate) + { + foreach (var item in this) + { + if (predicate(item, arg)) + { + return item; + } + } + + return default; + } + + /// + /// Returns the first element in this builder that satisfies a condition, or a default value + /// if no such element is found. + /// + /// + /// An argument to pass to . + /// + /// + /// A function to test each element for a condition. + /// + /// + /// The default value to return if this builder is empty. + /// + /// + /// if this builder is empty or if no element + /// passes the test specified by ; otherwise, the first element in this + /// builder that passes the test specified by . + /// + public readonly T FirstOrDefault(TArg arg, Func predicate, T defaultValue) + { + foreach (var item in this) + { + if (predicate(item, arg)) + { + return item; + } + } + + return defaultValue; + } + + /// + /// Returns the last element in this builder. + /// + /// + /// The value at the last position in this builder. + /// + /// + /// The builder is empty. + /// + public readonly T Last() + => Count > 0 ? this[^1] : ThrowInvalidOperation(SR.Contains_no_elements); + + /// + /// Returns the last element in this builder that satisfies a specified condition. + /// + /// + /// A function to test each element for a condition. + /// + /// + /// The last element in this builder that passes the test in the specified predicate function. + /// + /// + /// No element satisfies the condition in . + /// + public readonly T Last(Func predicate) + { + for (var i = Count - 1; i >= 0; i--) + { + var item = this[i]; + if (predicate(item)) + { + return item; + } + } + + return ThrowInvalidOperation(SR.Contains_no_matching_elements); + } + + /// + /// Returns the last element in this builder that satisfies a specified condition. + /// + /// + /// An argument to pass to . + /// + /// + /// A function to test each element for a condition. + /// + /// + /// The last element in this builder that passes the test in the specified predicate function. + /// + /// + /// No element satisfies the condition in . + /// + public readonly T Last(TArg arg, Func predicate) + { + for (var i = Count - 1; i >= 0; i--) + { + var item = this[i]; + if (predicate(item, arg)) + { + return item; + } + } + + return ThrowInvalidOperation(SR.Contains_no_matching_elements); + } + + /// + /// Returns the last element in this builder, or a default value if the builder is empty. + /// + /// + /// () if this builder is empty; otherwise, + /// the last element in this builder. + /// + public readonly T? LastOrDefault() + => Count > 0 ? this[^1] : default; + + /// + /// Returns the last element in this builder, or a specified default value if the builder is empty. + /// + /// + /// The default value to return if this builder is empty. + /// + /// + /// if this builder is empty; otherwise, + /// the last element in this builder. + /// + public readonly T LastOrDefault(T defaultValue) + => Count > 0 ? this[^1] : defaultValue; + + /// + /// Returns the last element in this builder that satisfies a condition, or a default value + /// if no such element is found. + /// + /// + /// A function to test each element for a condition. + /// + /// + /// () if this builder is empty or if no element + /// passes the test specified by ; otherwise, the last element in this + /// builder that passes the test specified by . + /// + public readonly T? LastOrDefault(Func predicate) + { + for (var i = Count - 1; i >= 0; i--) + { + var item = this[i]; + if (predicate(item)) + { + return item; + } + } + + return default; + } + + /// + /// Returns the last element in this builder that satisfies a condition, or a specified default value + /// if no such element is found. + /// + /// + /// A function to test each element for a condition. + /// + /// + /// The default value to return if this builder is empty. + /// + /// + /// if this builder is empty or if no element + /// passes the test specified by ; otherwise, the last element in this + /// builder that passes the test specified by . + /// + public readonly T LastOrDefault(Func predicate, T defaultValue) + { + for (var i = Count - 1; i >= 0; i--) + { + var item = this[i]; + if (predicate(item)) + { + return item; + } + } + + return defaultValue; + } + + /// + /// Returns the last element in this builder that satisfies a condition, or a default value + /// if no such element is found. + /// + /// + /// An argument to pass to . + /// + /// + /// A function to test each element for a condition. + /// + /// + /// () if this builder is empty or if no element + /// passes the test specified by ; otherwise, the last element in this + /// builder that passes the test specified by . + /// + public readonly T? LastOrDefault(TArg arg, Func predicate) + { + for (var i = Count - 1; i >= 0; i--) + { + var item = this[i]; + if (predicate(item, arg)) + { + return item; + } + } + + return default; + } + + /// + /// Returns the last element in this builder that satisfies a condition, or a default value + /// if no such element is found. + /// + /// + /// An argument to pass to . + /// + /// + /// A function to test each element for a condition. + /// + /// + /// The default value to return if this builder is empty. + /// + /// + /// if this builder is empty or if no element + /// passes the test specified by ; otherwise, the last element in this + /// builder that passes the test specified by . + /// + public readonly T LastOrDefault(TArg arg, Func predicate, T defaultValue) + { + for (var i = Count - 1; i >= 0; i--) + { + var item = this[i]; + if (predicate(item, arg)) + { + return item; + } + } + + return defaultValue; + } + + /// + /// Returns the only element in this builder, and throws an exception if there is not exactly one element. + /// + /// + /// The single element in this builder. + /// + /// + /// The builder is empty. + /// + /// + /// The builder contains more than one element. + /// + public readonly T Single() + { + return Count switch + { + 1 => this[0], + 0 => ThrowInvalidOperation(SR.Contains_no_elements), + _ => ThrowInvalidOperation(SR.Contains_more_than_one_element) + }; + } + + /// + /// Returns the only element in this builder that satisfies a specified condition, + /// and throws an exception if more than one such element exists. + /// + /// + /// A function to test an element for a condition. + /// + /// + /// The single element in this builder that satisfies a condition. + /// + /// + /// No element satisfies the condition in . + /// + /// + /// More than one element satisfies the condition in . + /// + public readonly T Single(Func predicate) + { + var firstSeen = false; + T? result = default; + + foreach (var item in this) + { + if (predicate(item)) + { + if (firstSeen) + { + return ThrowInvalidOperation(SR.Contains_more_than_one_matching_element); + } + + firstSeen = true; + result = item; + } + } + + if (!firstSeen) + { + return ThrowInvalidOperation(SR.Contains_no_matching_elements); + } + + return result!; + } + + /// + /// Returns the only element in this builder that satisfies a specified condition, + /// and throws an exception if more than one such element exists. + /// + /// + /// An argument to pass to . + /// + /// + /// A function to test an element for a condition. + /// + /// + /// The single element in this builder that satisfies a condition. + /// + /// + /// No element satisfies the condition in . + /// + /// + /// More than one element satisfies the condition in . + /// + public readonly T Single(TArg arg, Func predicate) + { + var firstSeen = false; + T? result = default; + + foreach (var item in this) + { + if (predicate(item, arg)) + { + if (firstSeen) + { + return ThrowInvalidOperation(SR.Contains_more_than_one_matching_element); + } + + firstSeen = true; + result = item; + } + } + + if (!firstSeen) + { + return ThrowInvalidOperation(SR.Contains_no_matching_elements); + } + + return result!; + } + + /// + /// Returns the only element in this builder, or a default value if the builder is empty; + /// this method throws an exception if there is more than one element in the builder. + /// + /// + /// The single element in this builder, or () + /// if this builder contains no elements. + /// + /// + /// The builder contains more than one element. + /// + public readonly T? SingleOrDefault() + { + return Count switch + { + 1 => this[0], + 0 => default, + _ => ThrowInvalidOperation(SR.Contains_more_than_one_element) + }; + } + + /// + /// Returns the only element in this builder, or a specified default value if the builder is empty; + /// this method throws an exception if there is more than one element in the builder. + /// + /// + /// The default value to return if this builder is empty. + /// + /// + /// The single element in this builder, or + /// if this builder contains no elements. + /// + /// + /// The builder contains more than one element. + /// + public readonly T SingleOrDefault(T defaultValue) + { + return Count switch + { + 1 => this[0], + 0 => defaultValue, + _ => ThrowInvalidOperation(SR.Contains_more_than_one_element) + }; + } + + /// + /// Returns the only element in this builder that satisfies a specified condition or a default + /// value if no such element exists; this method throws an exception if more than one element + /// satisfies the condition. + /// + /// + /// A function to test an element for a condition. + /// + /// + /// The single element in this builder that satisfies the condition, or + /// () if no such element is found. + /// + /// + /// More than one element satisfies the condition in predicate. + /// + public readonly T? SingleOrDefault(Func predicate) + { + var firstSeen = false; + T? result = default; + + foreach (var item in this) + { + if (predicate(item)) + { + if (firstSeen) + { + return ThrowInvalidOperation(SR.Contains_more_than_one_matching_element); + } + + firstSeen = true; + result = item; + } + } + + return result; + } + + /// + /// Returns the only element in this builder that satisfies a specified condition or a specified default + /// value if no such element exists; this method throws an exception if more than one element + /// satisfies the condition. + /// + /// + /// A function to test an element for a condition. + /// + /// + /// The default value to return if this builder is empty. + /// + /// + /// The single element in this builder that satisfies the condition, or + /// if no such element is found. + /// + /// + /// More than one element satisfies the condition in predicate. + /// + public readonly T SingleOrDefault(Func predicate, T defaultValue) + { + var firstSeen = false; + var result = defaultValue; + + foreach (var item in this) + { + if (predicate(item)) + { + if (firstSeen) + { + return ThrowInvalidOperation(SR.Contains_more_than_one_matching_element); + } + + firstSeen = true; + result = item; + } + } + + return result; + } + + /// + /// Returns the only element in this builder that satisfies a specified condition or a default + /// value if no such element exists; this method throws an exception if more than one element + /// satisfies the condition. + /// + /// + /// An argument to pass to . + /// + /// + /// A function to test an element for a condition. + /// + /// + /// The single element in this builder that satisfies the condition, or + /// () if no such element is found. + /// + /// + /// More than one element satisfies the condition in predicate. + /// + public readonly T? SingleOrDefault(TArg arg, Func predicate) + { + var firstSeen = false; + T? result = default; + + foreach (var item in this) + { + if (predicate(item, arg)) + { + if (firstSeen) + { + return ThrowInvalidOperation(SR.Contains_more_than_one_matching_element); + } + + firstSeen = true; + result = item; + } + } + + return result; + } + + /// + /// Returns the only element in this builder that satisfies a specified condition or a specified default + /// value if no such element exists; this method throws an exception if more than one element + /// satisfies the condition. + /// + /// + /// An argument to pass to . + /// + /// + /// A function to test an element for a condition. + /// + /// + /// The default value to return if this builder is empty. + /// + /// + /// The single element in this builder that satisfies the condition, or + /// if no such element is found. + /// + /// + /// More than one element satisfies the condition in predicate. + /// + public readonly T SingleOrDefault(TArg arg, Func predicate, T defaultValue) + { + var firstSeen = false; + var result = defaultValue; + + foreach (var item in this) + { + if (predicate(item, arg)) + { + if (firstSeen) + { + return ThrowInvalidOperation(SR.Contains_more_than_one_matching_element); + } + + firstSeen = true; + result = item; + } + } + + return result; + } + + /// + /// This is present to help the JIT inline methods that need to throw an . + /// + [DoesNotReturn] + private static void ThrowIndexOutOfRangeException() + => throw new IndexOutOfRangeException(); + + /// + /// This is present to help the JIT inline methods that need to throw an . + /// + [DoesNotReturn] + private static T ThrowInvalidOperation(string message) + => ThrowHelper.ThrowInvalidOperationException(message); + + [MemberNotNull(nameof(_builder))] + private void MoveInlineItemsToBuilder() + { + Debug.Assert(_builder is null); + + _builderPool ??= ArrayBuilderPool.Default; + var builder = _builderPool.Get(); + + if (_capacity is int capacity) + { + builder.Capacity = capacity; + } + else if (builder.Capacity < InlineCapacity) + { + builder.Capacity = InlineCapacity; + } + + _builder = builder; + + // Add the inline items and clear their field values. + for (var i = 0; i < _inlineCount; i++) + { + builder.Add(GetInlineElement(i)); + ClearInlineElement(i); + } + + // Since _inlineCount tracks the number of inline items used, we zero it out here. + _inlineCount = 0; + } + + private void ShiftInlineItemsByOffset(int index, int offset) + { + Debug.Assert(_builder is null); + Debug.Assert(index >= 0 && index < _inlineCount); + Debug.Assert(offset > 0); + Debug.Assert(offset + _inlineCount <= InlineCapacity); + + for (var i = _inlineCount - 1; i >= index; i--) + { + SetInlineElement(i + offset, GetInlineElement(i)); + } + } + + /// + /// Sorts the contents of this builder. + /// + public void Sort() + { + var builder = GetBuilder(); + builder.Sort(); + } + + /// + /// Sorts the contents of this builder using the provided . + /// + public void Sort(IComparer comparer) + { + var builder = GetBuilder(); + builder.Sort(comparer); + } + + /// + /// Sorts the contents of this builder using the provided . + /// + public void Sort(Comparison comparison) + { + var builder = GetBuilder(); + builder.Sort(comparison); + } + + public readonly ImmutableArray ToImmutableOrdered() + { + var result = ToImmutable(); + result.Unsafe().Order(); + + return result; + } + + public readonly ImmutableArray ToImmutableOrdered(IComparer comparer) + { + var result = ToImmutable(); + result.Unsafe().Order(comparer); + + return result; + } + + public readonly ImmutableArray ToImmutableOrdered(Comparison comparison) + { + var result = ToImmutable(); + result.Unsafe().Order(comparison); + + return result; + } + + public readonly ImmutableArray ToImmutableOrderedDescending() + { + var result = ToImmutable(); + result.Unsafe().OrderDescending(); + + return result; + } + + public readonly ImmutableArray ToImmutableOrderedDescending(IComparer comparer) + { + var result = ToImmutable(); + result.Unsafe().OrderDescending(comparer); + + return result; + } + + public readonly ImmutableArray ToImmutableOrderedDescending(Comparison comparison) + { + var result = ToImmutable(); + result.Unsafe().OrderDescending(comparison); + + return result; + } + + public readonly ImmutableArray ToImmutableOrderedBy(Func keySelector) + { + var result = ToImmutable(); + result.Unsafe().OrderBy(keySelector); + + return result; + } + + public readonly ImmutableArray ToImmutableOrderedBy(Func keySelector, IComparer comparer) + { + var result = ToImmutable(); + result.Unsafe().OrderBy(keySelector, comparer); + + return result; + } + + public readonly ImmutableArray ToImmutableOrderedBy(Func keySelector, Comparison comparison) + { + var result = ToImmutable(); + result.Unsafe().OrderBy(keySelector, comparison); + + return result; + } + + public readonly ImmutableArray ToImmutableOrderedByDescending(Func keySelector) + { + var result = ToImmutable(); + result.Unsafe().OrderByDescending(keySelector); + + return result; + } + + public readonly ImmutableArray ToImmutableOrderedByDescending(Func keySelector, IComparer comparer) + { + var result = ToImmutable(); + result.Unsafe().OrderByDescending(keySelector, comparer); + + return result; + } + + public readonly ImmutableArray ToImmutableOrderedByDescending(Func keySelector, Comparison comparison) + { + var result = ToImmutable(); + result.Unsafe().OrderByDescending(keySelector, comparison); + + return result; + } + + public readonly ImmutableArray ToImmutableReversed() + { + var result = ToImmutable(); + result.Unsafe().Reverse(); + + return result; + } + + public ImmutableArray ToImmutableOrderedAndClear() + { + var result = ToImmutableAndClear(); + result.Unsafe().Order(); + + return result; + } + + public ImmutableArray ToImmutableOrderedAndClear(IComparer comparer) + { + var result = ToImmutableAndClear(); + result.Unsafe().Order(comparer); + + return result; + } + + public ImmutableArray ToImmutableOrderedAndClear(Comparison comparison) + { + var result = ToImmutableAndClear(); + result.Unsafe().Order(comparison); + + return result; + } + + public ImmutableArray ToImmutableOrderedDescendingAndClear() + { + var result = ToImmutableAndClear(); + result.Unsafe().OrderDescending(); + + return result; + } + + public ImmutableArray ToImmutableOrderedDescendingAndClear(IComparer comparer) + { + var result = ToImmutableAndClear(); + result.Unsafe().OrderDescending(comparer); + + return result; + } + + public ImmutableArray ToImmutableOrderedDescendingAndClear(Comparison comparison) + { + var result = ToImmutableAndClear(); + result.Unsafe().OrderDescending(comparison); + + return result; + } + + public ImmutableArray ToImmutableOrderedByAndClear(Func keySelector) + { + var result = ToImmutableAndClear(); + result.Unsafe().OrderBy(keySelector); + + return result; + } + + public ImmutableArray ToImmutableOrderedByAndClear(Func keySelector, IComparer comparer) + { + var result = ToImmutableAndClear(); + result.Unsafe().OrderBy(keySelector, comparer); + + return result; + } + + public ImmutableArray ToImmutableOrderedByAndClear(Func keySelector, Comparison comparison) + { + var result = ToImmutableAndClear(); + result.Unsafe().OrderBy(keySelector, comparison); + + return result; + } + + public ImmutableArray ToImmutableOrderedByDescendingAndClear(Func keySelector) + { + var result = ToImmutableAndClear(); + result.Unsafe().OrderByDescending(keySelector); + + return result; + } + + public ImmutableArray ToImmutableOrderedByDescendingAndClear(Func keySelector, IComparer comparer) + { + var result = ToImmutableAndClear(); + result.Unsafe().OrderByDescending(keySelector, comparer); + + return result; + } + + public ImmutableArray ToImmutableOrderedByDescendingAndClear(Func keySelector, Comparison comparison) + { + var result = ToImmutableAndClear(); + result.Unsafe().OrderByDescending(keySelector, comparison); + + return result; + } + + public ImmutableArray ToImmutableReversedAndClear() + { + var result = ToImmutableAndClear(); + result.Unsafe().Reverse(); + + return result; + } + + internal readonly TestAccessor GetTestAccessor() => new(in this); + + internal readonly struct TestAccessor(ref readonly PooledArrayBuilder builder) + { + public ImmutableArray.Builder? InnerArrayBuilder { get; } = builder._builder; + public int? Capacity { get; } = builder._capacity; + public int InlineItemCount { get; } = builder._inlineCount; + } +} From 69423247d5a3f70f90e0418fd3075d9b6cb9510c Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Sat, 9 Aug 2025 10:28:19 -0700 Subject: [PATCH 4/6] Add tests (thanks copilot!) --- .../PooledObjects/PooledSpanBuilderTests.cs | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared.Test/PooledObjects/PooledSpanBuilderTests.cs diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared.Test/PooledObjects/PooledSpanBuilderTests.cs b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared.Test/PooledObjects/PooledSpanBuilderTests.cs new file mode 100644 index 00000000000..282758056d8 --- /dev/null +++ b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared.Test/PooledObjects/PooledSpanBuilderTests.cs @@ -0,0 +1,141 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; +using Microsoft.AspNetCore.Razor.PooledObjects; + +namespace Microsoft.AspNetCore.Razor.Utilities.Shared.Test.PooledObjects; + +public class PooledSpanBuilderTests +{ + [Fact] + public void Create_FromSpan_CreatesWithElements() + { + var source = new[] { 1, 2, 3, 4 }; + using var builder = PooledSpanBuilder.Create(source); + + Assert.Equal(source.Length, builder.Count); + Assert.True(builder.Any()); + Assert.Equal(source, builder.AsSpan().ToArray()); + } + + [Fact] + public void Add_AddsElements() + { + using var builder = PooledSpanBuilder.Empty; + builder.Add(10); + builder.Add(20); + + Assert.Equal(2, builder.Count); + Assert.Equal([10, 20], builder.AsSpan().ToArray()); + } + + [Fact] + public void AddRange_AddsSpan() + { + using var builder = PooledSpanBuilder.Empty; + builder.AddRange(new[] { 1, 2, 3 }); + + Assert.Equal([1, 2, 3], builder.AsSpan().ToArray()); + } + + [Fact] + public void Insert_InsertsAtIndex() + { + using var builder = PooledSpanBuilder.Empty; + builder.AddRange(new[] { 1, 3 }); + builder.Insert(1, 2); + + Assert.Equal([1, 2, 3], builder.AsSpan().ToArray()); + } + + [Fact] + public void RemoveAt_RemovesElement() + { + using var builder = PooledSpanBuilder.Empty; + builder.AddRange(new[] { 1, 2, 3 }); + builder.RemoveAt(1); + + Assert.Equal([1, 3], builder.AsSpan().ToArray()); + } + + [Fact] + public void Clear_ResetsCount() + { + using var builder = PooledSpanBuilder.Empty; + builder.AddRange(new[] { 1, 2, 3 }); + builder.Clear(); + + Assert.Equal(0, builder.Count); + Assert.Empty(builder.AsSpan().ToArray()); + } + + [Fact] + public void ToImmutableAndClear_ReturnsImmutableAndClears() + { + using var builder = PooledSpanBuilder.Empty; + builder.AddRange(new[] { 1, 2, 3 }); + var immutable = builder.ToImmutableAndClear(); + + Assert.Equal([ 1, 2, 3 ], immutable); + Assert.Equal(0, builder.Count); + } + + [Fact] + public void PeekAndPop_WorkAsStack() + { + using var builder = PooledSpanBuilder.Empty; + builder.Push(1); + builder.Push(2); + + Assert.Equal(2, builder.Peek()); + Assert.Equal(2, builder.Pop()); + Assert.Equal(1, builder.Peek()); + } + + [Fact] + public void TryPop_Empty_ReturnsFalse() + { + using var builder = PooledSpanBuilder.Empty; + var result = builder.TryPop(out var value); + + Assert.False(result); + Assert.Equal(0, value); + } + + [Fact] + public void Any_All_First_Last_Single() + { + using var builder = PooledSpanBuilder.Empty; + builder.AddRange(new[] { 1, 2, 3 }); + + Assert.True(builder.Any()); + Assert.True(builder.Any(x => x == 2)); + Assert.True(builder.All(x => x > 0)); + Assert.Equal(1, builder.First()); + Assert.Equal(3, builder.Last()); + Assert.Equal(2, builder.Single(x => x == 2)); + } + + [Fact] + public void ToArrayAndClear_ReturnsArrayAndClears() + { + using var builder = PooledSpanBuilder.Empty; + builder.AddRange(new[] { 5, 6, 7 }); + var arr = builder.ToArrayAndClear(); + + Assert.Equal([5, 6, 7], arr); + Assert.Equal(0, builder.Count); + } + + [Fact] + public void Dispose_ReturnsArrayToPool() + { + var builder = new PooledSpanBuilder(4); + builder.AddRange(new[] { 1, 2, 3, 4 }); + builder.Dispose(); + + Assert.Equal(0, builder.Count); + Assert.Equal(0, builder.Capacity); + } +} From 95e730f532376e4c74f0b4da16660e2baac0fffd Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Sat, 9 Aug 2025 10:29:40 -0700 Subject: [PATCH 5/6] Use PooledSpanBuilder in product code --- .../test/MetadataCollectionTests.cs | 16 ++++----- .../ComponentTagHelperDescriptorProvider.cs | 4 +-- .../src/Language/MetadataBuilder.cs | 36 ++++++------------- .../src/Language/MetadataCollection.cs | 36 ++++++------------- 4 files changed, 32 insertions(+), 60 deletions(-) diff --git a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/MetadataCollectionTests.cs b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/MetadataCollectionTests.cs index e09d4f9f9f0..ca931b3bc9d 100644 --- a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/MetadataCollectionTests.cs +++ b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/MetadataCollectionTests.cs @@ -62,7 +62,7 @@ public void CreateAndCompareCollections(int size) [InlineData(64)] public void EnumeratorReturnsAllItemsInCollection(int size) { - var pairs = new List>(); + var pairs = new KeyValuePair[size]; var map = new Dictionary(); for (var i = 0; i < size; i++) @@ -70,7 +70,7 @@ public void EnumeratorReturnsAllItemsInCollection(int size) var key = i.ToString(CultureInfo.InvariantCulture); var value = (int.MaxValue - i).ToString(CultureInfo.InvariantCulture); - pairs.Add(new(key, value)); + pairs[i] = new(key, value); map.Add(key, false); } @@ -196,8 +196,8 @@ static void Swap(ref KeyValuePair pair1, ref KeyValuePair> pairs1, ImmutableArray> pairs2) { - var collection1 = MetadataCollection.Create(pairs1); - var collection2 = MetadataCollection.Create(pairs2); + var collection1 = MetadataCollection.Create(pairs1.AsSpan()); + var collection2 = MetadataCollection.Create(pairs2.AsSpan()); Assert.Equal(collection1, collection2); Assert.Equal(collection1.GetHashCode(), collection2.GetHashCode()); @@ -207,8 +207,8 @@ public void TestEquality_TwoItems(ImmutableArray> [MemberData(nameof(ThreePairs))] public void TestEquality_ThreeItems(ImmutableArray> pairs1, ImmutableArray> pairs2) { - var collection1 = MetadataCollection.Create(pairs1); - var collection2 = MetadataCollection.Create(pairs2); + var collection1 = MetadataCollection.Create(pairs1.AsSpan()); + var collection2 = MetadataCollection.Create(pairs2.AsSpan()); Assert.Equal(collection1, collection2); Assert.Equal(collection1.GetHashCode(), collection2.GetHashCode()); @@ -218,8 +218,8 @@ public void TestEquality_ThreeItems(ImmutableArray [MemberData(nameof(FourPairs))] public void TestEquality_FourItems(ImmutableArray> pairs1, ImmutableArray> pairs2) { - var collection1 = MetadataCollection.Create(pairs1); - var collection2 = MetadataCollection.Create(pairs2); + var collection1 = MetadataCollection.Create(pairs1.AsSpan()); + var collection2 = MetadataCollection.Create(pairs2.AsSpan()); Assert.Equal(collection1, collection2); Assert.Equal(collection1.GetHashCode(), collection2.GetHashCode()); diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/ComponentTagHelperDescriptorProvider.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/ComponentTagHelperDescriptorProvider.cs index ea8f3ff61bd..14cc7b75ec1 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/ComponentTagHelperDescriptorProvider.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/ComponentTagHelperDescriptorProvider.cs @@ -398,7 +398,7 @@ private static void CreateTypeParameterProperty(TagHelperDescriptorBuilder build pb.Name = typeParameter.Name; pb.TypeName = typeof(Type).FullName; - using var _ = ListPool>.GetPooledObject(out var metadataPairs); + using var metadataPairs = PooledSpanBuilder>.Empty; metadataPairs.Add(PropertyName(typeParameter.Name)); metadataPairs.Add(MakeTrue(ComponentMetadata.Component.TypeParameterKey)); metadataPairs.Add(new(ComponentMetadata.Component.TypeParameterIsCascadingKey, cascade.ToString())); @@ -504,7 +504,7 @@ private static void CreateTypeParameterProperty(TagHelperDescriptorBuilder build metadataPairs.Add(new(ComponentMetadata.Component.TypeParameterWithAttributesKey, withAttributes.ToString())); } - pb.SetMetadata(MetadataCollection.Create(metadataPairs)); + pb.SetMetadata(MetadataCollection.Create(metadataPairs.AsSpan())); pb.SetDocumentation( DocumentationDescriptor.From( diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/MetadataBuilder.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/MetadataBuilder.cs index 70a6c07e5f5..a37cc3ee878 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/MetadataBuilder.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/MetadataBuilder.cs @@ -1,25 +1,20 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.AspNetCore.Razor.PooledObjects; -using Microsoft.Extensions.ObjectPool; using System.Collections.Generic; +using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.AspNetCore.Razor.Utilities; namespace Microsoft.AspNetCore.Razor.Language; +[NonCopyable] internal ref struct MetadataBuilder { - private readonly ObjectPool>> _pool; - private List>? _list; + private PooledSpanBuilder> _list; public MetadataBuilder() - : this(ListPool>.Default) - { - } - - public MetadataBuilder(ObjectPool>> pool) { - _pool = pool; + _list = PooledSpanBuilder>.Empty; } public void Dispose() @@ -28,32 +23,23 @@ public void Dispose() } public void Add(string key, string? value) - { - _list ??= _pool.Get(); - _list.Add(new(key, value)); - } + => _list.Add(new(key, value)); public void Add(KeyValuePair pair) - { - _list ??= _pool.Get(); - _list.Add(pair); - } + => Add(pair.Key, pair.Value); public MetadataCollection Build() { - var result = MetadataCollection.CreateOrEmpty(_list); + var result = MetadataCollection.CreateOrEmpty(_list.AsSpan()); - _list = null; + ClearAndFree(); return result; } public void ClearAndFree() { - if (_list is { } list) - { - _pool.Return(list); - _list = null; - } + _list.Dispose(); + _list = PooledSpanBuilder>.Empty; } } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/MetadataCollection.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/MetadataCollection.cs index 9fb712ccbf0..915c5e2ff5d 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/MetadataCollection.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/MetadataCollection.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Buffers; using System.Collections; using System.Collections.Generic; using System.Diagnostics; @@ -87,17 +88,9 @@ public static MetadataCollection Create(KeyValuePair pair1, Key => new OneToThreeItems(pair1.Key, pair1.Value, pair2.Key, pair2.Value, pair3.Key, pair3.Value); public static MetadataCollection Create(params KeyValuePair[] pairs) - => pairs switch - { - [] => Empty, - [var pair] => new OneToThreeItems(pair.Key, pair.Value), - [var pair1, var pair2] => new OneToThreeItems(pair1.Key, pair1.Value, pair2.Key, pair2.Value), - [var pair1, var pair2, var pair3] => new OneToThreeItems(pair1.Key, pair1.Value, pair2.Key, pair2.Value, pair3.Key, pair3.Value), - _ => new FourOrMoreItems(pairs), - }; + => Create(pairs.AsSpan()); - public static MetadataCollection Create(T pairs) - where T : IReadOnlyList> + public static MetadataCollection Create(ReadOnlySpan> pairs) => pairs switch { [] => Empty, @@ -160,18 +153,17 @@ public static MetadataCollection Create(Dictionary map) return new OneToThreeItems(pair1.Key, pair1.Value, pair2.Key, pair2.Value, pair3.Key, pair3.Value); } - // Finally, if there are four or more items, add the pairs to a list in order to construct + // Finally, if there are four or more items, add the pairs to an array in order to construct // a FourOrMoreItems instance. Note that the constructor will copy the key-value pairs and won't - // hold onto the list we're passing, so it's safe to use a pooled list. - using var _ = ListPool>.GetPooledObject(out var list); - list.SetCapacityIfLarger(count); + // hold onto the list we're passing, so it's safe to use a pooled array. + using var array = new PooledSpanBuilder>(count); foreach (var pair in map) { - list.Add(pair); + array.Add(pair); } - return Create(list); + return Create(array.AsSpan()); } public static MetadataCollection Create(IReadOnlyDictionary map) @@ -188,8 +180,7 @@ public static MetadataCollection Create(IReadOnlyDictionary map return Create(map.ToArray()); } - public static MetadataCollection CreateOrEmpty(T? pairs) - where T : IReadOnlyList> + public static MetadataCollection CreateOrEmpty(ReadOnlySpan> pairs) => pairs is { } realPairs ? Create(realPairs) : Empty; public static MetadataCollection CreateOrEmpty(Dictionary? map) @@ -533,14 +524,9 @@ private sealed class FourOrMoreItems : MetadataCollection private readonly int _count; - public FourOrMoreItems(IReadOnlyList> pairs) + public FourOrMoreItems(ReadOnlySpan> pairs) { - if (pairs is null) - { - throw new ArgumentNullException(nameof(pairs)); - } - - var count = pairs.Count; + var count = pairs.Length; // Create a sorted array of keys. var keys = new string[count]; From 605b4d9b22b818d19204fe4c6561d3259f5dbc33 Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Sat, 9 Aug 2025 15:01:37 -0700 Subject: [PATCH 6/6] Fix test failure from using a collection expression --- .../PooledObjects/PooledSpanBuilderTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared.Test/PooledObjects/PooledSpanBuilderTests.cs b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared.Test/PooledObjects/PooledSpanBuilderTests.cs index 282758056d8..91fc0f8026f 100644 --- a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared.Test/PooledObjects/PooledSpanBuilderTests.cs +++ b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared.Test/PooledObjects/PooledSpanBuilderTests.cs @@ -77,7 +77,7 @@ public void ToImmutableAndClear_ReturnsImmutableAndClears() builder.AddRange(new[] { 1, 2, 3 }); var immutable = builder.ToImmutableAndClear(); - Assert.Equal([ 1, 2, 3 ], immutable); + Assert.Equal(new[] { 1, 2, 3 }, immutable); Assert.Equal(0, builder.Count); }