mirror of https://github.com/SixLabors/ImageSharp
27 changed files with 988 additions and 564 deletions
@ -0,0 +1,21 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Memory.Internals |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Defines an common interface for ref-counted objects.
|
||||
|
/// </summary>
|
||||
|
internal interface IRefCounted |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Increments the reference counter.
|
||||
|
/// </summary>
|
||||
|
void AddRef(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Decrements the reference counter.
|
||||
|
/// </summary>
|
||||
|
void ReleaseRef(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,56 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Runtime.InteropServices; |
||||
|
using System.Threading; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Memory.Internals |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Implements reference counting lifetime guard mechanism similar to the one provided by <see cref="SafeHandle"/>,
|
||||
|
/// but without the restriction of the guarded object being a handle.
|
||||
|
/// </summary>
|
||||
|
internal abstract class RefCountedLifetimeGuard : IDisposable |
||||
|
{ |
||||
|
private int refCount = 1; |
||||
|
private int disposed; |
||||
|
private int released; |
||||
|
|
||||
|
~RefCountedLifetimeGuard() |
||||
|
{ |
||||
|
Interlocked.Exchange(ref this.disposed, 1); |
||||
|
this.ReleaseRef(); |
||||
|
} |
||||
|
|
||||
|
public bool IsDisposed => this.disposed == 1; |
||||
|
|
||||
|
public void AddRef() => Interlocked.Increment(ref this.refCount); |
||||
|
|
||||
|
public void ReleaseRef() |
||||
|
{ |
||||
|
Interlocked.Decrement(ref this.refCount); |
||||
|
if (this.refCount == 0) |
||||
|
{ |
||||
|
int wasReleased = Interlocked.Exchange(ref this.released, 1); |
||||
|
|
||||
|
if (wasReleased == 0) |
||||
|
{ |
||||
|
this.Release(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
int wasDisposed = Interlocked.Exchange(ref this.disposed, 1); |
||||
|
if (wasDisposed == 0) |
||||
|
{ |
||||
|
this.ReleaseRef(); |
||||
|
GC.SuppressFinalize(this); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected abstract void Release(); |
||||
|
} |
||||
|
} |
||||
@ -1,72 +0,0 @@ |
|||||
// 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 class Buffer<T> : UnmanagedBuffer<T> |
|
||||
where T : struct |
|
||||
{ |
|
||||
private UniformUnmanagedMemoryPool pool; |
|
||||
|
|
||||
public Buffer(UniformUnmanagedMemoryPool pool, UnmanagedMemoryHandle bufferHandle, int length) |
|
||||
: base(bufferHandle, length) => |
|
||||
this.pool = pool; |
|
||||
|
|
||||
/// <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 sealed class FinalizableBuffer<T> : Buffer<T> |
|
||||
where T : struct |
|
||||
{ |
|
||||
public FinalizableBuffer(UniformUnmanagedMemoryPool pool, UnmanagedMemoryHandle bufferHandle, int length) |
|
||||
: base(pool, bufferHandle, length) |
|
||||
{ |
|
||||
bufferHandle.AssignedToNewOwner(); |
|
||||
} |
|
||||
|
|
||||
// A VERY poorly written user code holding a Span<TPixel> on the stack,
|
|
||||
// while loosing the reference to Image<TPixel> (or disposing it) may write to (now unrelated) pool buffer,
|
|
||||
// or cause memory corruption if the underlying UmnanagedMemoryHandle has been released.
|
|
||||
// This is an unlikely scenario we mitigate a warning in DangerousGetRowSpan(i) APIs.
|
|
||||
#pragma warning disable CA2015 // Adding a finalizer to a type derived from MemoryManager<T> may permit memory to be freed while it is still in use by a Span<T>
|
|
||||
~FinalizableBuffer() => this.Dispose(false); |
|
||||
#pragma warning restore
|
|
||||
|
|
||||
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); |
|
||||
GC.SuppressFinalize(this); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,50 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Memory.Internals |
||||
|
{ |
||||
|
internal partial class UniformUnmanagedMemoryPool |
||||
|
{ |
||||
|
public UnmanagedBuffer<T> CreateGuardedBuffer<T>( |
||||
|
UnmanagedMemoryHandle handle, |
||||
|
int lengthInElements, |
||||
|
AllocationOptions options) |
||||
|
where T : struct |
||||
|
{ |
||||
|
var buffer = new UnmanagedBuffer<T>(lengthInElements, new ReturnToPoolBufferLifetimeGuard(this, handle)); |
||||
|
if (options.Has(AllocationOptions.Clean)) |
||||
|
{ |
||||
|
buffer.Clear(); |
||||
|
} |
||||
|
|
||||
|
return buffer; |
||||
|
} |
||||
|
|
||||
|
public RefCountedLifetimeGuard CreateGroupLifetimeGuard(UnmanagedMemoryHandle[] handles) => new GroupLifetimeGuard(this, handles); |
||||
|
|
||||
|
private sealed class GroupLifetimeGuard : RefCountedLifetimeGuard |
||||
|
{ |
||||
|
private readonly UniformUnmanagedMemoryPool pool; |
||||
|
private readonly UnmanagedMemoryHandle[] handles; |
||||
|
|
||||
|
public GroupLifetimeGuard(UniformUnmanagedMemoryPool pool, UnmanagedMemoryHandle[] handles) |
||||
|
{ |
||||
|
this.pool = pool; |
||||
|
this.handles = handles; |
||||
|
} |
||||
|
|
||||
|
protected override void Release() => this.pool.Return(this.handles); |
||||
|
} |
||||
|
|
||||
|
private sealed class ReturnToPoolBufferLifetimeGuard : UnmanagedBufferLifetimeGuard |
||||
|
{ |
||||
|
private readonly UniformUnmanagedMemoryPool pool; |
||||
|
|
||||
|
public ReturnToPoolBufferLifetimeGuard(UniformUnmanagedMemoryPool pool, UnmanagedMemoryHandle handle) |
||||
|
: base(handle) => |
||||
|
this.pool = pool; |
||||
|
|
||||
|
protected override void Release() => this.pool.Return(this.Handle); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Memory.Internals |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Defines a strategy for managing unmanaged memory ownership.
|
||||
|
/// </summary>
|
||||
|
internal abstract class UnmanagedBufferLifetimeGuard : RefCountedLifetimeGuard |
||||
|
{ |
||||
|
private UnmanagedMemoryHandle handle; |
||||
|
|
||||
|
protected UnmanagedBufferLifetimeGuard(UnmanagedMemoryHandle handle) => this.handle = handle; |
||||
|
|
||||
|
public UnmanagedMemoryHandle Handle => this.handle; |
||||
|
|
||||
|
public sealed class FreeHandle : UnmanagedBufferLifetimeGuard |
||||
|
{ |
||||
|
public FreeHandle(UnmanagedMemoryHandle handle) |
||||
|
: base(handle) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
protected override void Release() => this.handle.Free(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,116 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
using Microsoft.DotNet.RemoteExecutor; |
||||
|
using SixLabors.ImageSharp.Memory.Internals; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Memory.Allocators |
||||
|
{ |
||||
|
public class RefCountingLifetimeGuardTests |
||||
|
{ |
||||
|
[Theory] |
||||
|
[InlineData(1)] |
||||
|
[InlineData(3)] |
||||
|
public void Dispose_ResultsInSingleRelease(int disposeCount) |
||||
|
{ |
||||
|
var guard = new MockLifetimeGuard(); |
||||
|
Assert.Equal(0, guard.ReleaseInvocationCount); |
||||
|
|
||||
|
for (int i = 0; i < disposeCount; i++) |
||||
|
{ |
||||
|
guard.Dispose(); |
||||
|
} |
||||
|
|
||||
|
Assert.Equal(1, guard.ReleaseInvocationCount); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Finalize_ResultsInSingleRelease() |
||||
|
{ |
||||
|
RemoteExecutor.Invoke(RunTest).Dispose(); |
||||
|
|
||||
|
static void RunTest() |
||||
|
{ |
||||
|
Assert.Equal(0, MockLifetimeGuard.GlobalReleaseInvocationCount); |
||||
|
LeakGuard(false); |
||||
|
GC.Collect(); |
||||
|
GC.WaitForPendingFinalizers(); |
||||
|
Assert.Equal(1, MockLifetimeGuard.GlobalReleaseInvocationCount); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(1)] |
||||
|
[InlineData(3)] |
||||
|
public void AddRef_PreventsReleaseOnDispose(int addRefCount) |
||||
|
{ |
||||
|
var guard = new MockLifetimeGuard(); |
||||
|
|
||||
|
for (int i = 0; i < addRefCount; i++) |
||||
|
{ |
||||
|
guard.AddRef(); |
||||
|
} |
||||
|
|
||||
|
guard.Dispose(); |
||||
|
|
||||
|
for (int i = 0; i < addRefCount; i++) |
||||
|
{ |
||||
|
Assert.Equal(0, guard.ReleaseInvocationCount); |
||||
|
guard.ReleaseRef(); |
||||
|
} |
||||
|
|
||||
|
Assert.Equal(1, guard.ReleaseInvocationCount); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void AddRef_PreventsReleaseOnFinalize() |
||||
|
{ |
||||
|
RemoteExecutor.Invoke(RunTest).Dispose(); |
||||
|
|
||||
|
static void RunTest() |
||||
|
{ |
||||
|
LeakGuard(true); |
||||
|
GC.Collect(); |
||||
|
GC.WaitForPendingFinalizers(); |
||||
|
Assert.Equal(0, MockLifetimeGuard.GlobalReleaseInvocationCount); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void AddRefReleaseRefMisuse_DoesntLeadToMultipleReleases() |
||||
|
{ |
||||
|
var guard = new MockLifetimeGuard(); |
||||
|
guard.Dispose(); |
||||
|
guard.AddRef(); |
||||
|
guard.ReleaseRef(); |
||||
|
|
||||
|
Assert.Equal(1, guard.ReleaseInvocationCount); |
||||
|
} |
||||
|
|
||||
|
[MethodImpl(MethodImplOptions.NoInlining)] |
||||
|
private static void LeakGuard(bool addRef) |
||||
|
{ |
||||
|
var guard = new MockLifetimeGuard(); |
||||
|
if (addRef) |
||||
|
{ |
||||
|
guard.AddRef(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private class MockLifetimeGuard : RefCountedLifetimeGuard |
||||
|
{ |
||||
|
public int ReleaseInvocationCount { get; private set; } |
||||
|
|
||||
|
public static int GlobalReleaseInvocationCount { get; private set; } |
||||
|
|
||||
|
protected override void Release() |
||||
|
{ |
||||
|
this.ReleaseInvocationCount++; |
||||
|
GlobalReleaseInvocationCount++; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,60 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Buffers; |
||||
|
using System.Linq; |
||||
|
using Microsoft.DotNet.RemoteExecutor; |
||||
|
using SixLabors.ImageSharp.Memory.Internals; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Memory.Allocators |
||||
|
{ |
||||
|
public class SharedArrayPoolBufferTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void AllocatesArrayPoolArray() |
||||
|
{ |
||||
|
RemoteExecutor.Invoke(RunTest).Dispose(); |
||||
|
|
||||
|
static void RunTest() |
||||
|
{ |
||||
|
using (var buffer = new SharedArrayPoolBuffer<byte>(900)) |
||||
|
{ |
||||
|
Assert.Equal(900, buffer.GetSpan().Length); |
||||
|
buffer.GetSpan().Fill(42); |
||||
|
} |
||||
|
|
||||
|
byte[] array = ArrayPool<byte>.Shared.Rent(900); |
||||
|
byte[] expected = Enumerable.Repeat((byte)42, 900).ToArray(); |
||||
|
|
||||
|
Assert.True(expected.AsSpan().SequenceEqual(array.AsSpan(0, 900))); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void OutstandingReferences_RetainArrays() |
||||
|
{ |
||||
|
RemoteExecutor.Invoke(RunTest).Dispose(); |
||||
|
|
||||
|
static void RunTest() |
||||
|
{ |
||||
|
var buffer = new SharedArrayPoolBuffer<byte>(900); |
||||
|
Span<byte> span = buffer.GetSpan(); |
||||
|
|
||||
|
buffer.AddRef(); |
||||
|
((IDisposable)buffer).Dispose(); |
||||
|
span.Fill(42); |
||||
|
byte[] array = ArrayPool<byte>.Shared.Rent(900); |
||||
|
Assert.NotEqual(42, array[0]); |
||||
|
ArrayPool<byte>.Shared.Return(array); |
||||
|
|
||||
|
buffer.ReleaseRef(); |
||||
|
array = ArrayPool<byte>.Shared.Rent(900); |
||||
|
byte[] expected = Enumerable.Repeat((byte)42, 900).ToArray(); |
||||
|
Assert.True(expected.AsSpan().SequenceEqual(array.AsSpan(0, 900))); |
||||
|
ArrayPool<byte>.Shared.Return(array); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,93 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Buffers; |
||||
|
using System.Collections.Generic; |
||||
|
using Microsoft.DotNet.RemoteExecutor; |
||||
|
using SixLabors.ImageSharp.Memory; |
||||
|
using SixLabors.ImageSharp.Memory.Internals; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Memory.Allocators |
||||
|
{ |
||||
|
public class UnmanagedBufferTests |
||||
|
{ |
||||
|
public class AllocatorBufferTests : BufferTestSuite |
||||
|
{ |
||||
|
public AllocatorBufferTests() |
||||
|
: base(new UnmanagedMemoryAllocator(1024 * 64)) |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Allocate_CreatesValidBuffer() |
||||
|
{ |
||||
|
using var buffer = UnmanagedBuffer<int>.Allocate(10); |
||||
|
Span<int> span = buffer.GetSpan(); |
||||
|
Assert.Equal(10, span.Length); |
||||
|
span[9] = 123; |
||||
|
Assert.Equal(123, span[9]); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public unsafe void Dispose_DoesNotReleaseOutstandingReferences() |
||||
|
{ |
||||
|
RemoteExecutor.Invoke(RunTest).Dispose(); |
||||
|
|
||||
|
static void RunTest() |
||||
|
{ |
||||
|
var buffer = UnmanagedBuffer<int>.Allocate(10); |
||||
|
Assert.Equal(1, UnmanagedMemoryHandle.TotalOutstandingHandles); |
||||
|
Span<int> span = buffer.GetSpan(); |
||||
|
|
||||
|
// Pin should AddRef
|
||||
|
using (MemoryHandle h = buffer.Pin()) |
||||
|
{ |
||||
|
int* ptr = (int*)h.Pointer; |
||||
|
((IDisposable)buffer).Dispose(); |
||||
|
Assert.Equal(1, UnmanagedMemoryHandle.TotalOutstandingHandles); |
||||
|
ptr[3] = 13; |
||||
|
Assert.Equal(13, span[3]); |
||||
|
} // Unpin should ReleaseRef
|
||||
|
|
||||
|
Assert.Equal(0, UnmanagedMemoryHandle.TotalOutstandingHandles); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(2)] |
||||
|
[InlineData(12)] |
||||
|
public void BufferFinalization_TracksAllocations(int count) |
||||
|
{ |
||||
|
RemoteExecutor.Invoke(RunTest, count.ToString()).Dispose(); |
||||
|
|
||||
|
static void RunTest(string countStr) |
||||
|
{ |
||||
|
int countInner = int.Parse(countStr); |
||||
|
List<UnmanagedBuffer<byte>> 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<UnmanagedBuffer<byte>> FillList(int countInner) |
||||
|
{ |
||||
|
var l = new List<UnmanagedBuffer<byte>>(); |
||||
|
for (int i = 0; i < countInner; i++) |
||||
|
{ |
||||
|
var h = UnmanagedBuffer<byte>.Allocate(42); |
||||
|
l.Add(h); |
||||
|
} |
||||
|
|
||||
|
return l; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue