using System; using System.Collections.Generic; using System.Threading; using BestHTTP.PlatformSupport.Threading; using System.Linq; #if NET_STANDARD_2_0 || NETFX_CORE using System.Runtime.CompilerServices; #endif namespace BestHTTP.PlatformSupport.Memory { [BestHTTP.PlatformSupport.IL2CPP.Il2CppEagerStaticClassConstructionAttribute] public static class BufferPool { public static readonly byte[] NoData = new byte[0]; /// /// Setting this property to false the pooling mechanism can be disabled. /// public static bool IsEnabled { get { return _isEnabled; } set { _isEnabled = value; // When set to non-enabled remove all stored entries if (!_isEnabled) Clear(); } } private static volatile bool _isEnabled = true; /// /// Buffer entries that released back to the pool and older than this value are moved when next maintenance is triggered. /// public static TimeSpan RemoveOlderThan = TimeSpan.FromSeconds(30); /// /// How often pool maintenance must run. /// public static TimeSpan RunMaintenanceEvery = TimeSpan.FromSeconds(10); /// /// Minimum buffer size that the plugin will allocate when the requested size is smaller than this value, and canBeLarger is set to true. /// public static long MinBufferSize = 32; /// /// Maximum size of a buffer that the plugin will store. /// public static long MaxBufferSize = long.MaxValue; /// /// Maximum accumulated size of the stored buffers. /// public static long MaxPoolSize = 30 * 1024 * 1024; /// /// Whether to remove empty buffer stores from the free list. /// public static bool RemoveEmptyLists = false; /// /// If it set to true and a byte[] is released more than once it will log out an error. /// public static bool IsDoubleReleaseCheckEnabled = false; // It must be sorted by buffer size! private readonly static List FreeBuffers = new List(); private static DateTime lastMaintenance = DateTime.MinValue; // Statistics private static long PoolSize = 0; private static long GetBuffers = 0; private static long ReleaseBuffers = 0; private static long Borrowed = 0; private static long ArrayAllocations = 0; #if BESTHTTP_ENABLE_BUFFERPOOL_BORROWED_BUFFERS_COLLECTION private static Dictionary BorrowedBuffers = new Dictionary(); #endif private readonly static ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); static BufferPool() { #if UNITY_EDITOR IsDoubleReleaseCheckEnabled = true; #else IsDoubleReleaseCheckEnabled = false; #endif } /// /// Get byte[] from the pool. If canBeLarge is true, the returned buffer might be larger than the requested size. /// public static byte[] Get(long size, bool canBeLarger) { if (!_isEnabled) return new byte[size]; // Return a fix reference for 0 length requests. Any resize call (even Array.Resize) creates a new reference // so we are safe to expose it to multiple callers. if (size == 0) return BufferPool.NoData; if (canBeLarger) { if (size < MinBufferSize) size = MinBufferSize; else if (!IsPowerOfTwo(size)) size = NextPowerOf2(size); } else { if (size < MinBufferSize) return new byte[size]; } if (FreeBuffers.Count == 0) { Interlocked.Add(ref Borrowed, size); Interlocked.Increment(ref ArrayAllocations); var result = new byte[size]; #if BESTHTTP_ENABLE_BUFFERPOOL_BORROWED_BUFFERS_COLLECTION lock (FreeBuffers) BorrowedBuffers.Add(result, ProcessStackTrace(Environment.StackTrace)); #endif return result; } BufferDesc bufferDesc = FindFreeBuffer(size, canBeLarger); if (bufferDesc.buffer == null) { Interlocked.Add(ref Borrowed, size); Interlocked.Increment(ref ArrayAllocations); var result = new byte[size]; #if BESTHTTP_ENABLE_BUFFERPOOL_BORROWED_BUFFERS_COLLECTION lock (FreeBuffers) BorrowedBuffers.Add(result, ProcessStackTrace(Environment.StackTrace)); #endif return result; } else { #if BESTHTTP_ENABLE_BUFFERPOOL_BORROWED_BUFFERS_COLLECTION lock (FreeBuffers) BorrowedBuffers.Add(bufferDesc.buffer, ProcessStackTrace(Environment.StackTrace)); #endif Interlocked.Increment(ref GetBuffers); } Interlocked.Add(ref Borrowed, bufferDesc.buffer.Length); Interlocked.Add(ref PoolSize, -bufferDesc.buffer.Length); return bufferDesc.buffer; } /// /// Release back a BufferSegment's data to the pool. /// /// #if NET_STANDARD_2_0 || NETFX_CORE [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif public static void Release(BufferSegment segment) { Release(segment.Data); } /// /// Release back a byte array to the pool. /// public static void Release(byte[] buffer) { if (!_isEnabled || buffer == null) return; int size = buffer.Length; #if BESTHTTP_ENABLE_BUFFERPOOL_BORROWED_BUFFERS_COLLECTION lock (FreeBuffers) BorrowedBuffers.Remove(buffer); #endif Interlocked.Add(ref Borrowed, -size); if (size == 0 || size < MinBufferSize || size > MaxBufferSize) return; using (new WriteLock(rwLock)) { var ps = Interlocked.Read(ref PoolSize); if (ps + size > MaxPoolSize) return; Interlocked.Add(ref PoolSize, size); ReleaseBuffers++; AddFreeBuffer(buffer); } } /// /// Resize a byte array. It will release the old one to the pool, and the new one is from the pool too. /// public static byte[] Resize(ref byte[] buffer, int newSize, bool canBeLarger, bool clear) { if (!_isEnabled) { Array.Resize(ref buffer, newSize); return buffer; } byte[] newBuf = BufferPool.Get(newSize, canBeLarger); if (buffer != null) { if (!clear) Array.Copy(buffer, 0, newBuf, 0, Math.Min(newBuf.Length, buffer.Length)); BufferPool.Release(buffer); } if (clear) Array.Clear(newBuf, 0, newSize); return buffer = newBuf; } public static KeyValuePair[] GetBorrowedBuffers() { #if BESTHTTP_ENABLE_BUFFERPOOL_BORROWED_BUFFERS_COLLECTION lock (FreeBuffers) return BorrowedBuffers.ToArray(); #else return new KeyValuePair[0]; #endif } #if true //UNITY_EDITOR public struct BufferStats { public long Size; public int Count; } public struct BufferPoolStats { public long GetBuffers; public long ReleaseBuffers; public long PoolSize; public long MaxPoolSize; public long MinBufferSize; public long MaxBufferSize; public long Borrowed; public long ArrayAllocations; public int FreeBufferCount; public List FreeBufferStats; public TimeSpan NextMaintenance; } public static void GetStatistics(ref BufferPoolStats stats) { using (new ReadLock(rwLock)) { stats.GetBuffers = GetBuffers; stats.ReleaseBuffers = ReleaseBuffers; stats.PoolSize = PoolSize; stats.MinBufferSize = MinBufferSize; stats.MaxBufferSize = MaxBufferSize; stats.MaxPoolSize = MaxPoolSize; stats.Borrowed = Borrowed; stats.ArrayAllocations = ArrayAllocations; stats.FreeBufferCount = FreeBuffers.Count; if (stats.FreeBufferStats == null) stats.FreeBufferStats = new List(FreeBuffers.Count); else stats.FreeBufferStats.Clear(); for (int i = 0; i < FreeBuffers.Count; ++i) { BufferStore store = FreeBuffers[i]; List buffers = store.buffers; BufferStats bufferStats = new BufferStats(); bufferStats.Size = store.Size; bufferStats.Count = buffers.Count; stats.FreeBufferStats.Add(bufferStats); } stats.NextMaintenance = (lastMaintenance + RunMaintenanceEvery) - DateTime.UtcNow; } } #endif /// /// Remove all stored entries instantly. /// public static void Clear() { using (new WriteLock(rwLock)) { FreeBuffers.Clear(); Interlocked.Exchange(ref PoolSize, 0); } } /// /// Internal function called by the plugin to remove old, non-used buffers. /// internal static void Maintain() { DateTime now = DateTime.UtcNow; if (!_isEnabled || lastMaintenance + RunMaintenanceEvery > now) return; lastMaintenance = now; DateTime olderThan = now - RemoveOlderThan; using (new WriteLock(rwLock)) { for (int i = 0; i < FreeBuffers.Count; ++i) { BufferStore store = FreeBuffers[i]; List buffers = store.buffers; for (int cv = buffers.Count - 1; cv >= 0; cv--) { BufferDesc desc = buffers[cv]; if (desc.released < olderThan) { // buffers stores available buffers ascending by age. So, when we find an old enough, we can // delete all entries in the [0..cv] range. int removeCount = cv + 1; buffers.RemoveRange(0, removeCount); PoolSize -= (int)(removeCount * store.Size); break; } } if (RemoveEmptyLists && buffers.Count == 0) FreeBuffers.RemoveAt(i--); } } } #region Private helper functions #if NET_STANDARD_2_0 || NETFX_CORE [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif public static bool IsPowerOfTwo(long x) { return (x & (x - 1)) == 0; } #if NET_STANDARD_2_0 || NETFX_CORE [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif public static long NextPowerOf2(long x) { long pow = 1; while (pow <= x) pow *= 2; return pow; } #if NET_STANDARD_2_0 || NETFX_CORE [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif private static BufferDesc FindFreeBuffer(long size, bool canBeLarger) { // Previously it was an upgradable read lock, and later a write lock around store.buffers.RemoveAt. // However, checking store.buffers.Count in the if statement, and then get the last buffer and finally write lock the RemoveAt call // has plenty of time for race conditions. // Another thread could change store.buffers after checking count and getting the last element and before the write lock, // so in theory we could return with an element and remove another one from the buffers list. // A new FindFreeBuffer call could return it again causing malformed data and/or releasing it could duplicate it in the store. // I tried to reproduce both issues (malformed data, duble entries) with a test where creating growin number of threads getting buffers writing to them, check the buffers and finally release them // would fail _only_ if i used a plain Enter/Exit ReadLock pair, or no locking at all. // But, because there's quite a few different platforms and unity's implementation can be different too, switching from an upgradable lock to a more stricter write lock seems safer. // // An interesting read can be found here: https://stackoverflow.com/questions/21411018/readerwriterlockslim-enterupgradeablereadlock-always-a-deadlock using (new WriteLock(rwLock)) { for (int i = 0; i < FreeBuffers.Count; ++i) { BufferStore store = FreeBuffers[i]; if (store.buffers.Count > 0 && (store.Size == size || (canBeLarger && store.Size > size))) { // Getting the last one has two desired effect: // 1.) RemoveAt should be quicker as it don't have to move all the remaining entries // 2.) Old, non-used buffers will age. Getting a buffer and putting it back will not keep buffers fresh. BufferDesc lastFree = store.buffers[store.buffers.Count - 1]; store.buffers.RemoveAt(store.buffers.Count - 1); return lastFree; } } } return BufferDesc.Empty; } #if NET_STANDARD_2_0 || NETFX_CORE [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif private static void AddFreeBuffer(byte[] buffer) { int bufferLength = buffer.Length; for (int i = 0; i < FreeBuffers.Count; ++i) { BufferStore store = FreeBuffers[i]; if (store.Size == bufferLength) { // We highly assume here that every buffer will be released only once. // Checking for double-release would mean that we have to do another O(n) operation, where n is the // count of the store's elements. if (IsDoubleReleaseCheckEnabled) for (int cv = 0; cv < store.buffers.Count; ++cv) { var entry = store.buffers[cv]; if (System.Object.ReferenceEquals(entry.buffer, buffer)) { HTTPManager.Logger.Error("BufferPool", string.Format("Buffer ({0}) already added to the pool!", entry.ToString())); return; } } store.buffers.Add(new BufferDesc(buffer)); return; } if (store.Size > bufferLength) { FreeBuffers.Insert(i, new BufferStore(bufferLength, buffer)); return; } } // When we reach this point, there's no same sized or larger BufferStore present, so we have to add a new one // to the end of our list. FreeBuffers.Add(new BufferStore(bufferLength, buffer)); } #if BESTHTTP_ENABLE_BUFFERPOOL_BORROWED_BUFFERS_COLLECTION private static System.Text.StringBuilder stacktraceBuilder; private static string ProcessStackTrace(string stackTrace) { if (string.IsNullOrEmpty(stackTrace)) return string.Empty; var lines = stackTrace.Split('\n'); if (stacktraceBuilder == null) stacktraceBuilder = new System.Text.StringBuilder(lines.Length); else stacktraceBuilder.Length = 0; // skip top 4 lines that would show the logger. for (int i = 0; i < lines.Length; ++i) if (!lines[i].Contains(".Memory.BufferPool") && !lines[i].Contains("Environment") && !lines[i].Contains("System.Threading")) stacktraceBuilder.Append(lines[i].Replace("BestHTTP.", "")); return stacktraceBuilder.ToString(); } #endif #endregion } }