using System; using System.Collections.Concurrent; using System.Diagnostics; using System.Runtime.InteropServices; using System.Threading; namespace Mirror.SimpleWeb { public interface IBufferOwner { void Return(ArrayBuffer buffer); } public sealed class ArrayBuffer : IDisposable { readonly IBufferOwner owner; public readonly byte[] array; /// /// number of bytes writen to buffer /// internal int count; /// /// How many times release needs to be called before buffer is returned to pool /// This allows the buffer to be used in multiple places at the same time /// public void SetReleasesRequired(int required) { releasesRequired = required; } /// /// How many times release needs to be called before buffer is returned to pool /// This allows the buffer to be used in multiple places at the same time /// /// /// This value is normally 0, but can be changed to require release to be called multiple times /// int releasesRequired; public ArrayBuffer(IBufferOwner owner, int size) { this.owner = owner; array = new byte[size]; } public void Release() { int newValue = Interlocked.Decrement(ref releasesRequired); if (newValue <= 0) { count = 0; owner.Return(this); } } public void Dispose() { Release(); } public void CopyTo(byte[] target, int offset) { if (count > (target.Length + offset)) throw new ArgumentException($"{nameof(count)} was greater than {nameof(target)}.length", nameof(target)); Buffer.BlockCopy(array, 0, target, offset, count); } public void CopyFrom(ArraySegment segment) { CopyFrom(segment.Array, segment.Offset, segment.Count); } public void CopyFrom(byte[] source, int offset, int length) { if (length > array.Length) throw new ArgumentException($"{nameof(length)} was greater than {nameof(array)}.length", nameof(length)); count = length; Buffer.BlockCopy(source, offset, array, 0, length); } public void CopyFrom(IntPtr bufferPtr, int length) { if (length > array.Length) throw new ArgumentException($"{nameof(length)} was greater than {nameof(array)}.length", nameof(length)); count = length; Marshal.Copy(bufferPtr, array, 0, length); } public ArraySegment ToSegment() { return new ArraySegment(array, 0, count); } [Conditional("UNITY_ASSERTIONS")] internal void Validate(int arraySize) { if (array.Length != arraySize) { Log.Error("Buffer that was returned had an array of the wrong size"); } } } internal class BufferBucket : IBufferOwner { public readonly int arraySize; readonly ConcurrentQueue buffers; /// /// keeps track of how many arrays are taken vs returned /// internal int _current = 0; public BufferBucket(int arraySize) { this.arraySize = arraySize; buffers = new ConcurrentQueue(); } public ArrayBuffer Take() { IncrementCreated(); if (buffers.TryDequeue(out ArrayBuffer buffer)) { return buffer; } else { Log.Verbose($"BufferBucket({arraySize}) create new"); return new ArrayBuffer(this, arraySize); } } public void Return(ArrayBuffer buffer) { DecrementCreated(); buffer.Validate(arraySize); buffers.Enqueue(buffer); } [Conditional("DEBUG")] void IncrementCreated() { int next = Interlocked.Increment(ref _current); Log.Verbose($"BufferBucket({arraySize}) count:{next}"); } [Conditional("DEBUG")] void DecrementCreated() { int next = Interlocked.Decrement(ref _current); Log.Verbose($"BufferBucket({arraySize}) count:{next}"); } } /// /// Collection of different sized buffers /// /// /// /// Problem:
/// * Need to cached byte[] so that new ones aren't created each time
/// * Arrays sent are multiple different sizes
/// * Some message might be big so need buffers to cover that size
/// * Most messages will be small compared to max message size
///
///
/// /// Solution:
/// * Create multiple groups of buffers covering the range of allowed sizes
/// * Split range exponentially (using math.log) so that there are more groups for small buffers
///
///
public class BufferPool { internal readonly BufferBucket[] buckets; readonly int bucketCount; readonly int smallest; readonly int largest; public BufferPool(int bucketCount, int smallest, int largest) { if (bucketCount < 2) throw new ArgumentException("Count must be at least 2"); if (smallest < 1) throw new ArgumentException("Smallest must be at least 1"); if (largest < smallest) throw new ArgumentException("Largest must be greater than smallest"); this.bucketCount = bucketCount; this.smallest = smallest; this.largest = largest; // split range over log scale (more buckets for smaller sizes) double minLog = Math.Log(this.smallest); double maxLog = Math.Log(this.largest); double range = maxLog - minLog; double each = range / (bucketCount - 1); buckets = new BufferBucket[bucketCount]; for (int i = 0; i < bucketCount; i++) { double size = smallest * Math.Pow(Math.E, each * i); buckets[i] = new BufferBucket((int)Math.Ceiling(size)); } Validate(); // Example // 5 count // 20 smallest // 16400 largest // 3.0 log 20 // 9.7 log 16400 // 6.7 range 9.7 - 3 // 1.675 each 6.7 / (5-1) // 20 e^ (3 + 1.675 * 0) // 107 e^ (3 + 1.675 * 1) // 572 e^ (3 + 1.675 * 2) // 3056 e^ (3 + 1.675 * 3) // 16,317 e^ (3 + 1.675 * 4) // perceision wont be lose when using doubles } [Conditional("UNITY_ASSERTIONS")] void Validate() { if (buckets[0].arraySize != smallest) { Log.Error($"BufferPool Failed to create bucket for smallest. bucket:{buckets[0].arraySize} smallest{smallest}"); } int largestBucket = buckets[bucketCount - 1].arraySize; // rounded using Ceiling, so allowed to be 1 more that largest if (largestBucket != largest && largestBucket != largest + 1) { Log.Error($"BufferPool Failed to create bucket for largest. bucket:{largestBucket} smallest{largest}"); } } public ArrayBuffer Take(int size) { if (size > largest) { throw new ArgumentException($"Size ({size}) is greatest that largest ({largest})"); } for (int i = 0; i < bucketCount; i++) { if (size <= buckets[i].arraySize) { return buckets[i].Take(); } } throw new ArgumentException($"Size ({size}) is greatest that largest ({largest})"); } } }