mirror of https://github.com/SixLabors/ImageSharp
Browse Source
* add parallel processing benchmarks * introduce RunExperiment and fix warnings in test code * fix benchmarks * proper ProcessorThroughputTest * test additional processors * ProcessorThroughputTest -> ProcessorThroughputBenchmark * add DrawImage to ParallelProcessing benchmark * readonly * Disable BMP tests -- https://github.com/SixLabors/ImageSharp/issues/3112 * revert ImageMagick update * delete CountingUnit * rename fieldpull/3114/head
committed by
GitHub
6 changed files with 344 additions and 70 deletions
@ -0,0 +1,65 @@ |
|||||
|
// 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<Rgba32> image; |
||||
|
private Image<Rgba32> foreground; |
||||
|
private Configuration configuration; |
||||
|
|
||||
|
public static IEnumerable<int> 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<Rgba32>(2048, 2048); |
||||
|
this.foreground = new Image<Rgba32>(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 DrawImage() => this.image.Mutate(this.configuration, x => x.DrawImage(this.foreground, 0.5f)); |
||||
|
|
||||
|
[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(); |
||||
|
this.foreground.Dispose(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,243 @@ |
|||||
|
// 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 ProcessorThroughputBenchmark |
||||
|
{ |
||||
|
private readonly CommandLineOptions options; |
||||
|
private readonly Configuration configuration; |
||||
|
private ulong totalProcessedPixels; |
||||
|
|
||||
|
private ProcessorThroughputBenchmark(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 ProcessorThroughputBenchmark(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, |
||||
|
Method.Edges => this.DetectEdges, |
||||
|
Method.DrawImage => this.DrawImage, |
||||
|
Method.BinaryThreshold => this.BinaryThreshold, |
||||
|
Method.Histogram => this.Histogram, |
||||
|
Method.OilPaint => this.OilPaint, |
||||
|
_ => throw new NotImplementedException(), |
||||
|
}; |
||||
|
|
||||
|
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(); |
||||
|
Interlocked.Add(ref this.totalProcessedPixels, 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.totalProcessedPixels / 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 OilPaint() |
||||
|
{ |
||||
|
using Image<Rgba32> image = new(this.options.Width, this.options.Height); |
||||
|
image.Mutate(this.configuration, x => x.OilPaint()); |
||||
|
return image.Width * image.Height; |
||||
|
} |
||||
|
|
||||
|
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 int DrawImage() |
||||
|
{ |
||||
|
using Image<Rgba32> image = new(this.options.Width, this.options.Height); |
||||
|
using Image<Rgba32> foreground = new(this.options.Width, this.options.Height); |
||||
|
image.Mutate(c => c.DrawImage(foreground, 0.5f)); |
||||
|
return image.Width * image.Height; |
||||
|
} |
||||
|
|
||||
|
private int BinaryThreshold() |
||||
|
{ |
||||
|
using Image<Rgba32> image = new(this.options.Width, this.options.Height); |
||||
|
image.Mutate(c => c.BinaryThreshold(0.5f)); |
||||
|
return image.Width * image.Height; |
||||
|
} |
||||
|
|
||||
|
private int Histogram() |
||||
|
{ |
||||
|
using Image<Rgba32> image = new(this.options.Width, this.options.Height); |
||||
|
image.Mutate(c => c.HistogramEqualization()); |
||||
|
return image.Width * image.Height; |
||||
|
} |
||||
|
|
||||
|
private enum Method |
||||
|
{ |
||||
|
Edges, |
||||
|
Crop, |
||||
|
DrawImage, |
||||
|
BinaryThreshold, |
||||
|
Histogram, |
||||
|
OilPaint |
||||
|
} |
||||
|
|
||||
|
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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,76 +1,38 @@ |
|||||
// Copyright (c) Six Labors.
|
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
using System.Reflection; |
|
||||
using SixLabors.ImageSharp.Tests.PixelFormats.PixelOperations; |
using SixLabors.ImageSharp.Tests.PixelFormats.PixelOperations; |
||||
using SixLabors.ImageSharp.Tests.ProfilingBenchmarks; |
using SixLabors.ImageSharp.Tests.ProfilingBenchmarks; |
||||
|
using SixLabors.ImageSharp.Tests.ProfilingSandbox; |
||||
using Xunit.Abstractions; |
using Xunit.Abstractions; |
||||
|
|
||||
// in this file, comments are used for disabling stuff for local execution
|
// in this file, comments are used for disabling stuff for local execution
|
||||
#pragma warning disable SA1515
|
#pragma warning disable SA1515
|
||||
#pragma warning disable SA1512
|
#pragma warning disable SA1512
|
||||
|
|
||||
namespace SixLabors.ImageSharp.Tests.ProfilingSandbox; |
// LoadResizeSaveParallelMemoryStress.Run(args);
|
||||
|
// ParallelProcessingStress.RunExperiment(args);
|
||||
|
// ParallelProcessingStress.Run(args);
|
||||
|
await ProcessorThroughputBenchmark.RunAsync(args); |
||||
|
|
||||
public class Program |
// RunToVector4ProfilingTest();
|
||||
{ |
// RunResizeProfilingTest();
|
||||
private 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); |
|
||||
} |
|
||||
catch (Exception ex) |
|
||||
{ |
|
||||
Console.WriteLine(ex); |
|
||||
} |
|
||||
|
|
||||
// RunJpegEncoderProfilingTests();
|
|
||||
// RunJpegColorProfilingTests();
|
|
||||
// RunDecodeJpegProfilingTests();
|
|
||||
// RunToVector4ProfilingTest();
|
|
||||
// RunResizeProfilingTest();
|
|
||||
|
|
||||
// Console.ReadLine();
|
|
||||
} |
|
||||
|
|
||||
private static Version GetNetCoreVersion() |
static void RunResizeProfilingTest() |
||||
{ |
{ |
||||
Assembly assembly = typeof(System.Runtime.GCSettings).GetTypeInfo().Assembly; |
ResizeProfilingBenchmarks test = new(new ConsoleOutput()); |
||||
Console.WriteLine(assembly.Location); |
test.ResizeBicubic(4000, 4000); |
||||
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]); |
|
||||
} |
|
||||
|
|
||||
return null; |
static void RunToVector4ProfilingTest() |
||||
} |
{ |
||||
|
PixelOperationsTests.Rgba32_OperationsTests tests = new(new ConsoleOutput()); |
||||
|
tests.Benchmark_ToVector4(); |
||||
|
} |
||||
|
|
||||
private static void RunResizeProfilingTest() |
sealed class ConsoleOutput : ITestOutputHelper |
||||
{ |
{ |
||||
ResizeProfilingBenchmarks test = new(new ConsoleOutput()); |
public void WriteLine(string message) => Console.WriteLine(message); |
||||
test.ResizeBicubic(4000, 4000); |
|
||||
} |
|
||||
|
|
||||
private static void RunToVector4ProfilingTest() |
public void WriteLine(string format, params object[] args) => Console.WriteLine(format, args); |
||||
{ |
|
||||
PixelOperationsTests.Rgba32_OperationsTests tests = new(new ConsoleOutput()); |
|
||||
tests.Benchmark_ToVector4(); |
|
||||
} |
|
||||
} |
} |
||||
|
|||||
Loading…
Reference in new issue