mirror of https://github.com/SixLabors/ImageSharp
61 changed files with 2484 additions and 387 deletions
@ -1,21 +1,30 @@ |
|||||
// Copyright (c) Six Labors.
|
// Copyright (c) Six Labors.
|
||||
// Licensed under the Apache License, Version 2.0.
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
|
||||
namespace SixLabors.ImageSharp.Memory |
namespace SixLabors.ImageSharp.Memory |
||||
{ |
{ |
||||
/// <summary>
|
/// <summary>
|
||||
/// Options for allocating buffers.
|
/// Options for allocating buffers.
|
||||
/// </summary>
|
/// </summary>
|
||||
|
[Flags] |
||||
public enum AllocationOptions |
public enum AllocationOptions |
||||
{ |
{ |
||||
/// <summary>
|
/// <summary>
|
||||
/// Indicates that the buffer should just be allocated.
|
/// Indicates that the buffer should just be allocated.
|
||||
/// </summary>
|
/// </summary>
|
||||
None, |
None = 0, |
||||
|
|
||||
/// <summary>
|
/// <summary>
|
||||
/// Indicates that the allocated buffer should be cleaned following allocation.
|
/// Indicates that the allocated buffer should be cleaned following allocation.
|
||||
/// </summary>
|
/// </summary>
|
||||
Clean |
Clean = 1, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Affects only group allocations.
|
||||
|
/// Indicates that the requested <see cref="MemoryGroup{T}"/> or <see cref="Buffer2D{T}"/> should be made of contiguous blocks up to <see cref="int.MaxValue"/>.
|
||||
|
/// </summary>
|
||||
|
Contiguous = 2 |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -0,0 +1,10 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Memory |
||||
|
{ |
||||
|
internal static class AllocationOptionsExtensions |
||||
|
{ |
||||
|
public static bool Has(this AllocationOptions options, AllocationOptions flag) => (options & flag) == flag; |
||||
|
} |
||||
|
} |
||||
@ -1,18 +0,0 @@ |
|||||
// Copyright (c) Six Labors.
|
|
||||
// Licensed under the Apache License, Version 2.0.
|
|
||||
|
|
||||
using System.Buffers; |
|
||||
|
|
||||
namespace SixLabors.ImageSharp.Memory |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Represents a byte buffer backed by a managed array. Useful for interop with classic .NET API-s.
|
|
||||
/// </summary>
|
|
||||
public interface IManagedByteBuffer : IMemoryOwner<byte> |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Gets the managed array backing this buffer instance.
|
|
||||
/// </summary>
|
|
||||
byte[] Array { get; } |
|
||||
} |
|
||||
} |
|
||||
@ -1,20 +0,0 @@ |
|||||
// Copyright (c) Six Labors.
|
|
||||
// Licensed under the Apache License, Version 2.0.
|
|
||||
|
|
||||
namespace SixLabors.ImageSharp.Memory.Internals |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Provides an <see cref="IManagedByteBuffer"/> based on <see cref="BasicArrayBuffer{T}"/>.
|
|
||||
/// </summary>
|
|
||||
internal sealed class BasicByteBuffer : BasicArrayBuffer<byte>, IManagedByteBuffer |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Initializes a new instance of the <see cref="BasicByteBuffer"/> class.
|
|
||||
/// </summary>
|
|
||||
/// <param name="array">The byte array.</param>
|
|
||||
internal BasicByteBuffer(byte[] array) |
|
||||
: base(array) |
|
||||
{ |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,115 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
// Port of BCL internal utility:
|
||||
|
// https://github.com/dotnet/runtime/blob/57bfe474518ab5b7cfe6bf7424a79ce3af9d6657/src/libraries/System.Private.CoreLib/src/System/Gen2GcCallback.cs
|
||||
|
#if NETCORE31COMPATIBLE
|
||||
|
using System; |
||||
|
using System.Runtime.ConstrainedExecution; |
||||
|
using System.Runtime.InteropServices; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Memory.Internals |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Schedules a callback roughly every gen 2 GC (you may see a Gen 0 an Gen 1 but only once)
|
||||
|
/// (We can fix this by capturing the Gen 2 count at startup and testing, but I mostly don't care)
|
||||
|
/// </summary>
|
||||
|
internal sealed class Gen2GcCallback : CriticalFinalizerObject |
||||
|
{ |
||||
|
private readonly Func<bool> callback0; |
||||
|
private readonly Func<object, bool> callback1; |
||||
|
private GCHandle weakTargetObj; |
||||
|
|
||||
|
private Gen2GcCallback(Func<bool> callback) |
||||
|
{ |
||||
|
this.callback0 = callback; |
||||
|
} |
||||
|
|
||||
|
private Gen2GcCallback(Func<object, bool> callback, object targetObj) |
||||
|
{ |
||||
|
this.callback1 = callback; |
||||
|
this.weakTargetObj = GCHandle.Alloc(targetObj, GCHandleType.Weak); |
||||
|
} |
||||
|
|
||||
|
~Gen2GcCallback() |
||||
|
{ |
||||
|
if (this.weakTargetObj.IsAllocated) |
||||
|
{ |
||||
|
// Check to see if the target object is still alive.
|
||||
|
object targetObj = this.weakTargetObj.Target; |
||||
|
if (targetObj == null) |
||||
|
{ |
||||
|
// The target object is dead, so this callback object is no longer needed.
|
||||
|
this.weakTargetObj.Free(); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Execute the callback method.
|
||||
|
try |
||||
|
{ |
||||
|
if (!this.callback1(targetObj)) |
||||
|
{ |
||||
|
// If the callback returns false, this callback object is no longer needed.
|
||||
|
this.weakTargetObj.Free(); |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
// Ensure that we still get a chance to resurrect this object, even if the callback throws an exception.
|
||||
|
#if DEBUG
|
||||
|
// Except in DEBUG, as we really shouldn't be hitting any exceptions here.
|
||||
|
throw; |
||||
|
#endif
|
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// Execute the callback method.
|
||||
|
try |
||||
|
{ |
||||
|
if (!this.callback0()) |
||||
|
{ |
||||
|
// If the callback returns false, this callback object is no longer needed.
|
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
// Ensure that we still get a chance to resurrect this object, even if the callback throws an exception.
|
||||
|
#if DEBUG
|
||||
|
// Except in DEBUG, as we really shouldn't be hitting any exceptions here.
|
||||
|
throw; |
||||
|
#endif
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Resurrect ourselves by re-registering for finalization.
|
||||
|
GC.ReRegisterForFinalize(this); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Schedule 'callback' to be called in the next GC. If the callback returns true it is
|
||||
|
/// rescheduled for the next Gen 2 GC. Otherwise the callbacks stop.
|
||||
|
/// </summary>
|
||||
|
public static void Register(Func<bool> callback) |
||||
|
{ |
||||
|
// Create a unreachable object that remembers the callback function and target object.
|
||||
|
_ = new Gen2GcCallback(callback); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Schedule 'callback' to be called in the next GC. If the callback returns true it is
|
||||
|
/// rescheduled for the next Gen 2 GC. Otherwise the callbacks stop.
|
||||
|
///
|
||||
|
/// NOTE: This callback will be kept alive until either the callback function returns false,
|
||||
|
/// or the target object dies.
|
||||
|
/// </summary>
|
||||
|
public static void Register(Func<object, bool> callback, object targetObj) |
||||
|
{ |
||||
|
// Create a unreachable object that remembers the callback function and target object.
|
||||
|
_ = new Gen2GcCallback(callback, targetObj); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
#endif
|
||||
@ -0,0 +1,40 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Buffers; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
using System.Runtime.InteropServices; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Memory.Internals |
||||
|
{ |
||||
|
internal class SharedArrayPoolBuffer<T> : ManagedBufferBase<T> |
||||
|
where T : struct |
||||
|
{ |
||||
|
private readonly int lengthInBytes; |
||||
|
private byte[] array; |
||||
|
|
||||
|
public SharedArrayPoolBuffer(int lengthInElements) |
||||
|
{ |
||||
|
this.lengthInBytes = lengthInElements * Unsafe.SizeOf<T>(); |
||||
|
this.array = ArrayPool<byte>.Shared.Rent(this.lengthInBytes); |
||||
|
} |
||||
|
|
||||
|
~SharedArrayPoolBuffer() => this.Dispose(false); |
||||
|
|
||||
|
protected override void Dispose(bool disposing) |
||||
|
{ |
||||
|
if (this.array == null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
ArrayPool<byte>.Shared.Return(this.array); |
||||
|
this.array = null; |
||||
|
} |
||||
|
|
||||
|
public override Span<T> GetSpan() => MemoryMarshal.Cast<byte, T>(this.array.AsSpan(0, this.lengthInBytes)); |
||||
|
|
||||
|
protected override object GetPinnableObject() => this.array; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,89 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Buffers; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Memory.Internals |
||||
|
{ |
||||
|
internal partial class UniformUnmanagedMemoryPool |
||||
|
{ |
||||
|
public unsafe class Buffer<T> : MemoryManager<T> |
||||
|
where T : struct |
||||
|
{ |
||||
|
private UniformUnmanagedMemoryPool pool; |
||||
|
private readonly int length; |
||||
|
|
||||
|
public Buffer(UniformUnmanagedMemoryPool pool, UnmanagedMemoryHandle bufferHandle, int length) |
||||
|
{ |
||||
|
this.pool = pool; |
||||
|
this.BufferHandle = bufferHandle; |
||||
|
this.length = length; |
||||
|
} |
||||
|
|
||||
|
private void* Pointer => (void*)this.BufferHandle.DangerousGetHandle(); |
||||
|
|
||||
|
protected UnmanagedMemoryHandle BufferHandle { get; private set; } |
||||
|
|
||||
|
public override Span<T> GetSpan() => new Span<T>(this.Pointer, this.length); |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
public override MemoryHandle Pin(int elementIndex = 0) |
||||
|
{ |
||||
|
// Will be released in Unpin
|
||||
|
bool unused = false; |
||||
|
this.BufferHandle.DangerousAddRef(ref unused); |
||||
|
|
||||
|
void* pbData = Unsafe.Add<T>(this.Pointer, elementIndex); |
||||
|
return new MemoryHandle(pbData); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
public override void Unpin() => this.BufferHandle.DangerousRelease(); |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
protected override void Dispose(bool disposing) |
||||
|
{ |
||||
|
if (this.pool == null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
this.pool.Return(this.BufferHandle); |
||||
|
this.pool = null; |
||||
|
this.BufferHandle = null; |
||||
|
} |
||||
|
|
||||
|
internal void MarkDisposed() |
||||
|
{ |
||||
|
this.pool = null; |
||||
|
this.BufferHandle = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public class FinalizableBuffer<T> : Buffer<T> |
||||
|
where T : struct |
||||
|
{ |
||||
|
public FinalizableBuffer(UniformUnmanagedMemoryPool pool, UnmanagedMemoryHandle bufferHandle, int length) |
||||
|
: base(pool, bufferHandle, length) |
||||
|
{ |
||||
|
bufferHandle.AssignedToNewOwner(); |
||||
|
} |
||||
|
|
||||
|
~FinalizableBuffer() => this.Dispose(false); |
||||
|
|
||||
|
protected override void Dispose(bool disposing) |
||||
|
{ |
||||
|
if (!disposing && this.BufferHandle != null) |
||||
|
{ |
||||
|
// We need to prevent handle finalization here.
|
||||
|
// See comments on UnmanagedMemoryHandle.Resurrect()
|
||||
|
this.BufferHandle.Resurrect(); |
||||
|
} |
||||
|
|
||||
|
base.Dispose(disposing); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,323 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Diagnostics; |
||||
|
using System.Threading; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Memory.Internals |
||||
|
{ |
||||
|
internal partial class UniformUnmanagedMemoryPool |
||||
|
{ |
||||
|
private static readonly Stopwatch Stopwatch = Stopwatch.StartNew(); |
||||
|
|
||||
|
private readonly TrimSettings trimSettings; |
||||
|
private UnmanagedMemoryHandle[] buffers; |
||||
|
private int index; |
||||
|
private Timer trimTimer; |
||||
|
private long lastTrimTimestamp; |
||||
|
|
||||
|
public UniformUnmanagedMemoryPool(int bufferLength, int capacity) |
||||
|
: this(bufferLength, capacity, TrimSettings.Default) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public UniformUnmanagedMemoryPool(int bufferLength, int capacity, TrimSettings trimSettings) |
||||
|
{ |
||||
|
this.trimSettings = trimSettings; |
||||
|
this.Capacity = capacity; |
||||
|
this.BufferLength = bufferLength; |
||||
|
this.buffers = new UnmanagedMemoryHandle[capacity]; |
||||
|
|
||||
|
if (trimSettings.Enabled) |
||||
|
{ |
||||
|
// Invoke the timer callback more frequently, than trimSettings.TrimPeriodMilliseconds,
|
||||
|
// and also invoke it on Gen 2 GC.
|
||||
|
// We are checking in the callback if enough time passed since the last trimming. If not, we do nothing.
|
||||
|
this.trimTimer = new Timer( |
||||
|
s => ((UniformUnmanagedMemoryPool)s)?.Trim(), |
||||
|
this, |
||||
|
this.trimSettings.TrimPeriodMilliseconds / 4, |
||||
|
this.trimSettings.TrimPeriodMilliseconds / 4); |
||||
|
|
||||
|
#if NETCORE31COMPATIBLE
|
||||
|
Gen2GcCallback.Register(s => ((UniformUnmanagedMemoryPool)s).Trim(), this); |
||||
|
#endif
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public int BufferLength { get; } |
||||
|
|
||||
|
public int Capacity { get; } |
||||
|
|
||||
|
public UnmanagedMemoryHandle Rent(AllocationOptions allocationOptions = AllocationOptions.None) |
||||
|
{ |
||||
|
UnmanagedMemoryHandle[] buffersLocal = this.buffers; |
||||
|
|
||||
|
// Avoid taking the lock if the pool is released or is over limit:
|
||||
|
if (buffersLocal == null || this.index == buffersLocal.Length) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
UnmanagedMemoryHandle array; |
||||
|
|
||||
|
lock (buffersLocal) |
||||
|
{ |
||||
|
// Check again after taking the lock:
|
||||
|
if (this.buffers == null || this.index == buffersLocal.Length) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
array = buffersLocal[this.index]; |
||||
|
buffersLocal[this.index++] = null; |
||||
|
} |
||||
|
|
||||
|
if (array == null) |
||||
|
{ |
||||
|
array = new UnmanagedMemoryHandle(this.BufferLength); |
||||
|
} |
||||
|
|
||||
|
if (allocationOptions.Has(AllocationOptions.Clean)) |
||||
|
{ |
||||
|
this.GetSpan(array).Clear(); |
||||
|
} |
||||
|
|
||||
|
return array; |
||||
|
} |
||||
|
|
||||
|
public UnmanagedMemoryHandle[] Rent(int bufferCount, AllocationOptions allocationOptions = AllocationOptions.None) |
||||
|
{ |
||||
|
UnmanagedMemoryHandle[] buffersLocal = this.buffers; |
||||
|
|
||||
|
// Avoid taking the lock if the pool is released or is over limit:
|
||||
|
if (buffersLocal == null || this.index + bufferCount >= buffersLocal.Length + 1) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
UnmanagedMemoryHandle[] result; |
||||
|
lock (buffersLocal) |
||||
|
{ |
||||
|
// Check again after taking the lock:
|
||||
|
if (this.buffers == null || this.index + bufferCount >= buffersLocal.Length + 1) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
result = new UnmanagedMemoryHandle[bufferCount]; |
||||
|
for (int i = 0; i < bufferCount; i++) |
||||
|
{ |
||||
|
result[i] = buffersLocal[this.index]; |
||||
|
buffersLocal[this.index++] = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
for (int i = 0; i < result.Length; i++) |
||||
|
{ |
||||
|
if (result[i] == null) |
||||
|
{ |
||||
|
result[i] = new UnmanagedMemoryHandle(this.BufferLength); |
||||
|
} |
||||
|
|
||||
|
if (allocationOptions.Has(AllocationOptions.Clean)) |
||||
|
{ |
||||
|
this.GetSpan(result[i]).Clear(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
public void Return(UnmanagedMemoryHandle buffer) |
||||
|
{ |
||||
|
UnmanagedMemoryHandle[] buffersLocal = this.buffers; |
||||
|
if (buffersLocal == null) |
||||
|
{ |
||||
|
buffer.Dispose(); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
lock (buffersLocal) |
||||
|
{ |
||||
|
// Check again after taking the lock:
|
||||
|
if (this.buffers == null) |
||||
|
{ |
||||
|
buffer.Dispose(); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (this.index == 0) |
||||
|
{ |
||||
|
ThrowReturnedMoreArraysThanRented(); // DEBUG-only exception
|
||||
|
buffer.Dispose(); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
this.buffers[--this.index] = buffer; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Return(Span<UnmanagedMemoryHandle> buffers) |
||||
|
{ |
||||
|
UnmanagedMemoryHandle[] buffersLocal = this.buffers; |
||||
|
if (buffersLocal == null) |
||||
|
{ |
||||
|
DisposeAll(buffers); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
lock (buffersLocal) |
||||
|
{ |
||||
|
// Check again after taking the lock:
|
||||
|
if (this.buffers == null) |
||||
|
{ |
||||
|
DisposeAll(buffers); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (this.index - buffers.Length + 1 <= 0) |
||||
|
{ |
||||
|
ThrowReturnedMoreArraysThanRented(); |
||||
|
DisposeAll(buffers); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
for (int i = buffers.Length - 1; i >= 0; i--) |
||||
|
{ |
||||
|
buffersLocal[--this.index] = buffers[i]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Release() |
||||
|
{ |
||||
|
this.trimTimer?.Dispose(); |
||||
|
this.trimTimer = null; |
||||
|
UnmanagedMemoryHandle[] oldBuffers = Interlocked.Exchange(ref this.buffers, null); |
||||
|
DebugGuard.NotNull(oldBuffers, nameof(oldBuffers)); |
||||
|
DisposeAll(oldBuffers); |
||||
|
} |
||||
|
|
||||
|
private static void DisposeAll(Span<UnmanagedMemoryHandle> buffers) |
||||
|
{ |
||||
|
foreach (UnmanagedMemoryHandle handle in buffers) |
||||
|
{ |
||||
|
handle?.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private unsafe Span<byte> GetSpan(UnmanagedMemoryHandle h) => |
||||
|
new Span<byte>((byte*)h.DangerousGetHandle(), this.BufferLength); |
||||
|
|
||||
|
// This indicates a bug in the library, however Return() might be called from a finalizer,
|
||||
|
// therefore we should never throw here in production.
|
||||
|
[Conditional("DEBUG")] |
||||
|
private static void ThrowReturnedMoreArraysThanRented() => |
||||
|
throw new InvalidMemoryOperationException("Returned more arrays then rented"); |
||||
|
|
||||
|
private bool Trim() |
||||
|
{ |
||||
|
UnmanagedMemoryHandle[] buffersLocal = this.buffers; |
||||
|
if (buffersLocal == null) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
bool isHighPressure = this.IsHighMemoryPressure(); |
||||
|
|
||||
|
if (isHighPressure) |
||||
|
{ |
||||
|
return this.TrimHighPressure(buffersLocal); |
||||
|
} |
||||
|
|
||||
|
long millisecondsSinceLastTrim = Stopwatch.ElapsedMilliseconds - this.lastTrimTimestamp; |
||||
|
if (millisecondsSinceLastTrim > this.trimSettings.TrimPeriodMilliseconds) |
||||
|
{ |
||||
|
return this.TrimLowPressure(buffersLocal); |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
private bool TrimHighPressure(UnmanagedMemoryHandle[] buffersLocal) |
||||
|
{ |
||||
|
lock (buffersLocal) |
||||
|
{ |
||||
|
if (this.buffers == null) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
// Trim all:
|
||||
|
for (int i = this.index; i < buffersLocal.Length && buffersLocal[i] != null; i++) |
||||
|
{ |
||||
|
buffersLocal[i] = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
private bool TrimLowPressure(UnmanagedMemoryHandle[] arraysLocal) |
||||
|
{ |
||||
|
lock (arraysLocal) |
||||
|
{ |
||||
|
if (this.buffers == null) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
// Count the arrays in the pool:
|
||||
|
int retainedCount = 0; |
||||
|
for (int i = this.index; i < arraysLocal.Length && arraysLocal[i] != null; i++) |
||||
|
{ |
||||
|
retainedCount++; |
||||
|
} |
||||
|
|
||||
|
// Trim 'trimRate' of 'retainedCount':
|
||||
|
int trimCount = (int)Math.Ceiling(retainedCount * this.trimSettings.Rate); |
||||
|
int trimStart = this.index + retainedCount - 1; |
||||
|
int trimStop = this.index + retainedCount - trimCount; |
||||
|
for (int i = trimStart; i >= trimStop; i--) |
||||
|
{ |
||||
|
arraysLocal[i].Dispose(); |
||||
|
arraysLocal[i] = null; |
||||
|
} |
||||
|
|
||||
|
this.lastTrimTimestamp = Stopwatch.ElapsedMilliseconds; |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
private bool IsHighMemoryPressure() |
||||
|
{ |
||||
|
#if NETCORE31COMPATIBLE
|
||||
|
GCMemoryInfo memoryInfo = GC.GetGCMemoryInfo(); |
||||
|
return memoryInfo.MemoryLoadBytes >= memoryInfo.HighMemoryLoadThresholdBytes * this.trimSettings.HighPressureThresholdRate; |
||||
|
#else
|
||||
|
// We don't have high pressure detection triggering full trimming on other platforms,
|
||||
|
// to counterpart this, the maximum pool size is small.
|
||||
|
return false; |
||||
|
#endif
|
||||
|
} |
||||
|
|
||||
|
public class TrimSettings |
||||
|
{ |
||||
|
// Trim half of the retained pool buffers every minute.
|
||||
|
public int TrimPeriodMilliseconds { get; set; } = 60_000; |
||||
|
|
||||
|
public float Rate { get; set; } = 0.5f; |
||||
|
|
||||
|
// Be more strict about high pressure threshold than ArrayPool<T>.Shared.
|
||||
|
// A 32 bit process can OOM before reaching HighMemoryLoadThresholdBytes.
|
||||
|
public float HighPressureThresholdRate { get; set; } = 0.5f; |
||||
|
|
||||
|
public bool Enabled => this.Rate > 0; |
||||
|
|
||||
|
public static TrimSettings Default => new TrimSettings(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,65 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Buffers; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
using System.Runtime.InteropServices; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Memory.Internals |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Allocates and provides an <see cref="IMemoryOwner{T}"/> implementation giving
|
||||
|
/// access to unmanaged buffers allocated by <see cref="Marshal.AllocHGlobal(int)"/>.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="T">The element type.</typeparam>
|
||||
|
internal sealed unsafe class UnmanagedBuffer<T> : MemoryManager<T> |
||||
|
where T : struct |
||||
|
{ |
||||
|
private readonly UnmanagedMemoryHandle bufferHandle; |
||||
|
private readonly int lengthInElements; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="UnmanagedBuffer{T}"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="lengthInElements">The number of elements to allocate.</param>
|
||||
|
public UnmanagedBuffer(int lengthInElements) |
||||
|
{ |
||||
|
this.lengthInElements = lengthInElements; |
||||
|
this.bufferHandle = new UnmanagedMemoryHandle(lengthInElements * Unsafe.SizeOf<T>()); |
||||
|
} |
||||
|
|
||||
|
private void* Pointer => (void*)this.bufferHandle.DangerousGetHandle(); |
||||
|
|
||||
|
public override Span<T> GetSpan() |
||||
|
=> new Span<T>(this.Pointer, this.lengthInElements); |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
public override MemoryHandle Pin(int elementIndex = 0) |
||||
|
{ |
||||
|
// Will be released in Unpin
|
||||
|
bool unused = false; |
||||
|
this.bufferHandle.DangerousAddRef(ref unused); |
||||
|
|
||||
|
void* pbData = Unsafe.Add<T>(this.Pointer, elementIndex); |
||||
|
return new MemoryHandle(pbData); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
public override void Unpin() => this.bufferHandle.DangerousRelease(); |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
protected override void Dispose(bool disposing) |
||||
|
{ |
||||
|
if (this.bufferHandle.IsInvalid) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (disposing) |
||||
|
{ |
||||
|
this.bufferHandle.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,80 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Runtime.InteropServices; |
||||
|
using System.Threading; |
||||
|
using Microsoft.Win32.SafeHandles; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Memory.Internals |
||||
|
{ |
||||
|
internal sealed class UnmanagedMemoryHandle : SafeHandle |
||||
|
{ |
||||
|
private readonly int lengthInBytes; |
||||
|
private bool resurrected; |
||||
|
|
||||
|
// Track allocations for testing purposes:
|
||||
|
private static int totalOutstandingHandles; |
||||
|
|
||||
|
public UnmanagedMemoryHandle(int lengthInBytes) |
||||
|
: base(IntPtr.Zero, true) |
||||
|
{ |
||||
|
this.SetHandle(Marshal.AllocHGlobal(lengthInBytes)); |
||||
|
this.lengthInBytes = lengthInBytes; |
||||
|
if (lengthInBytes > 0) |
||||
|
{ |
||||
|
GC.AddMemoryPressure(lengthInBytes); |
||||
|
} |
||||
|
|
||||
|
Interlocked.Increment(ref totalOutstandingHandles); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets a value indicating the total outstanding handle allocations for testing purposes.
|
||||
|
/// </summary>
|
||||
|
internal static int TotalOutstandingHandles => totalOutstandingHandles; |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
public override bool IsInvalid => this.handle == IntPtr.Zero; |
||||
|
|
||||
|
protected override bool ReleaseHandle() |
||||
|
{ |
||||
|
if (this.IsInvalid) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
Marshal.FreeHGlobal(this.handle); |
||||
|
if (this.lengthInBytes > 0) |
||||
|
{ |
||||
|
GC.RemoveMemoryPressure(this.lengthInBytes); |
||||
|
} |
||||
|
|
||||
|
this.handle = IntPtr.Zero; |
||||
|
Interlocked.Decrement(ref totalOutstandingHandles); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// UnmanagedMemoryHandle's finalizer would release the underlying handle returning the memory to the OS.
|
||||
|
/// We want to prevent this when a finalizable owner (buffer or MemoryGroup) is returning the handle to
|
||||
|
/// <see cref="UniformUnmanagedMemoryPool"/> in it's finalizer.
|
||||
|
/// Since UnmanagedMemoryHandle is CriticalFinalizable, it is guaranteed that the owner's finalizer is called first.
|
||||
|
/// </summary>
|
||||
|
internal void Resurrect() |
||||
|
{ |
||||
|
GC.SuppressFinalize(this); |
||||
|
this.resurrected = true; |
||||
|
} |
||||
|
|
||||
|
internal void AssignedToNewOwner() |
||||
|
{ |
||||
|
if (this.resurrected) |
||||
|
{ |
||||
|
// The handle has been resurrected
|
||||
|
GC.ReRegisterForFinalize(this); |
||||
|
this.resurrected = false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,57 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Memory |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Defines options for creating the default <see cref="MemoryAllocator"/>.
|
||||
|
/// </summary>
|
||||
|
public class MemoryAllocatorOptions |
||||
|
{ |
||||
|
private int? maximumPoolSizeMegabytes; |
||||
|
private int? minimumContiguousBlockBytes; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets a value defining the maximum size of the <see cref="MemoryAllocator"/>'s internal memory pool
|
||||
|
/// in Megabytes. <see langword="null"/> means platform default.
|
||||
|
/// </summary>
|
||||
|
public int? MaximumPoolSizeMegabytes |
||||
|
{ |
||||
|
get => this.maximumPoolSizeMegabytes; |
||||
|
set |
||||
|
{ |
||||
|
if (value.HasValue) |
||||
|
{ |
||||
|
Guard.MustBeGreaterThanOrEqualTo(value.Value, 0, nameof(this.MaximumPoolSizeMegabytes)); |
||||
|
} |
||||
|
|
||||
|
this.maximumPoolSizeMegabytes = value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets a value defining the minimum contiguous block size when allocating buffers for
|
||||
|
/// <see cref="MemoryGroup{T}"/>, <see cref="Buffer2D{T}"/> or <see cref="Image{TPixel}"/>.
|
||||
|
/// <see langword="null"/> means platform default.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// Overriding this value is useful for interop scenarios
|
||||
|
/// ensuring <see cref="Image{TPixel}.TryGetSinglePixelSpan"/> succeeds.
|
||||
|
/// </remarks>
|
||||
|
public int? MinimumContiguousBlockBytes |
||||
|
{ |
||||
|
get => this.minimumContiguousBlockBytes; |
||||
|
set |
||||
|
{ |
||||
|
if (value.HasValue) |
||||
|
{ |
||||
|
// It doesn't make sense to set this to small values in practice.
|
||||
|
// Defining an arbitrary minimum of 65536.
|
||||
|
Guard.MustBeGreaterThanOrEqualTo(value.Value, 65536, nameof(this.MaximumPoolSizeMegabytes)); |
||||
|
} |
||||
|
|
||||
|
this.minimumContiguousBlockBytes = value; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,149 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Buffers; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
using System.Runtime.InteropServices; |
||||
|
using System.Threading; |
||||
|
using SixLabors.ImageSharp.Memory.Internals; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Memory |
||||
|
{ |
||||
|
internal sealed class UniformUnmanagedMemoryPoolMemoryAllocator : MemoryAllocator |
||||
|
{ |
||||
|
private const int OneMegabyte = 1 << 20; |
||||
|
private const int DefaultContiguousPoolBlockSizeBytes = 4 * OneMegabyte; |
||||
|
private const int DefaultNonPoolBlockSizeBytes = 32 * OneMegabyte; |
||||
|
private readonly int sharedArrayPoolThresholdInBytes; |
||||
|
private readonly int poolBufferSizeInBytes; |
||||
|
private readonly int poolCapacity; |
||||
|
|
||||
|
private UniformUnmanagedMemoryPool pool; |
||||
|
private readonly UnmanagedMemoryAllocator nonPoolAllocator; |
||||
|
|
||||
|
public UniformUnmanagedMemoryPoolMemoryAllocator(int? maxPoolSizeMegabytes, int? minimumContiguousBlockBytes) |
||||
|
: this( |
||||
|
minimumContiguousBlockBytes.HasValue ? minimumContiguousBlockBytes.Value : DefaultContiguousPoolBlockSizeBytes, |
||||
|
maxPoolSizeMegabytes.HasValue ? (long)maxPoolSizeMegabytes.Value * OneMegabyte : GetDefaultMaxPoolSizeBytes(), |
||||
|
minimumContiguousBlockBytes.HasValue ? Math.Max(minimumContiguousBlockBytes.Value, DefaultNonPoolBlockSizeBytes) : DefaultNonPoolBlockSizeBytes) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public UniformUnmanagedMemoryPoolMemoryAllocator( |
||||
|
int poolBufferSizeInBytes, |
||||
|
long maxPoolSizeInBytes, |
||||
|
int unmanagedBufferSizeInBytes) |
||||
|
: this( |
||||
|
OneMegabyte, |
||||
|
poolBufferSizeInBytes, |
||||
|
maxPoolSizeInBytes, |
||||
|
unmanagedBufferSizeInBytes) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
// Internal constructor allowing to change the shared array pool threshold for testing purposes.
|
||||
|
internal UniformUnmanagedMemoryPoolMemoryAllocator( |
||||
|
int sharedArrayPoolThresholdInBytes, |
||||
|
int poolBufferSizeInBytes, |
||||
|
long maxPoolSizeInBytes, |
||||
|
int unmanagedBufferSizeInBytes) |
||||
|
{ |
||||
|
this.sharedArrayPoolThresholdInBytes = sharedArrayPoolThresholdInBytes; |
||||
|
this.poolBufferSizeInBytes = poolBufferSizeInBytes; |
||||
|
this.poolCapacity = (int)(maxPoolSizeInBytes / poolBufferSizeInBytes); |
||||
|
this.pool = new UniformUnmanagedMemoryPool(this.poolBufferSizeInBytes, this.poolCapacity); |
||||
|
this.nonPoolAllocator = new UnmanagedMemoryAllocator(unmanagedBufferSizeInBytes); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
protected internal override int GetBufferCapacityInBytes() => this.poolBufferSizeInBytes; |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
public override IMemoryOwner<T> Allocate<T>( |
||||
|
int length, |
||||
|
AllocationOptions options = AllocationOptions.None) |
||||
|
{ |
||||
|
Guard.MustBeGreaterThanOrEqualTo(length, 0, nameof(length)); |
||||
|
int lengthInBytes = length * Unsafe.SizeOf<T>(); |
||||
|
|
||||
|
if (lengthInBytes <= this.sharedArrayPoolThresholdInBytes) |
||||
|
{ |
||||
|
var buffer = new SharedArrayPoolBuffer<T>(length); |
||||
|
if (options.Has(AllocationOptions.Clean)) |
||||
|
{ |
||||
|
buffer.GetSpan().Clear(); |
||||
|
} |
||||
|
|
||||
|
return buffer; |
||||
|
} |
||||
|
|
||||
|
if (lengthInBytes <= this.poolBufferSizeInBytes) |
||||
|
{ |
||||
|
UnmanagedMemoryHandle array = this.pool.Rent(options); |
||||
|
if (array != null) |
||||
|
{ |
||||
|
return new UniformUnmanagedMemoryPool.FinalizableBuffer<T>(this.pool, array, length); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return this.nonPoolAllocator.Allocate<T>(length, options); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
internal override MemoryGroup<T> AllocateGroup<T>( |
||||
|
long totalLength, |
||||
|
int bufferAlignment, |
||||
|
AllocationOptions options = AllocationOptions.None) |
||||
|
{ |
||||
|
long totalLengthInBytes = totalLength * Unsafe.SizeOf<T>(); |
||||
|
if (totalLengthInBytes <= this.sharedArrayPoolThresholdInBytes) |
||||
|
{ |
||||
|
var buffer = new SharedArrayPoolBuffer<T>((int)totalLength); |
||||
|
return MemoryGroup<T>.CreateContiguous(buffer, options.Has(AllocationOptions.Clean)); |
||||
|
} |
||||
|
|
||||
|
if (totalLengthInBytes <= this.poolBufferSizeInBytes) |
||||
|
{ |
||||
|
// Optimized path renting single array from the pool
|
||||
|
UnmanagedMemoryHandle array = this.pool.Rent(options); |
||||
|
if (array != null) |
||||
|
{ |
||||
|
var buffer = new UniformUnmanagedMemoryPool.FinalizableBuffer<T>(this.pool, array, (int)totalLength); |
||||
|
return MemoryGroup<T>.CreateContiguous(buffer, options.Has(AllocationOptions.Clean)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Attempt to rent the whole group from the pool, allocate a group of unmanaged buffers if the attempt fails:
|
||||
|
MemoryGroup<T> poolGroup = options.Has(AllocationOptions.Contiguous) ? |
||||
|
null : |
||||
|
MemoryGroup<T>.Allocate(this.pool, totalLength, bufferAlignment, options); |
||||
|
return poolGroup ?? MemoryGroup<T>.Allocate(this.nonPoolAllocator, totalLength, bufferAlignment, options); |
||||
|
} |
||||
|
|
||||
|
public override void ReleaseRetainedResources() |
||||
|
{ |
||||
|
UniformUnmanagedMemoryPool oldPool = Interlocked.Exchange( |
||||
|
ref this.pool, |
||||
|
new UniformUnmanagedMemoryPool(this.poolBufferSizeInBytes, this.poolCapacity)); |
||||
|
oldPool.Release(); |
||||
|
} |
||||
|
|
||||
|
private static long GetDefaultMaxPoolSizeBytes() |
||||
|
{ |
||||
|
#if NETCORE31COMPATIBLE
|
||||
|
// On .NET Core 3.1+, determine the pool as portion of the total available memory.
|
||||
|
// There is a bug in GC.GetGCMemoryInfo() on .NET 5 + 32 bit, making TotalAvailableMemoryBytes unreliable:
|
||||
|
// https://github.com/dotnet/runtime/issues/55126#issuecomment-876779327
|
||||
|
if (Environment.Is64BitProcess || !RuntimeInformation.FrameworkDescription.StartsWith(".NET 5.0")) |
||||
|
{ |
||||
|
GCMemoryInfo info = GC.GetGCMemoryInfo(); |
||||
|
return info.TotalAvailableMemoryBytes / 8; |
||||
|
} |
||||
|
#endif
|
||||
|
|
||||
|
// Stick to a conservative value of 128 Megabytes on other platforms and 32 bit .NET 5.0:
|
||||
|
return 128 * OneMegabyte; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,32 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Buffers; |
||||
|
using SixLabors.ImageSharp.Memory.Internals; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Memory |
||||
|
{ |
||||
|
internal class UnmanagedMemoryAllocator : MemoryAllocator |
||||
|
{ |
||||
|
private readonly int bufferCapacityInBytes; |
||||
|
|
||||
|
public UnmanagedMemoryAllocator(int bufferCapacityInBytes) |
||||
|
{ |
||||
|
this.bufferCapacityInBytes = bufferCapacityInBytes; |
||||
|
} |
||||
|
|
||||
|
protected internal override int GetBufferCapacityInBytes() => this.bufferCapacityInBytes; |
||||
|
|
||||
|
public override IMemoryOwner<T> Allocate<T>(int length, AllocationOptions options = AllocationOptions.None) |
||||
|
{ |
||||
|
var buffer = new UnmanagedBuffer<T>(length); |
||||
|
if (options.Has(AllocationOptions.Clean)) |
||||
|
{ |
||||
|
buffer.GetSpan().Clear(); |
||||
|
} |
||||
|
|
||||
|
return buffer; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,101 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading; |
||||
|
using Microsoft.DotNet.RemoteExecutor; |
||||
|
using SixLabors.ImageSharp.Memory.Internals; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Memory.Allocators |
||||
|
{ |
||||
|
public partial class UniformUnmanagedMemoryPoolTests |
||||
|
{ |
||||
|
[CollectionDefinition(nameof(NonParallelTests), DisableParallelization = true)] |
||||
|
public class NonParallelTests |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
[Collection(nameof(NonParallelTests))] |
||||
|
public class Trim |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void TrimPeriodElapsed_TrimsHalfOfUnusedArrays() |
||||
|
{ |
||||
|
RemoteExecutor.Invoke(RunTest).Dispose(); |
||||
|
static void RunTest() |
||||
|
{ |
||||
|
var trimSettings = new UniformUnmanagedMemoryPool.TrimSettings { TrimPeriodMilliseconds = 5_000 }; |
||||
|
var pool = new UniformUnmanagedMemoryPool(128, 256, trimSettings); |
||||
|
|
||||
|
UnmanagedMemoryHandle[] a = pool.Rent(64); |
||||
|
UnmanagedMemoryHandle[] b = pool.Rent(64); |
||||
|
pool.Return(a); |
||||
|
Assert.Equal(128, UnmanagedMemoryHandle.TotalOutstandingHandles); |
||||
|
Thread.Sleep(15_000); |
||||
|
|
||||
|
// We expect at least 2 Trim actions, first trim 32, then 16 arrays.
|
||||
|
// 128 - 32 - 16 = 80
|
||||
|
Assert.True( |
||||
|
UnmanagedMemoryHandle.TotalOutstandingHandles <= 80, |
||||
|
$"UnmanagedMemoryHandle.TotalOutstandingHandles={UnmanagedMemoryHandle.TotalOutstandingHandles} > 80"); |
||||
|
pool.Return(b); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
#if NETCORE31COMPATIBLE
|
||||
|
public static readonly bool Is32BitProcess = !Environment.Is64BitProcess; |
||||
|
private static readonly List<byte[]> PressureArrays = new List<byte[]>(); |
||||
|
|
||||
|
[ConditionalFact(nameof(Is32BitProcess))] |
||||
|
public static void GC_Collect_OnHighLoad_TrimsEntirePool() |
||||
|
{ |
||||
|
RemoteExecutor.Invoke(RunTest).Dispose(); |
||||
|
static void RunTest() |
||||
|
{ |
||||
|
Assert.False(Environment.Is64BitProcess); |
||||
|
const int OneMb = 1024 * 1024; |
||||
|
|
||||
|
var trimSettings = new UniformUnmanagedMemoryPool.TrimSettings { HighPressureThresholdRate = 0.2f }; |
||||
|
|
||||
|
GCMemoryInfo memInfo = GC.GetGCMemoryInfo(); |
||||
|
int highLoadThreshold = (int)(memInfo.HighMemoryLoadThresholdBytes / OneMb); |
||||
|
highLoadThreshold = (int)(trimSettings.HighPressureThresholdRate * highLoadThreshold); |
||||
|
|
||||
|
var pool = new UniformUnmanagedMemoryPool(OneMb, 16, trimSettings); |
||||
|
pool.Return(pool.Rent(16)); |
||||
|
Assert.Equal(16, UnmanagedMemoryHandle.TotalOutstandingHandles); |
||||
|
|
||||
|
for (int i = 0; i < highLoadThreshold; i++) |
||||
|
{ |
||||
|
byte[] array = new byte[OneMb]; |
||||
|
TouchPage(array); |
||||
|
PressureArrays.Add(array); |
||||
|
} |
||||
|
|
||||
|
GC.Collect(); |
||||
|
GC.WaitForPendingFinalizers(); |
||||
|
GC.Collect(); |
||||
|
GC.WaitForPendingFinalizers(); // The pool should be fully trimmed after this point
|
||||
|
|
||||
|
Assert.Equal(0, UnmanagedMemoryHandle.TotalOutstandingHandles); |
||||
|
|
||||
|
static void TouchPage(byte[] b) |
||||
|
{ |
||||
|
uint size = (uint)b.Length; |
||||
|
const uint pageSize = 4096; |
||||
|
uint numPages = size / pageSize; |
||||
|
|
||||
|
for (uint i = 0; i < numPages; i++) |
||||
|
{ |
||||
|
b[i * pageSize] = (byte)(i % 256); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
#endif
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,292 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.DotNet.RemoteExecutor; |
||||
|
using SixLabors.ImageSharp.Memory.Internals; |
||||
|
using Xunit; |
||||
|
using Xunit.Abstractions; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Memory.Allocators |
||||
|
{ |
||||
|
public partial class UniformUnmanagedMemoryPoolTests |
||||
|
{ |
||||
|
private readonly ITestOutputHelper output; |
||||
|
|
||||
|
public UniformUnmanagedMemoryPoolTests(ITestOutputHelper output) |
||||
|
{ |
||||
|
this.output = output; |
||||
|
} |
||||
|
|
||||
|
private static unsafe Span<byte> GetSpan(UniformUnmanagedMemoryPool pool, UnmanagedMemoryHandle h) => |
||||
|
new Span<byte>((void*)h.DangerousGetHandle(), pool.BufferLength); |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(3, 11)] |
||||
|
[InlineData(7, 4)] |
||||
|
public void Constructor_InitializesProperties(int arrayLength, int capacity) |
||||
|
{ |
||||
|
var pool = new UniformUnmanagedMemoryPool(arrayLength, capacity); |
||||
|
Assert.Equal(arrayLength, pool.BufferLength); |
||||
|
Assert.Equal(capacity, pool.Capacity); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(1, 3)] |
||||
|
[InlineData(8, 10)] |
||||
|
public void Rent_SingleBuffer_ReturnsCorrectBuffer(int length, int capacity) |
||||
|
{ |
||||
|
var pool = new UniformUnmanagedMemoryPool(length, capacity); |
||||
|
for (int i = 0; i < capacity; i++) |
||||
|
{ |
||||
|
UnmanagedMemoryHandle h = pool.Rent(); |
||||
|
CheckBuffer(length, pool, h); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Return_DoesNotDeallocateMemory() |
||||
|
{ |
||||
|
RemoteExecutor.Invoke(RunTest).Dispose(); |
||||
|
|
||||
|
static void RunTest() |
||||
|
{ |
||||
|
var pool = new UniformUnmanagedMemoryPool(16, 16); |
||||
|
UnmanagedMemoryHandle a = pool.Rent(); |
||||
|
UnmanagedMemoryHandle[] b = pool.Rent(2); |
||||
|
|
||||
|
Assert.Equal(3, UnmanagedMemoryHandle.TotalOutstandingHandles); |
||||
|
pool.Return(a); |
||||
|
pool.Return(b); |
||||
|
Assert.Equal(3, UnmanagedMemoryHandle.TotalOutstandingHandles); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static void CheckBuffer(int length, UniformUnmanagedMemoryPool pool, UnmanagedMemoryHandle h) |
||||
|
{ |
||||
|
Assert.NotNull(h); |
||||
|
Assert.False(h.IsClosed); |
||||
|
Span<byte> span = GetSpan(pool, h); |
||||
|
span.Fill(123); |
||||
|
|
||||
|
byte[] expected = new byte[length]; |
||||
|
expected.AsSpan().Fill(123); |
||||
|
Assert.True(span.SequenceEqual(expected)); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(1, 1)] |
||||
|
[InlineData(1, 5)] |
||||
|
[InlineData(42, 7)] |
||||
|
[InlineData(5, 10)] |
||||
|
public void Rent_MultiBuffer_ReturnsCorrectBuffers(int length, int bufferCount) |
||||
|
{ |
||||
|
var pool = new UniformUnmanagedMemoryPool(length, 10); |
||||
|
UnmanagedMemoryHandle[] handles = pool.Rent(bufferCount); |
||||
|
Assert.NotNull(handles); |
||||
|
Assert.Equal(bufferCount, handles.Length); |
||||
|
|
||||
|
foreach (UnmanagedMemoryHandle h in handles) |
||||
|
{ |
||||
|
CheckBuffer(length, pool, h); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Rent_MultipleTimesWithoutReturn_ReturnsDifferentHandles() |
||||
|
{ |
||||
|
var pool = new UniformUnmanagedMemoryPool(128, 10); |
||||
|
UnmanagedMemoryHandle[] a = pool.Rent(2); |
||||
|
UnmanagedMemoryHandle b = pool.Rent(); |
||||
|
|
||||
|
Assert.NotEqual(a[0].DangerousGetHandle(), a[1].DangerousGetHandle()); |
||||
|
Assert.NotEqual(a[0].DangerousGetHandle(), b.DangerousGetHandle()); |
||||
|
Assert.NotEqual(a[1].DangerousGetHandle(), b.DangerousGetHandle()); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(4, 2, 10)] |
||||
|
[InlineData(5, 1, 6)] |
||||
|
[InlineData(12, 4, 12)] |
||||
|
public void RentReturnRent_SameBuffers(int totalCount, int rentUnit, int capacity) |
||||
|
{ |
||||
|
var pool = new UniformUnmanagedMemoryPool(128, capacity); |
||||
|
var allHandles = new HashSet<UnmanagedMemoryHandle>(); |
||||
|
var handleUnits = new List<UnmanagedMemoryHandle[]>(); |
||||
|
|
||||
|
UnmanagedMemoryHandle[] handles; |
||||
|
for (int i = 0; i < totalCount; i += rentUnit) |
||||
|
{ |
||||
|
handles = pool.Rent(rentUnit); |
||||
|
Assert.NotNull(handles); |
||||
|
handleUnits.Add(handles); |
||||
|
foreach (UnmanagedMemoryHandle array in handles) |
||||
|
{ |
||||
|
allHandles.Add(array); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
foreach (UnmanagedMemoryHandle[] arrayUnit in handleUnits) |
||||
|
{ |
||||
|
if (arrayUnit.Length == 1) |
||||
|
{ |
||||
|
// Test single-array return:
|
||||
|
pool.Return(arrayUnit.Single()); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
pool.Return(arrayUnit); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
handles = pool.Rent(totalCount); |
||||
|
|
||||
|
Assert.NotNull(handles); |
||||
|
|
||||
|
foreach (UnmanagedMemoryHandle array in handles) |
||||
|
{ |
||||
|
Assert.Contains(array, allHandles); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Rent_SingleBuffer_OverCapacity_ReturnsNull() |
||||
|
{ |
||||
|
var pool = new UniformUnmanagedMemoryPool(7, 1000); |
||||
|
Assert.NotNull(pool.Rent(1000)); |
||||
|
Assert.Null(pool.Rent()); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(0, 6, 5)] |
||||
|
[InlineData(5, 1, 5)] |
||||
|
[InlineData(4, 7, 10)] |
||||
|
public void Rent_MultiBuffer_OverCapacity_ReturnsNull(int initialRent, int attempt, int capacity) |
||||
|
{ |
||||
|
var pool = new UniformUnmanagedMemoryPool(128, capacity); |
||||
|
Assert.NotNull(pool.Rent(initialRent)); |
||||
|
Assert.Null(pool.Rent(attempt)); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(0, 5, 5)] |
||||
|
[InlineData(5, 1, 6)] |
||||
|
[InlineData(4, 7, 11)] |
||||
|
[InlineData(3, 3, 7)] |
||||
|
public void Rent_MultiBuff_BelowCapacity_Succeeds(int initialRent, int attempt, int capacity) |
||||
|
{ |
||||
|
var pool = new UniformUnmanagedMemoryPool(128, capacity); |
||||
|
Assert.NotNull(pool.Rent(initialRent)); |
||||
|
Assert.NotNull(pool.Rent(attempt)); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(false)] |
||||
|
[InlineData(true)] |
||||
|
public void Release_SubsequentRentReturnsNull(bool multiple) |
||||
|
{ |
||||
|
var pool = new UniformUnmanagedMemoryPool(16, 16); |
||||
|
pool.Rent(); // Dummy rent
|
||||
|
pool.Release(); |
||||
|
if (multiple) |
||||
|
{ |
||||
|
UnmanagedMemoryHandle b = pool.Rent(); |
||||
|
Assert.Null(b); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
UnmanagedMemoryHandle[] b = pool.Rent(2); |
||||
|
Assert.Null(b); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(false)] |
||||
|
[InlineData(true)] |
||||
|
public void Release_SubsequentReturnClosesHandle(bool multiple) |
||||
|
{ |
||||
|
var pool = new UniformUnmanagedMemoryPool(16, 16); |
||||
|
if (multiple) |
||||
|
{ |
||||
|
UnmanagedMemoryHandle[] b = pool.Rent(2); |
||||
|
pool.Release(); |
||||
|
|
||||
|
Assert.False(b[0].IsClosed); |
||||
|
Assert.False(b[1].IsClosed); |
||||
|
|
||||
|
pool.Return(b); |
||||
|
|
||||
|
Assert.True(b[0].IsClosed); |
||||
|
Assert.True(b[1].IsClosed); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
UnmanagedMemoryHandle b = pool.Rent(); |
||||
|
pool.Release(); |
||||
|
Assert.False(b.IsClosed); |
||||
|
pool.Return(b); |
||||
|
Assert.True(b.IsClosed); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Release_ShouldFreeRetainedMemory() |
||||
|
{ |
||||
|
RemoteExecutor.Invoke(RunTest).Dispose(); |
||||
|
|
||||
|
static void RunTest() |
||||
|
{ |
||||
|
var pool = new UniformUnmanagedMemoryPool(16, 16); |
||||
|
UnmanagedMemoryHandle a = pool.Rent(); |
||||
|
UnmanagedMemoryHandle[] b = pool.Rent(2); |
||||
|
pool.Return(a); |
||||
|
pool.Return(b); |
||||
|
|
||||
|
Assert.Equal(3, UnmanagedMemoryHandle.TotalOutstandingHandles); |
||||
|
pool.Release(); |
||||
|
Assert.Equal(0, UnmanagedMemoryHandle.TotalOutstandingHandles); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void RentReturn_IsThreadSafe() |
||||
|
{ |
||||
|
int count = Environment.ProcessorCount * 200; |
||||
|
var pool = new UniformUnmanagedMemoryPool(8, count); |
||||
|
var rnd = new Random(0); |
||||
|
|
||||
|
Parallel.For(0, Environment.ProcessorCount, (int i) => |
||||
|
{ |
||||
|
var allArrays = new List<UnmanagedMemoryHandle>(); |
||||
|
int pauseAt = rnd.Next(100); |
||||
|
for (int j = 0; j < 100; j++) |
||||
|
{ |
||||
|
UnmanagedMemoryHandle[] data = pool.Rent(2); |
||||
|
|
||||
|
GetSpan(pool, data[0]).Fill((byte)i); |
||||
|
GetSpan(pool, data[1]).Fill((byte)i); |
||||
|
allArrays.Add(data[0]); |
||||
|
allArrays.Add(data[1]); |
||||
|
|
||||
|
if (j == pauseAt) |
||||
|
{ |
||||
|
Thread.Sleep(15); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Span<byte> expected = new byte[8]; |
||||
|
expected.Fill((byte)i); |
||||
|
|
||||
|
foreach (UnmanagedMemoryHandle array in allArrays) |
||||
|
{ |
||||
|
Assert.True(expected.SequenceEqual(GetSpan(pool, array))); |
||||
|
pool.Return(new[] { array }); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,289 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Buffers; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
using Microsoft.DotNet.RemoteExecutor; |
||||
|
using SixLabors.ImageSharp.Memory; |
||||
|
using SixLabors.ImageSharp.Memory.Internals; |
||||
|
using SixLabors.ImageSharp.Tests.Memory.DiscontiguousBuffers; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Memory.Allocators |
||||
|
{ |
||||
|
public class UniformUnmanagedPoolMemoryAllocatorTests |
||||
|
{ |
||||
|
public class BufferTests1 : BufferTestSuite |
||||
|
{ |
||||
|
private static MemoryAllocator CreateMemoryAllocator() => |
||||
|
new UniformUnmanagedMemoryPoolMemoryAllocator( |
||||
|
sharedArrayPoolThresholdInBytes: 1024, |
||||
|
poolBufferSizeInBytes: 2048, |
||||
|
maxPoolSizeInBytes: 2048 * 4, |
||||
|
unmanagedBufferSizeInBytes: 4096); |
||||
|
|
||||
|
public BufferTests1() |
||||
|
: base(CreateMemoryAllocator()) |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public class BufferTests2 : BufferTestSuite |
||||
|
{ |
||||
|
private static MemoryAllocator CreateMemoryAllocator() => |
||||
|
new UniformUnmanagedMemoryPoolMemoryAllocator( |
||||
|
sharedArrayPoolThresholdInBytes: 512, |
||||
|
poolBufferSizeInBytes: 1024, |
||||
|
maxPoolSizeInBytes: 1024 * 4, |
||||
|
unmanagedBufferSizeInBytes: 2048); |
||||
|
|
||||
|
public BufferTests2() |
||||
|
: base(CreateMemoryAllocator()) |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static TheoryData<object, int, int, int, int, long, int, int, int, int> AllocateData = |
||||
|
new TheoryData<object, int, int, int, int, long, int, int, int, int>() |
||||
|
{ |
||||
|
{ default(S4), 16, 256, 256, 1024, 64, 64, 1, -1, 64 }, |
||||
|
{ default(S4), 16, 256, 256, 1024, 256, 256, 1, -1, 256 }, |
||||
|
{ default(S4), 16, 256, 256, 1024, 250, 256, 1, -1, 250 }, |
||||
|
{ default(S4), 16, 256, 256, 1024, 248, 250, 1, -1, 248 }, |
||||
|
{ default(S4), 16, 1024, 2048, 4096, 512, 256, 2, 256, 256 }, |
||||
|
{ default(S4), 16, 1024, 1024, 1024, 511, 256, 2, 256, 255 }, |
||||
|
}; |
||||
|
|
||||
|
[Theory] |
||||
|
[MemberData(nameof(AllocateData))] |
||||
|
public void AllocateGroup_BufferSizesAreCorrect<T>( |
||||
|
T dummy, |
||||
|
int sharedArrayPoolThresholdInBytes, |
||||
|
int maxContiguousPoolBufferInBytes, |
||||
|
int maxPoolSizeInBytes, |
||||
|
int maxCapacityOfUnmanagedBuffers, |
||||
|
long allocationLengthInElements, |
||||
|
int bufferAlignmentInElements, |
||||
|
int expectedNumberOfBuffers, |
||||
|
int expectedBufferSize, |
||||
|
int expectedSizeOfLastBuffer) |
||||
|
where T : struct |
||||
|
{ |
||||
|
var allocator = new UniformUnmanagedMemoryPoolMemoryAllocator( |
||||
|
sharedArrayPoolThresholdInBytes, |
||||
|
maxContiguousPoolBufferInBytes, |
||||
|
maxPoolSizeInBytes, |
||||
|
maxCapacityOfUnmanagedBuffers); |
||||
|
|
||||
|
using MemoryGroup<T> g = allocator.AllocateGroup<T>(allocationLengthInElements, bufferAlignmentInElements); |
||||
|
MemoryGroupTests.Allocate.ValidateAllocateMemoryGroup( |
||||
|
expectedNumberOfBuffers, |
||||
|
expectedBufferSize, |
||||
|
expectedSizeOfLastBuffer, |
||||
|
g); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void AllocateGroup_MultipleTimes_ExceedPoolLimit() |
||||
|
{ |
||||
|
var allocator = new UniformUnmanagedMemoryPoolMemoryAllocator( |
||||
|
64, |
||||
|
128, |
||||
|
1024, |
||||
|
1024); |
||||
|
|
||||
|
var groups = new List<MemoryGroup<S4>>(); |
||||
|
for (int i = 0; i < 16; i++) |
||||
|
{ |
||||
|
int lengthInElements = 128 / Unsafe.SizeOf<S4>(); |
||||
|
MemoryGroup<S4> g = allocator.AllocateGroup<S4>(lengthInElements, 32); |
||||
|
MemoryGroupTests.Allocate.ValidateAllocateMemoryGroup(1, -1, lengthInElements, g); |
||||
|
groups.Add(g); |
||||
|
} |
||||
|
|
||||
|
foreach (MemoryGroup<S4> g in groups) |
||||
|
{ |
||||
|
g.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(512)] |
||||
|
[InlineData(2048)] |
||||
|
[InlineData(8192)] |
||||
|
[InlineData(65536)] |
||||
|
public void AllocateGroup_OptionsContiguous_AllocatesContiguousBuffer(int lengthInBytes) |
||||
|
{ |
||||
|
var allocator = new UniformUnmanagedMemoryPoolMemoryAllocator( |
||||
|
128, |
||||
|
1024, |
||||
|
2048, |
||||
|
4096); |
||||
|
int length = lengthInBytes / Unsafe.SizeOf<S4>(); |
||||
|
using MemoryGroup<S4> g = allocator.AllocateGroup<S4>(length, 32, AllocationOptions.Contiguous); |
||||
|
Assert.Equal(length, g.BufferLength); |
||||
|
Assert.Equal(length, g.TotalLength); |
||||
|
Assert.Equal(1, g.Count); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void MemoryAllocator_CreateDefault_WithoutOptions_AllocatesDiscontiguousMemory() |
||||
|
{ |
||||
|
RemoteExecutor.Invoke(RunTest).Dispose(); |
||||
|
|
||||
|
static void RunTest() |
||||
|
{ |
||||
|
var allocator = MemoryAllocator.CreateDefault(); |
||||
|
long sixteenMegabytes = 16 * (1 << 20); |
||||
|
|
||||
|
// Should allocate 4 times 4MB discontiguos blocks:
|
||||
|
MemoryGroup<byte> g = allocator.AllocateGroup<byte>(sixteenMegabytes, 1024); |
||||
|
Assert.Equal(4, g.Count); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(false)] |
||||
|
[InlineData(true)] |
||||
|
public void MemoryAllocator_CreateDefault_WithOptions_CanForceContiguousAllocation(bool poolAllocation) |
||||
|
{ |
||||
|
RemoteExecutor.Invoke(RunTest, poolAllocation.ToString()).Dispose(); |
||||
|
|
||||
|
static void RunTest(string poolAllocationStr) |
||||
|
{ |
||||
|
int fortyEightMegabytes = 48 * (1 << 20); |
||||
|
var allocator = MemoryAllocator.CreateDefault(new MemoryAllocatorOptions() |
||||
|
{ |
||||
|
MaximumPoolSizeMegabytes = bool.Parse(poolAllocationStr) ? 64 : 0, |
||||
|
MinimumContiguousBlockBytes = fortyEightMegabytes |
||||
|
}); |
||||
|
|
||||
|
MemoryGroup<byte> g = allocator.AllocateGroup<byte>(fortyEightMegabytes, 1024); |
||||
|
Assert.Equal(1, g.Count); |
||||
|
Assert.Equal(fortyEightMegabytes, g.TotalLength); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(true)] |
||||
|
[InlineData(false)] |
||||
|
public void BufferDisposal_ReturnsToPool(bool shared) |
||||
|
{ |
||||
|
RemoteExecutor.Invoke(RunTest, shared.ToString()).Dispose(); |
||||
|
|
||||
|
static void RunTest(string sharedStr) |
||||
|
{ |
||||
|
var allocator = new UniformUnmanagedMemoryPoolMemoryAllocator(512, 1024, 16 * 1024, 1024); |
||||
|
IMemoryOwner<byte> buffer0 = allocator.Allocate<byte>(bool.Parse(sharedStr) ? 300 : 600); |
||||
|
buffer0.GetSpan()[0] = 42; |
||||
|
buffer0.Dispose(); |
||||
|
using IMemoryOwner<byte> buffer1 = allocator.Allocate<byte>(bool.Parse(sharedStr) ? 300 : 600); |
||||
|
Assert.Equal(42, buffer1.GetSpan()[0]); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(true)] |
||||
|
[InlineData(false)] |
||||
|
public void MemoryGroupDisposal_ReturnsToPool(bool shared) |
||||
|
{ |
||||
|
RemoteExecutor.Invoke(RunTest, shared.ToString()).Dispose(); |
||||
|
|
||||
|
static void RunTest(string sharedStr) |
||||
|
{ |
||||
|
var allocator = new UniformUnmanagedMemoryPoolMemoryAllocator(512, 1024, 16 * 1024, 1024); |
||||
|
MemoryGroup<byte> g0 = allocator.AllocateGroup<byte>(bool.Parse(sharedStr) ? 300 : 600, 100); |
||||
|
g0.Single().Span[0] = 42; |
||||
|
g0.Dispose(); |
||||
|
using MemoryGroup<byte> g1 = allocator.AllocateGroup<byte>(bool.Parse(sharedStr) ? 300 : 600, 100); |
||||
|
Assert.Equal(42, g1.Single().Span[0]); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void ReleaseRetainedResources_ShouldFreePooledMemory() |
||||
|
{ |
||||
|
RemoteExecutor.Invoke(RunTest).Dispose(); |
||||
|
static void RunTest() |
||||
|
{ |
||||
|
var allocator = new UniformUnmanagedMemoryPoolMemoryAllocator(128, 512, 16 * 512, 1024); |
||||
|
MemoryGroup<byte> g = allocator.AllocateGroup<byte>(2048, 128); |
||||
|
g.Dispose(); |
||||
|
Assert.Equal(4, UnmanagedMemoryHandle.TotalOutstandingHandles); |
||||
|
allocator.ReleaseRetainedResources(); |
||||
|
Assert.Equal(0, UnmanagedMemoryHandle.TotalOutstandingHandles); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void ReleaseRetainedResources_DoesNotFreeOutstandingBuffers() |
||||
|
{ |
||||
|
RemoteExecutor.Invoke(RunTest).Dispose(); |
||||
|
static void RunTest() |
||||
|
{ |
||||
|
var allocator = new UniformUnmanagedMemoryPoolMemoryAllocator(128, 512, 16 * 512, 1024); |
||||
|
IMemoryOwner<byte> b = allocator.Allocate<byte>(256); |
||||
|
MemoryGroup<byte> g = allocator.AllocateGroup<byte>(2048, 128); |
||||
|
Assert.Equal(5, UnmanagedMemoryHandle.TotalOutstandingHandles); |
||||
|
allocator.ReleaseRetainedResources(); |
||||
|
Assert.Equal(5, UnmanagedMemoryHandle.TotalOutstandingHandles); |
||||
|
b.Dispose(); |
||||
|
g.Dispose(); |
||||
|
Assert.Equal(0, UnmanagedMemoryHandle.TotalOutstandingHandles); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(300)] |
||||
|
[InlineData(600)] |
||||
|
[InlineData(1200)] |
||||
|
public void MemoryGroupFinalizer_ReturnsToPool(int length) |
||||
|
{ |
||||
|
// RunTest(length.ToString());
|
||||
|
RemoteExecutor.Invoke(RunTest, length.ToString()).Dispose(); |
||||
|
|
||||
|
static void RunTest(string lengthStr) |
||||
|
{ |
||||
|
var allocator = new UniformUnmanagedMemoryPoolMemoryAllocator(512, 1024, 16 * 1024, 1024); |
||||
|
int lengthInner = int.Parse(lengthStr); |
||||
|
|
||||
|
AllocateGroupAndForget(allocator, lengthInner); |
||||
|
GC.Collect(); |
||||
|
GC.WaitForPendingFinalizers(); |
||||
|
GC.Collect(); |
||||
|
GC.WaitForPendingFinalizers(); |
||||
|
|
||||
|
AllocateGroupAndForget(allocator, lengthInner, true); |
||||
|
GC.Collect(); |
||||
|
GC.WaitForPendingFinalizers(); |
||||
|
GC.Collect(); |
||||
|
GC.WaitForPendingFinalizers(); |
||||
|
|
||||
|
using MemoryGroup<byte> g = allocator.AllocateGroup<byte>(lengthInner, 100); |
||||
|
Assert.Equal(42, g.First().Span[0]); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static void AllocateGroupAndForget(UniformUnmanagedMemoryPoolMemoryAllocator allocator, int length, bool check = false) |
||||
|
{ |
||||
|
MemoryGroup<byte> g = allocator.AllocateGroup<byte>(length, 100); |
||||
|
if (check) |
||||
|
{ |
||||
|
Assert.Equal(42, g.First().Span[0]); |
||||
|
} |
||||
|
|
||||
|
g.First().Span[0] = 42; |
||||
|
|
||||
|
if (length < 512) |
||||
|
{ |
||||
|
// For ArrayPool.Shared, first array will be returned to the TLS storage of the finalizer thread,
|
||||
|
// repeat rental to make sure per-core buckets are also utilized.
|
||||
|
MemoryGroup<byte> g1 = allocator.AllocateGroup<byte>(length, 100); |
||||
|
g1.First().Span[0] = 42; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,184 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using Microsoft.DotNet.RemoteExecutor; |
||||
|
using SixLabors.ImageSharp.Memory.Internals; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Memory.Allocators |
||||
|
{ |
||||
|
public class UnmanagedMemoryHandleTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public unsafe void Constructor_AllocatesReadWriteMemory() |
||||
|
{ |
||||
|
using var h = new UnmanagedMemoryHandle(128); |
||||
|
Assert.False(h.IsClosed); |
||||
|
Assert.False(h.IsInvalid); |
||||
|
byte* ptr = (byte*)h.DangerousGetHandle(); |
||||
|
for (int i = 0; i < 128; i++) |
||||
|
{ |
||||
|
ptr[i] = (byte)i; |
||||
|
} |
||||
|
|
||||
|
for (int i = 0; i < 128; i++) |
||||
|
{ |
||||
|
Assert.Equal((byte)i, ptr[i]); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Dispose_ClosesHandle() |
||||
|
{ |
||||
|
var h = new UnmanagedMemoryHandle(128); |
||||
|
h.Dispose(); |
||||
|
Assert.True(h.IsClosed); |
||||
|
Assert.True(h.IsInvalid); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(1)] |
||||
|
[InlineData(13)] |
||||
|
public void CreateDispose_TracksAllocations(int count) |
||||
|
{ |
||||
|
RemoteExecutor.Invoke(RunTest, count.ToString()).Dispose(); |
||||
|
|
||||
|
static void RunTest(string countStr) |
||||
|
{ |
||||
|
int countInner = int.Parse(countStr); |
||||
|
var l = new List<UnmanagedMemoryHandle>(); |
||||
|
for (int i = 0; i < countInner; i++) |
||||
|
{ |
||||
|
Assert.Equal(i, UnmanagedMemoryHandle.TotalOutstandingHandles); |
||||
|
var h = new UnmanagedMemoryHandle(42); |
||||
|
Assert.Equal(i + 1, UnmanagedMemoryHandle.TotalOutstandingHandles); |
||||
|
l.Add(h); |
||||
|
} |
||||
|
|
||||
|
for (int i = 0; i < countInner; i++) |
||||
|
{ |
||||
|
Assert.Equal(countInner - i, UnmanagedMemoryHandle.TotalOutstandingHandles); |
||||
|
l[i].Dispose(); |
||||
|
Assert.Equal(countInner - i - 1, UnmanagedMemoryHandle.TotalOutstandingHandles); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(2)] |
||||
|
[InlineData(12)] |
||||
|
public void CreateFinalize_TracksAllocations(int count) |
||||
|
{ |
||||
|
RemoteExecutor.Invoke(RunTest, count.ToString()).Dispose(); |
||||
|
|
||||
|
static void RunTest(string countStr) |
||||
|
{ |
||||
|
int countInner = int.Parse(countStr); |
||||
|
List<UnmanagedMemoryHandle> l = FillList(countInner); |
||||
|
|
||||
|
l.RemoveRange(0, l.Count / 2); |
||||
|
|
||||
|
GC.Collect(); |
||||
|
GC.WaitForPendingFinalizers(); |
||||
|
|
||||
|
Assert.Equal(countInner / 2, l.Count); // This is here to prevent eager finalization of the list's elements
|
||||
|
Assert.Equal(countInner / 2, UnmanagedMemoryHandle.TotalOutstandingHandles); |
||||
|
} |
||||
|
|
||||
|
static List<UnmanagedMemoryHandle> FillList(int countInner) |
||||
|
{ |
||||
|
var l = new List<UnmanagedMemoryHandle>(); |
||||
|
for (int i = 0; i < countInner; i++) |
||||
|
{ |
||||
|
var h = new UnmanagedMemoryHandle(42); |
||||
|
l.Add(h); |
||||
|
} |
||||
|
|
||||
|
return l; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Resurrect_PreventsFinalization() |
||||
|
{ |
||||
|
RemoteExecutor.Invoke(RunTest).Dispose(); |
||||
|
|
||||
|
static void RunTest() |
||||
|
{ |
||||
|
AllocateResurrect(); |
||||
|
Assert.Equal(1, UnmanagedMemoryHandle.TotalOutstandingHandles); |
||||
|
GC.Collect(); |
||||
|
GC.WaitForPendingFinalizers(); |
||||
|
Assert.Equal(1, UnmanagedMemoryHandle.TotalOutstandingHandles); |
||||
|
GC.Collect(); |
||||
|
GC.WaitForPendingFinalizers(); |
||||
|
Assert.Equal(1, UnmanagedMemoryHandle.TotalOutstandingHandles); |
||||
|
} |
||||
|
|
||||
|
static void AllocateResurrect() |
||||
|
{ |
||||
|
var h = new UnmanagedMemoryHandle(42); |
||||
|
h.Resurrect(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static UnmanagedMemoryHandle resurrectedHandle; |
||||
|
|
||||
|
private class HandleOwner |
||||
|
{ |
||||
|
private UnmanagedMemoryHandle handle; |
||||
|
|
||||
|
public HandleOwner(UnmanagedMemoryHandle handle) => this.handle = handle; |
||||
|
|
||||
|
~HandleOwner() |
||||
|
{ |
||||
|
resurrectedHandle = this.handle; |
||||
|
this.handle.Resurrect(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void AssignedToNewOwner_ReRegistersForFinalization() |
||||
|
{ |
||||
|
RemoteExecutor.Invoke(RunTest).Dispose(); |
||||
|
|
||||
|
static void RunTest() |
||||
|
{ |
||||
|
AllocateAndForget(); |
||||
|
Assert.Equal(1, UnmanagedMemoryHandle.TotalOutstandingHandles); |
||||
|
GC.Collect(); |
||||
|
GC.WaitForPendingFinalizers(); |
||||
|
VerifyResurrectedHandle(true); |
||||
|
GC.Collect(); |
||||
|
GC.WaitForPendingFinalizers(); |
||||
|
VerifyResurrectedHandle(false); |
||||
|
GC.Collect(); |
||||
|
GC.WaitForPendingFinalizers(); |
||||
|
|
||||
|
Assert.Equal(0, UnmanagedMemoryHandle.TotalOutstandingHandles); |
||||
|
} |
||||
|
|
||||
|
static void AllocateAndForget() |
||||
|
{ |
||||
|
_ = new HandleOwner(new UnmanagedMemoryHandle(42)); |
||||
|
} |
||||
|
|
||||
|
static void VerifyResurrectedHandle(bool reAssign) |
||||
|
{ |
||||
|
Assert.NotNull(resurrectedHandle); |
||||
|
Assert.Equal(1, UnmanagedMemoryHandle.TotalOutstandingHandles); |
||||
|
Assert.False(resurrectedHandle.IsClosed); |
||||
|
Assert.False(resurrectedHandle.IsInvalid); |
||||
|
resurrectedHandle.AssignedToNewOwner(); |
||||
|
if (reAssign) |
||||
|
{ |
||||
|
_ = new HandleOwner(resurrectedHandle); |
||||
|
} |
||||
|
|
||||
|
resurrectedHandle = null; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue