Skip to content

Commit 77bb287

Browse files
committed
xunit helper to track undisposed memory
1 parent 136cc14 commit 77bb287

File tree

7 files changed

+177
-27
lines changed

7 files changed

+177
-27
lines changed

src/ImageSharp/Diagnostics/MemoryDiagnostics.cs

Lines changed: 84 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,8 @@ namespace SixLabors.ImageSharp.Diagnostics
1515
/// </summary>
1616
public static class MemoryDiagnostics
1717
{
18-
private static int totalUndisposedAllocationCount;
19-
20-
private static UndisposedAllocationDelegate undisposedAllocation;
21-
private static int undisposedAllocationSubscriptionCounter;
18+
internal static readonly InteralMemoryDiagnostics Default = new();
19+
private static AsyncLocal<InteralMemoryDiagnostics> localInstance = null;
2220
private static readonly object SyncRoot = new();
2321

2422
/// <summary>
@@ -28,56 +26,116 @@ public static class MemoryDiagnostics
2826
/// </summary>
2927
public static event UndisposedAllocationDelegate UndisposedAllocation
3028
{
31-
add
29+
add => Current.UndisposedAllocation += value;
30+
remove => Current.UndisposedAllocation -= value;
31+
}
32+
33+
internal static InteralMemoryDiagnostics Current
34+
{
35+
get
3236
{
33-
lock (SyncRoot)
37+
if (localInstance != null && localInstance.Value != null)
3438
{
35-
undisposedAllocationSubscriptionCounter++;
36-
undisposedAllocation += value;
39+
return localInstance.Value;
3740
}
41+
42+
return Default;
3843
}
3944

40-
remove
45+
set
4146
{
42-
lock (SyncRoot)
47+
if (localInstance == null)
4348
{
44-
undisposedAllocation -= value;
45-
undisposedAllocationSubscriptionCounter--;
49+
lock (SyncRoot)
50+
{
51+
localInstance ??= new AsyncLocal<InteralMemoryDiagnostics>();
52+
}
4653
}
54+
55+
localInstance.Value = value;
4756
}
4857
}
4958

5059
/// <summary>
5160
/// Gets a value indicating the total number of memory resource objects leaked to the finalizer.
5261
/// </summary>
53-
public static int TotalUndisposedAllocationCount => totalUndisposedAllocationCount;
62+
public static int TotalUndisposedAllocationCount => Current.TotalUndisposedAllocationCount;
5463

55-
internal static bool UndisposedAllocationSubscribed => Volatile.Read(ref undisposedAllocationSubscriptionCounter) > 0;
64+
internal static bool UndisposedAllocationSubscribed => Current.UndisposedAllocationSubscribed;
5665

57-
internal static void IncrementTotalUndisposedAllocationCount() =>
58-
Interlocked.Increment(ref totalUndisposedAllocationCount);
66+
internal static void IncrementTotalUndisposedAllocationCount() => Current.IncrementTotalUndisposedAllocationCount();
5967

60-
internal static void DecrementTotalUndisposedAllocationCount() =>
61-
Interlocked.Decrement(ref totalUndisposedAllocationCount);
68+
internal static void DecrementTotalUndisposedAllocationCount() => Current.DecrementTotalUndisposedAllocationCount();
6269

6370
internal static void RaiseUndisposedMemoryResource(string allocationStackTrace)
71+
=> Current.RaiseUndisposedMemoryResource(allocationStackTrace);
72+
73+
internal class InteralMemoryDiagnostics
6474
{
65-
if (undisposedAllocation is null)
75+
private int totalUndisposedAllocationCount;
76+
77+
private UndisposedAllocationDelegate undisposedAllocation;
78+
private int undisposedAllocationSubscriptionCounter;
79+
private readonly object syncRoot = new();
80+
81+
/// <summary>
82+
/// Fires when an ImageSharp object's undisposed memory resource leaks to the finalizer.
83+
/// The event brings significant overhead, and is intended to be used for troubleshooting only.
84+
/// For production diagnostics, use <see cref="TotalUndisposedAllocationCount"/>.
85+
/// </summary>
86+
public event UndisposedAllocationDelegate UndisposedAllocation
6687
{
67-
return;
88+
add
89+
{
90+
lock (this.syncRoot)
91+
{
92+
this.undisposedAllocationSubscriptionCounter++;
93+
this.undisposedAllocation += value;
94+
}
95+
}
96+
97+
remove
98+
{
99+
lock (this.syncRoot)
100+
{
101+
this.undisposedAllocation -= value;
102+
this.undisposedAllocationSubscriptionCounter--;
103+
}
104+
}
68105
}
69106

70-
// Schedule on the ThreadPool, to avoid user callback messing up the finalizer thread.
107+
/// <summary>
108+
/// Gets a value indicating the total number of memory resource objects leaked to the finalizer.
109+
/// </summary>
110+
public int TotalUndisposedAllocationCount => this.totalUndisposedAllocationCount;
111+
112+
internal bool UndisposedAllocationSubscribed => Volatile.Read(ref this.undisposedAllocationSubscriptionCounter) > 0;
113+
114+
internal void IncrementTotalUndisposedAllocationCount() =>
115+
Interlocked.Increment(ref this.totalUndisposedAllocationCount);
116+
117+
internal void DecrementTotalUndisposedAllocationCount() =>
118+
Interlocked.Decrement(ref this.totalUndisposedAllocationCount);
119+
120+
internal void RaiseUndisposedMemoryResource(string allocationStackTrace)
121+
{
122+
if (this.undisposedAllocation is null)
123+
{
124+
return;
125+
}
126+
127+
// Schedule on the ThreadPool, to avoid user callback messing up the finalizer thread.
71128
#if NETSTANDARD2_1 || NETCOREAPP2_1_OR_GREATER
72-
ThreadPool.QueueUserWorkItem(
73-
stackTrace => undisposedAllocation?.Invoke(stackTrace),
74-
allocationStackTrace,
75-
preferLocal: false);
129+
ThreadPool.QueueUserWorkItem(
130+
stackTrace => this.undisposedAllocation?.Invoke(stackTrace),
131+
allocationStackTrace,
132+
preferLocal: false);
76133
#else
77134
ThreadPool.QueueUserWorkItem(
78-
stackTrace => undisposedAllocation?.Invoke((string)stackTrace),
135+
stackTrace => this.undisposedAllocation?.Invoke((string)stackTrace),
79136
allocationStackTrace);
80137
#endif
138+
}
81139
}
82140
}
83141
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,14 @@ public async Task DecodeAsync_NonGeneric_CreatesCorrectImageType(string path, Ty
103103

104104
[Theory]
105105
[WithFileCollection(nameof(CommonTestImages), PixelTypes.Rgba32)]
106+
[ValidateDisposedMemoryAllocations]
106107
public void Decode<TPixel>(TestImageProvider<TPixel> provider)
107108
where TPixel : unmanaged, IPixel<TPixel>
108109
{
109110
using Image<TPixel> image = provider.GetImage(PngDecoder);
111+
//var testFile = TestFile.Create(provider.SourceFileOrDescription);
112+
//using Image<TPixel> image = Image.Load<TPixel>(provider.Configuration, testFile.Bytes, PngDecoder);
113+
110114
image.DebugSave(provider);
111115
image.CompareToOriginal(provider, ImageComparer.Exact);
112116
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public class LoadPixelData
1414
[Theory]
1515
[InlineData(false)]
1616
[InlineData(true)]
17+
[ValidateDisposedMemoryAllocations]
1718
public void FromPixels(bool useSpan)
1819
{
1920
Rgba32[] data = { Color.Black, Color.White, Color.White, Color.Black, };

tests/ImageSharp.Tests/Image/LargeImageIntegrationTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ public void PreferContiguousImageBuffers_LoadImage_BufferIsContiguous(string for
5555

5656
static void RunTest(string formatInner)
5757
{
58+
using IDisposable mem = MemoryAllocatorValidator.MonitorAllocations();
59+
5860
Configuration configuration = Configuration.Default.Clone();
5961
configuration.PreferContiguousImageBuffers = true;
6062
IImageEncoder encoder = configuration.ImageFormatsManager.FindEncoder(
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System;
5+
using System.Diagnostics;
6+
using SixLabors.ImageSharp.Diagnostics;
7+
using Xunit;
8+
9+
namespace SixLabors.ImageSharp.Tests
10+
{
11+
public static class MemoryAllocatorValidator
12+
{
13+
public static IDisposable MonitorAllocations(int max = 0)
14+
{
15+
MemoryDiagnostics.Current = new();
16+
return new TestMemoryAllocatorDisposable(max);
17+
}
18+
19+
public static void ValidateAllocation(int max = 0)
20+
{
21+
var count = MemoryDiagnostics.TotalUndisposedAllocationCount;
22+
var pass = count <= max;
23+
Assert.True(pass, $"Expected a max of {max} undisposed buffers but found {count}");
24+
25+
if (count > 0)
26+
{
27+
Debug.WriteLine("We should have Zero undisposed memory allocations.");
28+
}
29+
30+
MemoryDiagnostics.Current = null;
31+
}
32+
33+
public struct TestMemoryAllocatorDisposable : IDisposable
34+
{
35+
private readonly int max;
36+
37+
public TestMemoryAllocatorDisposable(int max) => this.max = max;
38+
39+
public void Dispose()
40+
=> ValidateAllocation(this.max);
41+
}
42+
}
43+
}

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.IO;
88
using System.Reflection;
99
using System.Threading.Tasks;
10+
using SixLabors.ImageSharp.Diagnostics;
1011
using SixLabors.ImageSharp.Formats;
1112
using SixLabors.ImageSharp.Memory;
1213
using SixLabors.ImageSharp.PixelFormats;
@@ -158,8 +159,13 @@ public override Image<TPixel> GetImage(IImageDecoder decoder)
158159
return this.LoadImage(decoder);
159160
}
160161

161-
var key = new Key(this.PixelType, this.FilePath, decoder);
162+
// do cache so we can track allocation correctly when validating memory
163+
if (MemoryDiagnostics.Current != MemoryDiagnostics.Default)
164+
{
165+
return this.LoadImage(decoder);
166+
}
162167

168+
var key = new Key(this.PixelType, this.FilePath, decoder);
163169
Image<TPixel> cachedImage = Cache.GetOrAdd(key, _ => this.LoadImage(decoder));
164170

165171
return cachedImage.Clone(this.Configuration);
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System;
5+
using System.Diagnostics;
6+
using System.Reflection;
7+
using Xunit.Sdk;
8+
9+
namespace SixLabors.ImageSharp.Tests
10+
{
11+
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
12+
public class ValidateDisposedMemoryAllocationsAttribute : BeforeAfterTestAttribute
13+
{
14+
private readonly int max = 0;
15+
16+
public ValidateDisposedMemoryAllocationsAttribute()
17+
: this(0)
18+
{
19+
}
20+
21+
public ValidateDisposedMemoryAllocationsAttribute(int max)
22+
{
23+
this.max = max;
24+
if (max > 0)
25+
{
26+
Debug.WriteLine("Needs fixing, we shoudl have Zero undisposed memory allocations.");
27+
}
28+
}
29+
30+
public override void Before(MethodInfo methodUnderTest)
31+
=> MemoryAllocatorValidator.MonitorAllocations(this.max); // the disposable isn't important cause the validate below does the same thing
32+
33+
public override void After(MethodInfo methodUnderTest)
34+
=> MemoryAllocatorValidator.ValidateAllocation(this.max);
35+
}
36+
}

0 commit comments

Comments
 (0)