diff --git a/src/ServiceStack.Text/Pools/ObjectPool.cs b/src/ServiceStack.Text/Pools/ObjectPool.cs new file mode 100644 index 000000000..de9c19f52 --- /dev/null +++ b/src/ServiceStack.Text/Pools/ObjectPool.cs @@ -0,0 +1,278 @@ +namespace ServiceStack.Text.Pools +{ + // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + + // define TRACE_LEAKS to get additional diagnostics that can lead to the leak sources. note: it will + // make everything about 2-3x slower + // + // #define TRACE_LEAKS + + // define DETECT_LEAKS to detect possible leaks + // #if DEBUG + // #define DETECT_LEAKS //for now always enable DETECT_LEAKS in debug. + // #endif + + using System; + using System.Diagnostics; + using System.Threading; + +#if DETECT_LEAKS +using System.Runtime.CompilerServices; +#endif + /// + /// 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: + /// 1) it is not the goal to keep all returned objects. Pool is not meant for storage. If there + /// is no space in the pool, extra returned objects will be dropped. + /// + /// 2) it is implied that if object was obtained from a pool, the caller will return it back in + /// a relatively short time. Keeping checked out objects for long durations is ok, but + /// reduces usefulness of pooling. Just new up your own. + /// + /// 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". + /// + public class ObjectPool where T : class + { + [DebuggerDisplay("{Value,nq}")] + private struct Element + { + internal T Value; + } + + /// + /// Not using System.Func{T} because this file is linked into the (debugger) Formatter, + /// which does not have that type (since it compiles against .NET 2.0). + /// + internal delegate T Factory(); + + // 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; + + // factory is stored for the lifetime of the pool. We will call this only when pool needs to + // expand. compared to "new T()", Func gives more flexibility to implementers and faster + // than "new T()". + private readonly Factory _factory; + +#if DETECT_LEAKS + private static readonly ConditionalWeakTable leakTrackers = new ConditionalWeakTable(); + + private class LeakTracker : IDisposable + { + private volatile bool disposed; + +#if TRACE_LEAKS + internal volatile object Trace = null; +#endif + + public void Dispose() + { + disposed = true; + GC.SuppressFinalize(this); + } + + private string GetTrace() + { +#if TRACE_LEAKS + return Trace == null ? "" : Trace.ToString(); +#else + return "Leak tracing information is disabled. Define TRACE_LEAKS on ObjectPool`1.cs to get more info \n"; +#endif + } + + ~LeakTracker() + { + if (!this.disposed && !Environment.HasShutdownStarted) + { + var trace = GetTrace(); + + // If you are seeing this message it means that object has been allocated from the pool + // and has not been returned back. This is not critical, but turns pool into rather + // inefficient kind of "new". + Debug.WriteLine($"TRACEOBJECTPOOLLEAKS_BEGIN\nPool detected potential leaking of {typeof(T)}. \n Location of the leak: \n {GetTrace()} TRACEOBJECTPOOLLEAKS_END"); + } + } + } +#endif + + internal ObjectPool(Factory factory) + : this(factory, Environment.ProcessorCount * 2) + { } + + internal ObjectPool(Factory factory, int size) + { + Debug.Assert(size >= 1); + _factory = factory; + _items = new Element[size - 1]; + } + + private T CreateInstance() + { + var inst = _factory(); + return inst; + } + + /// + /// Produces an instance. + /// + /// + /// Search strategy is a simple linear probing which is chosen for it cache-friendliness. + /// Note that Free will try to store recycled objects close to the start thus statistically + /// reducing how far we will typically search. + /// + internal 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. + T inst = _firstItem; + if (inst == null || inst != Interlocked.CompareExchange(ref _firstItem, null, inst)) + { + inst = AllocateSlow(); + } + +#if DETECT_LEAKS + var tracker = new LeakTracker(); + leakTrackers.Add(inst, tracker); + +#if TRACE_LEAKS + var frame = CaptureStackTrace(); + tracker.Trace = frame; +#endif +#endif + return inst; + } + + private T AllocateSlow() + { + var items = _items; + + for (int 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. + T inst = items[i].Value; + if (inst != null) + { + if (inst == Interlocked.CompareExchange(ref items[i].Value, null, inst)) + { + return inst; + } + } + } + + return CreateInstance(); + } + + /// + /// Returns objects to the pool. + /// + /// + /// Search strategy is a simple linear probing which is chosen for it cache-friendliness. + /// Note that Free will try to store recycled objects close to the start thus statistically + /// reducing how far we will typically search in Allocate. + /// + internal void Free(T obj) + { + Validate(obj); + ForgetTrackedObject(obj); + + if (_firstItem == 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 (int i = 0; i < items.Length; i++) + { + if (items[i].Value == 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; + } + } + } + + /// + /// Removes an object from leak tracking. + /// + /// This is called when an object is returned to the pool. It may also be explicitly + /// called if an object allocated from the pool is intentionally not being returned + /// to the pool. This can be of use with pooled arrays if the consumer wants to + /// return a larger array to the pool than was originally allocated. + /// + [Conditional("DEBUG")] + internal void ForgetTrackedObject(T old, T replacement = null) + { +#if DETECT_LEAKS + LeakTracker tracker; + if (leakTrackers.TryGetValue(old, out tracker)) + { + tracker.Dispose(); + leakTrackers.Remove(old); + } + else + { + var trace = CaptureStackTrace(); + Debug.WriteLine($"TRACEOBJECTPOOLLEAKS_BEGIN\nObject of type {typeof(T)} was freed, but was not from pool. \n Callstack: \n {trace} TRACEOBJECTPOOLLEAKS_END"); + } + + if (replacement != null) + { + tracker = new LeakTracker(); + leakTrackers.Add(replacement, tracker); + } +#endif + } + +#if DETECT_LEAKS + private static Lazy _stackTraceType = new Lazy(() => Type.GetType("System.Diagnostics.StackTrace")); + + private static object CaptureStackTrace() + { + return Activator.CreateInstance(_stackTraceType.Value); + } +#endif + + [Conditional("DEBUG")] + private void Validate(object obj) + { + Debug.Assert(obj != null, "freeing null?"); + + Debug.Assert(_firstItem != obj, "freeing twice?"); + + var items = _items; + for (int i = 0; i < items.Length; i++) + { + var value = items[i].Value; + if (value == null) + { + return; + } + + Debug.Assert(value != obj, "freeing twice?"); + } + } + } +} diff --git a/src/ServiceStack.Text/Pools/PooledObject.cs b/src/ServiceStack.Text/Pools/PooledObject.cs new file mode 100644 index 000000000..016251c41 --- /dev/null +++ b/src/ServiceStack.Text/Pools/PooledObject.cs @@ -0,0 +1,136 @@ +namespace ServiceStack.Text.Pools +{ + // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + + using System; + using System.Collections.Generic; + using System.Text; + + /// + /// this is RAII object to automatically release pooled object when its owning pool + /// + public struct PooledObject : IDisposable where T : class + { + private readonly Action, T> _releaser; + private readonly ObjectPool _pool; + private T _pooledObject; + + public PooledObject(ObjectPool pool, Func, T> allocator, Action, T> releaser) : this() + { + _pool = pool; + _pooledObject = allocator(pool); + _releaser = releaser; + } + + public T Object + { + get + { + return _pooledObject; + } + } + + public void Dispose() + { + if (_pooledObject != null) + { + _releaser(_pool, _pooledObject); + _pooledObject = null; + } + } + + #region factory + public static PooledObject Create(ObjectPool pool) + { + return new PooledObject(pool, Allocator, Releaser); + } + + public static PooledObject> Create(ObjectPool> pool) + { + return new PooledObject>(pool, Allocator, Releaser); + } + + public static PooledObject> Create(ObjectPool> pool) + { + return new PooledObject>(pool, Allocator, Releaser); + } + + public static PooledObject> Create(ObjectPool> pool) + { + return new PooledObject>(pool, Allocator, Releaser); + } + + public static PooledObject> Create(ObjectPool> pool) + { + return new PooledObject>(pool, Allocator, Releaser); + } + + public static PooledObject> Create(ObjectPool> pool) + { + return new PooledObject>(pool, Allocator, Releaser); + } + #endregion + + #region allocators and releasers + private static StringBuilder Allocator(ObjectPool pool) + { + return pool.AllocateAndClear(); + } + + private static void Releaser(ObjectPool pool, StringBuilder sb) + { + pool.ClearAndFree(sb); + } + + private static Stack Allocator(ObjectPool> pool) + { + return pool.AllocateAndClear(); + } + + private static void Releaser(ObjectPool> pool, Stack obj) + { + pool.ClearAndFree(obj); + } + + private static Queue Allocator(ObjectPool> pool) + { + return pool.AllocateAndClear(); + } + + private static void Releaser(ObjectPool> pool, Queue obj) + { + pool.ClearAndFree(obj); + } + + private static HashSet Allocator(ObjectPool> pool) + { + return pool.AllocateAndClear(); + } + + private static void Releaser(ObjectPool> pool, HashSet obj) + { + pool.ClearAndFree(obj); + } + + private static Dictionary Allocator(ObjectPool> pool) + { + return pool.AllocateAndClear(); + } + + private static void Releaser(ObjectPool> pool, Dictionary obj) + { + pool.ClearAndFree(obj); + } + + private static List Allocator(ObjectPool> pool) + { + return pool.AllocateAndClear(); + } + + private static void Releaser(ObjectPool> pool, List obj) + { + pool.ClearAndFree(obj); + } + #endregion + } +} \ No newline at end of file diff --git a/src/ServiceStack.Text/Pools/SharedPoolExtensions.cs b/src/ServiceStack.Text/Pools/SharedPoolExtensions.cs new file mode 100644 index 000000000..d485c2571 --- /dev/null +++ b/src/ServiceStack.Text/Pools/SharedPoolExtensions.cs @@ -0,0 +1,201 @@ +namespace ServiceStack.Text.Pools +{ + // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + + using System.Collections.Generic; + using System.Text; + + internal static class SharedPoolExtensions + { + private const int Threshold = 512; + + public static PooledObject GetPooledObject(this ObjectPool pool) + { + return PooledObject.Create(pool); + } + + public static PooledObject> GetPooledObject(this ObjectPool> pool) + { + return PooledObject>.Create(pool); + } + + public static PooledObject> GetPooledObject(this ObjectPool> pool) + { + return PooledObject>.Create(pool); + } + + public static PooledObject> GetPooledObject(this ObjectPool> pool) + { + return PooledObject>.Create(pool); + } + + public static PooledObject> GetPooledObject(this ObjectPool> pool) + { + return PooledObject>.Create(pool); + } + + public static PooledObject> GetPooledObject(this ObjectPool> pool) + { + return PooledObject>.Create(pool); + } + + public static PooledObject GetPooledObject(this ObjectPool pool) where T : class + { + return new PooledObject(pool, p => p.Allocate(), (p, o) => p.Free(o)); + } + + public static StringBuilder AllocateAndClear(this ObjectPool pool) + { + var sb = pool.Allocate(); + sb.Clear(); + + return sb; + } + + public static Stack AllocateAndClear(this ObjectPool> pool) + { + var set = pool.Allocate(); + set.Clear(); + + return set; + } + + public static Queue AllocateAndClear(this ObjectPool> pool) + { + var set = pool.Allocate(); + set.Clear(); + + return set; + } + + public static HashSet AllocateAndClear(this ObjectPool> pool) + { + var set = pool.Allocate(); + set.Clear(); + + return set; + } + + public static Dictionary AllocateAndClear(this ObjectPool> pool) + { + var map = pool.Allocate(); + map.Clear(); + + return map; + } + + public static List AllocateAndClear(this ObjectPool> pool) + { + var list = pool.Allocate(); + list.Clear(); + + return list; + } + + public static void ClearAndFree(this ObjectPool pool, StringBuilder sb) + { + if (sb == null) + { + return; + } + + sb.Clear(); + + if (sb.Capacity > Threshold) + { + sb.Capacity = Threshold; + } + + pool.Free(sb); + } + + public static void ClearAndFree(this ObjectPool> pool, HashSet set) + { + if (set == null) + { + return; + } + + var count = set.Count; + set.Clear(); + + if (count > Threshold) + { + set.TrimExcess(); + } + + pool.Free(set); + } + + public static void ClearAndFree(this ObjectPool> pool, Stack set) + { + if (set == null) + { + return; + } + + var count = set.Count; + set.Clear(); + + if (count > Threshold) + { + set.TrimExcess(); + } + + pool.Free(set); + } + + public static void ClearAndFree(this ObjectPool> pool, Queue set) + { + if (set == null) + { + return; + } + + var count = set.Count; + set.Clear(); + + if (count > Threshold) + { + set.TrimExcess(); + } + + pool.Free(set); + } + + public static void ClearAndFree(this ObjectPool> pool, Dictionary map) + { + if (map == null) + { + return; + } + + // if map grew too big, don't put it back to pool + if (map.Count > Threshold) + { + pool.ForgetTrackedObject(map); + return; + } + + map.Clear(); + pool.Free(map); + } + + public static void ClearAndFree(this ObjectPool> pool, List list) + { + if (list == null) + { + return; + } + + list.Clear(); + + if (list.Capacity > Threshold) + { + list.Capacity = Threshold; + } + + pool.Free(list); + } + } +} \ No newline at end of file diff --git a/src/ServiceStack.Text/Pools/SharedPools.cs b/src/ServiceStack.Text/Pools/SharedPools.cs new file mode 100644 index 000000000..b5aaf77ff --- /dev/null +++ b/src/ServiceStack.Text/Pools/SharedPools.cs @@ -0,0 +1,81 @@ +namespace ServiceStack.Text.Pools +{ + // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + + using System; + using System.Collections.Generic; + + /// + /// Shared object pool for roslyn + /// + /// Use this shared pool if only concern is reducing object allocations. + /// if perf of an object pool itself is also a concern, use ObjectPool directly. + /// + /// For example, if you want to create a million of small objects within a second, + /// use the ObjectPool directly. it should have much less overhead than using this. + /// + public static class SharedPools + { + /// + /// pool that uses default constructor with 100 elements pooled + /// + public static ObjectPool BigDefault() where T : class, new() + { + return DefaultBigPool.Instance; + } + + /// + /// pool that uses default constructor with 20 elements pooled + /// + public static ObjectPool Default() where T : class, new() + { + return DefaultNormalPool.Instance; + } + + /// + /// pool that uses string as key with StringComparer.OrdinalIgnoreCase as key comparer + /// + public static ObjectPool> StringIgnoreCaseDictionary() + { + return StringIgnoreCaseDictionaryNormalPool.Instance; + } + + /// + /// pool that uses string as element with StringComparer.OrdinalIgnoreCase as element comparer + /// + public static readonly ObjectPool> StringIgnoreCaseHashSet = + new ObjectPool>(() => new HashSet(StringComparer.OrdinalIgnoreCase), 20); + + /// + /// pool that uses string as element with StringComparer.Ordinal as element comparer + /// + public static readonly ObjectPool> StringHashSet = + new ObjectPool>(() => new HashSet(StringComparer.Ordinal), 20); + + /// + /// Used to reduce the # of temporary byte[]s created to satisfy serialization and + /// other I/O requests + /// + public static readonly ObjectPool ByteArray = new ObjectPool(() => new byte[ByteBufferSize], ByteBufferCount); + + /// pooled memory : 4K * 512 = 4MB + public const int ByteBufferSize = 4 * 1024; + private const int ByteBufferCount = 512; + + private static class DefaultBigPool where T : class, new() + { + public static readonly ObjectPool Instance = new ObjectPool(() => new T(), 100); + } + + private static class DefaultNormalPool where T : class, new() + { + public static readonly ObjectPool Instance = new ObjectPool(() => new T(), 20); + } + + private static class StringIgnoreCaseDictionaryNormalPool + { + public static readonly ObjectPool> Instance = + new ObjectPool>(() => new Dictionary(StringComparer.OrdinalIgnoreCase), 20); + } + } +} diff --git a/src/ServiceStack.Text/Pools/StringBuilderPool.cs b/src/ServiceStack.Text/Pools/StringBuilderPool.cs new file mode 100644 index 000000000..fdef6eb91 --- /dev/null +++ b/src/ServiceStack.Text/Pools/StringBuilderPool.cs @@ -0,0 +1,25 @@ +namespace ServiceStack.Text.Pools +{ + // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + + using System.Text; + + public static class StringBuilderPool + { + public static StringBuilder Allocate() + { + return SharedPools.Default().AllocateAndClear(); + } + + public static void Free(StringBuilder builder) + { + SharedPools.Default().ClearAndFree(builder); + } + + public static string ReturnAndFree(StringBuilder builder) + { + SharedPools.Default().ForgetTrackedObject(builder); + return builder.ToString(); + } + } +} \ No newline at end of file