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