Browse Source

proper ProcessorThroughputTest

pull/3111/head
antonfirsov 4 weeks ago
parent
commit
1ccd05ec3c
  1. 1
      tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj
  2. 88
      tests/ImageSharp.Tests.ProfilingSandbox/ParallelProcessingStress.Experiment.cs
  3. 168
      tests/ImageSharp.Tests.ProfilingSandbox/ProcessorThroughputTest.cs
  4. 77
      tests/ImageSharp.Tests.ProfilingSandbox/Program.cs

1
tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj

@ -8,7 +8,6 @@
<Prefer32Bit>false</Prefer32Bit>
<RootNamespace>SixLabors.ImageSharp.Tests.ProfilingSandbox</RootNamespace>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<StartupObject>SixLabors.ImageSharp.Tests.ProfilingSandbox.Program</StartupObject>
<!--Used to hide test project from dotnet test-->
<IsTestProject>false</IsTestProject>
<EnsureNETCoreAppRuntime>false</EnsureNETCoreAppRuntime>

88
tests/ImageSharp.Tests.ProfilingSandbox/ParallelProcessingStress.Experiment.cs

@ -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<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;
}
}
}

168
tests/ImageSharp.Tests.ProfilingSandbox/ParallelProcessingStress.cs → 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<int> 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<long> 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;

77
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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="args">
/// The arguments to pass to the program.
/// </param>
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);
}

Loading…
Cancel
Save