diff --git a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs index 999a44ff3..eda054968 100644 --- a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs +++ b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs @@ -44,6 +44,8 @@ namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave public double TotalProcessedMegapixels { get; private set; } + public Size LastProcessedImageSize { get; private set; } + private string outputDirectory; public int ImageCount { get; set; } = int.MaxValue; @@ -54,9 +56,6 @@ namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave public int ThumbnailSize { get; set; } = 150; - // Inject leaking memory allocation requests to ImageSharp processing code to stress-test finalizer behavior. - public bool EmulateLeakedAllocations { get; set; } - private static readonly string[] ProgressiveFiles = { "ancyloscelis-apiformis-m-paraguay-face_2014-08-08-095255-zs-pmax_15046500892_o.jpg", @@ -125,8 +124,9 @@ namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave new ParallelOptions { MaxDegreeOfParallelism = this.MaxDegreeOfParallelism }, action); - private void IncreaseTotalMegapixels(int width, int height) + private void LogImageProcessed(int width, int height) { + this.LastProcessedImageSize = new Size(width, height); double pixels = width * (double)height; this.TotalProcessedMegapixels += pixels / 1_000_000.0; } @@ -156,7 +156,7 @@ namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave public void SystemDrawingResize(string input) { using var image = SystemDrawingImage.FromFile(input, true); - this.IncreaseTotalMegapixels(image.Width, image.Height); + this.LogImageProcessed(image.Width, image.Height); (int Width, int Height) scaled = this.ScaledSize(image.Width, image.Height, this.ThumbnailSize); var resized = new Bitmap(scaled.Width, scaled.Height); @@ -182,13 +182,7 @@ namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave // Resize it to fit a 150x150 square using var image = ImageSharpImage.Load(input); - this.IncreaseTotalMegapixels(image.Width, image.Height); - - if (this.EmulateLeakedAllocations) - { - _ = Configuration.Default.MemoryAllocator.Allocate(image.Width * image.Height); - _ = Configuration.Default.MemoryAllocator.Allocate(1 << 21); - } + this.LogImageProcessed(image.Width, image.Height); image.Mutate(i => i.Resize(new ResizeOptions { @@ -201,17 +195,12 @@ namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave // Save the results image.Save(output, this.imageSharpJpegEncoder); - - if (this.EmulateLeakedAllocations) - { - _ = Configuration.Default.MemoryAllocator.Allocate2D(image.Width, image.Height); - } } public void MagickResize(string input) { using var image = new MagickImage(input); - this.IncreaseTotalMegapixels(image.Width, image.Height); + this.LogImageProcessed(image.Width, image.Height); // Resize it to fit a 150x150 square image.Resize(this.ThumbnailSize, this.ThumbnailSize); @@ -246,7 +235,7 @@ namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave public void SkiaCanvasResize(string input) { using var original = SKBitmap.Decode(input); - this.IncreaseTotalMegapixels(original.Width, original.Height); + this.LogImageProcessed(original.Width, original.Height); (int Width, int Height) scaled = this.ScaledSize(original.Width, original.Height, this.ThumbnailSize); using var surface = SKSurface.Create(new SKImageInfo(scaled.Width, scaled.Height, original.ColorType, original.AlphaType)); using var paint = new SKPaint() { FilterQuality = SKFilterQuality.High }; @@ -264,7 +253,7 @@ namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave public void SkiaBitmapResize(string input) { using var original = SKBitmap.Decode(input); - this.IncreaseTotalMegapixels(original.Width, original.Height); + this.LogImageProcessed(original.Width, original.Height); (int Width, int Height) scaled = this.ScaledSize(original.Width, original.Height, this.ThumbnailSize); using var resized = original.Resize(new SKImageInfo(scaled.Width, scaled.Height), SKFilterQuality.High); if (resized == null) @@ -283,7 +272,7 @@ namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave using var codec = SKCodec.Create(input); SKImageInfo info = codec.Info; - this.IncreaseTotalMegapixels(info.Width, info.Height); + this.LogImageProcessed(info.Width, info.Height); (int Width, int Height) scaled = this.ScaledSize(info.Width, info.Height, this.ThumbnailSize); SKSizeI supportedScale = codec.GetScaledDimensions((float)scaled.Width / info.Width); diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs b/tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs index e8b3a744c..c7484daa0 100644 --- a/tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs +++ b/tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics; using System.Globalization; using System.IO; +using System.Runtime.CompilerServices; using System.Text; using System.Threading; using CommandLine; @@ -27,13 +28,19 @@ namespace SixLabors.ImageSharp.Tests.ProfilingSandbox this.Benchmarks.Init(); } + private int gcFrequency; + + private int leakFrequency; + + private int imageCounter; + public LoadResizeSaveStressRunner Benchmarks { get; } public static void Run(string[] args) { Console.WriteLine($"Running: {typeof(LoadResizeSaveParallelMemoryStress).Assembly.Location}"); Console.WriteLine($"64 bit: {Environment.Is64BitProcess}"); - var options = args.Length > 0 ? CommandLineOptions.Parse(args) : null; + CommandLineOptions options = args.Length > 0 ? CommandLineOptions.Parse(args) : null; var lrs = new LoadResizeSaveParallelMemoryStress(); if (options != null) @@ -55,10 +62,12 @@ namespace SixLabors.ImageSharp.Tests.ProfilingSandbox if (!options.KeepDefaultAllocator) { - MemoryAllocator.Default = Configuration.Default.MemoryAllocator = options.CreateMemoryAllocator(); + Configuration.Default.MemoryAllocator = options.CreateMemoryAllocator(); } - lrs.Benchmarks.EmulateLeakedAllocations = options.LeakAllocations; + lrs.leakFrequency = options.LeakFrequency; + lrs.gcFrequency = options.GcFrequency; + timer = Stopwatch.StartNew(); try @@ -80,16 +89,23 @@ namespace SixLabors.ImageSharp.Tests.ProfilingSandbox Configuration.Default.MemoryAllocator.ReleaseRetainedResources(); } - for (int i = 0; i < options.FinalGcCount; i++) + int finalGcCount = -Math.Min(0, options.GcFrequency); + + if (finalGcCount > 0) { - GC.Collect(); - GC.WaitForPendingFinalizers(); - Thread.Sleep(1000); + Console.WriteLine($"TotalOutstandingHandles: {UnmanagedMemoryHandle.TotalOutstandingHandles}"); + Console.WriteLine($"GC x {finalGcCount}, with 3 seconds wait."); + for (int i = 0; i < finalGcCount; i++) + { + Thread.Sleep(3000); + GC.Collect(); + GC.WaitForPendingFinalizers(); + } } } var stats = new Stats(timer, lrs.Benchmarks.TotalProcessedMegapixels); - Console.WriteLine($"Total Megapixels: {stats.TotalMegapixels}, TotalOomRetries: {UnmanagedMemoryHandle.TotalOomRetries}"); + Console.WriteLine($"Total Megapixels: {stats.TotalMegapixels}, TotalOomRetries: {UnmanagedMemoryHandle.TotalOomRetries}, TotalOutstandingHandles: {UnmanagedMemoryHandle.TotalOutstandingHandles}, Total Gen2 GC count: {GC.CollectionCount(2)}"); Console.WriteLine(stats.GetMarkdown()); if (options?.FileOutput != null) { @@ -121,8 +137,9 @@ namespace SixLabors.ImageSharp.Tests.ProfilingSandbox 2. ImageSharp 3. MagicScaler 4. SkiaSharp -5. NetVips -6. ImageMagick +5. SkiaSharp - Decode to target size +6. NetVips +7. ImageMagick "); ConsoleKey key = Console.ReadKey().Key; @@ -149,9 +166,12 @@ namespace SixLabors.ImageSharp.Tests.ProfilingSandbox lrs.SkiaBitmapBenchmarkParallel(); break; case ConsoleKey.D5: - lrs.NetVipsBenchmarkParallel(); + lrs.SkiaBitmapDecodeToTargetSizeBenchmarkParallel(); break; case ConsoleKey.D6: + lrs.NetVipsBenchmarkParallel(); + break; + case ConsoleKey.D7: lrs.MagickBenchmarkParallel(); break; } @@ -222,8 +242,8 @@ namespace SixLabors.ImageSharp.Tests.ProfilingSandbox [Option('r', "repeat-count", Required = false, Default = 1, HelpText = "Times to run the whole benchmark")] public int RepeatCount { get; set; } = 1; - [Option('g', "final-gc-count", Required = false, Default = 0, HelpText = "How many times to GC.Collect after execution")] - public int FinalGcCount { get; set; } + [Option('g', "gc-frequency", Required = false, Default = 0, HelpText = "Positive number: call GC every 'g'-th resize. Negative number: call GC '-g' times in the end.")] + public int GcFrequency { get; set; } [Option('e', "release-at-end", Required = false, Default = false, HelpText = "Specify to run ReleaseRetainedResources() after execution")] public bool ReleaseRetainedResourcesAtEnd { get; set; } @@ -237,8 +257,8 @@ namespace SixLabors.ImageSharp.Tests.ProfilingSandbox [Option('t', "trim-period", Required = false, Default = null, HelpText = "Trim period for the pool in seconds")] public int? TrimTimeSeconds { get; set; } - [Option('l', "leak-allocations", Required = false, Default = false, HelpText = "Inject leaking memory allocation requests to stress-test finalizer behavior.")] - public bool LeakAllocations { get; set; } + [Option('l', "leak-frequency", Required = false, Default = 0, HelpText = "Inject leaking memory allocations after every 'l'-th resize to stress test finalizer behavior.")] + public int LeakFrequency { get; set; } public static CommandLineOptions Parse(string[] args) { @@ -257,7 +277,7 @@ namespace SixLabors.ImageSharp.Tests.ProfilingSandbox } public override string ToString() => - $"p({this.MaxDegreeOfParallelism})_i({this.ImageSharp})_d({this.KeepDefaultAllocator})_m({this.MaxContiguousPoolBufferMegaBytes})_s({this.MaxPoolSizeMegaBytes})_u({this.MaxCapacityOfNonPoolBuffersMegaBytes})_r({this.RepeatCount})_g({this.FinalGcCount})_e({this.ReleaseRetainedResourcesAtEnd}_l({this.LeakAllocations}))"; + $"p({this.MaxDegreeOfParallelism})_i({this.ImageSharp})_d({this.KeepDefaultAllocator})_m({this.MaxContiguousPoolBufferMegaBytes})_s({this.MaxPoolSizeMegaBytes})_u({this.MaxCapacityOfNonPoolBuffersMegaBytes})_r({this.RepeatCount})_g({this.GcFrequency})_e({this.ReleaseRetainedResourcesAtEnd})_l({this.LeakFrequency})"; public MemoryAllocator CreateMemoryAllocator() { @@ -290,28 +310,32 @@ namespace SixLabors.ImageSharp.Tests.ProfilingSandbox private void SystemDrawingBenchmarkParallel() => this.ForEachImage(this.Benchmarks.SystemDrawingResize); - private void ImageSharpBenchmarkParallel() - { - int cnt = 0; + private void ImageSharpBenchmarkParallel() => this.ForEachImage(f => { + int cnt = Interlocked.Increment(ref this.imageCounter); this.Benchmarks.ImageSharpResize(f); - if (cnt % 4 == 0 && this.Benchmarks.EmulateLeakedAllocations) + if (this.leakFrequency > 0 && cnt % this.leakFrequency == 0) + { + _ = Configuration.Default.MemoryAllocator.Allocate(1 << 16); + Size size = this.Benchmarks.LastProcessedImageSize; + _ = new Image(size.Width, size.Height); + } + + if (this.gcFrequency > 0 && cnt % this.gcFrequency == 0) { GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); } - - cnt++; }); - } private void MagickBenchmarkParallel() => this.ForEachImage(this.Benchmarks.MagickResize); private void MagicScalerBenchmarkParallel() => this.ForEachImage(this.Benchmarks.MagicScalerResize); private void SkiaBitmapBenchmarkParallel() => this.ForEachImage(this.Benchmarks.SkiaBitmapResize); + private void SkiaBitmapDecodeToTargetSizeBenchmarkParallel() => this.ForEachImage(this.Benchmarks.SkiaBitmapDecodeToTargetSize); private void NetVipsBenchmarkParallel() => this.ForEachImage(this.Benchmarks.NetVipsResize); } diff --git a/tests/ImageSharp.Tests/RunTestsInLoop.ps1 b/tests/ImageSharp.Tests/RunTestsInLoop.ps1 index ef4bd5ccb..c7c5c9ac5 100644 --- a/tests/ImageSharp.Tests/RunTestsInLoop.ps1 +++ b/tests/ImageSharp.Tests/RunTestsInLoop.ps1 @@ -1,16 +1,17 @@ # This script can be used to collect logs from sporadic bugs Param( [int]$TestRunCount=10, - [string]$TargetFramework="netcoreapp3.1" + [string]$TargetFramework="netcoreapp3.1", + [string]$Configuration="Release" ) $runId = Get-Random -Minimum 0 -Maximum 9999 -dotnet build -c Release -f $TargetFramework +dotnet build -c $Configuration -f $TargetFramework for ($i = 0; $i -lt $TestRunCount; $i++) { $logFile = ".\_testlog-" + $runId.ToString("d4") + "-run-" + $i.ToString("d3") + ".log" Write-Host "Test run $i ..." - & dotnet test --no-build -c Release -f $TargetFramework 3>&1 2>&1 > $logFile + & dotnet test --no-build -c $Configuration -f $TargetFramework 3>&1 2>&1 > $logFile if ($LastExitCode -eq 0) { Write-Host "Success!" Remove-Item $logFile