BufferPool.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Threading;
  4. using BestHTTP.PlatformSupport.Threading;
  5. using System.Linq;
  6. #if NET_STANDARD_2_0 || NETFX_CORE
  7. using System.Runtime.CompilerServices;
  8. #endif
  9. namespace BestHTTP.PlatformSupport.Memory
  10. {
  11. [BestHTTP.PlatformSupport.IL2CPP.Il2CppEagerStaticClassConstructionAttribute]
  12. public static class BufferPool
  13. {
  14. public static readonly byte[] NoData = new byte[0];
  15. /// <summary>
  16. /// Setting this property to false the pooling mechanism can be disabled.
  17. /// </summary>
  18. public static bool IsEnabled {
  19. get { return _isEnabled; }
  20. set
  21. {
  22. _isEnabled = value;
  23. // When set to non-enabled remove all stored entries
  24. if (!_isEnabled)
  25. Clear();
  26. }
  27. }
  28. private static volatile bool _isEnabled = true;
  29. /// <summary>
  30. /// Buffer entries that released back to the pool and older than this value are moved when next maintenance is triggered.
  31. /// </summary>
  32. public static TimeSpan RemoveOlderThan = TimeSpan.FromSeconds(30);
  33. /// <summary>
  34. /// How often pool maintenance must run.
  35. /// </summary>
  36. public static TimeSpan RunMaintenanceEvery = TimeSpan.FromSeconds(10);
  37. /// <summary>
  38. /// Minimum buffer size that the plugin will allocate when the requested size is smaller than this value, and canBeLarger is set to true.
  39. /// </summary>
  40. public static long MinBufferSize = 32;
  41. /// <summary>
  42. /// Maximum size of a buffer that the plugin will store.
  43. /// </summary>
  44. public static long MaxBufferSize = long.MaxValue;
  45. /// <summary>
  46. /// Maximum accumulated size of the stored buffers.
  47. /// </summary>
  48. public static long MaxPoolSize = 30 * 1024 * 1024;
  49. /// <summary>
  50. /// Whether to remove empty buffer stores from the free list.
  51. /// </summary>
  52. public static bool RemoveEmptyLists = false;
  53. /// <summary>
  54. /// If it set to true and a byte[] is released more than once it will log out an error.
  55. /// </summary>
  56. public static bool IsDoubleReleaseCheckEnabled = false;
  57. // It must be sorted by buffer size!
  58. private readonly static List<BufferStore> FreeBuffers = new List<BufferStore>();
  59. private static DateTime lastMaintenance = DateTime.MinValue;
  60. // Statistics
  61. private static long PoolSize = 0;
  62. private static long GetBuffers = 0;
  63. private static long ReleaseBuffers = 0;
  64. private static long Borrowed = 0;
  65. private static long ArrayAllocations = 0;
  66. #if BESTHTTP_ENABLE_BUFFERPOOL_BORROWED_BUFFERS_COLLECTION
  67. private static Dictionary<byte[], string> BorrowedBuffers = new Dictionary<byte[], string>();
  68. #endif
  69. private readonly static ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
  70. static BufferPool()
  71. {
  72. #if UNITY_EDITOR
  73. IsDoubleReleaseCheckEnabled = true;
  74. #else
  75. IsDoubleReleaseCheckEnabled = false;
  76. #endif
  77. }
  78. /// <summary>
  79. /// Get byte[] from the pool. If canBeLarge is true, the returned buffer might be larger than the requested size.
  80. /// </summary>
  81. public static byte[] Get(long size, bool canBeLarger)
  82. {
  83. if (!_isEnabled)
  84. return new byte[size];
  85. // Return a fix reference for 0 length requests. Any resize call (even Array.Resize) creates a new reference
  86. // so we are safe to expose it to multiple callers.
  87. if (size == 0)
  88. return BufferPool.NoData;
  89. if (canBeLarger)
  90. {
  91. if (size < MinBufferSize)
  92. size = MinBufferSize;
  93. else if (!IsPowerOfTwo(size))
  94. size = NextPowerOf2(size);
  95. }
  96. else
  97. {
  98. if (size < MinBufferSize)
  99. return new byte[size];
  100. }
  101. if (FreeBuffers.Count == 0)
  102. {
  103. Interlocked.Add(ref Borrowed, size);
  104. Interlocked.Increment(ref ArrayAllocations);
  105. var result = new byte[size];
  106. #if BESTHTTP_ENABLE_BUFFERPOOL_BORROWED_BUFFERS_COLLECTION
  107. lock (FreeBuffers)
  108. BorrowedBuffers.Add(result, ProcessStackTrace(Environment.StackTrace));
  109. #endif
  110. return result;
  111. }
  112. BufferDesc bufferDesc = FindFreeBuffer(size, canBeLarger);
  113. if (bufferDesc.buffer == null)
  114. {
  115. Interlocked.Add(ref Borrowed, size);
  116. Interlocked.Increment(ref ArrayAllocations);
  117. var result = new byte[size];
  118. #if BESTHTTP_ENABLE_BUFFERPOOL_BORROWED_BUFFERS_COLLECTION
  119. lock (FreeBuffers)
  120. BorrowedBuffers.Add(result, ProcessStackTrace(Environment.StackTrace));
  121. #endif
  122. return result;
  123. }
  124. else
  125. {
  126. #if BESTHTTP_ENABLE_BUFFERPOOL_BORROWED_BUFFERS_COLLECTION
  127. lock (FreeBuffers)
  128. BorrowedBuffers.Add(bufferDesc.buffer, ProcessStackTrace(Environment.StackTrace));
  129. #endif
  130. Interlocked.Increment(ref GetBuffers);
  131. }
  132. Interlocked.Add(ref Borrowed, bufferDesc.buffer.Length);
  133. Interlocked.Add(ref PoolSize, -bufferDesc.buffer.Length);
  134. return bufferDesc.buffer;
  135. }
  136. /// <summary>
  137. /// Release back a BufferSegment's data to the pool.
  138. /// </summary>
  139. /// <param name="segment"></param>
  140. #if NET_STANDARD_2_0 || NETFX_CORE
  141. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  142. #endif
  143. public static void Release(BufferSegment segment)
  144. {
  145. Release(segment.Data);
  146. }
  147. /// <summary>
  148. /// Release back a byte array to the pool.
  149. /// </summary>
  150. public static void Release(byte[] buffer)
  151. {
  152. if (!_isEnabled || buffer == null)
  153. return;
  154. int size = buffer.Length;
  155. #if BESTHTTP_ENABLE_BUFFERPOOL_BORROWED_BUFFERS_COLLECTION
  156. lock (FreeBuffers)
  157. BorrowedBuffers.Remove(buffer);
  158. #endif
  159. Interlocked.Add(ref Borrowed, -size);
  160. if (size == 0 || size < MinBufferSize || size > MaxBufferSize)
  161. return;
  162. using (new WriteLock(rwLock))
  163. {
  164. var ps = Interlocked.Read(ref PoolSize);
  165. if (ps + size > MaxPoolSize)
  166. return;
  167. Interlocked.Add(ref PoolSize, size);
  168. ReleaseBuffers++;
  169. AddFreeBuffer(buffer);
  170. }
  171. }
  172. /// <summary>
  173. /// Resize a byte array. It will release the old one to the pool, and the new one is from the pool too.
  174. /// </summary>
  175. public static byte[] Resize(ref byte[] buffer, int newSize, bool canBeLarger, bool clear)
  176. {
  177. if (!_isEnabled)
  178. {
  179. Array.Resize<byte>(ref buffer, newSize);
  180. return buffer;
  181. }
  182. byte[] newBuf = BufferPool.Get(newSize, canBeLarger);
  183. if (buffer != null)
  184. {
  185. if (!clear)
  186. Array.Copy(buffer, 0, newBuf, 0, Math.Min(newBuf.Length, buffer.Length));
  187. BufferPool.Release(buffer);
  188. }
  189. if (clear)
  190. Array.Clear(newBuf, 0, newSize);
  191. return buffer = newBuf;
  192. }
  193. public static KeyValuePair<byte[], string>[] GetBorrowedBuffers()
  194. {
  195. #if BESTHTTP_ENABLE_BUFFERPOOL_BORROWED_BUFFERS_COLLECTION
  196. lock (FreeBuffers)
  197. return BorrowedBuffers.ToArray();
  198. #else
  199. return new KeyValuePair<byte[], string>[0];
  200. #endif
  201. }
  202. #if true //UNITY_EDITOR
  203. public struct BufferStats
  204. {
  205. public long Size;
  206. public int Count;
  207. }
  208. public struct BufferPoolStats
  209. {
  210. public long GetBuffers;
  211. public long ReleaseBuffers;
  212. public long PoolSize;
  213. public long MaxPoolSize;
  214. public long MinBufferSize;
  215. public long MaxBufferSize;
  216. public long Borrowed;
  217. public long ArrayAllocations;
  218. public int FreeBufferCount;
  219. public List<BufferStats> FreeBufferStats;
  220. public TimeSpan NextMaintenance;
  221. }
  222. public static void GetStatistics(ref BufferPoolStats stats)
  223. {
  224. using (new ReadLock(rwLock))
  225. {
  226. stats.GetBuffers = GetBuffers;
  227. stats.ReleaseBuffers = ReleaseBuffers;
  228. stats.PoolSize = PoolSize;
  229. stats.MinBufferSize = MinBufferSize;
  230. stats.MaxBufferSize = MaxBufferSize;
  231. stats.MaxPoolSize = MaxPoolSize;
  232. stats.Borrowed = Borrowed;
  233. stats.ArrayAllocations = ArrayAllocations;
  234. stats.FreeBufferCount = FreeBuffers.Count;
  235. if (stats.FreeBufferStats == null)
  236. stats.FreeBufferStats = new List<BufferStats>(FreeBuffers.Count);
  237. else
  238. stats.FreeBufferStats.Clear();
  239. for (int i = 0; i < FreeBuffers.Count; ++i)
  240. {
  241. BufferStore store = FreeBuffers[i];
  242. List<BufferDesc> buffers = store.buffers;
  243. BufferStats bufferStats = new BufferStats();
  244. bufferStats.Size = store.Size;
  245. bufferStats.Count = buffers.Count;
  246. stats.FreeBufferStats.Add(bufferStats);
  247. }
  248. stats.NextMaintenance = (lastMaintenance + RunMaintenanceEvery) - DateTime.UtcNow;
  249. }
  250. }
  251. #endif
  252. /// <summary>
  253. /// Remove all stored entries instantly.
  254. /// </summary>
  255. public static void Clear()
  256. {
  257. using (new WriteLock(rwLock))
  258. {
  259. FreeBuffers.Clear();
  260. Interlocked.Exchange(ref PoolSize, 0);
  261. }
  262. }
  263. /// <summary>
  264. /// Internal function called by the plugin to remove old, non-used buffers.
  265. /// </summary>
  266. internal static void Maintain()
  267. {
  268. DateTime now = DateTime.UtcNow;
  269. if (!_isEnabled || lastMaintenance + RunMaintenanceEvery > now)
  270. return;
  271. lastMaintenance = now;
  272. DateTime olderThan = now - RemoveOlderThan;
  273. using (new WriteLock(rwLock))
  274. {
  275. for (int i = 0; i < FreeBuffers.Count; ++i)
  276. {
  277. BufferStore store = FreeBuffers[i];
  278. List<BufferDesc> buffers = store.buffers;
  279. for (int cv = buffers.Count - 1; cv >= 0; cv--)
  280. {
  281. BufferDesc desc = buffers[cv];
  282. if (desc.released < olderThan)
  283. {
  284. // buffers stores available buffers ascending by age. So, when we find an old enough, we can
  285. // delete all entries in the [0..cv] range.
  286. int removeCount = cv + 1;
  287. buffers.RemoveRange(0, removeCount);
  288. PoolSize -= (int)(removeCount * store.Size);
  289. break;
  290. }
  291. }
  292. if (RemoveEmptyLists && buffers.Count == 0)
  293. FreeBuffers.RemoveAt(i--);
  294. }
  295. }
  296. }
  297. #region Private helper functions
  298. #if NET_STANDARD_2_0 || NETFX_CORE
  299. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  300. #endif
  301. public static bool IsPowerOfTwo(long x)
  302. {
  303. return (x & (x - 1)) == 0;
  304. }
  305. #if NET_STANDARD_2_0 || NETFX_CORE
  306. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  307. #endif
  308. public static long NextPowerOf2(long x)
  309. {
  310. long pow = 1;
  311. while (pow <= x)
  312. pow *= 2;
  313. return pow;
  314. }
  315. #if NET_STANDARD_2_0 || NETFX_CORE
  316. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  317. #endif
  318. private static BufferDesc FindFreeBuffer(long size, bool canBeLarger)
  319. {
  320. // Previously it was an upgradable read lock, and later a write lock around store.buffers.RemoveAt.
  321. // However, checking store.buffers.Count in the if statement, and then get the last buffer and finally write lock the RemoveAt call
  322. // has plenty of time for race conditions.
  323. // Another thread could change store.buffers after checking count and getting the last element and before the write lock,
  324. // so in theory we could return with an element and remove another one from the buffers list.
  325. // A new FindFreeBuffer call could return it again causing malformed data and/or releasing it could duplicate it in the store.
  326. // 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
  327. // would fail _only_ if i used a plain Enter/Exit ReadLock pair, or no locking at all.
  328. // 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.
  329. //
  330. // An interesting read can be found here: https://stackoverflow.com/questions/21411018/readerwriterlockslim-enterupgradeablereadlock-always-a-deadlock
  331. using (new WriteLock(rwLock))
  332. {
  333. for (int i = 0; i < FreeBuffers.Count; ++i)
  334. {
  335. BufferStore store = FreeBuffers[i];
  336. if (store.buffers.Count > 0 && (store.Size == size || (canBeLarger && store.Size > size)))
  337. {
  338. // Getting the last one has two desired effect:
  339. // 1.) RemoveAt should be quicker as it don't have to move all the remaining entries
  340. // 2.) Old, non-used buffers will age. Getting a buffer and putting it back will not keep buffers fresh.
  341. BufferDesc lastFree = store.buffers[store.buffers.Count - 1];
  342. store.buffers.RemoveAt(store.buffers.Count - 1);
  343. return lastFree;
  344. }
  345. }
  346. }
  347. return BufferDesc.Empty;
  348. }
  349. #if NET_STANDARD_2_0 || NETFX_CORE
  350. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  351. #endif
  352. private static void AddFreeBuffer(byte[] buffer)
  353. {
  354. int bufferLength = buffer.Length;
  355. for (int i = 0; i < FreeBuffers.Count; ++i)
  356. {
  357. BufferStore store = FreeBuffers[i];
  358. if (store.Size == bufferLength)
  359. {
  360. // We highly assume here that every buffer will be released only once.
  361. // Checking for double-release would mean that we have to do another O(n) operation, where n is the
  362. // count of the store's elements.
  363. if (IsDoubleReleaseCheckEnabled)
  364. for (int cv = 0; cv < store.buffers.Count; ++cv)
  365. {
  366. var entry = store.buffers[cv];
  367. if (System.Object.ReferenceEquals(entry.buffer, buffer))
  368. {
  369. HTTPManager.Logger.Error("BufferPool", string.Format("Buffer ({0}) already added to the pool!", entry.ToString()));
  370. return;
  371. }
  372. }
  373. store.buffers.Add(new BufferDesc(buffer));
  374. return;
  375. }
  376. if (store.Size > bufferLength)
  377. {
  378. FreeBuffers.Insert(i, new BufferStore(bufferLength, buffer));
  379. return;
  380. }
  381. }
  382. // When we reach this point, there's no same sized or larger BufferStore present, so we have to add a new one
  383. // to the end of our list.
  384. FreeBuffers.Add(new BufferStore(bufferLength, buffer));
  385. }
  386. #if BESTHTTP_ENABLE_BUFFERPOOL_BORROWED_BUFFERS_COLLECTION
  387. private static System.Text.StringBuilder stacktraceBuilder;
  388. private static string ProcessStackTrace(string stackTrace)
  389. {
  390. if (string.IsNullOrEmpty(stackTrace))
  391. return string.Empty;
  392. var lines = stackTrace.Split('\n');
  393. if (stacktraceBuilder == null)
  394. stacktraceBuilder = new System.Text.StringBuilder(lines.Length);
  395. else
  396. stacktraceBuilder.Length = 0;
  397. // skip top 4 lines that would show the logger.
  398. for (int i = 0; i < lines.Length; ++i)
  399. if (!lines[i].Contains(".Memory.BufferPool") &&
  400. !lines[i].Contains("Environment") &&
  401. !lines[i].Contains("System.Threading"))
  402. stacktraceBuilder.Append(lines[i].Replace("BestHTTP.", ""));
  403. return stacktraceBuilder.ToString();
  404. }
  405. #endif
  406. #endregion
  407. }
  408. }