From e616f60cea6ea600201d6293f9620ad07fd91611 Mon Sep 17 00:00:00 2001 From: antonfirsov Date: Sat, 4 Apr 2026 19:23:02 +0200 Subject: [PATCH] add parallel processing benchmarks --- .../Processing/ParallelProcessing.cs | 56 +++++ .../ParallelProcessingStress.cs | 202 ++++++++++++++++++ .../Program.cs | 8 +- 3 files changed, 260 insertions(+), 6 deletions(-) create mode 100644 tests/ImageSharp.Benchmarks/Processing/ParallelProcessing.cs create mode 100644 tests/ImageSharp.Tests.ProfilingSandbox/ParallelProcessingStress.cs diff --git a/tests/ImageSharp.Benchmarks/Processing/ParallelProcessing.cs b/tests/ImageSharp.Benchmarks/Processing/ParallelProcessing.cs new file mode 100644 index 0000000000..14635136a5 --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Processing/ParallelProcessing.cs @@ -0,0 +1,56 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using BenchmarkDotNet.Attributes; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Tests; + +namespace SixLabors.ImageSharp.Benchmarks; + +public class ParallelProcessing +{ + private Image image; + private Configuration configuration; + + public static IEnumerable MaxDegreeOfParallelismValues() + { + int processorCount = Environment.ProcessorCount; + for (int p = 1; p <= processorCount; p *= 2) + { + yield return p; + } + + if ((processorCount & (processorCount - 1)) != 0) + { + yield return processorCount; + } + } + + [ParamsSource(nameof(MaxDegreeOfParallelismValues))] + public int MaxDegreeOfParallelism { get; set; } + + [GlobalSetup] + public void Setup() + { + this.image = new Image(2048, 2048); + this.configuration = Configuration.Default.Clone(); + this.configuration.MaxDegreeOfParallelism = this.MaxDegreeOfParallelism; + } + + [Benchmark] + public void DetectEdges() => this.image.Mutate(this.configuration, x => x.DetectEdges()); + + [Benchmark] + public void Crop() + { + Rectangle bounds = this.image.Bounds; + bounds = new Rectangle(1, 1, bounds.Width - 2, bounds.Height - 2); + this.image + .Clone(this.configuration, x => x.Crop(bounds)) + .Dispose(); + } + + [GlobalCleanup] + public void Cleanup() => this.image.Dispose(); +} diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/ParallelProcessingStress.cs b/tests/ImageSharp.Tests.ProfilingSandbox/ParallelProcessingStress.cs new file mode 100644 index 0000000000..a9bfda9c36 --- /dev/null +++ b/tests/ImageSharp.Tests.ProfilingSandbox/ParallelProcessingStress.cs @@ -0,0 +1,202 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using System.Text; +using CommandLine; +using CommandLine.Text; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Tests.ProfilingSandbox; + +public class ParallelProcessingStress +{ + private CommandLineOptions options; + private Configuration configuration; + private ulong totalKiloPixels; + + public static Stats Run(string[] args) + { + CommandLineOptions options = null; + if (args.Length > 0) + { + options = CommandLineOptions.Parse(args); + if (options == null) + { + return null; + } + } + + options ??= new CommandLineOptions(); + ParallelProcessingStress stress = new(options.Normalize()); + return stress.Run(); + } + + private ParallelProcessingStress(CommandLineOptions options) + { + this.options = options; + this.configuration = Configuration.Default.Clone(); + this.configuration.MaxDegreeOfParallelism = options.ProcessorParallelism > 0 + ? options.ProcessorParallelism + : Environment.ProcessorCount; + } + + private Stats Run() + { + ParallelOptions systemOptions = new() { MaxDegreeOfParallelism = this.options.SystemParallelism }; + Func action = this.options.Method switch + { + Method.Crop => this.Crop, + _ => this.DetectEdges, + }; + Console.WriteLine($"Running {this.options.Method} for {this.options.Seconds} seconds ..."); + Stopwatch stopwatch = Stopwatch.StartNew(); + TimeSpan runFor = TimeSpan.FromSeconds(this.options.Seconds); + Parallel.ForEach(InfiniteSequence(), systemOptions, (_, state) => + { + ulong kiloPixels = (ulong)action() / 1000; + Interlocked.Add(ref this.totalKiloPixels, kiloPixels); + + if (stopwatch.Elapsed >= runFor) + { + state.Stop(); + } + }); + stopwatch.Stop(); + + double totalMegaPixels = this.totalKiloPixels / 1000.0; + Stats stats = new(stopwatch, totalMegaPixels, systemOptions.MaxDegreeOfParallelism); + Console.WriteLine(stats.GetMarkdown()); + return stats; + } + + private static IEnumerable InfiniteSequence() + { + long i = 0; + while (true) + { + yield return i++; + } + } + + private int DetectEdges() + { + using Image image = new(this.options.Width, this.options.Height); + image.Mutate(this.configuration, x => x.DetectEdges()); + return image.Width * image.Height; + } + + private int Crop() + { + using Image image = new(this.options.Width, this.options.Height); + Rectangle bounds = image.Bounds; + bounds = new Rectangle(1, 1, bounds.Width - 2, bounds.Height - 2); + image.Clone(this.configuration, x => x.Crop(bounds)).Dispose(); + return image.Width * image.Height; + } + + public record Stats + { + public double TotalSeconds { get; } + + public double TotalMegapixels { get; } + + public double MegapixelsPerSec { get; } + + public double MegapixelsPerSecPerCpu { get; } + + public Stats(Stopwatch sw, double totalMegapixels, int cpuCount) + { + this.TotalMegapixels = totalMegapixels; + this.TotalSeconds = sw.ElapsedMilliseconds / 1000.0; + this.MegapixelsPerSec = totalMegapixels / this.TotalSeconds; + this.MegapixelsPerSecPerCpu = this.MegapixelsPerSec / cpuCount; + } + + public string GetMarkdown() + { + StringBuilder bld = new(); + bld.AppendLine($"| {nameof(this.TotalSeconds)} | {nameof(this.MegapixelsPerSec)} | {nameof(this.MegapixelsPerSecPerCpu)} |"); + bld.AppendLine( + $"| {L(nameof(this.TotalSeconds))} | {L(nameof(this.MegapixelsPerSec))} | {L(nameof(this.MegapixelsPerSecPerCpu))} |"); + + bld.Append("| "); + bld.AppendFormat(F(nameof(this.TotalSeconds)), this.TotalSeconds); + bld.Append(" | "); + bld.AppendFormat(F(nameof(this.MegapixelsPerSec)), this.MegapixelsPerSec); + bld.Append(" | "); + bld.AppendFormat(F(nameof(this.MegapixelsPerSecPerCpu)), this.MegapixelsPerSecPerCpu); + bld.AppendLine(" |"); + + return bld.ToString(); + + static string L(string header) => new('-', header.Length); + static string F(string column) => $"{{0,{column.Length}:f3}}"; + } + } + + private enum Method { Edges, Crop } + + private 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; + + [Option('p', "processor-parallelism", Required = false, Default = -1, HelpText = "Level of parallelism for the image processor")] + public int ProcessorParallelism { get; set; } = -1; + + [Option('t', "system-parallelism", Required = false, Default = -1, HelpText = "Level of parallelism for the outer loop")] + public int SystemParallelism { get; set; } = -1; + + [Option('w', "width", Required = false, Default = 4000, HelpText = "Width of the test image")] + public int Width { get; set; } = 4000; + + [Option('h', "height", Required = false, Default = 4000, HelpText = "Height of the test image")] + public int Height { get; set; } = 4000; + + [Option('s', "seconds", Required = false, Default = 5, HelpText = "Duration of the stress test in seconds")] + public int Seconds { get; set; } = 5; + + public override string ToString() => string.Join( + Environment.NewLine, + $"method: {this.Method}", + $"processor-parallelism: {this.ProcessorParallelism}", + $"system-parallelism: {this.SystemParallelism}", + $"width: {this.Width}", + $"height: {this.Height}", + $"seconds: {this.Seconds}"); + + public CommandLineOptions Normalize() + { + if (this.ProcessorParallelism < 0) + { + this.ProcessorParallelism = Environment.ProcessorCount; + } + + if (this.SystemParallelism < 0) + { + this.SystemParallelism = Environment.ProcessorCount; + } + + return this; + } + + public static CommandLineOptions Parse(string[] args) + { + CommandLineOptions result = null; + using Parser parser = new(settings => settings.CaseInsensitiveEnumValues = true); + ParserResult parserResult = parser.ParseArguments(args).WithParsed(o => + { + result = o; + }); + + if (result == null) + { + Console.WriteLine(HelpText.RenderUsageText(parserResult)); + } + + 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 8ba862560b..3245198734 100644 --- a/tests/ImageSharp.Tests.ProfilingSandbox/Program.cs +++ b/tests/ImageSharp.Tests.ProfilingSandbox/Program.cs @@ -32,20 +32,16 @@ public class Program { try { - LoadResizeSaveParallelMemoryStress.Run(args); + // LoadResizeSaveParallelMemoryStress.Run(args); + ParallelProcessingStress.Run(args); } catch (Exception ex) { Console.WriteLine(ex); } - // RunJpegEncoderProfilingTests(); - // RunJpegColorProfilingTests(); - // RunDecodeJpegProfilingTests(); // RunToVector4ProfilingTest(); // RunResizeProfilingTest(); - - // Console.ReadLine(); } private static Version GetNetCoreVersion()