diff --git a/tests/Directory.Build.targets b/tests/Directory.Build.targets
index 8c88ff647d..6b25509ed8 100644
--- a/tests/Directory.Build.targets
+++ b/tests/Directory.Build.targets
@@ -24,7 +24,7 @@
Do not update to 14+ yet. There's differnce in how the BMP decoder handles rounding in 16 bit images.
See https://github.com/ImageMagick/ImageMagick/commit/27a0a9c37f18af9c8d823a3ea076f600843b553c
-->
-
+
diff --git a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs
index f8bf19d576..804a60e2cc 100644
--- a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs
+++ b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs
@@ -6,6 +6,7 @@ using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
using ImageMagick;
using PhotoSauce.MagicScaler;
using SixLabors.ImageSharp.Formats;
@@ -27,7 +28,8 @@ public enum JpegKind
Any = Baseline | Progressive
}
-public class LoadResizeSaveStressRunner
+[SupportedOSPlatform("windows")]
+public sealed class LoadResizeSaveStressRunner
{
private const int Quality = 75;
@@ -158,7 +160,7 @@ public class LoadResizeSaveStressRunner
this.outputDirectory,
Path.GetFileNameWithoutExtension(inputPath) + "-" + postfix + Path.GetExtension(inputPath));
- private (int Width, int Height) ScaledSize(int inWidth, int inHeight, int outSize)
+ private static (int Width, int Height) ScaledSize(int inWidth, int inHeight, int outSize)
{
int width, height;
if (inWidth > inHeight)
@@ -180,7 +182,7 @@ public class LoadResizeSaveStressRunner
using SystemDrawingImage image = SystemDrawingImage.FromFile(input, true);
this.LogImageProcessed(image.Width, image.Height);
- (int width, int height) = this.ScaledSize(image.Width, image.Height, this.ThumbnailSize);
+ (int width, int height) = ScaledSize(image.Width, image.Height, this.ThumbnailSize);
Bitmap resized = new(width, height);
using Graphics graphics = Graphics.FromImage(resized);
using ImageAttributes attributes = new();
@@ -248,10 +250,10 @@ public class LoadResizeSaveStressRunner
public void MagickResize(string input)
{
using MagickImage image = new(input);
- this.LogImageProcessed(image.Width, image.Height);
+ this.LogImageProcessed((int)image.Width, (int)image.Height);
// Resize it to fit a 150x150 square
- image.Resize(this.ThumbnailSize, this.ThumbnailSize);
+ image.Resize((uint)this.ThumbnailSize, (uint)this.ThumbnailSize);
// Reduce the size of the file
image.Strip();
@@ -282,7 +284,7 @@ public class LoadResizeSaveStressRunner
{
using SKBitmap original = SKBitmap.Decode(input);
this.LogImageProcessed(original.Width, original.Height);
- (int width, int height) = this.ScaledSize(original.Width, original.Height, this.ThumbnailSize);
+ (int width, int height) = ScaledSize(original.Width, original.Height, this.ThumbnailSize);
using SKSurface surface = SKSurface.Create(new SKImageInfo(width, height, original.ColorType, original.AlphaType));
using SKPaint paint = new() { FilterQuality = SKFilterQuality.High };
SKCanvas canvas = surface.Canvas;
@@ -300,7 +302,7 @@ public class LoadResizeSaveStressRunner
{
using SKBitmap original = SKBitmap.Decode(input);
this.LogImageProcessed(original.Width, original.Height);
- (int width, int height) = this.ScaledSize(original.Width, original.Height, this.ThumbnailSize);
+ (int width, int height) = ScaledSize(original.Width, original.Height, this.ThumbnailSize);
using SKBitmap resized = original.Resize(new SKImageInfo(width, height), SKFilterQuality.High);
if (resized == null)
{
@@ -319,7 +321,7 @@ public class LoadResizeSaveStressRunner
SKImageInfo info = codec.Info;
this.LogImageProcessed(info.Width, info.Height);
- (int width, int height) = this.ScaledSize(info.Width, info.Height, this.ThumbnailSize);
+ (int width, int height) = ScaledSize(info.Width, info.Height, this.ThumbnailSize);
SKSizeI supportedScale = codec.GetScaledDimensions((float)width / info.Width);
using SKBitmap original = SKBitmap.Decode(codec, new SKImageInfo(supportedScale.Width, supportedScale.Height));
diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs b/tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs
index 6850756dfe..5e21b7cc1a 100644
--- a/tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs
+++ b/tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs
@@ -3,6 +3,7 @@
using System.Diagnostics;
using System.Globalization;
+using System.Runtime.Versioning;
using System.Text;
using CommandLine;
using CommandLine.Text;
@@ -13,7 +14,8 @@ using SixLabors.ImageSharp.Memory.Internals;
namespace SixLabors.ImageSharp.Tests.ProfilingSandbox;
// See ImageSharp.Benchmarks/LoadResizeSave/README.md
-internal class LoadResizeSaveParallelMemoryStress
+[SupportedOSPlatform("windows")]
+internal sealed class LoadResizeSaveParallelMemoryStress
{
private LoadResizeSaveParallelMemoryStress()
{
@@ -206,14 +208,15 @@ internal class LoadResizeSaveParallelMemoryStress
StringBuilder bld = new();
bld.AppendLine($"| {nameof(this.TotalSeconds)} | {nameof(this.MegapixelsPerSec)} | {nameof(this.MegapixelsPerSecPerCpu)} |");
bld.AppendLine(
+ CultureInfo.InvariantCulture,
$"| {L(nameof(this.TotalSeconds))} | {L(nameof(this.MegapixelsPerSec))} | {L(nameof(this.MegapixelsPerSecPerCpu))} |");
bld.Append("| ");
- bld.AppendFormat(F(nameof(this.TotalSeconds)), this.TotalSeconds);
+ bld.AppendFormat(CultureInfo.InvariantCulture, F(nameof(this.TotalSeconds)), this.TotalSeconds);
bld.Append(" | ");
- bld.AppendFormat(F(nameof(this.MegapixelsPerSec)), this.MegapixelsPerSec);
+ bld.AppendFormat(CultureInfo.InvariantCulture, F(nameof(this.MegapixelsPerSec)), this.MegapixelsPerSec);
bld.Append(" | ");
- bld.AppendFormat(F(nameof(this.MegapixelsPerSecPerCpu)), this.MegapixelsPerSecPerCpu);
+ bld.AppendFormat(CultureInfo.InvariantCulture, F(nameof(this.MegapixelsPerSecPerCpu)), this.MegapixelsPerSecPerCpu);
bld.AppendLine(" |");
return bld.ToString();
@@ -223,7 +226,7 @@ internal class LoadResizeSaveParallelMemoryStress
}
}
- private class CommandLineOptions
+ private sealed class CommandLineOptions
{
[Option('a', "async-imagesharp", Required = false, Default = false, HelpText = "Async ImageSharp without benchmark switching")]
public bool AsyncImageSharp { get; set; }
diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/ParallelProcessingStress.Experiment.cs b/tests/ImageSharp.Tests.ProfilingSandbox/ParallelProcessingStress.Experiment.cs
new file mode 100644
index 0000000000..ffe09a35b3
--- /dev/null
+++ b/tests/ImageSharp.Tests.ProfilingSandbox/ParallelProcessingStress.Experiment.cs
@@ -0,0 +1,88 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using CommandLine;
+using CommandLine.Text;
+
+namespace SixLabors.ImageSharp.Tests.ProfilingSandbox;
+
+public partial class ParallelProcessingStress
+{
+ public static void RunExperiment(string[] args)
+ {
+ ExperimentOptions options = null;
+ using Parser parser = new(settings => settings.CaseInsensitiveEnumValues = true);
+ ParserResult result = parser.ParseArguments(args).WithParsed(o => options = o);
+ if (options == null)
+ {
+ Console.WriteLine(HelpText.RenderUsageText(result));
+ return;
+ }
+
+ RunExperiment(options.Method, options.Seconds, options.IterationCount);
+ }
+
+ public static void RunExperiment(Method method, int seconds = 5, int times = 5)
+ {
+ // Warmup
+ Console.WriteLine("Warming up...");
+ CommandLineOptions warmupOptions = new() { Method = method, Seconds = 1 };
+ warmupOptions.Normalize();
+ new ParallelProcessingStress(warmupOptions).Run();
+
+ // Outer loop: run inner loop for each parallelism level
+ List<(int Parallelism, double AvgMpxPerSecPerCpu)> results = new();
+
+ foreach (int parallelism in ParallelismLevels())
+ {
+ Console.WriteLine($"\nRunning {method} with ProcessorParallelism={parallelism} ({times}x {seconds}s)...");
+
+ double totalMpxPerSecPerCpu = 0;
+ for (int i = 0; i < times; i++)
+ {
+ CommandLineOptions options = new() { Method = method, ProcessorParallelism = parallelism, Seconds = seconds };
+ options.Normalize();
+ Stats stats = new ParallelProcessingStress(options).Run();
+ totalMpxPerSecPerCpu += stats.MegapixelsPerSecPerCpu;
+ }
+
+ results.Add((parallelism, totalMpxPerSecPerCpu / times));
+ }
+
+ // Print results as markdown table
+ Console.WriteLine();
+ Console.WriteLine("| ProcessorParallelism | MegapixelsPerSecPerCpu |");
+ Console.WriteLine("|---------------------:|-----------------------:|");
+ foreach ((int parallelism, double avg) in results)
+ {
+ Console.WriteLine($"| {parallelism,20} | {avg,22:f3} |");
+ }
+ }
+
+ private sealed class ExperimentOptions
+ {
+ [Option('m', "method", Required = false, Default = Method.Edges, HelpText = "The stress test method to run (Edges, Crop)")]
+ public Method Method { get; set; } = Method.Edges;
+
+ [Option('s', "seconds", Required = false, Default = 5, HelpText = "Duration of each run in seconds")]
+ public int Seconds { get; set; } = 5;
+
+ [Option('i', "iterations", Required = false, Default = 5, HelpText = "Number of runs per parallelism level")]
+ public int IterationCount { get; set; } = 5;
+ }
+
+ private static IEnumerable ParallelismLevels()
+ {
+ int cpuCount = Environment.ProcessorCount;
+ for (int p = 1; p <= cpuCount; p *= 2)
+ {
+ yield return p;
+ }
+
+ // When cpuCount is not a power of two, append it as the final step
+ if ((cpuCount & (cpuCount - 1)) != 0)
+ {
+ yield return cpuCount;
+ }
+ }
+}
diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/ParallelProcessingStress.cs b/tests/ImageSharp.Tests.ProfilingSandbox/ParallelProcessingStress.cs
index a9bfda9c36..f5bda66461 100644
--- a/tests/ImageSharp.Tests.ProfilingSandbox/ParallelProcessingStress.cs
+++ b/tests/ImageSharp.Tests.ProfilingSandbox/ParallelProcessingStress.cs
@@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Diagnostics;
+using System.Globalization;
using System.Text;
using CommandLine;
using CommandLine.Text;
@@ -10,13 +11,13 @@ using SixLabors.ImageSharp.Processing;
namespace SixLabors.ImageSharp.Tests.ProfilingSandbox;
-public class ParallelProcessingStress
+public sealed partial class ParallelProcessingStress
{
private CommandLineOptions options;
private Configuration configuration;
private ulong totalKiloPixels;
- public static Stats Run(string[] args)
+ public static void Run(string[] args)
{
CommandLineOptions options = null;
if (args.Length > 0)
@@ -24,13 +25,13 @@ public class ParallelProcessingStress
options = CommandLineOptions.Parse(args);
if (options == null)
{
- return null;
+ return;
}
}
options ??= new CommandLineOptions();
ParallelProcessingStress stress = new(options.Normalize());
- return stress.Run();
+ stress.Run();
}
private ParallelProcessingStress(CommandLineOptions options)
@@ -66,7 +67,7 @@ public class ParallelProcessingStress
stopwatch.Stop();
double totalMegaPixels = this.totalKiloPixels / 1000.0;
- Stats stats = new(stopwatch, totalMegaPixels, systemOptions.MaxDegreeOfParallelism);
+ Stats stats = new(stopwatch.ElapsedMilliseconds, totalMegaPixels, systemOptions.MaxDegreeOfParallelism);
Console.WriteLine(stats.GetMarkdown());
return stats;
}
@@ -96,7 +97,7 @@ public class ParallelProcessingStress
return image.Width * image.Height;
}
- public record Stats
+ private sealed record Stats
{
public double TotalSeconds { get; }
@@ -106,10 +107,10 @@ public class ParallelProcessingStress
public double MegapixelsPerSecPerCpu { get; }
- public Stats(Stopwatch sw, double totalMegapixels, int cpuCount)
+ public Stats(long elapsedMilliseconds, double totalMegapixels, int cpuCount)
{
this.TotalMegapixels = totalMegapixels;
- this.TotalSeconds = sw.ElapsedMilliseconds / 1000.0;
+ this.TotalSeconds = elapsedMilliseconds / 1000.0;
this.MegapixelsPerSec = totalMegapixels / this.TotalSeconds;
this.MegapixelsPerSecPerCpu = this.MegapixelsPerSec / cpuCount;
}
@@ -117,16 +118,19 @@ public class ParallelProcessingStress
public string GetMarkdown()
{
StringBuilder bld = new();
- bld.AppendLine($"| {nameof(this.TotalSeconds)} | {nameof(this.MegapixelsPerSec)} | {nameof(this.MegapixelsPerSecPerCpu)} |");
bld.AppendLine(
+ CultureInfo.InvariantCulture,
+ $"| {nameof(this.TotalSeconds)} | {nameof(this.MegapixelsPerSec)} | {nameof(this.MegapixelsPerSecPerCpu)} |");
+ bld.AppendLine(
+ CultureInfo.InvariantCulture,
$"| {L(nameof(this.TotalSeconds))} | {L(nameof(this.MegapixelsPerSec))} | {L(nameof(this.MegapixelsPerSecPerCpu))} |");
bld.Append("| ");
- bld.AppendFormat(F(nameof(this.TotalSeconds)), this.TotalSeconds);
+ bld.AppendFormat(CultureInfo.InvariantCulture, F(nameof(this.TotalSeconds)), this.TotalSeconds);
bld.Append(" | ");
- bld.AppendFormat(F(nameof(this.MegapixelsPerSec)), this.MegapixelsPerSec);
+ bld.AppendFormat(CultureInfo.InvariantCulture, F(nameof(this.MegapixelsPerSec)), this.MegapixelsPerSec);
bld.Append(" | ");
- bld.AppendFormat(F(nameof(this.MegapixelsPerSecPerCpu)), this.MegapixelsPerSecPerCpu);
+ bld.AppendFormat(CultureInfo.InvariantCulture, F(nameof(this.MegapixelsPerSecPerCpu)), this.MegapixelsPerSecPerCpu);
bld.AppendLine(" |");
return bld.ToString();
@@ -136,9 +140,13 @@ public class ParallelProcessingStress
}
}
- private enum Method { Edges, Crop }
+ public enum Method
+ {
+ Edges,
+ Crop
+ }
- private class CommandLineOptions
+ private sealed class CommandLineOptions
{
[Option('m', "method", Required = false, Default = Method.Edges, HelpText = "The stress test method to run (Edges, Crop)")]
public Method Method { get; set; } = Method.Edges;
@@ -199,4 +207,4 @@ public class ParallelProcessingStress
return result;
}
}
-}
\ No newline at end of file
+}
diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/Program.cs b/tests/ImageSharp.Tests.ProfilingSandbox/Program.cs
index 3245198734..0a1c9b80b1 100644
--- a/tests/ImageSharp.Tests.ProfilingSandbox/Program.cs
+++ b/tests/ImageSharp.Tests.ProfilingSandbox/Program.cs
@@ -14,7 +14,7 @@ namespace SixLabors.ImageSharp.Tests.ProfilingSandbox;
public class Program
{
- private class ConsoleOutput : ITestOutputHelper
+ private sealed class ConsoleOutput : ITestOutputHelper
{
public void WriteLine(string message) => Console.WriteLine(message);
@@ -33,7 +33,8 @@ public class Program
try
{
// LoadResizeSaveParallelMemoryStress.Run(args);
- ParallelProcessingStress.Run(args);
+ ParallelProcessingStress.RunExperiment(args);
+ // ParallelProcessingStress.Run(args);
}
catch (Exception ex)
{
diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs
index 862d4b64d3..9d46e4dce6 100644
--- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs
+++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs
@@ -58,14 +58,14 @@ public class MagickReferenceDecoder : ImageDecoder
MagickReadSettings settings = new()
{
- FrameCount = (int)options.MaxFrames
+ FrameCount = options.MaxFrames
};
settings.SetDefines(bmpReadDefines);
settings.SetDefines(pngReadDefines);
using MagickImageCollection magickImageCollection = new(stream, settings);
- int imageWidth = magickImageCollection.Max(x => x.Width);
- int imageHeight = magickImageCollection.Max(x => x.Height);
+ int imageWidth = (int)magickImageCollection.Max(x => x.Width);
+ int imageHeight = (int)magickImageCollection.Max(x => x.Height);
List> framesList = [];
foreach (IMagickImage magicFrame in magickImageCollection)
@@ -74,10 +74,10 @@ public class MagickReferenceDecoder : ImageDecoder
framesList.Add(frame);
Buffer2DRegion buffer = frame.PixelBuffer.GetRegion(
- imageWidth - magicFrame.Width,
- imageHeight - magicFrame.Height,
- magicFrame.Width,
- magicFrame.Height);
+ imageWidth - (int)magicFrame.Width,
+ imageHeight - (int)magicFrame.Height,
+ (int)magicFrame.Width,
+ (int)magicFrame.Height);
using IUnsafePixelCollection pixels = magicFrame.GetPixelsUnsafe();
if (magicFrame.Depth is 12 or 10 or 8 or 6 or 5 or 4 or 3 or 2 or 1)