Browse Source

Merge branch 'master' into js/expose-orientation

pull/1976/head
James Jackson-South 4 years ago
committed by GitHub
parent
commit
a7f4e23e4f
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 83
      src/ImageSharp/Diagnostics/MemoryDiagnostics.cs
  2. 56
      src/ImageSharp/Memory/Allocators/Internals/RefCountedLifetimeGuard.cs
  3. 79
      src/ImageSharp/Memory/Allocators/Internals/RefCountedMemoryLifetimeGuard.cs
  4. 2
      src/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs
  5. 4
      src/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.LifetimeGuards.cs
  6. 2
      src/ImageSharp/Memory/Allocators/Internals/UnmanagedBufferLifetimeGuard.cs
  7. 2
      src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs
  8. 122
      tests/ImageSharp.Tests/Memory/Allocators/MemoryDiagnosticsTests.cs
  9. 2
      tests/ImageSharp.Tests/Memory/Allocators/RefCountedLifetimeGuardTests.cs

83
src/ImageSharp/Diagnostics/MemoryDiagnostics.cs

@ -0,0 +1,83 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System.Threading;
namespace SixLabors.ImageSharp.Diagnostics
{
/// <summary>
/// Represents the method to handle <see cref="MemoryDiagnostics.UndisposedAllocation"/>.
/// </summary>
public delegate void UndisposedAllocationDelegate(string allocationStackTrace);
/// <summary>
/// Utilities to track memory usage and detect memory leaks from not disposing ImageSharp objects.
/// </summary>
public static class MemoryDiagnostics
{
private static int totalUndisposedAllocationCount;
private static UndisposedAllocationDelegate undisposedAllocation;
private static int undisposedAllocationSubscriptionCounter;
private static readonly object SyncRoot = new();
/// <summary>
/// Fires when an ImageSharp object's undisposed memory resource leaks to the finalizer.
/// The event brings significant overhead, and is intended to be used for troubleshooting only.
/// For production diagnostics, use <see cref="TotalUndisposedAllocationCount"/>.
/// </summary>
public static event UndisposedAllocationDelegate UndisposedAllocation
{
add
{
lock (SyncRoot)
{
undisposedAllocationSubscriptionCounter++;
undisposedAllocation += value;
}
}
remove
{
lock (SyncRoot)
{
undisposedAllocation -= value;
undisposedAllocationSubscriptionCounter--;
}
}
}
/// <summary>
/// Gets a value indicating the total number of memory resource objects leaked to the finalizer.
/// </summary>
public static int TotalUndisposedAllocationCount => totalUndisposedAllocationCount;
internal static bool UndisposedAllocationSubscribed => Volatile.Read(ref undisposedAllocationSubscriptionCounter) > 0;
internal static void IncrementTotalUndisposedAllocationCount() =>
Interlocked.Increment(ref totalUndisposedAllocationCount);
internal static void DecrementTotalUndisposedAllocationCount() =>
Interlocked.Decrement(ref totalUndisposedAllocationCount);
internal static void RaiseUndisposedMemoryResource(string allocationStackTrace)
{
if (undisposedAllocation is null)
{
return;
}
// Schedule on the ThreadPool, to avoid user callback messing up the finalizer thread.
#if NETSTANDARD2_1 || NETCOREAPP2_1_OR_GREATER
ThreadPool.QueueUserWorkItem(
stackTrace => undisposedAllocation?.Invoke(stackTrace),
allocationStackTrace,
preferLocal: false);
#else
ThreadPool.QueueUserWorkItem(
stackTrace => undisposedAllocation?.Invoke((string)stackTrace),
allocationStackTrace);
#endif
}
}
}

56
src/ImageSharp/Memory/Allocators/Internals/RefCountedLifetimeGuard.cs

@ -1,56 +0,0 @@
// 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();
}
}

79
src/ImageSharp/Memory/Allocators/Internals/RefCountedMemoryLifetimeGuard.cs

@ -0,0 +1,79 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Runtime.InteropServices;
using System.Threading;
using SixLabors.ImageSharp.Diagnostics;
namespace SixLabors.ImageSharp.Memory.Internals
{
/// <summary>
/// Implements reference counting lifetime guard mechanism for memory resources
/// and maintains the value of <see cref="MemoryDiagnostics.TotalUndisposedAllocationCount"/>.
/// </summary>
internal abstract class RefCountedMemoryLifetimeGuard : IDisposable
{
private int refCount = 1;
private int disposed;
private int released;
private string allocationStackTrace;
protected RefCountedMemoryLifetimeGuard()
{
if (MemoryDiagnostics.UndisposedAllocationSubscribed)
{
this.allocationStackTrace = Environment.StackTrace;
}
MemoryDiagnostics.IncrementTotalUndisposedAllocationCount();
}
~RefCountedMemoryLifetimeGuard()
{
Interlocked.Exchange(ref this.disposed, 1);
this.ReleaseRef(true);
}
public bool IsDisposed => this.disposed == 1;
public void AddRef() => Interlocked.Increment(ref this.refCount);
public void ReleaseRef() => this.ReleaseRef(false);
public void Dispose()
{
int wasDisposed = Interlocked.Exchange(ref this.disposed, 1);
if (wasDisposed == 0)
{
this.ReleaseRef();
GC.SuppressFinalize(this);
}
}
protected abstract void Release();
private void ReleaseRef(bool finalizing)
{
Interlocked.Decrement(ref this.refCount);
if (this.refCount == 0)
{
int wasReleased = Interlocked.Exchange(ref this.released, 1);
if (wasReleased == 0)
{
if (!finalizing)
{
MemoryDiagnostics.DecrementTotalUndisposedAllocationCount();
}
else if (this.allocationStackTrace != null)
{
MemoryDiagnostics.RaiseUndisposedMemoryResource(this.allocationStackTrace);
}
this.Release();
}
}
}
}
}

2
src/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs

@ -60,7 +60,7 @@ namespace SixLabors.ImageSharp.Memory.Internals
}
}
private sealed class LifetimeGuard : RefCountedLifetimeGuard
private sealed class LifetimeGuard : RefCountedMemoryLifetimeGuard
{
private byte[] array;

4
src/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.LifetimeGuards.cs

@ -20,9 +20,9 @@ namespace SixLabors.ImageSharp.Memory.Internals
return buffer;
}
public RefCountedLifetimeGuard CreateGroupLifetimeGuard(UnmanagedMemoryHandle[] handles) => new GroupLifetimeGuard(this, handles);
public RefCountedMemoryLifetimeGuard CreateGroupLifetimeGuard(UnmanagedMemoryHandle[] handles) => new GroupLifetimeGuard(this, handles);
private sealed class GroupLifetimeGuard : RefCountedLifetimeGuard
private sealed class GroupLifetimeGuard : RefCountedMemoryLifetimeGuard
{
private readonly UniformUnmanagedMemoryPool pool;
private readonly UnmanagedMemoryHandle[] handles;

2
src/ImageSharp/Memory/Allocators/Internals/UnmanagedBufferLifetimeGuard.cs

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Memory.Internals
/// <summary>
/// Defines a strategy for managing unmanaged memory ownership.
/// </summary>
internal abstract class UnmanagedBufferLifetimeGuard : RefCountedLifetimeGuard
internal abstract class UnmanagedBufferLifetimeGuard : RefCountedMemoryLifetimeGuard
{
private UnmanagedMemoryHandle handle;

2
src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs

@ -18,7 +18,7 @@ namespace SixLabors.ImageSharp.Memory
public sealed class Owned : MemoryGroup<T>, IEnumerable<Memory<T>>
{
private IMemoryOwner<T>[] memoryOwners;
private RefCountedLifetimeGuard groupLifetimeGuard;
private RefCountedMemoryLifetimeGuard groupLifetimeGuard;
public Owned(IMemoryOwner<T>[] memoryOwners, int bufferLength, long totalLength, bool swappable)
: base(bufferLength, totalLength)

122
tests/ImageSharp.Tests/Memory/Allocators/MemoryDiagnosticsTests.cs

@ -0,0 +1,122 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using Microsoft.DotNet.RemoteExecutor;
using SixLabors.ImageSharp.Diagnostics;
using SixLabors.ImageSharp.Memory;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Memory.Allocators
{
public class MemoryDiagnosticsTests
{
private const int OneMb = 1 << 20;
private static MemoryAllocator Allocator => Configuration.Default.MemoryAllocator;
[Theory]
[InlineData(false)]
[InlineData(true)]
public void PerfectCleanup_NoLeaksReported(bool isGroupOuter)
{
RemoteExecutor.Invoke(RunTest, isGroupOuter.ToString()).Dispose();
static void RunTest(string isGroupInner)
{
bool isGroup = bool.Parse(isGroupInner);
int leakCounter = 0;
MemoryDiagnostics.UndisposedAllocation += _ => Interlocked.Increment(ref leakCounter);
List<IDisposable> buffers = new();
Assert.Equal(0, MemoryDiagnostics.TotalUndisposedAllocationCount);
for (int length = 1024; length <= 64 * OneMb; length *= 2)
{
long cntBefore = MemoryDiagnostics.TotalUndisposedAllocationCount;
IDisposable buffer = isGroup ?
Allocator.AllocateGroup<byte>(length, 1024) :
Allocator.Allocate<byte>(length);
buffers.Add(buffer);
long cntAfter = MemoryDiagnostics.TotalUndisposedAllocationCount;
Assert.True(cntAfter > cntBefore);
}
foreach (IDisposable buffer in buffers)
{
long cntBefore = MemoryDiagnostics.TotalUndisposedAllocationCount;
buffer.Dispose();
long cntAfter = MemoryDiagnostics.TotalUndisposedAllocationCount;
Assert.True(cntAfter < cntBefore);
}
Assert.Equal(0, MemoryDiagnostics.TotalUndisposedAllocationCount);
Assert.Equal(0, leakCounter);
}
}
[Theory]
[InlineData(false, false)]
[InlineData(false, true)]
[InlineData(true, false)]
[InlineData(true, true)]
public void MissingCleanup_LeaksAreReported(bool isGroupOuter, bool subscribeLeakHandleOuter)
{
RemoteExecutor.Invoke(RunTest, isGroupOuter.ToString(), subscribeLeakHandleOuter.ToString()).Dispose();
static void RunTest(string isGroupInner, string subscribeLeakHandleInner)
{
bool isGroup = bool.Parse(isGroupInner);
bool subscribeLeakHandle = bool.Parse(subscribeLeakHandleInner);
int leakCounter = 0;
bool stackTraceOk = true;
if (subscribeLeakHandle)
{
MemoryDiagnostics.UndisposedAllocation += stackTrace =>
{
Interlocked.Increment(ref leakCounter);
stackTraceOk &= stackTrace.Contains(nameof(RunTest)) && stackTrace.Contains(nameof(AllocateAndForget));
Assert.Contains(nameof(AllocateAndForget), stackTrace);
};
}
Assert.Equal(0, MemoryDiagnostics.TotalUndisposedAllocationCount);
for (int length = 1024; length <= 64 * OneMb; length *= 2)
{
long cntBefore = MemoryDiagnostics.TotalUndisposedAllocationCount;
AllocateAndForget(length, isGroup);
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
long cntAfter = MemoryDiagnostics.TotalUndisposedAllocationCount;
Assert.True(cntAfter > cntBefore);
}
if (subscribeLeakHandle)
{
// Make sure at least some of the leak callbacks have time to complete on the ThreadPool
Thread.Sleep(200);
Assert.True(leakCounter > 3, $"leakCounter did not count enough leaks ({leakCounter} only)");
}
Assert.True(stackTraceOk);
}
[MethodImpl(MethodImplOptions.NoInlining)]
static void AllocateAndForget(int length, bool isGroup)
{
if (isGroup)
{
_ = Allocator.AllocateGroup<byte>(length, 1024);
}
else
{
_ = Allocator.Allocate<byte>(length);
}
}
}
}
}

2
tests/ImageSharp.Tests/Memory/Allocators/RefCountedLifetimeGuardTests.cs

@ -110,7 +110,7 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
}
}
private class MockLifetimeGuard : RefCountedLifetimeGuard
private class MockLifetimeGuard : RefCountedMemoryLifetimeGuard
{
public int ReleaseInvocationCount { get; private set; }

Loading…
Cancel
Save