diff --git a/tests/Directory.Build.targets b/tests/Directory.Build.targets
index 9c17881452..5ca7d2b937 100644
--- a/tests/Directory.Build.targets
+++ b/tests/Directory.Build.targets
@@ -29,6 +29,13 @@
+
+
+
+
+
+
+
diff --git a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj
index 17f6068d40..30fbbbda98 100644
--- a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj
+++ b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj
@@ -9,7 +9,7 @@
false
Debug;Release;Debug-InnerLoop;Release-InnerLoop
-
+ 9
@@ -41,6 +41,12 @@
+
+
+
+
+
+
diff --git a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs
new file mode 100644
index 0000000000..77585213fd
--- /dev/null
+++ b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs
@@ -0,0 +1,221 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Drawing;
+using System.Drawing.Drawing2D;
+using System.Drawing.Imaging;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using FreeImageAPI;
+using ImageMagick;
+using PhotoSauce.MagicScaler;
+using SixLabors.ImageSharp.Formats.Jpeg;
+using SixLabors.ImageSharp.Processing;
+using SixLabors.ImageSharp.Tests;
+using SkiaSharp;
+using ImageSharpImage = SixLabors.ImageSharp.Image;
+using ImageSharpSize = SixLabors.ImageSharp.Size;
+using NetVipsImage = NetVips.Image;
+using SystemDrawingImage = System.Drawing.Image;
+
+namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave
+{
+ public class LoadResizeSaveStressRunner
+ {
+ private const int ThumbnailSize = 150;
+ private const int Quality = 75;
+ private const string ImageSharp = nameof(ImageSharp);
+ private const string SystemDrawing = nameof(SystemDrawing);
+ private const string MagickNET = nameof(MagickNET);
+ private const string NetVips = nameof(NetVips);
+ private const string FreeImage = nameof(FreeImage);
+ private const string MagicScaler = nameof(MagicScaler);
+ private const string SkiaSharpCanvas = nameof(SkiaSharpCanvas);
+ private const string SkiaSharpBitmap = nameof(SkiaSharpBitmap);
+
+ // Set the quality for ImagSharp
+ private readonly JpegEncoder imageSharpJpegEncoder = new () { Quality = Quality };
+ private readonly ImageCodecInfo systemDrawingJpegCodec =
+ ImageCodecInfo.GetImageEncoders().First(codec => codec.FormatID == ImageFormat.Jpeg.Guid);
+
+ public string[] Images { get; private set; }
+
+ private string outputDirectory;
+
+ public int ImageCount { get; set; } = int.MaxValue;
+
+ public void Init()
+ {
+ if (RuntimeInformation.OSArchitecture is Architecture.X86 or Architecture.X64)
+ {
+ // Workaround ImageMagick issue
+ OpenCL.IsEnabled = false;
+ }
+
+ string imageDirectory = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, "MemoryStress");
+ if (!Directory.Exists(imageDirectory) || !Directory.EnumerateFiles(imageDirectory).Any())
+ {
+ throw new DirectoryNotFoundException($"Copy stress images to: {imageDirectory}");
+ }
+
+ // Get at most this.ImageCount images from there
+ this.Images = Directory.EnumerateFiles(imageDirectory).Take(this.ImageCount).ToArray();
+
+ // Create the output directory next to the images directory
+ this.outputDirectory = TestEnvironment.CreateOutputDirectory("MemoryStress");
+ }
+
+ private string OutputPath(string inputPath, string postfix) =>
+ Path.Combine(
+ this.outputDirectory,
+ Path.GetFileNameWithoutExtension(inputPath) + "-" + postfix + Path.GetExtension(inputPath));
+
+ private (int width, int height) ScaledSize(int inWidth, int inHeight, int outSize)
+ {
+ int width, height;
+ if (inWidth > inHeight)
+ {
+ width = outSize;
+ height = (int)Math.Round(inHeight * outSize / (double)inWidth);
+ }
+ else
+ {
+ width = (int)Math.Round(inWidth * outSize / (double)inHeight);
+ height = outSize;
+ }
+
+ return (width, height);
+ }
+
+ public void SystemDrawingResize(string input)
+ {
+ using var image = SystemDrawingImage.FromFile(input, true);
+ (int width, int height) scaled = this.ScaledSize(image.Width, image.Height, ThumbnailSize);
+ var resized = new Bitmap(scaled.width, scaled.height);
+ using var graphics = Graphics.FromImage(resized);
+ using var attributes = new ImageAttributes();
+ attributes.SetWrapMode(WrapMode.TileFlipXY);
+ graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
+ graphics.CompositingMode = CompositingMode.SourceCopy;
+ graphics.CompositingQuality = CompositingQuality.AssumeLinear;
+ graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
+ graphics.DrawImage(image, System.Drawing.Rectangle.FromLTRB(0, 0, resized.Width, resized.Height), 0, 0, image.Width, image.Height, GraphicsUnit.Pixel, attributes);
+
+ // Save the results
+ using var encoderParams = new EncoderParameters(1);
+ using var qualityParam = new EncoderParameter(Encoder.Quality, (long)Quality);
+ encoderParams.Param[0] = qualityParam;
+ resized.Save(this.OutputPath(input, SystemDrawing), this.systemDrawingJpegCodec, encoderParams);
+ }
+
+ public void ImageSharpResize(string input)
+ {
+ using FileStream output = File.Open(this.OutputPath(input, ImageSharp), FileMode.Create);
+
+ // Resize it to fit a 150x150 square
+ using var image = ImageSharpImage.Load(input);
+ image.Mutate(i => i.Resize(new ResizeOptions
+ {
+ Size = new ImageSharpSize(ThumbnailSize, ThumbnailSize),
+ Mode = ResizeMode.Max
+ }));
+
+ // Reduce the size of the file
+ image.Metadata.ExifProfile = null;
+
+ // Save the results
+ image.Save(output, this.imageSharpJpegEncoder);
+ }
+
+ public void MagickResize(string input)
+ {
+ using var image = new MagickImage(input);
+
+ // Resize it to fit a 150x150 square
+ image.Resize(ThumbnailSize, ThumbnailSize);
+
+ // Reduce the size of the file
+ image.Strip();
+
+ // Set the quality
+ image.Quality = Quality;
+
+ // Save the results
+ image.Write(this.OutputPath(input, MagickNET));
+ }
+
+ public void FreeImageResize(string input)
+ {
+ using var original = FreeImageBitmap.FromFile(input);
+ (int width, int height) scaled = this.ScaledSize(original.Width, original.Height, ThumbnailSize);
+ var resized = new FreeImageBitmap(original, scaled.width, scaled.height);
+
+ // JPEG_QUALITYGOOD is 75 JPEG.
+ // JPEG_BASELINE strips metadata (EXIF, etc.)
+ resized.Save(
+ this.OutputPath(input, FreeImage),
+ FREE_IMAGE_FORMAT.FIF_JPEG,
+ FREE_IMAGE_SAVE_FLAGS.JPEG_QUALITYGOOD | FREE_IMAGE_SAVE_FLAGS.JPEG_BASELINE);
+ }
+
+ public void MagicScalerResize(string input)
+ {
+ var settings = new ProcessImageSettings()
+ {
+ Width = ThumbnailSize,
+ Height = ThumbnailSize,
+ ResizeMode = CropScaleMode.Max,
+ SaveFormat = FileFormat.Jpeg,
+ JpegQuality = Quality,
+ JpegSubsampleMode = ChromaSubsampleMode.Subsample420
+ };
+
+ using var output = new FileStream(this.OutputPath(input, MagicScaler), FileMode.Create);
+ MagicImageProcessor.ProcessImage(input, output, settings);
+ }
+
+ public void SkiaCanvasResize(string input)
+ {
+ using var original = SKBitmap.Decode(input);
+ (int width, int height) scaled = this.ScaledSize(original.Width, original.Height, ThumbnailSize);
+ using var surface = SKSurface.Create(new SKImageInfo(scaled.width, scaled.height, original.ColorType, original.AlphaType));
+ using var paint = new SKPaint() { FilterQuality = SKFilterQuality.High };
+ SKCanvas canvas = surface.Canvas;
+ canvas.Scale((float)scaled.width / original.Width);
+ canvas.DrawBitmap(original, 0, 0, paint);
+ canvas.Flush();
+
+ using FileStream output = File.OpenWrite(this.OutputPath(input, SkiaSharpCanvas));
+ surface.Snapshot()
+ .Encode(SKEncodedImageFormat.Jpeg, Quality)
+ .SaveTo(output);
+ }
+
+ public void SkiaBitmapResize(string input)
+ {
+ using var original = SKBitmap.Decode(input);
+ (int width, int height) scaled = this.ScaledSize(original.Width, original.Height, ThumbnailSize);
+ using var resized = original.Resize(new SKImageInfo(scaled.width, scaled.height), SKFilterQuality.High);
+ if (resized == null)
+ {
+ return;
+ }
+
+ using var image = SKImage.FromBitmap(resized);
+ using FileStream output = File.OpenWrite(this.OutputPath(input, SkiaSharpBitmap));
+ image.Encode(SKEncodedImageFormat.Jpeg, Quality)
+ .SaveTo(output);
+ }
+
+ public void NetVipsResize(string input)
+ {
+ // Thumbnail to fit a 150x150 square
+ using var thumb = NetVipsImage.Thumbnail(input, ThumbnailSize, ThumbnailSize);
+
+ // Save the results
+ thumb.Jpegsave(this.OutputPath(input, NetVips), q: Quality, strip: true);
+ }
+ }
+}
diff --git a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStress_NonParallel.cs b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStress_NonParallel.cs
new file mode 100644
index 0000000000..99aeaad5e6
--- /dev/null
+++ b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStress_NonParallel.cs
@@ -0,0 +1,52 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using BenchmarkDotNet.Attributes;
+
+namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave
+{
+ public class LoadResizeSaveStress_NonParallel
+ {
+ private LoadResizeSaveStressRunner benchmarks;
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ this.benchmarks = new LoadResizeSaveStressRunner() { ImageCount = 20 };
+ this.benchmarks.Init();
+ }
+
+ private void ForEachImage(Action action)
+ {
+ foreach (string image in this.benchmarks.Images)
+ {
+ action(image);
+ }
+ }
+
+ [Benchmark(Baseline = true, Description = "System.Drawing Load, Resize, Save")]
+ public void SystemDrawingBenchmark() => this.ForEachImage(this.benchmarks.SystemDrawingResize);
+
+ [Benchmark(Description = "ImageSharp Load, Resize, Save")]
+ public void ImageSharpBenchmark() => this.ForEachImage(this.benchmarks.ImageSharpResize);
+
+ [Benchmark(Description = "ImageMagick Load, Resize, Save")]
+ public void MagickBenchmark() => this.ForEachImage(this.benchmarks.MagickResize);
+
+ [Benchmark(Description = "ImageFree Load, Resize, Save")]
+ public void FreeImageBenchmark() => this.ForEachImage(this.benchmarks.FreeImageResize);
+
+ [Benchmark(Description = "MagicScaler Load, Resize, Save")]
+ public void MagicScalerBenchmark() => this.ForEachImage(this.benchmarks.MagicScalerResize);
+
+ [Benchmark(Description = "SkiaSharp Canvas Load, Resize, Save")]
+ public void SkiaCanvasBenchmark() => this.ForEachImage(this.benchmarks.SkiaCanvasResize);
+
+ [Benchmark(Description = "SkiaSharp Bitmap Load, Resize, Save")]
+ public void SkiaBitmapBenchmark() => this.ForEachImage(this.benchmarks.SkiaBitmapResize);
+
+ [Benchmark(Description = "NetVips Load, Resize, Save")]
+ public void NetVipsBenchmark() => this.ForEachImage(this.benchmarks.NetVipsResize);
+ }
+}
diff --git a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStress_Parallel.cs b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStress_Parallel.cs
new file mode 100644
index 0000000000..78f02b71e0
--- /dev/null
+++ b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStress_Parallel.cs
@@ -0,0 +1,48 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Threading.Tasks;
+using BenchmarkDotNet.Attributes;
+
+namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave
+{
+ [MemoryDiagnoser]
+ public class LoadResizeSaveStress_Parallel
+ {
+ private LoadResizeSaveStressRunner benchmarks;
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ this.benchmarks = new LoadResizeSaveStressRunner() { ImageCount = 20 };
+ this.benchmarks.Init();
+ }
+
+ private void ForEachImage(Action action) => Parallel.ForEach(this.benchmarks.Images, action);
+
+ [Benchmark(Baseline = true, Description = "System.Drawing Load, Resize, Save - Parallel")]
+ public void SystemDrawingBenchmarkParallel() => this.ForEachImage(this.benchmarks.SystemDrawingResize);
+
+ [Benchmark(Description = "ImageSharp Load, Resize, Save - Parallel")]
+ public void ImageSharpBenchmarkParallel() => this.ForEachImage(this.benchmarks.ImageSharpResize);
+
+ [Benchmark(Description = "ImageMagick Load, Resize, Save - Parallel")]
+ public void MagickBenchmarkParallel() => this.ForEachImage(this.benchmarks.MagickResize);
+
+ [Benchmark(Description = "ImageFree Load, Resize, Save - Parallel")]
+ public void FreeImageBenchmarkParallel() => this.ForEachImage(this.benchmarks.FreeImageResize);
+
+ [Benchmark(Description = "MagicScaler Load, Resize, Save - Parallel")]
+ public void MagicScalerBenchmarkParallel() => this.ForEachImage(this.benchmarks.MagicScalerResize);
+
+ [Benchmark(Description = "SkiaSharp Canvas Load, Resize, Save - Parallel")]
+ public void SkiaCanvasBenchmarkParallel() => this.ForEachImage(this.benchmarks.SkiaCanvasResize);
+
+ [Benchmark(Description = "SkiaSharp Bitmap Load, Resize, Save - Parallel")]
+ public void SkiaBitmapBenchmarkParallel() => this.ForEachImage(this.benchmarks.SkiaBitmapResize);
+
+ [Benchmark(Description = "NetVips Load, Resize, Save - Parallel")]
+ public void NetVipsBenchmarkParallel() => this.ForEachImage(this.benchmarks.NetVipsResize);
+ }
+}
diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj b/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj
index a60ac604f1..c4fd2bf701 100644
--- a/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj
+++ b/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj
@@ -14,6 +14,7 @@
false
Debug;Release;Debug-InnerLoop;Release-InnerLoop
false
+ 9
@@ -31,6 +32,7 @@
+
diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs b/tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs
new file mode 100644
index 0000000000..61bdc33b34
--- /dev/null
+++ b/tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs
@@ -0,0 +1,100 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Diagnostics;
+using System.Threading.Tasks;
+using SixLabors.ImageSharp.Benchmarks.LoadResizeSave;
+
+namespace SixLabors.ImageSharp.Tests.ProfilingSandbox
+{
+ internal class LoadResizeSaveParallelMemoryStress
+ {
+ private readonly LoadResizeSaveStressRunner benchmarks;
+
+ public LoadResizeSaveParallelMemoryStress()
+ {
+ this.benchmarks = new LoadResizeSaveStressRunner();
+ this.benchmarks.Init();
+ }
+
+ public static void Run()
+ {
+ Console.WriteLine(@"Choose a library for image resizing stress test:
+
+1. System.Drawing
+2. ImageSharp
+3. MagicScaler
+4. SkiaSharp
+5. NetVips
+6. ImageMagick
+7. FreeImage
+");
+
+ ConsoleKey key = Console.ReadKey().Key;
+ if (key < ConsoleKey.D1 || key > ConsoleKey.D7)
+ {
+ Console.WriteLine("Unrecognized command.");
+ return;
+ }
+
+ try
+ {
+ var lrs = new LoadResizeSaveParallelMemoryStress();
+ Console.WriteLine("\nRunning...");
+ var timer = new Stopwatch();
+ timer.Start();
+
+ switch (key)
+ {
+ case ConsoleKey.D1:
+ lrs.SystemDrawingBenchmarkParallel();
+ break;
+ case ConsoleKey.D2:
+ lrs.ImageSharpBenchmarkParallel();
+ break;
+ case ConsoleKey.D3:
+ lrs.MagicScalerBenchmarkParallel();
+ break;
+ case ConsoleKey.D4:
+ lrs.SkiaCanvasBenchmarkParallel();
+ break;
+ case ConsoleKey.D5:
+ lrs.NetVipsBenchmarkParallel();
+ break;
+ case ConsoleKey.D6:
+ lrs.MagickBenchmarkParallel();
+ break;
+ case ConsoleKey.D7:
+ lrs.FreeImageBenchmarkParallel();
+ break;
+ }
+
+ timer.Stop();
+ Console.WriteLine($"Completed in {timer.ElapsedMilliseconds / 1000.0:f3}sec");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine(ex.ToString());
+ }
+ }
+
+ private void ForEachImage(Action action) => Parallel.ForEach(this.benchmarks.Images, action);
+
+ private void SystemDrawingBenchmarkParallel() => this.ForEachImage(this.benchmarks.SystemDrawingResize);
+
+ private void ImageSharpBenchmarkParallel() => this.ForEachImage(this.benchmarks.ImageSharpResize);
+
+ private void MagickBenchmarkParallel() => this.ForEachImage(this.benchmarks.MagickResize);
+
+ private void FreeImageBenchmarkParallel() => this.ForEachImage(this.benchmarks.FreeImageResize);
+
+ private void MagicScalerBenchmarkParallel() => this.ForEachImage(this.benchmarks.MagicScalerResize);
+
+ private void SkiaCanvasBenchmarkParallel() => this.ForEachImage(this.benchmarks.SkiaCanvasResize);
+
+ private void SkiaBitmapBenchmarkParallel() => this.ForEachImage(this.benchmarks.SkiaBitmapResize);
+
+ private void NetVipsBenchmarkParallel() => this.ForEachImage(this.benchmarks.NetVipsResize);
+ }
+}
diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/Program.cs b/tests/ImageSharp.Tests.ProfilingSandbox/Program.cs
index 50a930b6f1..8e03fbbec4 100644
--- a/tests/ImageSharp.Tests.ProfilingSandbox/Program.cs
+++ b/tests/ImageSharp.Tests.ProfilingSandbox/Program.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0.
using System;
+using System.Diagnostics;
using SixLabors.ImageSharp.Tests.Formats.Jpg;
using SixLabors.ImageSharp.Tests.PixelFormats.PixelOperations;
using SixLabors.ImageSharp.Tests.ProfilingBenchmarks;
@@ -31,7 +32,8 @@ namespace SixLabors.ImageSharp.Tests.ProfilingSandbox
///
public static void Main(string[] args)
{
- RunJpegEncoderProfilingTests();
+ LoadResizeSaveParallelMemoryStress.Run();
+ // RunJpegEncoderProfilingTests();
// RunJpegColorProfilingTests();
// RunDecodeJpegProfilingTests();
// RunToVector4ProfilingTest();