Browse Source

introduce RunExperiment and fix warnings in test code

pull/3111/head
antonfirsov 2 months ago
parent
commit
57ebbff98e
  1. 2
      tests/Directory.Build.targets
  2. 18
      tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs
  3. 13
      tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs
  4. 88
      tests/ImageSharp.Tests.ProfilingSandbox/ParallelProcessingStress.Experiment.cs
  5. 38
      tests/ImageSharp.Tests.ProfilingSandbox/ParallelProcessingStress.cs
  6. 5
      tests/ImageSharp.Tests.ProfilingSandbox/Program.cs
  7. 14
      tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs

2
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. 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 See https://github.com/ImageMagick/ImageMagick/commit/27a0a9c37f18af9c8d823a3ea076f600843b553c
--> -->
<PackageReference Update="Magick.NET-Q16-AnyCPU" Version="13.10.0" /> <PackageReference Update="Magick.NET-Q16-AnyCPU" Version="14.11.1" />
<PackageReference Update="Microsoft.DotNet.RemoteExecutor" Version="10.0.0-beta.25563.105" /> <PackageReference Update="Microsoft.DotNet.RemoteExecutor" Version="10.0.0-beta.25563.105" />
<PackageReference Update="Microsoft.DotNet.XUnitExtensions" Version="8.0.0-beta.23580.1" /> <PackageReference Update="Microsoft.DotNet.XUnitExtensions" Version="8.0.0-beta.23580.1" />
<PackageReference Update="Moq" Version="4.20.72" /> <PackageReference Update="Moq" Version="4.20.72" />

18
tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs

@ -6,6 +6,7 @@ using System.Drawing.Drawing2D;
using System.Drawing.Imaging; using System.Drawing.Imaging;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using ImageMagick; using ImageMagick;
using PhotoSauce.MagicScaler; using PhotoSauce.MagicScaler;
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
@ -27,7 +28,8 @@ public enum JpegKind
Any = Baseline | Progressive Any = Baseline | Progressive
} }
public class LoadResizeSaveStressRunner [SupportedOSPlatform("windows")]
public sealed class LoadResizeSaveStressRunner
{ {
private const int Quality = 75; private const int Quality = 75;
@ -158,7 +160,7 @@ public class LoadResizeSaveStressRunner
this.outputDirectory, this.outputDirectory,
Path.GetFileNameWithoutExtension(inputPath) + "-" + postfix + Path.GetExtension(inputPath)); 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; int width, height;
if (inWidth > inHeight) if (inWidth > inHeight)
@ -180,7 +182,7 @@ public class LoadResizeSaveStressRunner
using SystemDrawingImage image = SystemDrawingImage.FromFile(input, true); using SystemDrawingImage image = SystemDrawingImage.FromFile(input, true);
this.LogImageProcessed(image.Width, image.Height); 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); Bitmap resized = new(width, height);
using Graphics graphics = Graphics.FromImage(resized); using Graphics graphics = Graphics.FromImage(resized);
using ImageAttributes attributes = new(); using ImageAttributes attributes = new();
@ -248,10 +250,10 @@ public class LoadResizeSaveStressRunner
public void MagickResize(string input) public void MagickResize(string input)
{ {
using MagickImage image = new(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 // 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 // Reduce the size of the file
image.Strip(); image.Strip();
@ -282,7 +284,7 @@ public class LoadResizeSaveStressRunner
{ {
using SKBitmap original = SKBitmap.Decode(input); using SKBitmap original = SKBitmap.Decode(input);
this.LogImageProcessed(original.Width, original.Height); 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 SKSurface surface = SKSurface.Create(new SKImageInfo(width, height, original.ColorType, original.AlphaType));
using SKPaint paint = new() { FilterQuality = SKFilterQuality.High }; using SKPaint paint = new() { FilterQuality = SKFilterQuality.High };
SKCanvas canvas = surface.Canvas; SKCanvas canvas = surface.Canvas;
@ -300,7 +302,7 @@ public class LoadResizeSaveStressRunner
{ {
using SKBitmap original = SKBitmap.Decode(input); using SKBitmap original = SKBitmap.Decode(input);
this.LogImageProcessed(original.Width, original.Height); 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); using SKBitmap resized = original.Resize(new SKImageInfo(width, height), SKFilterQuality.High);
if (resized == null) if (resized == null)
{ {
@ -319,7 +321,7 @@ public class LoadResizeSaveStressRunner
SKImageInfo info = codec.Info; SKImageInfo info = codec.Info;
this.LogImageProcessed(info.Width, info.Height); 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); SKSizeI supportedScale = codec.GetScaledDimensions((float)width / info.Width);
using SKBitmap original = SKBitmap.Decode(codec, new SKImageInfo(supportedScale.Width, supportedScale.Height)); using SKBitmap original = SKBitmap.Decode(codec, new SKImageInfo(supportedScale.Width, supportedScale.Height));

13
tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs

@ -3,6 +3,7 @@
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.Runtime.Versioning;
using System.Text; using System.Text;
using CommandLine; using CommandLine;
using CommandLine.Text; using CommandLine.Text;
@ -13,7 +14,8 @@ using SixLabors.ImageSharp.Memory.Internals;
namespace SixLabors.ImageSharp.Tests.ProfilingSandbox; namespace SixLabors.ImageSharp.Tests.ProfilingSandbox;
// See ImageSharp.Benchmarks/LoadResizeSave/README.md // See ImageSharp.Benchmarks/LoadResizeSave/README.md
internal class LoadResizeSaveParallelMemoryStress [SupportedOSPlatform("windows")]
internal sealed class LoadResizeSaveParallelMemoryStress
{ {
private LoadResizeSaveParallelMemoryStress() private LoadResizeSaveParallelMemoryStress()
{ {
@ -206,14 +208,15 @@ internal class LoadResizeSaveParallelMemoryStress
StringBuilder bld = new(); StringBuilder bld = new();
bld.AppendLine($"| {nameof(this.TotalSeconds)} | {nameof(this.MegapixelsPerSec)} | {nameof(this.MegapixelsPerSecPerCpu)} |"); bld.AppendLine($"| {nameof(this.TotalSeconds)} | {nameof(this.MegapixelsPerSec)} | {nameof(this.MegapixelsPerSecPerCpu)} |");
bld.AppendLine( bld.AppendLine(
CultureInfo.InvariantCulture,
$"| {L(nameof(this.TotalSeconds))} | {L(nameof(this.MegapixelsPerSec))} | {L(nameof(this.MegapixelsPerSecPerCpu))} |"); $"| {L(nameof(this.TotalSeconds))} | {L(nameof(this.MegapixelsPerSec))} | {L(nameof(this.MegapixelsPerSecPerCpu))} |");
bld.Append("| "); bld.Append("| ");
bld.AppendFormat(F(nameof(this.TotalSeconds)), this.TotalSeconds); bld.AppendFormat(CultureInfo.InvariantCulture, F(nameof(this.TotalSeconds)), this.TotalSeconds);
bld.Append(" | "); bld.Append(" | ");
bld.AppendFormat(F(nameof(this.MegapixelsPerSec)), this.MegapixelsPerSec); bld.AppendFormat(CultureInfo.InvariantCulture, F(nameof(this.MegapixelsPerSec)), this.MegapixelsPerSec);
bld.Append(" | "); bld.Append(" | ");
bld.AppendFormat(F(nameof(this.MegapixelsPerSecPerCpu)), this.MegapixelsPerSecPerCpu); bld.AppendFormat(CultureInfo.InvariantCulture, F(nameof(this.MegapixelsPerSecPerCpu)), this.MegapixelsPerSecPerCpu);
bld.AppendLine(" |"); bld.AppendLine(" |");
return bld.ToString(); 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")] [Option('a', "async-imagesharp", Required = false, Default = false, HelpText = "Async ImageSharp without benchmark switching")]
public bool AsyncImageSharp { get; set; } public bool AsyncImageSharp { get; set; }

88
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<ExperimentOptions> result = parser.ParseArguments<ExperimentOptions>(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<int> 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;
}
}
}

38
tests/ImageSharp.Tests.ProfilingSandbox/ParallelProcessingStress.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Diagnostics; using System.Diagnostics;
using System.Globalization;
using System.Text; using System.Text;
using CommandLine; using CommandLine;
using CommandLine.Text; using CommandLine.Text;
@ -10,13 +11,13 @@ using SixLabors.ImageSharp.Processing;
namespace SixLabors.ImageSharp.Tests.ProfilingSandbox; namespace SixLabors.ImageSharp.Tests.ProfilingSandbox;
public class ParallelProcessingStress public sealed partial class ParallelProcessingStress
{ {
private CommandLineOptions options; private CommandLineOptions options;
private Configuration configuration; private Configuration configuration;
private ulong totalKiloPixels; private ulong totalKiloPixels;
public static Stats Run(string[] args) public static void Run(string[] args)
{ {
CommandLineOptions options = null; CommandLineOptions options = null;
if (args.Length > 0) if (args.Length > 0)
@ -24,13 +25,13 @@ public class ParallelProcessingStress
options = CommandLineOptions.Parse(args); options = CommandLineOptions.Parse(args);
if (options == null) if (options == null)
{ {
return null; return;
} }
} }
options ??= new CommandLineOptions(); options ??= new CommandLineOptions();
ParallelProcessingStress stress = new(options.Normalize()); ParallelProcessingStress stress = new(options.Normalize());
return stress.Run(); stress.Run();
} }
private ParallelProcessingStress(CommandLineOptions options) private ParallelProcessingStress(CommandLineOptions options)
@ -66,7 +67,7 @@ public class ParallelProcessingStress
stopwatch.Stop(); stopwatch.Stop();
double totalMegaPixels = this.totalKiloPixels / 1000.0; 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()); Console.WriteLine(stats.GetMarkdown());
return stats; return stats;
} }
@ -96,7 +97,7 @@ public class ParallelProcessingStress
return image.Width * image.Height; return image.Width * image.Height;
} }
public record Stats private sealed record Stats
{ {
public double TotalSeconds { get; } public double TotalSeconds { get; }
@ -106,10 +107,10 @@ public class ParallelProcessingStress
public double MegapixelsPerSecPerCpu { get; } public double MegapixelsPerSecPerCpu { get; }
public Stats(Stopwatch sw, double totalMegapixels, int cpuCount) public Stats(long elapsedMilliseconds, double totalMegapixels, int cpuCount)
{ {
this.TotalMegapixels = totalMegapixels; this.TotalMegapixels = totalMegapixels;
this.TotalSeconds = sw.ElapsedMilliseconds / 1000.0; this.TotalSeconds = elapsedMilliseconds / 1000.0;
this.MegapixelsPerSec = totalMegapixels / this.TotalSeconds; this.MegapixelsPerSec = totalMegapixels / this.TotalSeconds;
this.MegapixelsPerSecPerCpu = this.MegapixelsPerSec / cpuCount; this.MegapixelsPerSecPerCpu = this.MegapixelsPerSec / cpuCount;
} }
@ -117,16 +118,19 @@ public class ParallelProcessingStress
public string GetMarkdown() public string GetMarkdown()
{ {
StringBuilder bld = new(); StringBuilder bld = new();
bld.AppendLine($"| {nameof(this.TotalSeconds)} | {nameof(this.MegapixelsPerSec)} | {nameof(this.MegapixelsPerSecPerCpu)} |");
bld.AppendLine( 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))} |"); $"| {L(nameof(this.TotalSeconds))} | {L(nameof(this.MegapixelsPerSec))} | {L(nameof(this.MegapixelsPerSecPerCpu))} |");
bld.Append("| "); bld.Append("| ");
bld.AppendFormat(F(nameof(this.TotalSeconds)), this.TotalSeconds); bld.AppendFormat(CultureInfo.InvariantCulture, F(nameof(this.TotalSeconds)), this.TotalSeconds);
bld.Append(" | "); bld.Append(" | ");
bld.AppendFormat(F(nameof(this.MegapixelsPerSec)), this.MegapixelsPerSec); bld.AppendFormat(CultureInfo.InvariantCulture, F(nameof(this.MegapixelsPerSec)), this.MegapixelsPerSec);
bld.Append(" | "); bld.Append(" | ");
bld.AppendFormat(F(nameof(this.MegapixelsPerSecPerCpu)), this.MegapixelsPerSecPerCpu); bld.AppendFormat(CultureInfo.InvariantCulture, F(nameof(this.MegapixelsPerSecPerCpu)), this.MegapixelsPerSecPerCpu);
bld.AppendLine(" |"); bld.AppendLine(" |");
return bld.ToString(); 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)")] [Option('m', "method", Required = false, Default = Method.Edges, HelpText = "The stress test method to run (Edges, Crop)")]
public Method Method { get; set; } = Method.Edges; public Method Method { get; set; } = Method.Edges;
@ -199,4 +207,4 @@ public class ParallelProcessingStress
return result; return result;
} }
} }
} }

5
tests/ImageSharp.Tests.ProfilingSandbox/Program.cs

@ -14,7 +14,7 @@ namespace SixLabors.ImageSharp.Tests.ProfilingSandbox;
public class Program public class Program
{ {
private class ConsoleOutput : ITestOutputHelper private sealed class ConsoleOutput : ITestOutputHelper
{ {
public void WriteLine(string message) => Console.WriteLine(message); public void WriteLine(string message) => Console.WriteLine(message);
@ -33,7 +33,8 @@ public class Program
try try
{ {
// LoadResizeSaveParallelMemoryStress.Run(args); // LoadResizeSaveParallelMemoryStress.Run(args);
ParallelProcessingStress.Run(args); ParallelProcessingStress.RunExperiment(args);
// ParallelProcessingStress.Run(args);
} }
catch (Exception ex) catch (Exception ex)
{ {

14
tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs

@ -58,14 +58,14 @@ public class MagickReferenceDecoder : ImageDecoder
MagickReadSettings settings = new() MagickReadSettings settings = new()
{ {
FrameCount = (int)options.MaxFrames FrameCount = options.MaxFrames
}; };
settings.SetDefines(bmpReadDefines); settings.SetDefines(bmpReadDefines);
settings.SetDefines(pngReadDefines); settings.SetDefines(pngReadDefines);
using MagickImageCollection magickImageCollection = new(stream, settings); using MagickImageCollection magickImageCollection = new(stream, settings);
int imageWidth = magickImageCollection.Max(x => x.Width); int imageWidth = (int)magickImageCollection.Max(x => x.Width);
int imageHeight = magickImageCollection.Max(x => x.Height); int imageHeight = (int)magickImageCollection.Max(x => x.Height);
List<ImageFrame<TPixel>> framesList = []; List<ImageFrame<TPixel>> framesList = [];
foreach (IMagickImage<ushort> magicFrame in magickImageCollection) foreach (IMagickImage<ushort> magicFrame in magickImageCollection)
@ -74,10 +74,10 @@ public class MagickReferenceDecoder : ImageDecoder
framesList.Add(frame); framesList.Add(frame);
Buffer2DRegion<TPixel> buffer = frame.PixelBuffer.GetRegion( Buffer2DRegion<TPixel> buffer = frame.PixelBuffer.GetRegion(
imageWidth - magicFrame.Width, imageWidth - (int)magicFrame.Width,
imageHeight - magicFrame.Height, imageHeight - (int)magicFrame.Height,
magicFrame.Width, (int)magicFrame.Width,
magicFrame.Height); (int)magicFrame.Height);
using IUnsafePixelCollection<ushort> pixels = magicFrame.GetPixelsUnsafe(); using IUnsafePixelCollection<ushort> pixels = magicFrame.GetPixelsUnsafe();
if (magicFrame.Depth is 12 or 10 or 8 or 6 or 5 or 4 or 3 or 2 or 1) if (magicFrame.Depth is 12 or 10 or 8 or 6 or 5 or 4 or 3 or 2 or 1)

Loading…
Cancel
Save