250 lines
8.1 KiB
C#
250 lines
8.1 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// number of bytes written to buffer
|
|
/// </summary>
|
|
public int count { get; internal set; }
|
|
|
|
/// <summary>
|
|
/// How many times release needs to be called before buffer is returned to pool
|
|
/// <para>This allows the buffer to be used in multiple places at the same time</para>
|
|
/// </summary>
|
|
public void SetReleasesRequired(int required)
|
|
{
|
|
releasesRequired = required;
|
|
}
|
|
|
|
/// <summary>
|
|
/// How many times release needs to be called before buffer is returned to pool
|
|
/// <para>This allows the buffer to be used in multiple places at the same time</para>
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This value is normally 0, but can be changed to require release to be called multiple times
|
|
/// </remarks>
|
|
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<byte> 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<byte> ToSegment() => new ArraySegment<byte>(array, 0, count);
|
|
|
|
[Conditional("UNITY_ASSERTIONS")]
|
|
internal void Validate(int arraySize)
|
|
{
|
|
if (array.Length != arraySize)
|
|
Log.Error("[SWT-ArrayBuffer]: Buffer that was returned had an array of the wrong size");
|
|
}
|
|
}
|
|
|
|
internal class BufferBucket : IBufferOwner
|
|
{
|
|
public readonly int arraySize;
|
|
readonly ConcurrentQueue<ArrayBuffer> buffers;
|
|
|
|
/// <summary>
|
|
/// keeps track of how many arrays are taken vs returned
|
|
/// </summary>
|
|
internal int _current = 0;
|
|
|
|
public BufferBucket(int arraySize)
|
|
{
|
|
this.arraySize = arraySize;
|
|
buffers = new ConcurrentQueue<ArrayBuffer>();
|
|
}
|
|
|
|
public ArrayBuffer Take()
|
|
{
|
|
IncrementCreated();
|
|
if (buffers.TryDequeue(out ArrayBuffer buffer))
|
|
return buffer;
|
|
else
|
|
{
|
|
Log.Flood($"[SWT-BufferBucket]: 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.Flood($"[SWT-BufferBucket]: BufferBucket({arraySize}) count:{next}");
|
|
}
|
|
|
|
[Conditional("DEBUG")]
|
|
void DecrementCreated()
|
|
{
|
|
int next = Interlocked.Decrement(ref _current);
|
|
Log.Flood($"[SWT-BufferBucket]: BufferBucket({arraySize}) count:{next}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Collection of different sized buffers
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// Problem: <br/>
|
|
/// * Need to cached byte[] so that new ones aren't created each time <br/>
|
|
/// * Arrays sent are multiple different sizes <br/>
|
|
/// * Some message might be big so need buffers to cover that size <br/>
|
|
/// * Most messages will be small compared to max message size <br/>
|
|
/// </para>
|
|
/// <br/>
|
|
/// <para>
|
|
/// Solution: <br/>
|
|
/// * Create multiple groups of buffers covering the range of allowed sizes <br/>
|
|
/// * Split range exponentially (using math.log) so that there are more groups for small buffers <br/>
|
|
/// </para>
|
|
/// </remarks>
|
|
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)
|
|
|
|
// precision wont be lose when using doubles
|
|
}
|
|
|
|
[Conditional("UNITY_ASSERTIONS")]
|
|
void Validate()
|
|
{
|
|
if (buckets[0].arraySize != smallest)
|
|
Log.Error("[SWT-BufferPool]: BufferPool Failed to create bucket for smallest. bucket:{0} smallest:{1}", buckets[0].arraySize, 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("[SWT-BufferPool]: BufferPool Failed to create bucket for largest. bucket:{0} smallest:{1}", largestBucket, largest);
|
|
}
|
|
|
|
public ArrayBuffer Take(int size)
|
|
{
|
|
if (size > largest)
|
|
throw new ArgumentException($"Size ({size}) is greater than largest ({largest})");
|
|
|
|
for (int i = 0; i < bucketCount; i++)
|
|
if (size <= buckets[i].arraySize)
|
|
return buckets[i].Take();
|
|
|
|
throw new ArgumentException($"Size ({size}) is greater than largest ({largest})");
|
|
}
|
|
}
|
|
}
|