From 57ebbff98effbc54545b9d278b000efb3b4ee216 Mon Sep 17 00:00:00 2001 From: antonfirsov Date: Sat, 4 Apr 2026 21:44:03 +0200 Subject: [PATCH] introduce RunExperiment and fix warnings in test code --- tests/Directory.Build.targets | 2 +- .../LoadResizeSaveStressRunner.cs | 18 ++-- .../LoadResizeSaveParallelMemoryStress.cs | 13 +-- .../ParallelProcessingStress.Experiment.cs | 88 +++++++++++++++++++ .../ParallelProcessingStress.cs | 38 ++++---- .../Program.cs | 5 +- .../ReferenceCodecs/MagickReferenceDecoder.cs | 14 +-- 7 files changed, 140 insertions(+), 38 deletions(-) create mode 100644 tests/ImageSharp.Tests.ProfilingSandbox/ParallelProcessingStress.Experiment.cs 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)