Browse Source

xunit helper to track undisposed memory

pull/2082/head
Scott Williams 4 years ago
parent
commit
77bb287116
  1. 110
      src/ImageSharp/Diagnostics/MemoryDiagnostics.cs
  2. 4
      tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
  3. 1
      tests/ImageSharp.Tests/Image/ImageTests.LoadPixelData.cs
  4. 2
      tests/ImageSharp.Tests/Image/LargeImageIntegrationTests.cs
  5. 43
      tests/ImageSharp.Tests/MemoryAllocatorValidator.cs
  6. 8
      tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs
  7. 36
      tests/ImageSharp.Tests/ValidateDisposedMemoryAllocationsAttribute.cs

110
src/ImageSharp/Diagnostics/MemoryDiagnostics.cs

@ -15,10 +15,8 @@ namespace SixLabors.ImageSharp.Diagnostics
/// </summary> /// </summary>
public static class MemoryDiagnostics public static class MemoryDiagnostics
{ {
private static int totalUndisposedAllocationCount; internal static readonly InteralMemoryDiagnostics Default = new();
private static AsyncLocal<InteralMemoryDiagnostics> localInstance = null;
private static UndisposedAllocationDelegate undisposedAllocation;
private static int undisposedAllocationSubscriptionCounter;
private static readonly object SyncRoot = new(); private static readonly object SyncRoot = new();
/// <summary> /// <summary>
@ -28,56 +26,116 @@ namespace SixLabors.ImageSharp.Diagnostics
/// </summary> /// </summary>
public static event UndisposedAllocationDelegate UndisposedAllocation public static event UndisposedAllocationDelegate UndisposedAllocation
{ {
add add => Current.UndisposedAllocation += value;
remove => Current.UndisposedAllocation -= value;
}
internal static InteralMemoryDiagnostics Current
{
get
{ {
lock (SyncRoot) if (localInstance != null && localInstance.Value != null)
{ {
undisposedAllocationSubscriptionCounter++; return localInstance.Value;
undisposedAllocation += value;
} }
return Default;
} }
remove set
{ {
lock (SyncRoot) if (localInstance == null)
{ {
undisposedAllocation -= value; lock (SyncRoot)
undisposedAllocationSubscriptionCounter--; {
localInstance ??= new AsyncLocal<InteralMemoryDiagnostics>();
}
} }
localInstance.Value = value;
} }
} }
/// <summary> /// <summary>
/// Gets a value indicating the total number of memory resource objects leaked to the finalizer. /// Gets a value indicating the total number of memory resource objects leaked to the finalizer.
/// </summary> /// </summary>
public static int TotalUndisposedAllocationCount => totalUndisposedAllocationCount; public static int TotalUndisposedAllocationCount => Current.TotalUndisposedAllocationCount;
internal static bool UndisposedAllocationSubscribed => Volatile.Read(ref undisposedAllocationSubscriptionCounter) > 0; internal static bool UndisposedAllocationSubscribed => Current.UndisposedAllocationSubscribed;
internal static void IncrementTotalUndisposedAllocationCount() => internal static void IncrementTotalUndisposedAllocationCount() => Current.IncrementTotalUndisposedAllocationCount();
Interlocked.Increment(ref totalUndisposedAllocationCount);
internal static void DecrementTotalUndisposedAllocationCount() => internal static void DecrementTotalUndisposedAllocationCount() => Current.DecrementTotalUndisposedAllocationCount();
Interlocked.Decrement(ref totalUndisposedAllocationCount);
internal static void RaiseUndisposedMemoryResource(string allocationStackTrace) internal static void RaiseUndisposedMemoryResource(string allocationStackTrace)
=> Current.RaiseUndisposedMemoryResource(allocationStackTrace);
internal class InteralMemoryDiagnostics
{ {
if (undisposedAllocation is null) private int totalUndisposedAllocationCount;
private UndisposedAllocationDelegate undisposedAllocation;
private int undisposedAllocationSubscriptionCounter;
private 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 event UndisposedAllocationDelegate UndisposedAllocation
{ {
return; add
{
lock (this.syncRoot)
{
this.undisposedAllocationSubscriptionCounter++;
this.undisposedAllocation += value;
}
}
remove
{
lock (this.syncRoot)
{
this.undisposedAllocation -= value;
this.undisposedAllocationSubscriptionCounter--;
}
}
} }
// Schedule on the ThreadPool, to avoid user callback messing up the finalizer thread. /// <summary>
/// Gets a value indicating the total number of memory resource objects leaked to the finalizer.
/// </summary>
public int TotalUndisposedAllocationCount => this.totalUndisposedAllocationCount;
internal bool UndisposedAllocationSubscribed => Volatile.Read(ref this.undisposedAllocationSubscriptionCounter) > 0;
internal void IncrementTotalUndisposedAllocationCount() =>
Interlocked.Increment(ref this.totalUndisposedAllocationCount);
internal void DecrementTotalUndisposedAllocationCount() =>
Interlocked.Decrement(ref this.totalUndisposedAllocationCount);
internal void RaiseUndisposedMemoryResource(string allocationStackTrace)
{
if (this.undisposedAllocation is null)
{
return;
}
// Schedule on the ThreadPool, to avoid user callback messing up the finalizer thread.
#if NETSTANDARD2_1 || NETCOREAPP2_1_OR_GREATER #if NETSTANDARD2_1 || NETCOREAPP2_1_OR_GREATER
ThreadPool.QueueUserWorkItem( ThreadPool.QueueUserWorkItem(
stackTrace => undisposedAllocation?.Invoke(stackTrace), stackTrace => this.undisposedAllocation?.Invoke(stackTrace),
allocationStackTrace, allocationStackTrace,
preferLocal: false); preferLocal: false);
#else #else
ThreadPool.QueueUserWorkItem( ThreadPool.QueueUserWorkItem(
stackTrace => undisposedAllocation?.Invoke((string)stackTrace), stackTrace => this.undisposedAllocation?.Invoke((string)stackTrace),
allocationStackTrace); allocationStackTrace);
#endif #endif
}
} }
} }
} }

4
tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs

@ -103,10 +103,14 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
[Theory] [Theory]
[WithFileCollection(nameof(CommonTestImages), PixelTypes.Rgba32)] [WithFileCollection(nameof(CommonTestImages), PixelTypes.Rgba32)]
[ValidateDisposedMemoryAllocations]
public void Decode<TPixel>(TestImageProvider<TPixel> provider) public void Decode<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using Image<TPixel> image = provider.GetImage(PngDecoder); using Image<TPixel> image = provider.GetImage(PngDecoder);
//var testFile = TestFile.Create(provider.SourceFileOrDescription);
//using Image<TPixel> image = Image.Load<TPixel>(provider.Configuration, testFile.Bytes, PngDecoder);
image.DebugSave(provider); image.DebugSave(provider);
image.CompareToOriginal(provider, ImageComparer.Exact); image.CompareToOriginal(provider, ImageComparer.Exact);
} }

1
tests/ImageSharp.Tests/Image/ImageTests.LoadPixelData.cs

@ -14,6 +14,7 @@ namespace SixLabors.ImageSharp.Tests
[Theory] [Theory]
[InlineData(false)] [InlineData(false)]
[InlineData(true)] [InlineData(true)]
[ValidateDisposedMemoryAllocations]
public void FromPixels(bool useSpan) public void FromPixels(bool useSpan)
{ {
Rgba32[] data = { Color.Black, Color.White, Color.White, Color.Black, }; Rgba32[] data = { Color.Black, Color.White, Color.White, Color.Black, };

2
tests/ImageSharp.Tests/Image/LargeImageIntegrationTests.cs

@ -55,6 +55,8 @@ namespace SixLabors.ImageSharp.Tests
static void RunTest(string formatInner) static void RunTest(string formatInner)
{ {
using IDisposable mem = MemoryAllocatorValidator.MonitorAllocations();
Configuration configuration = Configuration.Default.Clone(); Configuration configuration = Configuration.Default.Clone();
configuration.PreferContiguousImageBuffers = true; configuration.PreferContiguousImageBuffers = true;
IImageEncoder encoder = configuration.ImageFormatsManager.FindEncoder( IImageEncoder encoder = configuration.ImageFormatsManager.FindEncoder(

43
tests/ImageSharp.Tests/MemoryAllocatorValidator.cs

@ -0,0 +1,43 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Diagnostics;
using SixLabors.ImageSharp.Diagnostics;
using Xunit;
namespace SixLabors.ImageSharp.Tests
{
public static class MemoryAllocatorValidator
{
public static IDisposable MonitorAllocations(int max = 0)
{
MemoryDiagnostics.Current = new();
return new TestMemoryAllocatorDisposable(max);
}
public static void ValidateAllocation(int max = 0)
{
var count = MemoryDiagnostics.TotalUndisposedAllocationCount;
var pass = count <= max;
Assert.True(pass, $"Expected a max of {max} undisposed buffers but found {count}");
if (count > 0)
{
Debug.WriteLine("We should have Zero undisposed memory allocations.");
}
MemoryDiagnostics.Current = null;
}
public struct TestMemoryAllocatorDisposable : IDisposable
{
private readonly int max;
public TestMemoryAllocatorDisposable(int max) => this.max = max;
public void Dispose()
=> ValidateAllocation(this.max);
}
}
}

8
tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs

@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using SixLabors.ImageSharp.Diagnostics;
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -158,8 +159,13 @@ namespace SixLabors.ImageSharp.Tests
return this.LoadImage(decoder); return this.LoadImage(decoder);
} }
var key = new Key(this.PixelType, this.FilePath, decoder); // do cache so we can track allocation correctly when validating memory
if (MemoryDiagnostics.Current != MemoryDiagnostics.Default)
{
return this.LoadImage(decoder);
}
var key = new Key(this.PixelType, this.FilePath, decoder);
Image<TPixel> cachedImage = Cache.GetOrAdd(key, _ => this.LoadImage(decoder)); Image<TPixel> cachedImage = Cache.GetOrAdd(key, _ => this.LoadImage(decoder));
return cachedImage.Clone(this.Configuration); return cachedImage.Clone(this.Configuration);

36
tests/ImageSharp.Tests/ValidateDisposedMemoryAllocationsAttribute.cs

@ -0,0 +1,36 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Diagnostics;
using System.Reflection;
using Xunit.Sdk;
namespace SixLabors.ImageSharp.Tests
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class ValidateDisposedMemoryAllocationsAttribute : BeforeAfterTestAttribute
{
private readonly int max = 0;
public ValidateDisposedMemoryAllocationsAttribute()
: this(0)
{
}
public ValidateDisposedMemoryAllocationsAttribute(int max)
{
this.max = max;
if (max > 0)
{
Debug.WriteLine("Needs fixing, we shoudl have Zero undisposed memory allocations.");
}
}
public override void Before(MethodInfo methodUnderTest)
=> MemoryAllocatorValidator.MonitorAllocations(this.max); // the disposable isn't important cause the validate below does the same thing
public override void After(MethodInfo methodUnderTest)
=> MemoryAllocatorValidator.ValidateAllocation(this.max);
}
}
Loading…
Cancel
Save