📷 A modern, cross-platform, 2D Graphics library for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

206 lines
6.9 KiB

// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics;
using CommandLine;
using CommandLine.Text;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
namespace SixLabors.ImageSharp.Tests.ProfilingSandbox;
public sealed class ProcessorThroughputTest
{
private const ulong CountingUnit = 1;
private CommandLineOptions options;
private Configuration configuration;
private ulong totalPixelsInUnit;
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)
{
options = CommandLineOptions.Parse(args);
if (options == null)
{
return Task.CompletedTask;
}
}
options ??= new CommandLineOptions();
return new ProcessorThroughputTest(options.Normalize())
.RunAsync();
}
private async Task RunAsync()
{
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 ...");
TimeSpan runFor = TimeSpan.FromSeconds(this.options.Seconds);
// 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)
{
await semaphore.WaitAsync();
if (stopwatch.Elapsed >= runFor)
{
semaphore.Release();
break;
}
Interlocked.Increment(ref inFlight);
_ = 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)
{
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()
{
using Image<Rgba32> 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<Rgba32> 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;
}
private enum Method
{
Edges,
Crop
}
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;
[Option('p', "processor-parallelism", Required = false, Default = -1, HelpText = "Level of parallelism for the image processor")]
public int ProcessorParallelism { 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;
[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(
"|",
$"method: {this.Method}",
$"processor-parallelism: {this.ProcessorParallelism}",
$"concurrent-requests: {this.ConcurrentRequests}",
$"width: {this.Width}",
$"height: {this.Height}",
$"seconds: {this.Seconds}");
public CommandLineOptions Normalize()
{
if (this.ProcessorParallelism < 0)
{
this.ProcessorParallelism = Environment.ProcessorCount;
}
if (this.ConcurrentRequests < 0)
{
this.ConcurrentRequests = Environment.ProcessorCount;
}
return this;
}
public static CommandLineOptions Parse(string[] args)
{
CommandLineOptions result = null;
using Parser parser = new(settings => settings.CaseInsensitiveEnumValues = true);
ParserResult<CommandLineOptions> parserResult = parser.ParseArguments<CommandLineOptions>(args).WithParsed(o =>
{
result = o;
});
if (result == null)
{
Console.WriteLine(HelpText.RenderUsageText(parserResult));
}
return result;
}
}
}