Browse Source

finalize and document the API

pull/1969/head
Anton Firszov 4 years ago
parent
commit
9d155a55aa
  1. 44
      src/ImageSharp/Diagnostics/MemoryDiagnostics.cs
  2. 13
      src/ImageSharp/Diagnostics/MemoryInfo.cs
  3. 17
      src/ImageSharp/Memory/Allocators/Internals/RefCountedMemoryLifetimeGuard.cs
  4. 59
      tests/ImageSharp.Tests/Memory/Allocators/MemoryDiagnosticsTests.cs

44
src/ImageSharp/Diagnostics/MemoryDiagnostics.cs

@ -6,18 +6,58 @@ using System.Threading;
namespace SixLabors.ImageSharp.Diagnostics namespace SixLabors.ImageSharp.Diagnostics
{ {
/// <summary>
/// Represents the method to handle <see cref="MemoryDiagnostics.UndisposedAllocation"/>.
/// </summary>
public delegate void UndisposedMemoryResourceDelegate(string allocationStackTrace);
/// <summary>
/// Utilities to track memory usage and detect memory leaks from not disposing ImageSharp objects.
/// </summary>
public static class MemoryDiagnostics public static class MemoryDiagnostics
{ {
private static int totalUndisposedAllocationCount; private static int totalUndisposedAllocationCount;
public static MemoryInfo GetMemoryInfo() => new MemoryInfo(totalUndisposedAllocationCount); private static UndisposedMemoryResourceDelegate undisposedMemoryResource;
private static int undisposedMemoryResourceSubscriptionCounter;
/// <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 UndisposedMemoryResourceDelegate UndisposedAllocation
{
add
{
Interlocked.Increment(ref undisposedMemoryResourceSubscriptionCounter);
undisposedMemoryResource += value;
}
remove
{
undisposedMemoryResource -= value;
Interlocked.Decrement(ref undisposedMemoryResourceSubscriptionCounter);
}
}
public static bool EnableStrictDisposeWatcher { get; set; } /// <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 MemoryResourceLeakedSubscribed => undisposedMemoryResourceSubscriptionCounter > 0;
internal static void IncrementTotalUndisposedAllocationCount() => internal static void IncrementTotalUndisposedAllocationCount() =>
Interlocked.Increment(ref totalUndisposedAllocationCount); Interlocked.Increment(ref totalUndisposedAllocationCount);
internal static void DecrementTotalUndisposedAllocationCount() => internal static void DecrementTotalUndisposedAllocationCount() =>
Interlocked.Decrement(ref totalUndisposedAllocationCount); Interlocked.Decrement(ref totalUndisposedAllocationCount);
internal static void RaiseUndisposedMemoryResource(string allocationStackTrace)
{
// Schedule on the ThreadPool, to avoid user callback messing up the finalizer thread.
ThreadPool.QueueUserWorkItem(_ => undisposedMemoryResource?.Invoke(allocationStackTrace));
}
} }
} }

13
src/ImageSharp/Diagnostics/MemoryInfo.cs

@ -1,13 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Diagnostics
{
public readonly struct MemoryInfo
{
internal MemoryInfo(long totalUndisposedAllocationCount)
=> this.TotalUndisposedAllocationCount = totalUndisposedAllocationCount;
public long TotalUndisposedAllocationCount { get; }
}
}

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

@ -10,15 +10,24 @@ namespace SixLabors.ImageSharp.Memory.Internals
{ {
/// <summary> /// <summary>
/// Implements reference counting lifetime guard mechanism for memory resources /// Implements reference counting lifetime guard mechanism for memory resources
/// also maintaining the current value of <see cref="MemoryInfo.TotalUndisposedAllocationCount"/>. /// and maintains the value of <see cref="MemoryDiagnostics.TotalUndisposedAllocationCount"/>.
/// </summary> /// </summary>
internal abstract class RefCountedMemoryLifetimeGuard : IDisposable internal abstract class RefCountedMemoryLifetimeGuard : IDisposable
{ {
private int refCount = 1; private int refCount = 1;
private int disposed; private int disposed;
private int released; private int released;
private string allocationStackTrace;
public RefCountedMemoryLifetimeGuard() => MemoryDiagnostics.IncrementTotalUndisposedAllocationCount(); protected RefCountedMemoryLifetimeGuard()
{
if (MemoryDiagnostics.MemoryResourceLeakedSubscribed)
{
this.allocationStackTrace = Environment.StackTrace;
}
MemoryDiagnostics.IncrementTotalUndisposedAllocationCount();
}
~RefCountedMemoryLifetimeGuard() ~RefCountedMemoryLifetimeGuard()
{ {
@ -57,6 +66,10 @@ namespace SixLabors.ImageSharp.Memory.Internals
{ {
MemoryDiagnostics.DecrementTotalUndisposedAllocationCount(); MemoryDiagnostics.DecrementTotalUndisposedAllocationCount();
} }
else if (this.allocationStackTrace != null)
{
MemoryDiagnostics.RaiseUndisposedMemoryResource(this.allocationStackTrace);
}
this.Release(); this.Release();
} }

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

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Threading;
using Microsoft.DotNet.RemoteExecutor; using Microsoft.DotNet.RemoteExecutor;
using SixLabors.ImageSharp.Diagnostics; using SixLabors.ImageSharp.Diagnostics;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
@ -20,60 +21,88 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
[Theory] [Theory]
[InlineData(false)] [InlineData(false)]
[InlineData(true)] [InlineData(true)]
public void AllocateDispose_Maintains_TotalUndisposedLogicalAllocationCount(bool isGroupOuter) public void PerfectCleanup_NoLeaksReported(bool isGroupOuter)
{ {
RemoteExecutor.Invoke(RunTest, isGroupOuter.ToString()).Dispose(); RemoteExecutor.Invoke(RunTest, isGroupOuter.ToString()).Dispose();
static void RunTest(string isGroupInner) static void RunTest(string isGroupInner)
{ {
bool isGroup = bool.Parse(isGroupInner); bool isGroup = bool.Parse(isGroupInner);
int leakCounter = 0;
MemoryDiagnostics.UndisposedAllocation += _ => Interlocked.Increment(ref leakCounter);
List<IDisposable> buffers = new(); List<IDisposable> buffers = new();
Assert.Equal(0, MemoryDiagnostics.GetMemoryInfo().TotalUndisposedAllocationCount); Assert.Equal(0, MemoryDiagnostics.TotalUndisposedAllocationCount);
for (int length = 1024; length <= 64 * OneMb; length *= 2) for (int length = 1024; length <= 64 * OneMb; length *= 2)
{ {
long cntBefore = MemoryDiagnostics.GetMemoryInfo().TotalUndisposedAllocationCount; long cntBefore = MemoryDiagnostics.TotalUndisposedAllocationCount;
IDisposable buffer = isGroup ? IDisposable buffer = isGroup ?
Allocator.AllocateGroup<byte>(length, 1024) : Allocator.AllocateGroup<byte>(length, 1024) :
Allocator.Allocate<byte>(length); Allocator.Allocate<byte>(length);
buffers.Add(buffer); buffers.Add(buffer);
long cntAfter = MemoryDiagnostics.GetMemoryInfo().TotalUndisposedAllocationCount; long cntAfter = MemoryDiagnostics.TotalUndisposedAllocationCount;
Assert.True(cntAfter > cntBefore); Assert.True(cntAfter > cntBefore);
} }
foreach (IDisposable buffer in buffers) foreach (IDisposable buffer in buffers)
{ {
long cntBefore = MemoryDiagnostics.GetMemoryInfo().TotalUndisposedAllocationCount; long cntBefore = MemoryDiagnostics.TotalUndisposedAllocationCount;
buffer.Dispose(); buffer.Dispose();
long cntAfter = MemoryDiagnostics.GetMemoryInfo().TotalUndisposedAllocationCount; long cntAfter = MemoryDiagnostics.TotalUndisposedAllocationCount;
Assert.True(cntAfter < cntBefore); Assert.True(cntAfter < cntBefore);
} }
Assert.Equal(0, MemoryDiagnostics.GetMemoryInfo().TotalUndisposedAllocationCount); Assert.Equal(0, MemoryDiagnostics.TotalUndisposedAllocationCount);
Assert.Equal(0, leakCounter);
} }
} }
[Theory] [Theory]
[InlineData(false)] [InlineData(false, false)]
[InlineData(true)] [InlineData(false, true)]
public void BufferAndGroupFinalizer_DoesNotReduce_TotalUndisposedLogicalAllocationCount(bool isGroupOuter) [InlineData(true, false)]
[InlineData(true, true)]
public void MissingCleanup_LeaksAreReported(bool isGroupOuter, bool subscribeLeakHandleOuter)
{ {
RemoteExecutor.Invoke(RunTest, isGroupOuter.ToString()).Dispose(); RemoteExecutor.Invoke(RunTest, isGroupOuter.ToString(), subscribeLeakHandleOuter.ToString()).Dispose();
static void RunTest(string isGroupInner) static void RunTest(string isGroupInner, string subscribeLeakHandleInner)
{ {
bool isGroup = bool.Parse(isGroupInner); bool isGroup = bool.Parse(isGroupInner);
Assert.Equal(0, MemoryDiagnostics.GetMemoryInfo().TotalUndisposedAllocationCount); 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) for (int length = 1024; length <= 64 * OneMb; length *= 2)
{ {
long cntBefore = MemoryDiagnostics.GetMemoryInfo().TotalUndisposedAllocationCount; long cntBefore = MemoryDiagnostics.TotalUndisposedAllocationCount;
AllocateAndForget(length, isGroup); AllocateAndForget(length, isGroup);
GC.Collect(); GC.Collect();
GC.WaitForPendingFinalizers(); GC.WaitForPendingFinalizers();
GC.Collect(); GC.Collect();
long cntAfter = MemoryDiagnostics.GetMemoryInfo().TotalUndisposedAllocationCount; long cntAfter = MemoryDiagnostics.TotalUndisposedAllocationCount;
Assert.True(cntAfter > cntBefore); 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)] [MethodImpl(MethodImplOptions.NoInlining)]

Loading…
Cancel
Save