From 254666f7df4b4efd1d26316dd8301a95e8bdfac6 Mon Sep 17 00:00:00 2001 From: hendriklhf <73221731+hendriklhf@users.noreply.github.com> Date: Sun, 14 Sep 2025 01:43:52 +0200 Subject: [PATCH 1/4] Add missing clearing of pooled arrays --- .../Components/src/NavigationManager.cs | 1 + .../CollectionAdapters/ArrayPoolBufferAdapter.cs | 15 +++++++++++++++ .../Endpoints/src/FormMapping/PrefixResolver.cs | 1 + .../src/OutputCacheEntryFormatter.cs | 5 +++++ .../src/RecyclableArrayBufferWriter.cs | 12 ++++++++++++ src/Mvc/Mvc.NewtonsoftJson/src/JsonArrayPool.cs | 6 ++++++ src/Shared/ValueStringBuilder/ValueListBuilder.cs | 11 +++++++++++ src/SignalR/common/Shared/JsonUtils.cs | 10 ++++++++++ 8 files changed, 61 insertions(+) diff --git a/src/Components/Components/src/NavigationManager.cs b/src/Components/Components/src/NavigationManager.cs index fecbfaca6c28..714c9f1a518e 100644 --- a/src/Components/Components/src/NavigationManager.cs +++ b/src/Components/Components/src/NavigationManager.cs @@ -447,6 +447,7 @@ protected async ValueTask NotifyLocationChangingAsync(string uri, string? } finally { + locationChangingHandlersCopy.AsSpan(0, handlerCount).Clear(); ArrayPool>.Shared.Return(locationChangingHandlersCopy); } } diff --git a/src/Components/Endpoints/src/FormMapping/Converters/CollectionAdapters/ArrayPoolBufferAdapter.cs b/src/Components/Endpoints/src/FormMapping/Converters/CollectionAdapters/ArrayPoolBufferAdapter.cs index 036d3308f34c..03e0dc86fd45 100644 --- a/src/Components/Endpoints/src/FormMapping/Converters/CollectionAdapters/ArrayPoolBufferAdapter.cs +++ b/src/Components/Endpoints/src/FormMapping/Converters/CollectionAdapters/ArrayPoolBufferAdapter.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; +using System.Runtime.CompilerServices; namespace Microsoft.AspNetCore.Components.Endpoints.FormMapping; @@ -17,7 +18,14 @@ public static PooledBuffer Add(ref PooledBuffer buffer, TElement element) { var newBuffer = ArrayPool.Shared.Rent(buffer.Data.Length * 2); Array.Copy(buffer.Data, newBuffer, buffer.Data.Length); + + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + { + buffer.Data.AsSpan(0, buffer.Count).Clear(); + } + ArrayPool.Shared.Return(buffer.Data); + buffer.Data = newBuffer; } @@ -28,7 +36,14 @@ public static PooledBuffer Add(ref PooledBuffer buffer, TElement element) public static TCollection ToResult(PooledBuffer buffer) { var result = TCollectionFactory.ToResultCore(buffer.Data, buffer.Count); + + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + { + buffer.Data.AsSpan(0, buffer.Count).Clear(); + } + ArrayPool.Shared.Return(buffer.Data); + return result; } diff --git a/src/Components/Endpoints/src/FormMapping/PrefixResolver.cs b/src/Components/Endpoints/src/FormMapping/PrefixResolver.cs index 6b6775af4be3..103e641b99bf 100644 --- a/src/Components/Endpoints/src/FormMapping/PrefixResolver.cs +++ b/src/Components/Endpoints/src/FormMapping/PrefixResolver.cs @@ -38,6 +38,7 @@ public void Dispose() { if (_sortedKeys != null) { + _sortedKeys.AsSpan(0, _length).Clear(); ArrayPool.Shared.Return(_sortedKeys); } } diff --git a/src/Middleware/OutputCaching/src/OutputCacheEntryFormatter.cs b/src/Middleware/OutputCaching/src/OutputCacheEntryFormatter.cs index 6a7862fc1488..c89a2e58a54e 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheEntryFormatter.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheEntryFormatter.cs @@ -49,6 +49,11 @@ public static async ValueTask StoreAsync(string key, OutputCacheEntry value, Has await bufferStore.SetAsync(key, new(buffer.GetCommittedMemory()), CopyToLeasedMemory(tags, out var lease), duration, cancellationToken); if (lease is not null) { + if (tags is not null) + { + lease.AsSpan(0, tags.Count).Clear(); + } + ArrayPool.Shared.Return(lease); } } diff --git a/src/Middleware/OutputCaching/src/RecyclableArrayBufferWriter.cs b/src/Middleware/OutputCaching/src/RecyclableArrayBufferWriter.cs index 2cb63f0de360..ad8d548bbdb1 100644 --- a/src/Middleware/OutputCaching/src/RecyclableArrayBufferWriter.cs +++ b/src/Middleware/OutputCaching/src/RecyclableArrayBufferWriter.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Diagnostics; +using System.Runtime.CompilerServices; namespace Microsoft.AspNetCore.OutputCaching; @@ -32,10 +33,16 @@ public RecyclableArrayBufferWriter() public void Dispose() { var tmp = _buffer; + var count = _index; _index = 0; _buffer = Array.Empty(); if (tmp.Length != 0) { + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + { + tmp.AsSpan(0, count).Clear(); + } + ArrayPool.Shared.Return(tmp); } } @@ -120,6 +127,11 @@ private void CheckAndResizeBuffer(int sizeHint) oldArray.AsSpan(0, _index).CopyTo(_buffer); if (oldArray.Length != 0) { + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + { + oldArray.AsSpan(0, _index).Clear(); + } + ArrayPool.Shared.Return(oldArray); } } diff --git a/src/Mvc/Mvc.NewtonsoftJson/src/JsonArrayPool.cs b/src/Mvc/Mvc.NewtonsoftJson/src/JsonArrayPool.cs index 876c93a9f3f7..e2e6fc474b57 100644 --- a/src/Mvc/Mvc.NewtonsoftJson/src/JsonArrayPool.cs +++ b/src/Mvc/Mvc.NewtonsoftJson/src/JsonArrayPool.cs @@ -3,6 +3,7 @@ using System.Buffers; using Newtonsoft.Json; +using System.Runtime.CompilerServices; namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson; @@ -26,6 +27,11 @@ public void Return(T[]? array) { ArgumentNullException.ThrowIfNull(array); + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + { + array.AsSpan().Clear(); + } + _inner.Return(array); } } diff --git a/src/Shared/ValueStringBuilder/ValueListBuilder.cs b/src/Shared/ValueStringBuilder/ValueListBuilder.cs index 19b0189a3f6b..a70037bee1c5 100644 --- a/src/Shared/ValueStringBuilder/ValueListBuilder.cs +++ b/src/Shared/ValueStringBuilder/ValueListBuilder.cs @@ -60,6 +60,12 @@ public void Dispose() if (toReturn != null) { _arrayFromPool = null; + + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + { + toReturn.AsSpan(0, _pos).Clear(); + } + ArrayPool.Shared.Return(toReturn); } } @@ -95,6 +101,11 @@ private void Grow(int additionalCapacityRequired = 1) _span = _arrayFromPool = array; if (toReturn != null) { + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + { + toReturn.AsSpan(0, _pos).Clear(); + } + ArrayPool.Shared.Return(toReturn); } } diff --git a/src/SignalR/common/Shared/JsonUtils.cs b/src/SignalR/common/Shared/JsonUtils.cs index f5253fd02ba3..e05337d8b842 100644 --- a/src/SignalR/common/Shared/JsonUtils.cs +++ b/src/SignalR/common/Shared/JsonUtils.cs @@ -5,6 +5,7 @@ using System.Buffers; using System.Globalization; using System.IO; +using System.Runtime.CompilerServices; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -216,6 +217,15 @@ public void Return(T[]? array) return; } +#if NET + if (RuntimeHelpers.IsReferenceOrContainsReferences()) +#else + if (!typeof(T).IsPrimitive) +#endif + { + array.AsSpan().Clear(); + } + _inner.Return(array); } } From 3cd9a0369f5b1c0663f1961d7d1248973aac1223 Mon Sep 17 00:00:00 2001 From: hendriklhf <73221731+hendriklhf@users.noreply.github.com> Date: Sun, 21 Sep 2025 14:37:09 +0200 Subject: [PATCH 2/4] Add `ArrayPool.Return(T[], int)` extension --- .../Microsoft.AspNetCore.Components.csproj | 1 + .../Components/src/NavigationManager.cs | 3 +-- .../ArrayPoolBufferAdapter.cs | 16 ++++++++------ .../src/FormMapping/PrefixResolver.cs | 3 +-- ...oft.AspNetCore.Components.Endpoints.csproj | 3 +-- ...rosoft.AspNetCore.Http.Abstractions.csproj | 1 + .../Microsoft.AspNetCore.OutputCaching.csproj | 1 + .../src/OutputCacheEntryFormatter.cs | 10 +++------ .../src/RecyclableArrayBufferWriter.cs | 16 ++++++++------ .../Microsoft.AspNetCore.WebSockets.csproj | 1 + .../Mvc.NewtonsoftJson/src/JsonArrayPool.cs | 8 ++++--- ...osoft.AspNetCore.Mvc.NewtonsoftJson.csproj | 1 + src/Shared/Buffers/ArrayPoolExtensions.cs | 13 ++++++++++++ .../ValueStringBuilder/ValueListBuilder.cs | 16 ++++++++------ .../Shared.Tests/ArrayPoolExtensionsTests.cs | 21 +++++++++++++++++++ .../Microsoft.AspNetCore.Shared.Tests.csproj | 1 + ...re.SignalR.Protocols.NewtonsoftJson.csproj | 1 + src/SignalR/common/Shared/JsonUtils.cs | 8 ++++--- 18 files changed, 87 insertions(+), 37 deletions(-) create mode 100644 src/Shared/Buffers/ArrayPoolExtensions.cs create mode 100644 src/Shared/test/Shared.Tests/ArrayPoolExtensionsTests.cs diff --git a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj index f165e6f82489..03c899191442 100644 --- a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj +++ b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj @@ -26,6 +26,7 @@ + diff --git a/src/Components/Components/src/NavigationManager.cs b/src/Components/Components/src/NavigationManager.cs index 714c9f1a518e..8f8a11e79128 100644 --- a/src/Components/Components/src/NavigationManager.cs +++ b/src/Components/Components/src/NavigationManager.cs @@ -447,8 +447,7 @@ protected async ValueTask NotifyLocationChangingAsync(string uri, string? } finally { - locationChangingHandlersCopy.AsSpan(0, handlerCount).Clear(); - ArrayPool>.Shared.Return(locationChangingHandlersCopy); + ArrayPool>.Shared.Return(locationChangingHandlersCopy, handlerCount); } } diff --git a/src/Components/Endpoints/src/FormMapping/Converters/CollectionAdapters/ArrayPoolBufferAdapter.cs b/src/Components/Endpoints/src/FormMapping/Converters/CollectionAdapters/ArrayPoolBufferAdapter.cs index 03e0dc86fd45..787f4c6061d5 100644 --- a/src/Components/Endpoints/src/FormMapping/Converters/CollectionAdapters/ArrayPoolBufferAdapter.cs +++ b/src/Components/Endpoints/src/FormMapping/Converters/CollectionAdapters/ArrayPoolBufferAdapter.cs @@ -21,10 +21,12 @@ public static PooledBuffer Add(ref PooledBuffer buffer, TElement element) if (RuntimeHelpers.IsReferenceOrContainsReferences()) { - buffer.Data.AsSpan(0, buffer.Count).Clear(); + ArrayPool.Shared.Return(buffer.Data, buffer.Count); + } + else + { + ArrayPool.Shared.Return(buffer.Data); } - - ArrayPool.Shared.Return(buffer.Data); buffer.Data = newBuffer; } @@ -39,10 +41,12 @@ public static TCollection ToResult(PooledBuffer buffer) if (RuntimeHelpers.IsReferenceOrContainsReferences()) { - buffer.Data.AsSpan(0, buffer.Count).Clear(); + ArrayPool.Shared.Return(buffer.Data, buffer.Count); + } + else + { + ArrayPool.Shared.Return(buffer.Data); } - - ArrayPool.Shared.Return(buffer.Data); return result; } diff --git a/src/Components/Endpoints/src/FormMapping/PrefixResolver.cs b/src/Components/Endpoints/src/FormMapping/PrefixResolver.cs index 103e641b99bf..291badc2a312 100644 --- a/src/Components/Endpoints/src/FormMapping/PrefixResolver.cs +++ b/src/Components/Endpoints/src/FormMapping/PrefixResolver.cs @@ -38,8 +38,7 @@ public void Dispose() { if (_sortedKeys != null) { - _sortedKeys.AsSpan(0, _length).Clear(); - ArrayPool.Shared.Return(_sortedKeys); + ArrayPool.Shared.Return(_sortedKeys, _length); } } diff --git a/src/Components/Endpoints/src/Microsoft.AspNetCore.Components.Endpoints.csproj b/src/Components/Endpoints/src/Microsoft.AspNetCore.Components.Endpoints.csproj index de0329c94e63..0788fc482e4d 100644 --- a/src/Components/Endpoints/src/Microsoft.AspNetCore.Components.Endpoints.csproj +++ b/src/Components/Endpoints/src/Microsoft.AspNetCore.Components.Endpoints.csproj @@ -35,9 +35,8 @@ - - + diff --git a/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj b/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj index 37e17a0f0f49..c57c44a9d9fa 100644 --- a/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj +++ b/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj @@ -32,6 +32,7 @@ Microsoft.AspNetCore.Http.HttpResponse + diff --git a/src/Middleware/OutputCaching/src/Microsoft.AspNetCore.OutputCaching.csproj b/src/Middleware/OutputCaching/src/Microsoft.AspNetCore.OutputCaching.csproj index 55cc936fec7b..8a0891b78695 100644 --- a/src/Middleware/OutputCaching/src/Microsoft.AspNetCore.OutputCaching.csproj +++ b/src/Middleware/OutputCaching/src/Microsoft.AspNetCore.OutputCaching.csproj @@ -23,6 +23,7 @@ + diff --git a/src/Middleware/OutputCaching/src/OutputCacheEntryFormatter.cs b/src/Middleware/OutputCaching/src/OutputCacheEntryFormatter.cs index c89a2e58a54e..144c0d1b59aa 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheEntryFormatter.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheEntryFormatter.cs @@ -46,15 +46,11 @@ public static async ValueTask StoreAsync(string key, OutputCacheEntry value, Has { if (store is IOutputCacheBufferStore bufferStore) { - await bufferStore.SetAsync(key, new(buffer.GetCommittedMemory()), CopyToLeasedMemory(tags, out var lease), duration, cancellationToken); + ReadOnlyMemory leasedTags = CopyToLeasedMemory(tags, out var lease); + await bufferStore.SetAsync(key, new(buffer.GetCommittedMemory()), leasedTags, duration, cancellationToken); if (lease is not null) { - if (tags is not null) - { - lease.AsSpan(0, tags.Count).Clear(); - } - - ArrayPool.Shared.Return(lease); + ArrayPool.Shared.Return(lease, leasedTags.Length); } } else diff --git a/src/Middleware/OutputCaching/src/RecyclableArrayBufferWriter.cs b/src/Middleware/OutputCaching/src/RecyclableArrayBufferWriter.cs index ad8d548bbdb1..ca18dac3957d 100644 --- a/src/Middleware/OutputCaching/src/RecyclableArrayBufferWriter.cs +++ b/src/Middleware/OutputCaching/src/RecyclableArrayBufferWriter.cs @@ -40,10 +40,12 @@ public void Dispose() { if (RuntimeHelpers.IsReferenceOrContainsReferences()) { - tmp.AsSpan(0, count).Clear(); + ArrayPool.Shared.Return(tmp, count); + } + else + { + ArrayPool.Shared.Return(tmp); } - - ArrayPool.Shared.Return(tmp); } } @@ -129,10 +131,12 @@ private void CheckAndResizeBuffer(int sizeHint) { if (RuntimeHelpers.IsReferenceOrContainsReferences()) { - oldArray.AsSpan(0, _index).Clear(); + ArrayPool.Shared.Return(oldArray, _index); + } + else + { + ArrayPool.Shared.Return(oldArray); } - - ArrayPool.Shared.Return(oldArray); } } diff --git a/src/Middleware/WebSockets/src/Microsoft.AspNetCore.WebSockets.csproj b/src/Middleware/WebSockets/src/Microsoft.AspNetCore.WebSockets.csproj index d27fdfb88622..ee333c943545 100644 --- a/src/Middleware/WebSockets/src/Microsoft.AspNetCore.WebSockets.csproj +++ b/src/Middleware/WebSockets/src/Microsoft.AspNetCore.WebSockets.csproj @@ -19,6 +19,7 @@ + diff --git a/src/Mvc/Mvc.NewtonsoftJson/src/JsonArrayPool.cs b/src/Mvc/Mvc.NewtonsoftJson/src/JsonArrayPool.cs index e2e6fc474b57..efbbc1813d0c 100644 --- a/src/Mvc/Mvc.NewtonsoftJson/src/JsonArrayPool.cs +++ b/src/Mvc/Mvc.NewtonsoftJson/src/JsonArrayPool.cs @@ -29,9 +29,11 @@ public void Return(T[]? array) if (RuntimeHelpers.IsReferenceOrContainsReferences()) { - array.AsSpan().Clear(); + _inner.Return(array, array.Length); + } + else + { + _inner.Return(array); } - - _inner.Return(array); } } diff --git a/src/Mvc/Mvc.NewtonsoftJson/src/Microsoft.AspNetCore.Mvc.NewtonsoftJson.csproj b/src/Mvc/Mvc.NewtonsoftJson/src/Microsoft.AspNetCore.Mvc.NewtonsoftJson.csproj index 71271939fa15..c79557643ddc 100644 --- a/src/Mvc/Mvc.NewtonsoftJson/src/Microsoft.AspNetCore.Mvc.NewtonsoftJson.csproj +++ b/src/Mvc/Mvc.NewtonsoftJson/src/Microsoft.AspNetCore.Mvc.NewtonsoftJson.csproj @@ -26,6 +26,7 @@ + diff --git a/src/Shared/Buffers/ArrayPoolExtensions.cs b/src/Shared/Buffers/ArrayPoolExtensions.cs new file mode 100644 index 000000000000..c99978c7ff02 --- /dev/null +++ b/src/Shared/Buffers/ArrayPoolExtensions.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Buffers; + +internal static class ArrayPoolExtensions +{ + public static void Return(this ArrayPool pool, T[] array, int lengthToClear) + { + array.AsSpan(0, lengthToClear).Clear(); + pool.Return(array); + } +} diff --git a/src/Shared/ValueStringBuilder/ValueListBuilder.cs b/src/Shared/ValueStringBuilder/ValueListBuilder.cs index a70037bee1c5..b845575f2c95 100644 --- a/src/Shared/ValueStringBuilder/ValueListBuilder.cs +++ b/src/Shared/ValueStringBuilder/ValueListBuilder.cs @@ -63,10 +63,12 @@ public void Dispose() if (RuntimeHelpers.IsReferenceOrContainsReferences()) { - toReturn.AsSpan(0, _pos).Clear(); + ArrayPool.Shared.Return(toReturn, _pos); + } + else + { + ArrayPool.Shared.Return(toReturn); } - - ArrayPool.Shared.Return(toReturn); } } @@ -103,10 +105,12 @@ private void Grow(int additionalCapacityRequired = 1) { if (RuntimeHelpers.IsReferenceOrContainsReferences()) { - toReturn.AsSpan(0, _pos).Clear(); + ArrayPool.Shared.Return(toReturn, _pos); + } + else + { + ArrayPool.Shared.Return(toReturn); } - - ArrayPool.Shared.Return(toReturn); } } } diff --git a/src/Shared/test/Shared.Tests/ArrayPoolExtensionsTests.cs b/src/Shared/test/Shared.Tests/ArrayPoolExtensionsTests.cs new file mode 100644 index 000000000000..44b72b91f0c1 --- /dev/null +++ b/src/Shared/test/Shared.Tests/ArrayPoolExtensionsTests.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Buffers; + +public sealed class ArrayPoolExtensionsTests +{ + [Fact] + public void Return_PartiallyClearsArray_WithSpecifiedLengthToClear() + { + ArrayPool pool = ArrayPool.Create(); + int[] array = pool.Rent(64); + + array.AsSpan().Fill(int.MaxValue); + + pool.Return(array, 42); + + Assert.True(array.AsSpan(0, 42).IndexOfAnyExcept(0) < 0); + Assert.True(array.AsSpan(42).IndexOfAnyExcept(int.MaxValue) < 0); + } +} diff --git a/src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj b/src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj index 72496024ef3b..a19a0184032f 100644 --- a/src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj +++ b/src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj @@ -42,6 +42,7 @@ + diff --git a/src/SignalR/common/Protocols.NewtonsoftJson/src/Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson.csproj b/src/SignalR/common/Protocols.NewtonsoftJson/src/Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson.csproj index 6b621b28bc00..1900a3da8e95 100644 --- a/src/SignalR/common/Protocols.NewtonsoftJson/src/Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson.csproj +++ b/src/SignalR/common/Protocols.NewtonsoftJson/src/Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson.csproj @@ -18,6 +18,7 @@ + diff --git a/src/SignalR/common/Shared/JsonUtils.cs b/src/SignalR/common/Shared/JsonUtils.cs index e05337d8b842..471fe92d61d2 100644 --- a/src/SignalR/common/Shared/JsonUtils.cs +++ b/src/SignalR/common/Shared/JsonUtils.cs @@ -223,10 +223,12 @@ public void Return(T[]? array) if (!typeof(T).IsPrimitive) #endif { - array.AsSpan().Clear(); + _inner.Return(array, array.Length); + } + else + { + _inner.Return(array); } - - _inner.Return(array); } } } From ad06bc487653b9be1f651570cdfe12bea5a2ba8f Mon Sep 17 00:00:00 2001 From: hendriklhf <73221731+hendriklhf@users.noreply.github.com> Date: Tue, 30 Sep 2025 22:27:32 +0200 Subject: [PATCH 3/4] Add `ArrayPool.ReturnAndClearReferences(T[], int)` extension --- .../ArrayPoolBufferAdapter.cs | 23 ++-------------- .../src/RecyclableArrayBufferWriter.cs | 19 ++----------- .../Mvc.NewtonsoftJson/src/JsonArrayPool.cs | 10 +------ src/Shared/Buffers/ArrayPoolExtensions.cs | 27 +++++++++++++++++++ .../ValueStringBuilder/ValueListBuilder.cs | 19 ++----------- src/SignalR/common/Shared/JsonUtils.cs | 14 +--------- 6 files changed, 35 insertions(+), 77 deletions(-) diff --git a/src/Components/Endpoints/src/FormMapping/Converters/CollectionAdapters/ArrayPoolBufferAdapter.cs b/src/Components/Endpoints/src/FormMapping/Converters/CollectionAdapters/ArrayPoolBufferAdapter.cs index 787f4c6061d5..679c35de4844 100644 --- a/src/Components/Endpoints/src/FormMapping/Converters/CollectionAdapters/ArrayPoolBufferAdapter.cs +++ b/src/Components/Endpoints/src/FormMapping/Converters/CollectionAdapters/ArrayPoolBufferAdapter.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; -using System.Runtime.CompilerServices; namespace Microsoft.AspNetCore.Components.Endpoints.FormMapping; @@ -18,16 +17,7 @@ public static PooledBuffer Add(ref PooledBuffer buffer, TElement element) { var newBuffer = ArrayPool.Shared.Rent(buffer.Data.Length * 2); Array.Copy(buffer.Data, newBuffer, buffer.Data.Length); - - if (RuntimeHelpers.IsReferenceOrContainsReferences()) - { - ArrayPool.Shared.Return(buffer.Data, buffer.Count); - } - else - { - ArrayPool.Shared.Return(buffer.Data); - } - + ArrayPool.Shared.ReturnAndClearReferences(buffer.Data, buffer.Count); buffer.Data = newBuffer; } @@ -38,16 +28,7 @@ public static PooledBuffer Add(ref PooledBuffer buffer, TElement element) public static TCollection ToResult(PooledBuffer buffer) { var result = TCollectionFactory.ToResultCore(buffer.Data, buffer.Count); - - if (RuntimeHelpers.IsReferenceOrContainsReferences()) - { - ArrayPool.Shared.Return(buffer.Data, buffer.Count); - } - else - { - ArrayPool.Shared.Return(buffer.Data); - } - + ArrayPool.Shared.ReturnAndClearReferences(buffer.Data, buffer.Count); return result; } diff --git a/src/Middleware/OutputCaching/src/RecyclableArrayBufferWriter.cs b/src/Middleware/OutputCaching/src/RecyclableArrayBufferWriter.cs index ca18dac3957d..504c1ef22576 100644 --- a/src/Middleware/OutputCaching/src/RecyclableArrayBufferWriter.cs +++ b/src/Middleware/OutputCaching/src/RecyclableArrayBufferWriter.cs @@ -3,7 +3,6 @@ using System.Buffers; using System.Diagnostics; -using System.Runtime.CompilerServices; namespace Microsoft.AspNetCore.OutputCaching; @@ -38,14 +37,7 @@ public void Dispose() _buffer = Array.Empty(); if (tmp.Length != 0) { - if (RuntimeHelpers.IsReferenceOrContainsReferences()) - { - ArrayPool.Shared.Return(tmp, count); - } - else - { - ArrayPool.Shared.Return(tmp); - } + ArrayPool.Shared.ReturnAndClearReferences(tmp, count); } } @@ -129,14 +121,7 @@ private void CheckAndResizeBuffer(int sizeHint) oldArray.AsSpan(0, _index).CopyTo(_buffer); if (oldArray.Length != 0) { - if (RuntimeHelpers.IsReferenceOrContainsReferences()) - { - ArrayPool.Shared.Return(oldArray, _index); - } - else - { - ArrayPool.Shared.Return(oldArray); - } + ArrayPool.Shared.ReturnAndClearReferences(oldArray, _index); } } diff --git a/src/Mvc/Mvc.NewtonsoftJson/src/JsonArrayPool.cs b/src/Mvc/Mvc.NewtonsoftJson/src/JsonArrayPool.cs index efbbc1813d0c..8ecd3fc9188e 100644 --- a/src/Mvc/Mvc.NewtonsoftJson/src/JsonArrayPool.cs +++ b/src/Mvc/Mvc.NewtonsoftJson/src/JsonArrayPool.cs @@ -3,7 +3,6 @@ using System.Buffers; using Newtonsoft.Json; -using System.Runtime.CompilerServices; namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson; @@ -27,13 +26,6 @@ public void Return(T[]? array) { ArgumentNullException.ThrowIfNull(array); - if (RuntimeHelpers.IsReferenceOrContainsReferences()) - { - _inner.Return(array, array.Length); - } - else - { - _inner.Return(array); - } + _inner.ReturnAndClearReferences(array, array.Length); } } diff --git a/src/Shared/Buffers/ArrayPoolExtensions.cs b/src/Shared/Buffers/ArrayPoolExtensions.cs index c99978c7ff02..08d1fdf9f4be 100644 --- a/src/Shared/Buffers/ArrayPoolExtensions.cs +++ b/src/Shared/Buffers/ArrayPoolExtensions.cs @@ -1,13 +1,40 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Runtime.CompilerServices; + namespace System.Buffers; internal static class ArrayPoolExtensions { + /// + /// Clears the specified range and returns the array to the pool. + /// public static void Return(this ArrayPool pool, T[] array, int lengthToClear) { array.AsSpan(0, lengthToClear).Clear(); pool.Return(array); } + + /// + /// Clears the specified range if is a reference type or + /// contains references and returns the array to the pool. + /// + /// + /// For .NET Framework, falls back to checking if is not a primitive type + /// where RuntimeHelpers.IsReferenceOrContainsReferences<T>() is not available. + /// + public static void ReturnAndClearReferences(this ArrayPool pool, T[] array, int lengthToClear) + { +#if NET + if (RuntimeHelpers.IsReferenceOrContainsReferences()) +#else + if (!typeof(T).IsPrimitive) +#endif + { + array.AsSpan(0, lengthToClear).Clear(); + } + + pool.Return(array); + } } diff --git a/src/Shared/ValueStringBuilder/ValueListBuilder.cs b/src/Shared/ValueStringBuilder/ValueListBuilder.cs index b845575f2c95..bb8630ea338b 100644 --- a/src/Shared/ValueStringBuilder/ValueListBuilder.cs +++ b/src/Shared/ValueStringBuilder/ValueListBuilder.cs @@ -60,15 +60,7 @@ public void Dispose() if (toReturn != null) { _arrayFromPool = null; - - if (RuntimeHelpers.IsReferenceOrContainsReferences()) - { - ArrayPool.Shared.Return(toReturn, _pos); - } - else - { - ArrayPool.Shared.Return(toReturn); - } + ArrayPool.Shared.ReturnAndClearReferences(toReturn, _pos); } } @@ -103,14 +95,7 @@ private void Grow(int additionalCapacityRequired = 1) _span = _arrayFromPool = array; if (toReturn != null) { - if (RuntimeHelpers.IsReferenceOrContainsReferences()) - { - ArrayPool.Shared.Return(toReturn, _pos); - } - else - { - ArrayPool.Shared.Return(toReturn); - } + ArrayPool.Shared.ReturnAndClearReferences(toReturn, _pos); } } } diff --git a/src/SignalR/common/Shared/JsonUtils.cs b/src/SignalR/common/Shared/JsonUtils.cs index 471fe92d61d2..9fa18a589459 100644 --- a/src/SignalR/common/Shared/JsonUtils.cs +++ b/src/SignalR/common/Shared/JsonUtils.cs @@ -5,7 +5,6 @@ using System.Buffers; using System.Globalization; using System.IO; -using System.Runtime.CompilerServices; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -217,18 +216,7 @@ public void Return(T[]? array) return; } -#if NET - if (RuntimeHelpers.IsReferenceOrContainsReferences()) -#else - if (!typeof(T).IsPrimitive) -#endif - { - _inner.Return(array, array.Length); - } - else - { - _inner.Return(array); - } + _inner.ReturnAndClearReferences(array, array.Length); } } } From 6661e276fc7253a71aa1164b0d1be60ddbc37230 Mon Sep 17 00:00:00 2001 From: hendriklhf <73221731+hendriklhf@users.noreply.github.com> Date: Tue, 30 Sep 2025 22:30:02 +0200 Subject: [PATCH 4/4] Add more thorough tests for all cases --- .../Shared.Tests/ArrayPoolExtensionsTests.cs | 247 +++++++++++++++++- 1 file changed, 246 insertions(+), 1 deletion(-) diff --git a/src/Shared/test/Shared.Tests/ArrayPoolExtensionsTests.cs b/src/Shared/test/Shared.Tests/ArrayPoolExtensionsTests.cs index 44b72b91f0c1..869b28a39029 100644 --- a/src/Shared/test/Shared.Tests/ArrayPoolExtensionsTests.cs +++ b/src/Shared/test/Shared.Tests/ArrayPoolExtensionsTests.cs @@ -5,8 +5,10 @@ namespace System.Buffers; public sealed class ArrayPoolExtensionsTests { + private record struct StructWithStringField(string Value); + [Fact] - public void Return_PartiallyClearsArray_WithSpecifiedLengthToClear() + public void Return_PartiallyClearsArray_UnmanagedType_WithPartialLengthSpecified() { ArrayPool pool = ArrayPool.Create(); int[] array = pool.Rent(64); @@ -18,4 +20,247 @@ public void Return_PartiallyClearsArray_WithSpecifiedLengthToClear() Assert.True(array.AsSpan(0, 42).IndexOfAnyExcept(0) < 0); Assert.True(array.AsSpan(42).IndexOfAnyExcept(int.MaxValue) < 0); } + + [Fact] + public void Return_PartiallyClearsArray_ManagedValueType_WithPartialLengthSpecified() + { + ArrayPool pool = ArrayPool.Create(); + StructWithStringField[] array = pool.Rent(64); + + StructWithStringField value = new("abc"); + array.AsSpan().Fill(value); + + pool.Return(array, 42); + + Assert.True(array.AsSpan(0, 42).IndexOfAnyExcept(default(StructWithStringField)) < 0); + Assert.True(array.AsSpan(42).IndexOfAnyExcept(value) < 0); + } + + [Fact] + public void Return_PartiallyClearsArray_ReferenceType_WithPartialLengthSpecified() + { + const string Value = "abc"; + + ArrayPool pool = ArrayPool.Create(); + string[] array = pool.Rent(64); + + array.AsSpan().Fill(Value); + + pool.Return(array, 42); + + Assert.True(array.AsSpan(0, 42).IndexOfAnyExcept((string)null) < 0); + Assert.True(array.AsSpan(42).IndexOfAnyExcept(Value) < 0); + } + + [Fact] + public void Return_CompletelyClearsArray_UnmanagedType_WithFullLengthSpecified() + { + ArrayPool pool = ArrayPool.Create(); + int[] array = pool.Rent(64); + + array.AsSpan().Fill(int.MaxValue); + + pool.Return(array, array.Length); + + Assert.True(array.AsSpan().IndexOfAnyExcept(0) < 0); + } + + [Fact] + public void Return_CompletelyClearsArray_ManagedValueType_WithFullLengthSpecified() + { + ArrayPool pool = ArrayPool.Create(); + StructWithStringField[] array = pool.Rent(64); + + StructWithStringField value = new("abc"); + array.AsSpan().Fill(value); + + pool.Return(array, array.Length); + + Assert.True(array.AsSpan().IndexOfAnyExcept(default(StructWithStringField)) < 0); + } + + [Fact] + public void Return_CompletelyClearsArray_ReferenceType_WithFullLengthSpecified() + { + const string Value = "abc"; + + ArrayPool pool = ArrayPool.Create(); + string[] array = pool.Rent(64); + + array.AsSpan().Fill(Value); + + pool.Return(array, array.Length); + + Assert.True(array.AsSpan().IndexOfAnyExcept((string)null) < 0); + } + + [Fact] + public void Return_DoesNotClearArray_UnmanagedType_WithZeroLengthSpecified() + { + ArrayPool pool = ArrayPool.Create(); + int[] array = pool.Rent(64); + + array.AsSpan().Fill(int.MaxValue); + + pool.Return(array, 0); + + Assert.True(array.AsSpan().IndexOfAnyExcept(int.MaxValue) < 0); + } + + [Fact] + public void Return_DoesNotClearArray_ManagedValueType_WithZeroLengthSpecified() + { + ArrayPool pool = ArrayPool.Create(); + StructWithStringField[] array = pool.Rent(64); + + StructWithStringField value = new("abc"); + array.AsSpan().Fill(value); + + pool.Return(array, 0); + + Assert.True(array.AsSpan().IndexOfAnyExcept(value) < 0); + } + + [Fact] + public void Return_DoesNotClearArray_ReferenceType_WithZeroLengthSpecified() + { + const string Value = "abc"; + + ArrayPool pool = ArrayPool.Create(); + string[] array = pool.Rent(64); + + array.AsSpan().Fill(Value); + + pool.Return(array, 0); + + Assert.True(array.AsSpan().IndexOfAnyExcept(Value) < 0); + } + + [Fact] + public void ReturnAndClearReferences_PartiallyClearsArray_ReferenceType_WithPartialLengthSpecified() + { + const string Value = "abc"; + + ArrayPool pool = ArrayPool.Create(); + string[] array = pool.Rent(64); + + array.AsSpan().Fill(Value); + + pool.ReturnAndClearReferences(array, 42); + + Assert.True(array.AsSpan(0, 42).IndexOfAnyExcept((string)null) < 0); + Assert.True(array.AsSpan(42).IndexOfAnyExcept(Value) < 0); + } + + [Fact] + public void ReturnAndClearReferences_PartiallyClearsArray_ManagedValueType_WithPartialLengthSpecified() + { + ArrayPool pool = ArrayPool.Create(); + StructWithStringField[] array = pool.Rent(64); + + StructWithStringField value = new("abc"); + array.AsSpan().Fill(value); + + pool.ReturnAndClearReferences(array, 42); + + Assert.True(array.AsSpan(0, 42).IndexOfAnyExcept(default(StructWithStringField)) < 0); + Assert.True(array.AsSpan(42).IndexOfAnyExcept(value) < 0); + } + + [Fact] + public void ReturnAndClearReferences_DoesNotClearArray_UnmanagedType_WithPartialLengthSpecified() + { + ArrayPool pool = ArrayPool.Create(); + int[] array = pool.Rent(64); + + array.AsSpan().Fill(int.MaxValue); + + pool.ReturnAndClearReferences(array, 42); + + Assert.True(array.AsSpan().IndexOfAnyExcept(int.MaxValue) < 0); + } + + [Fact] + public void ReturnAndClearReferences_CompletelyClearsArray_ReferenceType_WithFullLengthSpecified() + { + const string Value = "abc"; + + ArrayPool pool = ArrayPool.Create(); + string[] array = pool.Rent(64); + + array.AsSpan().Fill(Value); + + pool.ReturnAndClearReferences(array, array.Length); + + Assert.True(array.AsSpan().IndexOfAnyExcept((string)null) < 0); + } + + [Fact] + public void ReturnAndClearReferences_CompletelyClearsArray_ManagedValueType_WithFullLengthSpecified() + { + ArrayPool pool = ArrayPool.Create(); + StructWithStringField[] array = pool.Rent(64); + + StructWithStringField value = new("abc"); + array.AsSpan().Fill(value); + + pool.ReturnAndClearReferences(array, array.Length); + + Assert.True(array.AsSpan().IndexOfAnyExcept(default(StructWithStringField)) < 0); + } + + [Fact] + public void ReturnAndClearReferences_DoesNotClearArray_UnmanagedType_WithFullLengthSpecified() + { + ArrayPool pool = ArrayPool.Create(); + int[] array = pool.Rent(64); + + array.AsSpan().Fill(int.MaxValue); + + pool.ReturnAndClearReferences(array, array.Length); + + Assert.True(array.AsSpan().IndexOfAnyExcept(int.MaxValue) < 0); + } + + [Fact] + public void ReturnAndClearReferences_DoesNotClearArray_ReferenceType_WithZeroLengthSpecified() + { + const string Value = "abc"; + + ArrayPool pool = ArrayPool.Create(); + string[] array = pool.Rent(64); + + array.AsSpan().Fill(Value); + + pool.ReturnAndClearReferences(array, 0); + + Assert.True(array.AsSpan().IndexOfAnyExcept(Value) < 0); + } + + [Fact] + public void ReturnAndClearReferences_DoesNotClearArray_ManagedValueType_WithZeroLengthSpecified() + { + ArrayPool pool = ArrayPool.Create(); + StructWithStringField[] array = pool.Rent(64); + + StructWithStringField value = new("abc"); + array.AsSpan().Fill(value); + + pool.ReturnAndClearReferences(array, 0); + + Assert.True(array.AsSpan().IndexOfAnyExcept(value) < 0); + } + + [Fact] + public void ReturnAndClearReferences_DoesNotClearArray_UnmanagedType_WithZeroLengthSpecified() + { + ArrayPool pool = ArrayPool.Create(); + int[] array = pool.Rent(64); + + array.AsSpan().Fill(int.MaxValue); + + pool.ReturnAndClearReferences(array, 0); + + Assert.True(array.AsSpan().IndexOfAnyExcept(int.MaxValue) < 0); + } }