diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj b/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj
index bc52610d2c..f3aa910b96 100644
--- a/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj
+++ b/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj
@@ -8,7 +8,6 @@
false
SixLabors.ImageSharp.Tests.ProfilingSandbox
win-x64
- SixLabors.ImageSharp.Tests.ProfilingSandbox.Program
false
false
diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/ParallelProcessingStress.Experiment.cs b/tests/ImageSharp.Tests.ProfilingSandbox/ParallelProcessingStress.Experiment.cs
deleted file mode 100644
index ffe09a35b3..0000000000
--- a/tests/ImageSharp.Tests.ProfilingSandbox/ParallelProcessingStress.Experiment.cs
+++ /dev/null
@@ -1,88 +0,0 @@
-// 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/ProcessorThroughputTest.cs
similarity index 55%
rename from tests/ImageSharp.Tests.ProfilingSandbox/ParallelProcessingStress.cs
rename to tests/ImageSharp.Tests.ProfilingSandbox/ProcessorThroughputTest.cs
index f5bda66461..d94aaf1ba5 100644
--- a/tests/ImageSharp.Tests.ProfilingSandbox/ParallelProcessingStress.cs
+++ b/tests/ImageSharp.Tests.ProfilingSandbox/ProcessorThroughputTest.cs
@@ -2,8 +2,6 @@
// Licensed under the Six Labors Split License.
using System.Diagnostics;
-using System.Globalization;
-using System.Text;
using CommandLine;
using CommandLine.Text;
using SixLabors.ImageSharp.PixelFormats;
@@ -11,13 +9,23 @@ using SixLabors.ImageSharp.Processing;
namespace SixLabors.ImageSharp.Tests.ProfilingSandbox;
-public sealed partial class ParallelProcessingStress
+public sealed class ProcessorThroughputTest
{
+ private const ulong CountingUnit = 1;
private CommandLineOptions options;
private Configuration configuration;
- private ulong totalKiloPixels;
+ private ulong totalPixelsInUnit;
- public static void Run(string[] args)
+ private ProcessorThroughputTest(CommandLineOptions options)
+ {
+ this.options = options;
+ this.configuration = Configuration.Default.Clone();
+ this.configuration.MaxDegreeOfParallelism = options.ProcessorParallelism > 0
+ ? options.ProcessorParallelism
+ : Environment.ProcessorCount;
+ }
+
+ public static Task RunAsync(string[] args)
{
CommandLineOptions options = null;
if (args.Length > 0)
@@ -25,60 +33,91 @@ public sealed partial class ParallelProcessingStress
options = CommandLineOptions.Parse(args);
if (options == null)
{
- return;
+ return Task.CompletedTask;
}
}
options ??= new CommandLineOptions();
- ParallelProcessingStress stress = new(options.Normalize());
- stress.Run();
- }
-
- private ParallelProcessingStress(CommandLineOptions options)
- {
- this.options = options;
- this.configuration = Configuration.Default.Clone();
- this.configuration.MaxDegreeOfParallelism = options.ProcessorParallelism > 0
- ? options.ProcessorParallelism
- : Environment.ProcessorCount;
+ return new ProcessorThroughputTest(options.Normalize())
+ .RunAsync();
}
- private Stats Run()
+ private async Task RunAsync()
{
- ParallelOptions systemOptions = new() { MaxDegreeOfParallelism = this.options.SystemParallelism };
+ SemaphoreSlim semaphore = new(this.options.ConcurrentRequests);
+ Console.WriteLine(this.options.Method);
Func action = this.options.Method switch
{
Method.Crop => this.Crop,
_ => this.DetectEdges,
};
+
+ Console.WriteLine(this.options);
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) =>
+
+ // inFlight starts at 1 to represent the dispatch loop itself
+ int inFlight = 1;
+ TaskCompletionSource drainTcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ Stopwatch stopwatch = Stopwatch.StartNew();
+ while (stopwatch.Elapsed < runFor && !drainTcs.Task.IsCompleted)
{
- ulong kiloPixels = (ulong)action() / 1000;
- Interlocked.Add(ref this.totalKiloPixels, kiloPixels);
+ await semaphore.WaitAsync();
if (stopwatch.Elapsed >= runFor)
{
- state.Stop();
+ semaphore.Release();
+ break;
}
- });
- stopwatch.Stop();
- double totalMegaPixels = this.totalKiloPixels / 1000.0;
- Stats stats = new(stopwatch.ElapsedMilliseconds, totalMegaPixels, systemOptions.MaxDegreeOfParallelism);
- Console.WriteLine(stats.GetMarkdown());
- return stats;
- }
+ Interlocked.Increment(ref inFlight);
- private static IEnumerable InfiniteSequence()
- {
- long i = 0;
- while (true)
+ _ = ProcessImage();
+
+ async Task ProcessImage()
+ {
+ try
+ {
+ if (stopwatch.Elapsed >= runFor || drainTcs.Task.IsCompleted)
+ {
+ return;
+ }
+
+ await Task.Yield(); // "emulate IO", i.e., make sure the processing code is async
+ ulong pixels = (ulong)action() / CountingUnit;
+ Interlocked.Add(ref this.totalPixelsInUnit, pixels);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine(ex);
+ drainTcs.TrySetException(ex);
+ }
+ finally
+ {
+ semaphore.Release();
+ if (Interlocked.Decrement(ref inFlight) == 0)
+ {
+ drainTcs.TrySetResult();
+ }
+ }
+ }
+ }
+
+ // Release the dispatch loop's own count; if no work is in flight, this completes immediately
+ if (Interlocked.Decrement(ref inFlight) == 0)
{
- yield return i++;
+ drainTcs.TrySetResult();
}
+
+ await drainTcs.Task;
+ stopwatch.Stop();
+
+ double totalMegaPixels = this.totalPixelsInUnit * (double)CountingUnit / 1_000_000.0;
+ double totalSeconds = stopwatch.ElapsedMilliseconds / 1000.0;
+ double megapixelsPerSec = totalMegaPixels / totalSeconds;
+ Console.WriteLine($"TotalSeconds: {totalSeconds:F2}");
+ Console.WriteLine($"MegaPixelsPerSec: {megapixelsPerSec:F2}");
}
private int DetectEdges()
@@ -97,50 +136,7 @@ public sealed partial class ParallelProcessingStress
return image.Width * image.Height;
}
- private sealed record Stats
- {
- public double TotalSeconds { get; }
-
- public double TotalMegapixels { get; }
-
- public double MegapixelsPerSec { get; }
-
- public double MegapixelsPerSecPerCpu { get; }
-
- public Stats(long elapsedMilliseconds, double totalMegapixels, int cpuCount)
- {
- this.TotalMegapixels = totalMegapixels;
- this.TotalSeconds = elapsedMilliseconds / 1000.0;
- this.MegapixelsPerSec = totalMegapixels / this.TotalSeconds;
- this.MegapixelsPerSecPerCpu = this.MegapixelsPerSec / cpuCount;
- }
-
- public string GetMarkdown()
- {
- StringBuilder bld = new();
- 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(CultureInfo.InvariantCulture, F(nameof(this.TotalSeconds)), this.TotalSeconds);
- bld.Append(" | ");
- bld.AppendFormat(CultureInfo.InvariantCulture, F(nameof(this.MegapixelsPerSec)), this.MegapixelsPerSec);
- bld.Append(" | ");
- bld.AppendFormat(CultureInfo.InvariantCulture, 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}}";
- }
- }
-
- public enum Method
+ private enum Method
{
Edges,
Crop
@@ -154,8 +150,8 @@ public sealed partial class ParallelProcessingStress
[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('c', "concurrent-requests", Required = false, Default = -1, HelpText = "Number of concurrent in-flight requests")]
+ public int ConcurrentRequests { get; set; } = -1;
[Option('w', "width", Required = false, Default = 4000, HelpText = "Width of the test image")]
public int Width { get; set; } = 4000;
@@ -167,10 +163,10 @@ public sealed partial class ParallelProcessingStress
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}",
+ $"concurrent-requests: {this.ConcurrentRequests}",
$"width: {this.Width}",
$"height: {this.Height}",
$"seconds: {this.Seconds}");
@@ -182,9 +178,9 @@ public sealed partial class ParallelProcessingStress
this.ProcessorParallelism = Environment.ProcessorCount;
}
- if (this.SystemParallelism < 0)
+ if (this.ConcurrentRequests < 0)
{
- this.SystemParallelism = Environment.ProcessorCount;
+ this.ConcurrentRequests = Environment.ProcessorCount;
}
return this;
diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/Program.cs b/tests/ImageSharp.Tests.ProfilingSandbox/Program.cs
index 0a1c9b80b1..8ca57d0c3a 100644
--- a/tests/ImageSharp.Tests.ProfilingSandbox/Program.cs
+++ b/tests/ImageSharp.Tests.ProfilingSandbox/Program.cs
@@ -1,73 +1,38 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using System.Reflection;
using SixLabors.ImageSharp.Tests.PixelFormats.PixelOperations;
using SixLabors.ImageSharp.Tests.ProfilingBenchmarks;
+using SixLabors.ImageSharp.Tests.ProfilingSandbox;
using Xunit.Abstractions;
// in this file, comments are used for disabling stuff for local execution
#pragma warning disable SA1515
#pragma warning disable SA1512
-namespace SixLabors.ImageSharp.Tests.ProfilingSandbox;
+// LoadResizeSaveParallelMemoryStress.Run(args);
+// ParallelProcessingStress.RunExperiment(args);
+// ParallelProcessingStress.Run(args);
+await ProcessorThroughputTest.RunAsync(args);
-public class Program
-{
- private sealed class ConsoleOutput : ITestOutputHelper
- {
- public void WriteLine(string message) => Console.WriteLine(message);
-
- public void WriteLine(string format, params object[] args) => Console.WriteLine(format, args);
- }
-
- ///
- /// The main entry point. Useful for executing benchmarks and performance unit tests manually,
- /// when the IDE test runners lack some of the functionality. Eg.: it's not possible to run JetBrains memory profiler for unit tests.
- ///
- ///
- /// The arguments to pass to the program.
- ///
- public static void Main(string[] args)
- {
- try
- {
- // LoadResizeSaveParallelMemoryStress.Run(args);
- ParallelProcessingStress.RunExperiment(args);
- // ParallelProcessingStress.Run(args);
- }
- catch (Exception ex)
- {
- Console.WriteLine(ex);
- }
+// RunToVector4ProfilingTest();
+// RunResizeProfilingTest();
- // RunToVector4ProfilingTest();
- // RunResizeProfilingTest();
- }
-
- private static Version GetNetCoreVersion()
- {
- Assembly assembly = typeof(System.Runtime.GCSettings).GetTypeInfo().Assembly;
- Console.WriteLine(assembly.Location);
- string[] assemblyPath = assembly.Location.Split(['/', '\\'], StringSplitOptions.RemoveEmptyEntries);
- int netCoreAppIndex = Array.IndexOf(assemblyPath, "Microsoft.NETCore.App");
- if (netCoreAppIndex > 0 && netCoreAppIndex < assemblyPath.Length - 2)
- {
- return Version.Parse(assemblyPath[netCoreAppIndex + 1]);
- }
+static void RunResizeProfilingTest()
+{
+ ResizeProfilingBenchmarks test = new(new ConsoleOutput());
+ test.ResizeBicubic(4000, 4000);
+}
- return null;
- }
+static void RunToVector4ProfilingTest()
+{
+ PixelOperationsTests.Rgba32_OperationsTests tests = new(new ConsoleOutput());
+ tests.Benchmark_ToVector4();
+}
- private static void RunResizeProfilingTest()
- {
- ResizeProfilingBenchmarks test = new(new ConsoleOutput());
- test.ResizeBicubic(4000, 4000);
- }
+sealed class ConsoleOutput : ITestOutputHelper
+{
+ public void WriteLine(string message) => Console.WriteLine(message);
- private static void RunToVector4ProfilingTest()
- {
- PixelOperationsTests.Rgba32_OperationsTests tests = new(new ConsoleOutput());
- tests.Benchmark_ToVector4();
- }
+ public void WriteLine(string format, params object[] args) => Console.WriteLine(format, args);
}