mirror of https://github.com/SixLabors/ImageSharp
61 changed files with 2484 additions and 387 deletions
@ -1,21 +1,30 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
|
|||
namespace SixLabors.ImageSharp.Memory |
|||
{ |
|||
/// <summary>
|
|||
/// Options for allocating buffers.
|
|||
/// </summary>
|
|||
[Flags] |
|||
public enum AllocationOptions |
|||
{ |
|||
/// <summary>
|
|||
/// Indicates that the buffer should just be allocated.
|
|||
/// </summary>
|
|||
None, |
|||
None = 0, |
|||
|
|||
/// <summary>
|
|||
/// Indicates that the allocated buffer should be cleaned following allocation.
|
|||
/// </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