diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/ArrayBuilderPool`1.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/ArrayBuilderPool`1.cs new file mode 100644 index 0000000000..a157b88a14 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/ArrayBuilderPool`1.cs @@ -0,0 +1,53 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.Collections.Immutable; + +namespace Microsoft.AspNetCore.Razor.PooledObjects; + +/// +/// A pool of instances. +/// +/// +/// +/// Instances originating from this pool are intended to be short-lived and are suitable +/// for temporary work. Do not return them as the results of methods or store them in fields. +/// +internal static class ArrayBuilderPool +{ + private const int Threshold = 512; + + private static readonly Func.Builder>, ImmutableArray.Builder> s_allocate = AllocateAndClear; + private static readonly Action.Builder>, ImmutableArray.Builder> s_release = ClearAndFree; + + public static ObjectPool.Builder> DefaultPool { get; } = ObjectPool.Default(ImmutableArray.CreateBuilder); + + public static PooledObject.Builder> GetPooledObject() + => new(DefaultPool, s_allocate, s_release); + + private static ImmutableArray.Builder AllocateAndClear(ObjectPool.Builder> pool) + { + var builder = pool.Allocate(); + builder.Clear(); + + return builder; + } + + private static void ClearAndFree(ObjectPool.Builder> pool, ImmutableArray.Builder builder) + { + if (builder is null) + { + return; + } + + builder.Clear(); + + if (builder.Capacity > Threshold) + { + builder.Capacity = Threshold; + } + + pool.Free(builder); + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/HashSetPool`1.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/HashSetPool`1.cs new file mode 100644 index 0000000000..a98539ccca --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/HashSetPool`1.cs @@ -0,0 +1,54 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Razor.PooledObjects; + +/// +/// A pool of instances that compares items using default equality. +/// +/// +/// +/// Instances originating from this pool are intended to be short-lived and are suitable +/// for temporary work. Do not return them as the results of methods or store them in fields. +/// +internal static class HashSetPool +{ + private const int Threshold = 512; + + private static readonly Func>, HashSet> s_allocate = AllocateAndClear; + private static readonly Action>, HashSet> s_release = ClearAndFree; + + public static ObjectPool> DefaultPool { get; } = ObjectPool.Default>(); + + public static PooledObject> GetPooledObject() + => new(DefaultPool, s_allocate, s_release); + + private static HashSet AllocateAndClear(ObjectPool> pool) + { + var set = pool.Allocate(); + set.Clear(); + + return set; + } + + private static void ClearAndFree(ObjectPool> pool, HashSet set) + { + if (set is null) + { + return; + } + + var count = set.Count; + set.Clear(); + + if (count > Threshold) + { + set.TrimExcess(); + } + + pool.Free(set); + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/ListPool`1.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/ListPool`1.cs new file mode 100644 index 0000000000..78c1837047 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/ListPool`1.cs @@ -0,0 +1,54 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Razor.PooledObjects; + +/// +/// A pool of instances. +/// +/// +/// +/// Instances originating from this pool are intended to be short-lived and are suitable +/// for temporary work. Do not return them as the results of methods or store them in fields. +/// +internal static class ListPool +{ + private const int Threshold = 512; + + private static readonly Func>, List> s_allocate = AllocateAndClear; + private static readonly Action>, List> s_release = ClearAndFree; + + public static ObjectPool> DefaultPool { get; } = ObjectPool.Default>(); + + public static PooledObject> GetPooledObject() + => new(DefaultPool, s_allocate, s_release); + + private static List AllocateAndClear(ObjectPool> pool) + { + var list = pool.Allocate(); + list.Clear(); + + return list; + } + + private static void ClearAndFree(ObjectPool> pool, List list) + { + if (list is null) + { + return; + } + + var count = list.Count; + list.Clear(); + + if (count > Threshold) + { + list.TrimExcess(); + } + + pool.Free(list); + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/ObjectPool.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/ObjectPool.cs new file mode 100644 index 0000000000..1515904654 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/ObjectPool.cs @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Razor.PooledObjects; + +internal static class ObjectPool +{ + private const int DefaultSize = 20; + + public static ObjectPool Default() + where T : class, new() + => DefaultPool.Instance; + + public static ObjectPool Default(Func factory) + where T : class + => new(factory, DefaultSize); + + private static class DefaultPool + where T : class, new() + { + public static readonly ObjectPool Instance = new(() => new T(), DefaultSize); + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/ObjectPool`1.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/ObjectPool`1.cs new file mode 100644 index 0000000000..a0a28db977 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/ObjectPool`1.cs @@ -0,0 +1,142 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +// Copied from https://github/dotnet/roslyn + +using System; +using System.Diagnostics; +using System.Threading; + +namespace Microsoft.AspNetCore.Razor.PooledObjects; + +/// +/// Generic implementation of object pooling pattern with predefined pool size limit. The main +/// purpose is that limited number of frequently used objects can be kept in the pool for +/// further recycling. +/// +/// +/// +/// Notes: +/// +/// +/// +/// It is not the goal to keep all returned objects. The pool is not meant for storage. +/// If there is no space in the pool, extra returned objects will be dropped. +/// +/// +/// +/// It is implied that if object was obtained from this pool, the caller will return it back in +/// a relatively short time. Keeping checked out objects for long durations is fine, but reduces +/// the usefulness of pooling. Just new up your own object. +/// +/// +/// +/// +/// Not returning objects to the pool in not detrimental to the pool's work, but is a bad practice. +/// Rationale: If there is no intent for reusing the object, do not use pool - just use "new". +/// +/// +internal class ObjectPool + where T : class +{ + [DebuggerDisplay("{Value,nq}")] + private struct Element + { + public T? _value; + } + + // Storage for the pool objects. The first item is stored in a dedicated field because we + // expect to be able to satisfy most requests from it. + private T? _firstItem; + private readonly Element[] _items; + + private readonly Func _factory; + + public ObjectPool(Func factory) + : this(factory, Environment.ProcessorCount * 2) + { + } + + public ObjectPool(Func factory, int size) + { + _factory = factory; + _items = new Element[size - 1]; + } + + public ObjectPool(Func, T> factory, int size) + { + _factory = () => factory(this); + _items = new Element[size - 1]; + } + + private T CreateInstance() => _factory(); + + public T Allocate() + { + // PERF: Examine the first element. If that fails, AllocateSlow will look at the remaining elements. + // Note that the initial read is optimistically not synchronized. That is intentional. + // We will interlock only when we have a candidate. in a worst case we may miss some + // recently returned objects. Not a big deal. + + var item = _firstItem; + if (item is null || item != Interlocked.CompareExchange(ref _firstItem, null, item)) + { + item = AllocateSlow(); + } + + return item; + } + + private T AllocateSlow() + { + var items = _items; + + for (var i = 0; i < items.Length; i++) + { + // Note that the initial read is optimistically not synchronized. That is intentional. + // We will interlock only when we have a candidate. in a worst case we may miss some + // recently returned objects. Not a big deal. + + var item = _items[i]._value; + if (item is not null && + item == Interlocked.CompareExchange(ref items[i]._value, null, item)) + { + return item; + } + } + + return CreateInstance(); + } + + public void Free(T obj) + { + if (_firstItem is null) + { + // Intentionally not using interlocked here. + // In a worst case scenario two objects may be stored into same slot. + // It is very unlikely to happen and will only mean that one of the objects will get collected. + _firstItem = obj; + } + else + { + FreeSlow(obj); + } + } + + private void FreeSlow(T obj) + { + var items = _items; + + for (var i = 0; i < items.Length; i++) + { + if (items[i]._value is null) + { + // Intentionally not using interlocked here. + // In a worst case scenario two objects may be stored into same slot. + // It is very unlikely to happen and will only mean that one of the objects will get collected. + items[i]._value = obj; + break; + } + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/PooledObject`1.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/PooledObject`1.cs new file mode 100644 index 0000000000..0c0c33072c --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/PooledObject`1.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +// Copied from https://github/dotnet/roslyn + +using System; + +namespace Microsoft.AspNetCore.Razor.PooledObjects; + +internal struct PooledObject : IDisposable + where T : class +{ + private readonly ObjectPool _pool; + private readonly Action, T> _releaser; + private T? _object; + + // Because of how this API is intended to be used, we don't want the consumption code to have + // to deal with Object being a nullable reference type. Intead, the guarantee is that this is + // non-null until this is disposed. + public T Object => _object!; + + public PooledObject(ObjectPool pool, Func, T> allocator, Action, T> releaser) + : this() + { + _pool = pool; + _object = allocator(pool); + _releaser = releaser; + } + + public void Dispose() + { + if (_object is { } obj) + { + _releaser(_pool, obj); + _object = null; + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/ReferenceEqualityHashSetPool`1.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/ReferenceEqualityHashSetPool`1.cs new file mode 100644 index 0000000000..b834dbd453 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/ReferenceEqualityHashSetPool`1.cs @@ -0,0 +1,56 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Razor.Utilities; + +namespace Microsoft.AspNetCore.Razor.PooledObjects; + +/// +/// A pool of instances that compares items using reference equality. +/// +/// +/// +/// Instances originating from this pool are intended to be short-lived and are suitable +/// for temporary work. Do not return them as the results of methods or store them in fields. +/// +internal static class ReferenceEqualityHashSetPool + where T : class +{ + private const int Threshold = 512; + + private static readonly Func>, HashSet> s_allocate = AllocateAndClear; + private static readonly Action>, HashSet> s_release = ClearAndFree; + + public static ObjectPool> DefaultPool { get; } = ObjectPool.Default(() => new HashSet(ReferenceEqualityComparer.Instance)); + + public static PooledObject> GetPooledObject() + => new(DefaultPool, s_allocate, s_release); + + private static HashSet AllocateAndClear(ObjectPool> pool) + { + var set = pool.Allocate(); + set.Clear(); + + return set; + } + + private static void ClearAndFree(ObjectPool> pool, HashSet set) + { + if (set is null) + { + return; + } + + var count = set.Count; + set.Clear(); + + if (count > Threshold) + { + set.TrimExcess(); + } + + pool.Free(set); + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/StackPool`1.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/StackPool`1.cs new file mode 100644 index 0000000000..58b560c177 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/StackPool`1.cs @@ -0,0 +1,54 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Razor.PooledObjects; + +/// +/// A pool of instances. +/// +/// +/// +/// Instances originating from this pool are intended to be short-lived and are suitable +/// for temporary work. Do not return them as the results of methods or store them in fields. +/// +internal static class StackPool +{ + private const int Threshold = 512; + + private static readonly Func>, Stack> s_allocate = AllocateAndClear; + private static readonly Action>, Stack> s_release = ClearAndFree; + + public static ObjectPool> DefaultPool { get; } = ObjectPool.Default>(); + + public static PooledObject> GetPooledObject() + => new(DefaultPool, s_allocate, s_release); + + private static Stack AllocateAndClear(ObjectPool> pool) + { + var stack = pool.Allocate(); + stack.Clear(); + + return stack; + } + + private static void ClearAndFree(ObjectPool> pool, Stack stack) + { + if (stack is null) + { + return; + } + + var count = stack.Count; + stack.Clear(); + + if (count > Threshold) + { + stack.TrimExcess(); + } + + pool.Free(stack); + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/StringBuilderPool.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/StringBuilderPool.cs new file mode 100644 index 0000000000..e566911322 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/StringBuilderPool.cs @@ -0,0 +1,53 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.Text; + +namespace Microsoft.AspNetCore.Razor.PooledObjects; + +/// +/// A pool of instances. +/// +/// +/// +/// Instances originating from this pool are intended to be short-lived and are suitable +/// for temporary work. Do not return them as the results of methods or store them in fields. +/// +internal static class StringBuilderPool +{ + private const int Threshold = 512; + + private static readonly Func, StringBuilder> s_allocate = AllocateAndClear; + private static readonly Action, StringBuilder> s_release = ClearAndFree; + + public static ObjectPool DefaultPool { get; } = ObjectPool.Default(); + + public static PooledObject GetPooledObject() + => new(DefaultPool, s_allocate, s_release); + + private static StringBuilder AllocateAndClear(ObjectPool pool) + { + var builder = pool.Allocate(); + builder.Clear(); + + return builder; + } + + private static void ClearAndFree(ObjectPool pool, StringBuilder builder) + { + if (builder is null) + { + return; + } + + builder.Clear(); + + if (builder.Capacity > Threshold) + { + builder.Capacity = Threshold; + } + + pool.Free(builder); + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Common/Utilities/ReferenceEqualityComparer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/Utilities/ReferenceEqualityComparer.cs new file mode 100644 index 0000000000..0940f206d9 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/Utilities/ReferenceEqualityComparer.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Microsoft.AspNetCore.Razor.Utilities; + +internal class ReferenceEqualityComparer : IEqualityComparer + where T : class +{ + public static readonly ReferenceEqualityComparer Instance = new(); + + private ReferenceEqualityComparer() + { + } + + bool IEqualityComparer.Equals(T? x, T? y) + => ReferenceEquals(x, y); + + int IEqualityComparer.GetHashCode(T obj) + => RuntimeHelpers.GetHashCode(obj); +}