diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index d211992a94..faa29865f2 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -27,6 +27,7 @@
+
diff --git a/src/ImageSharp/Diagnostics/MemoryDiagnostics.cs b/src/ImageSharp/Diagnostics/MemoryDiagnostics.cs
index d83e737f12..89f18cff61 100644
--- a/src/ImageSharp/Diagnostics/MemoryDiagnostics.cs
+++ b/src/ImageSharp/Diagnostics/MemoryDiagnostics.cs
@@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
+using System;
using System.Threading;
namespace SixLabors.ImageSharp.Diagnostics
@@ -47,6 +48,16 @@ public static event UndisposedAllocationDelegate UndisposedAllocation
}
}
+ ///
+ /// Fires when ImageSharp allocates memory from a MemoryAllocator
+ ///
+ internal static event Action MemoryAllocated;
+
+ ///
+ /// Fires when ImageSharp releases memory allocated from a MemoryAllocator
+ ///
+ internal static event Action MemoryReleased;
+
///
/// Gets a value indicating the total number of memory resource objects leaked to the finalizer.
///
@@ -54,11 +65,17 @@ public static event UndisposedAllocationDelegate UndisposedAllocation
internal static bool UndisposedAllocationSubscribed => Volatile.Read(ref undisposedAllocationSubscriptionCounter) > 0;
- internal static void IncrementTotalUndisposedAllocationCount() =>
+ internal static void IncrementTotalUndisposedAllocationCount()
+ {
Interlocked.Increment(ref totalUndisposedAllocationCount);
+ MemoryAllocated?.Invoke();
+ }
- internal static void DecrementTotalUndisposedAllocationCount() =>
+ internal static void DecrementTotalUndisposedAllocationCount()
+ {
Interlocked.Decrement(ref totalUndisposedAllocationCount);
+ MemoryReleased?.Invoke();
+ }
internal static void RaiseUndisposedMemoryResource(string allocationStackTrace)
{
diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
index 962c9f0d67..ee0a312803 100644
--- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
@@ -122,11 +122,12 @@ public BmpDecoderCore(Configuration configuration, IBmpDecoderOptions options)
public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
+ Image image = null;
try
{
int bytesPerColorMapEntry = this.ReadImageHeaders(stream, out bool inverted, out byte[] palette);
- var image = new Image(this.Configuration, this.infoHeader.Width, this.infoHeader.Height, this.metadata);
+ image = new Image(this.Configuration, this.infoHeader.Width, this.infoHeader.Height, this.metadata);
Buffer2D pixels = image.GetRootFramePixelBuffer();
@@ -193,8 +194,14 @@ public Image Decode(BufferedReadStream stream, CancellationToken
}
catch (IndexOutOfRangeException e)
{
+ image?.Dispose();
throw new ImageFormatException("Bitmap does not have a valid format.", e);
}
+ catch
+ {
+ image?.Dispose();
+ throw;
+ }
}
///
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs
index 430adeb21d..532892e060 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs
@@ -95,7 +95,9 @@ public Buffer2D GetPixelBuffer(CancellationToken cancellationToken)
}
}
- return this.pixelBuffer;
+ var buffer = this.pixelBuffer;
+ this.pixelBuffer = null;
+ return buffer;
}
///
@@ -210,6 +212,7 @@ public void Dispose()
this.rgbBuffer?.Dispose();
this.paddedProxyPixelRow?.Dispose();
+ this.pixelBuffer?.Dispose();
}
}
}
diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
index 497dc39674..12770bc521 100644
--- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
@@ -227,10 +227,16 @@ public Image Decode(BufferedReadStream stream, CancellationToken
return image;
}
+ catch
+ {
+ image?.Dispose();
+ throw;
+ }
finally
{
this.scanline?.Dispose();
this.previousScanline?.Dispose();
+ this.nextChunk?.Data?.Dispose();
}
}
@@ -472,6 +478,8 @@ private void InitializeImage(ImageMetadata metadata, out Image i
this.bytesPerSample = this.header.BitDepth / 8;
}
+ this.previousScanline?.Dispose();
+ this.scanline?.Dispose();
this.previousScanline = this.memoryAllocator.Allocate(this.bytesPerScanline, AllocationOptions.Clean);
this.scanline = this.Configuration.MemoryAllocator.Allocate(this.bytesPerScanline, AllocationOptions.Clean);
}
@@ -1359,6 +1367,7 @@ private int ReadNextDataChunk()
{
if (chunk.Type == PngChunkType.Data)
{
+ chunk.Data?.Dispose();
return chunk.Length;
}
@@ -1453,6 +1462,9 @@ private void ValidateChunk(in PngChunk chunk)
if (validCrc != inputCrc)
{
string chunkTypeName = Encoding.ASCII.GetString(chunkType);
+
+ // ensure when throwing we dispose the data back to the memory allocator
+ chunk.Data?.Dispose();
PngThrowHelper.ThrowInvalidChunkCrc(chunkTypeName);
}
}
diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs
index cfbc32f4f6..4e788c76af 100644
--- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs
+++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs
@@ -65,7 +65,8 @@ protected override void Decompress(BufferedReadStream stream, int byteCount, int
jpegDecoder.ParseStream(stream, scanDecoderGray, CancellationToken.None);
// TODO: Should we pass through the CancellationToken from the tiff decoder?
- CopyImageBytesToBuffer(buffer, spectralConverterGray.GetPixelBuffer(CancellationToken.None));
+ using var decompressedBuffer = spectralConverterGray.GetPixelBuffer(CancellationToken.None);
+ CopyImageBytesToBuffer(buffer, decompressedBuffer);
break;
}
@@ -81,7 +82,8 @@ protected override void Decompress(BufferedReadStream stream, int byteCount, int
jpegDecoder.ParseStream(stream, scanDecoder, CancellationToken.None);
// TODO: Should we pass through the CancellationToken from the tiff decoder?
- CopyImageBytesToBuffer(buffer, spectralConverter.GetPixelBuffer(CancellationToken.None));
+ using var decompressedBuffer = spectralConverter.GetPixelBuffer(CancellationToken.None);
+ CopyImageBytesToBuffer(buffer, decompressedBuffer);
break;
}
diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
index 1198c519a2..1cd3d2c0c1 100644
--- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
@@ -157,40 +157,52 @@ public TiffDecoderCore(Configuration configuration, ITiffDecoderOptions options)
public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
- this.inputStream = stream;
- var reader = new DirectoryReader(stream, this.Configuration.MemoryAllocator);
-
- IEnumerable directories = reader.Read();
- this.byteOrder = reader.ByteOrder;
- this.isBigTiff = reader.IsBigTiff;
-
var frames = new List>();
- foreach (ExifProfile ifd in directories)
+ try
{
- cancellationToken.ThrowIfCancellationRequested();
- ImageFrame frame = this.DecodeFrame(ifd, cancellationToken);
- frames.Add(frame);
+ this.inputStream = stream;
+ var reader = new DirectoryReader(stream, this.Configuration.MemoryAllocator);
+
+ IEnumerable directories = reader.Read();
+ this.byteOrder = reader.ByteOrder;
+ this.isBigTiff = reader.IsBigTiff;
- if (this.decodingMode is FrameDecodingMode.First)
+ foreach (ExifProfile ifd in directories)
{
- break;
+ cancellationToken.ThrowIfCancellationRequested();
+ ImageFrame frame = this.DecodeFrame(ifd, cancellationToken);
+ frames.Add(frame);
+
+ if (this.decodingMode is FrameDecodingMode.First)
+ {
+ break;
+ }
}
- }
- ImageMetadata metadata = TiffDecoderMetadataCreator.Create(frames, this.ignoreMetadata, reader.ByteOrder, reader.IsBigTiff);
+ ImageMetadata metadata = TiffDecoderMetadataCreator.Create(frames, this.ignoreMetadata, reader.ByteOrder, reader.IsBigTiff);
- // TODO: Tiff frames can have different sizes.
- ImageFrame root = frames[0];
- this.Dimensions = root.Size();
- foreach (ImageFrame frame in frames)
- {
- if (frame.Size() != root.Size())
+ // TODO: Tiff frames can have different sizes.
+ ImageFrame root = frames[0];
+ this.Dimensions = root.Size();
+ foreach (ImageFrame frame in frames)
{
- TiffThrowHelper.ThrowNotSupported("Images with different sizes are not supported");
+ if (frame.Size() != root.Size())
+ {
+ TiffThrowHelper.ThrowNotSupported("Images with different sizes are not supported");
+ }
}
+
+ return new Image(this.Configuration, metadata, frames);
}
+ catch
+ {
+ foreach (ImageFrame f in frames)
+ {
+ f.Dispose();
+ }
- return new Image(this.Configuration, metadata, frames);
+ throw;
+ }
}
///
@@ -240,8 +252,8 @@ private ImageFrame DecodeFrame(ExifProfile tags, CancellationTok
var stripOffsetsArray = (Array)tags.GetValueInternal(ExifTag.StripOffsets).GetValue();
var stripByteCountsArray = (Array)tags.GetValueInternal(ExifTag.StripByteCounts).GetValue();
- IMemoryOwner stripOffsetsMemory = this.ConvertNumbers(stripOffsetsArray, out Span stripOffsets);
- IMemoryOwner stripByteCountsMemory = this.ConvertNumbers(stripByteCountsArray, out Span stripByteCounts);
+ using IMemoryOwner stripOffsetsMemory = this.ConvertNumbers(stripOffsetsArray, out Span stripOffsets);
+ using IMemoryOwner stripByteCountsMemory = this.ConvertNumbers(stripByteCountsArray, out Span stripByteCounts);
if (this.PlanarConfiguration == TiffPlanarConfiguration.Planar)
{
@@ -262,8 +274,6 @@ private ImageFrame DecodeFrame(ExifProfile tags, CancellationTok
cancellationToken);
}
- stripOffsetsMemory?.Dispose();
- stripByteCountsMemory?.Dispose();
return frame;
}
diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
index 9d18e5d821..7052be4ea6 100644
--- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
+++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
@@ -82,38 +82,47 @@ public WebpDecoderCore(Configuration configuration, IWebpDecoderOptions options)
public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
- this.Metadata = new ImageMetadata();
- this.currentStream = stream;
+ Image image = null;
+ try
+ {
+ this.Metadata = new ImageMetadata();
+ this.currentStream = stream;
- uint fileSize = this.ReadImageHeader();
+ uint fileSize = this.ReadImageHeader();
- using (this.webImageInfo = this.ReadVp8Info())
- {
- if (this.webImageInfo.Features is { Animation: true })
+ using (this.webImageInfo = this.ReadVp8Info())
{
- WebpThrowHelper.ThrowNotSupportedException("Animations are not supported");
- }
+ if (this.webImageInfo.Features is { Animation: true })
+ {
+ WebpThrowHelper.ThrowNotSupportedException("Animations are not supported");
+ }
- var image = new Image(this.Configuration, (int)this.webImageInfo.Width, (int)this.webImageInfo.Height, this.Metadata);
- Buffer2D pixels = image.GetRootFramePixelBuffer();
- if (this.webImageInfo.IsLossless)
- {
- var losslessDecoder = new WebpLosslessDecoder(this.webImageInfo.Vp8LBitReader, this.memoryAllocator, this.Configuration);
- losslessDecoder.Decode(pixels, image.Width, image.Height);
- }
- else
- {
- var lossyDecoder = new WebpLossyDecoder(this.webImageInfo.Vp8BitReader, this.memoryAllocator, this.Configuration);
- lossyDecoder.Decode(pixels, image.Width, image.Height, this.webImageInfo);
- }
+ image = new Image(this.Configuration, (int)this.webImageInfo.Width, (int)this.webImageInfo.Height, this.Metadata);
+ Buffer2D pixels = image.GetRootFramePixelBuffer();
+ if (this.webImageInfo.IsLossless)
+ {
+ var losslessDecoder = new WebpLosslessDecoder(this.webImageInfo.Vp8LBitReader, this.memoryAllocator, this.Configuration);
+ losslessDecoder.Decode(pixels, image.Width, image.Height);
+ }
+ else
+ {
+ var lossyDecoder = new WebpLossyDecoder(this.webImageInfo.Vp8BitReader, this.memoryAllocator, this.Configuration);
+ lossyDecoder.Decode(pixels, image.Width, image.Height, this.webImageInfo);
+ }
- // There can be optional chunks after the image data, like EXIF and XMP.
- if (this.webImageInfo.Features != null)
- {
- this.ParseOptionalChunks(this.webImageInfo.Features);
- }
+ // There can be optional chunks after the image data, like EXIF and XMP.
+ if (this.webImageInfo.Features != null)
+ {
+ this.ParseOptionalChunks(this.webImageInfo.Features);
+ }
- return image;
+ return image;
+ }
+ }
+ catch
+ {
+ image?.Dispose();
+ throw;
}
}
diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
index 71d0ab34a4..43ec45a34f 100644
--- a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
@@ -20,6 +20,7 @@
namespace SixLabors.ImageSharp.Tests.Formats.Bmp
{
[Trait("Format", "Bmp")]
+ [ValidateDisposedMemoryAllocations]
public class BmpDecoderTests
{
public const PixelTypes CommonNonDefaultPixelTypes = PixelTypes.Rgba32 | PixelTypes.Bgra32 | PixelTypes.RgbaVector;
diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs
index 1922b161f2..7a5241c5a8 100644
--- a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs
@@ -18,6 +18,7 @@
namespace SixLabors.ImageSharp.Tests.Formats.Gif
{
[Trait("Format", "Gif")]
+ [ValidateDisposedMemoryAllocations]
public class GifDecoderTests
{
private const PixelTypes TestPixelTypes = PixelTypes.Rgba32 | PixelTypes.RgbaVector | PixelTypes.Argb32;
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs
index 9864d62b8f..d9915f17d6 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs
@@ -179,11 +179,16 @@ private static void TestImageInfo(string imagePath, IImageDecoder decoder, bool
var testFile = TestFile.Create(imagePath);
using (var stream = new MemoryStream(testFile.Bytes, false))
{
- IImageInfo imageInfo = useIdentify
- ? ((IImageInfoDetector)decoder).Identify(Configuration.Default, stream, default)
- : decoder.Decode(Configuration.Default, stream, default);
-
- test(imageInfo);
+ if (useIdentify)
+ {
+ IImageInfo imageInfo = ((IImageInfoDetector)decoder).Identify(Configuration.Default, stream, default);
+ test(imageInfo);
+ }
+ else
+ {
+ using var img = decoder.Decode(Configuration.Default, stream, default);
+ test(img);
+ }
}
}
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
index faf40ccf7d..1faa6f0f4c 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
@@ -22,6 +22,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{
// TODO: Scatter test cases into multiple test classes
[Trait("Format", "Jpg")]
+ [ValidateDisposedMemoryAllocations]
public partial class JpegDecoderTests
{
public const PixelTypes CommonNonDefaultPixelTypes = PixelTypes.Rgba32 | PixelTypes.Argb32 | PixelTypes.Bgr24 | PixelTypes.RgbaVector;
diff --git a/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs
index 97237bca59..eb3bc8c9a5 100644
--- a/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs
@@ -11,6 +11,7 @@
namespace SixLabors.ImageSharp.Tests.Formats.Pbm
{
[Trait("Format", "Pbm")]
+ [ValidateDisposedMemoryAllocations]
public class PbmDecoderTests
{
[Theory]
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
index 752036126f..a4fcf63baf 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
@@ -19,6 +19,7 @@
namespace SixLabors.ImageSharp.Tests.Formats.Png
{
[Trait("Format", "Png")]
+ [ValidateDisposedMemoryAllocations]
public partial class PngDecoderTests
{
private const PixelTypes TestPixelTypes = PixelTypes.Rgba32 | PixelTypes.RgbaVector | PixelTypes.Argb32;
diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs
index 55dc2ecdd8..e83c5a98cc 100644
--- a/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs
@@ -16,6 +16,7 @@
namespace SixLabors.ImageSharp.Tests.Formats.Tga
{
[Trait("Format", "Tga")]
+ [ValidateDisposedMemoryAllocations]
public class TgaDecoderTests
{
private static TgaDecoder TgaDecoder => new();
diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
index 39fa50c93c..9460f3a351 100644
--- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
@@ -14,6 +14,7 @@
namespace SixLabors.ImageSharp.Tests.Formats.Tiff
{
[Trait("Format", "Tiff")]
+ [ValidateDisposedMemoryAllocations]
public class TiffDecoderTests : TiffDecoderBaseTester
{
public static readonly string[] MultiframeTestImages = Multiframes;
diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs
index 1c92fdf335..f29fa5d793 100644
--- a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs
@@ -13,6 +13,7 @@
namespace SixLabors.ImageSharp.Tests.Formats.Webp
{
[Trait("Format", "Webp")]
+ [ValidateDisposedMemoryAllocations]
public class WebpDecoderTests
{
private static WebpDecoder WebpDecoder => new();
diff --git a/tests/ImageSharp.Tests/Image/ImageTests.LoadPixelData.cs b/tests/ImageSharp.Tests/Image/ImageTests.LoadPixelData.cs
index a4044b906c..7683ee6889 100644
--- a/tests/ImageSharp.Tests/Image/ImageTests.LoadPixelData.cs
+++ b/tests/ImageSharp.Tests/Image/ImageTests.LoadPixelData.cs
@@ -14,6 +14,7 @@ public class LoadPixelData
[Theory]
[InlineData(false)]
[InlineData(true)]
+ [ValidateDisposedMemoryAllocations]
public void FromPixels(bool useSpan)
{
Rgba32[] data = { Color.Black, Color.White, Color.White, Color.Black, };
diff --git a/tests/ImageSharp.Tests/Image/LargeImageIntegrationTests.cs b/tests/ImageSharp.Tests/Image/LargeImageIntegrationTests.cs
index 357d02be4b..ce1f902e59 100644
--- a/tests/ImageSharp.Tests/Image/LargeImageIntegrationTests.cs
+++ b/tests/ImageSharp.Tests/Image/LargeImageIntegrationTests.cs
@@ -55,6 +55,8 @@ public void PreferContiguousImageBuffers_LoadImage_BufferIsContiguous(string for
static void RunTest(string formatInner)
{
+ using IDisposable mem = MemoryAllocatorValidator.MonitorAllocations();
+
Configuration configuration = Configuration.Default.Clone();
configuration.PreferContiguousImageBuffers = true;
IImageEncoder encoder = configuration.ImageFormatsManager.FindEncoder(
diff --git a/tests/ImageSharp.Tests/MemoryAllocatorValidator.cs b/tests/ImageSharp.Tests/MemoryAllocatorValidator.cs
new file mode 100644
index 0000000000..13664ee9b2
--- /dev/null
+++ b/tests/ImageSharp.Tests/MemoryAllocatorValidator.cs
@@ -0,0 +1,77 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Threading;
+using SixLabors.ImageSharp.Diagnostics;
+using Xunit;
+
+namespace SixLabors.ImageSharp.Tests
+{
+ public static class MemoryAllocatorValidator
+ {
+ private static readonly AsyncLocal LocalInstance = new();
+
+ public static bool MonitoringAllocations => LocalInstance.Value != null;
+
+ static MemoryAllocatorValidator()
+ {
+ MemoryDiagnostics.MemoryAllocated += MemoryDiagnostics_MemoryAllocated;
+ MemoryDiagnostics.MemoryReleased += MemoryDiagnostics_MemoryReleased;
+ }
+
+ private static void MemoryDiagnostics_MemoryReleased()
+ {
+ TestMemoryDiagnostics backing = LocalInstance.Value;
+ if (backing != null)
+ {
+ backing.TotalRemainingAllocated--;
+ }
+ }
+
+ private static void MemoryDiagnostics_MemoryAllocated()
+ {
+ TestMemoryDiagnostics backing = LocalInstance.Value;
+ if (backing != null)
+ {
+ backing.TotalAllocated++;
+ backing.TotalRemainingAllocated++;
+ }
+ }
+
+ public static TestMemoryDiagnostics MonitorAllocations()
+ {
+ var diag = new TestMemoryDiagnostics();
+ LocalInstance.Value = diag;
+ return diag;
+ }
+
+ public static void StopMonitoringAllocations() => LocalInstance.Value = null;
+
+ public static void ValidateAllocations(int expectedAllocationCount = 0)
+ => LocalInstance.Value?.Validate(expectedAllocationCount);
+
+ public class TestMemoryDiagnostics : IDisposable
+ {
+ public int TotalAllocated { get; set; }
+
+ public int TotalRemainingAllocated { get; set; }
+
+ public void Validate(int expectedAllocationCount)
+ {
+ var count = this.TotalRemainingAllocated;
+ var pass = expectedAllocationCount == count;
+ Assert.True(pass, $"Expected a {expectedAllocationCount} undisposed buffers but found {count}");
+ }
+
+ public void Dispose()
+ {
+ this.Validate(0);
+ if (LocalInstance.Value == this)
+ {
+ StopMonitoringAllocations();
+ }
+ }
+ }
+ }
+}
diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparingUtils.cs b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparingUtils.cs
index 1801d6b590..fa25846748 100644
--- a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparingUtils.cs
+++ b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparingUtils.cs
@@ -26,7 +26,7 @@ public static void CompareWithReferenceDecoder(
}
var testFile = TestFile.Create(path);
- Image magickImage = DecodeWithMagick(new FileInfo(testFile.FullPath));
+ using Image magickImage = DecodeWithMagick(new FileInfo(testFile.FullPath));
if (useExactComparer)
{
ImageComparer.Exact.VerifySimilarity(magickImage, image);
diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs
index e2e7d73bc6..63c5ce31a2 100644
--- a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs
+++ b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs
@@ -7,6 +7,7 @@
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
+using SixLabors.ImageSharp.Diagnostics;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@@ -158,8 +159,13 @@ public override Image GetImage(IImageDecoder decoder)
return this.LoadImage(decoder);
}
- var key = new Key(this.PixelType, this.FilePath, decoder);
+ // do not cache so we can track allocation correctly when validating memory
+ if (MemoryAllocatorValidator.MonitoringAllocations)
+ {
+ return this.LoadImage(decoder);
+ }
+ var key = new Key(this.PixelType, this.FilePath, decoder);
Image cachedImage = Cache.GetOrAdd(key, _ => this.LoadImage(decoder));
return cachedImage.Clone(this.Configuration);
diff --git a/tests/ImageSharp.Tests/ValidateDisposedMemoryAllocationsAttribute.cs b/tests/ImageSharp.Tests/ValidateDisposedMemoryAllocationsAttribute.cs
new file mode 100644
index 0000000000..65ed990dd7
--- /dev/null
+++ b/tests/ImageSharp.Tests/ValidateDisposedMemoryAllocationsAttribute.cs
@@ -0,0 +1,33 @@
+// 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 expected = 0;
+
+ public ValidateDisposedMemoryAllocationsAttribute()
+ : this(0)
+ {
+ }
+
+ public ValidateDisposedMemoryAllocationsAttribute(int expected)
+ => this.expected = expected;
+
+ public override void Before(MethodInfo methodUnderTest)
+ => MemoryAllocatorValidator.MonitorAllocations();
+
+ public override void After(MethodInfo methodUnderTest)
+ {
+ MemoryAllocatorValidator.ValidateAllocations(this.expected);
+ MemoryAllocatorValidator.StopMonitoringAllocations();
+ }
+ }
+}