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 fecbfaca6c28..8f8a11e79128 100644 --- a/src/Components/Components/src/NavigationManager.cs +++ b/src/Components/Components/src/NavigationManager.cs @@ -447,7 +447,7 @@ protected async ValueTask NotifyLocationChangingAsync(string uri, string? } finally { - 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 036d3308f34c..679c35de4844 100644 --- a/src/Components/Endpoints/src/FormMapping/Converters/CollectionAdapters/ArrayPoolBufferAdapter.cs +++ b/src/Components/Endpoints/src/FormMapping/Converters/CollectionAdapters/ArrayPoolBufferAdapter.cs @@ -17,7 +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); - ArrayPool.Shared.Return(buffer.Data); + ArrayPool.Shared.ReturnAndClearReferences(buffer.Data, buffer.Count); buffer.Data = newBuffer; } @@ -28,7 +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); - ArrayPool.Shared.Return(buffer.Data); + ArrayPool.Shared.ReturnAndClearReferences(buffer.Data, buffer.Count); return result; } diff --git a/src/Components/Endpoints/src/FormMapping/PrefixResolver.cs b/src/Components/Endpoints/src/FormMapping/PrefixResolver.cs index 6b6775af4be3..291badc2a312 100644 --- a/src/Components/Endpoints/src/FormMapping/PrefixResolver.cs +++ b/src/Components/Endpoints/src/FormMapping/PrefixResolver.cs @@ -38,7 +38,7 @@ public void Dispose() { if (_sortedKeys != null) { - 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 6a7862fc1488..144c0d1b59aa 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheEntryFormatter.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheEntryFormatter.cs @@ -46,10 +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) { - 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 2cb63f0de360..504c1ef22576 100644 --- a/src/Middleware/OutputCaching/src/RecyclableArrayBufferWriter.cs +++ b/src/Middleware/OutputCaching/src/RecyclableArrayBufferWriter.cs @@ -32,11 +32,12 @@ public RecyclableArrayBufferWriter() public void Dispose() { var tmp = _buffer; + var count = _index; _index = 0; _buffer = Array.Empty(); if (tmp.Length != 0) { - ArrayPool.Shared.Return(tmp); + ArrayPool.Shared.ReturnAndClearReferences(tmp, count); } } @@ -120,7 +121,7 @@ private void CheckAndResizeBuffer(int sizeHint) oldArray.AsSpan(0, _index).CopyTo(_buffer); if (oldArray.Length != 0) { - ArrayPool.Shared.Return(oldArray); + ArrayPool.Shared.ReturnAndClearReferences(oldArray, _index); } } 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 876c93a9f3f7..8ecd3fc9188e 100644 --- a/src/Mvc/Mvc.NewtonsoftJson/src/JsonArrayPool.cs +++ b/src/Mvc/Mvc.NewtonsoftJson/src/JsonArrayPool.cs @@ -26,6 +26,6 @@ public void Return(T[]? array) { ArgumentNullException.ThrowIfNull(array); - _inner.Return(array); + _inner.ReturnAndClearReferences(array, array.Length); } } 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..08d1fdf9f4be --- /dev/null +++ b/src/Shared/Buffers/ArrayPoolExtensions.cs @@ -0,0 +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 19b0189a3f6b..bb8630ea338b 100644 --- a/src/Shared/ValueStringBuilder/ValueListBuilder.cs +++ b/src/Shared/ValueStringBuilder/ValueListBuilder.cs @@ -60,7 +60,7 @@ public void Dispose() if (toReturn != null) { _arrayFromPool = null; - ArrayPool.Shared.Return(toReturn); + ArrayPool.Shared.ReturnAndClearReferences(toReturn, _pos); } } @@ -95,7 +95,7 @@ private void Grow(int additionalCapacityRequired = 1) _span = _arrayFromPool = array; if (toReturn != null) { - ArrayPool.Shared.Return(toReturn); + ArrayPool.Shared.ReturnAndClearReferences(toReturn, _pos); } } } diff --git a/src/Shared/test/Shared.Tests/ArrayPoolExtensionsTests.cs b/src/Shared/test/Shared.Tests/ArrayPoolExtensionsTests.cs new file mode 100644 index 000000000000..869b28a39029 --- /dev/null +++ b/src/Shared/test/Shared.Tests/ArrayPoolExtensionsTests.cs @@ -0,0 +1,266 @@ +// 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 +{ + private record struct StructWithStringField(string Value); + + [Fact] + public void Return_PartiallyClearsArray_UnmanagedType_WithPartialLengthSpecified() + { + 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); + } + + [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); + } +} 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 f5253fd02ba3..9fa18a589459 100644 --- a/src/SignalR/common/Shared/JsonUtils.cs +++ b/src/SignalR/common/Shared/JsonUtils.cs @@ -216,7 +216,7 @@ public void Return(T[]? array) return; } - _inner.Return(array); + _inner.ReturnAndClearReferences(array, array.Length); } } }