mirror of https://github.com/SixLabors/ImageSharp
committed by
GitHub
9 changed files with 290 additions and 62 deletions
@ -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
|
|||
} |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue