Browse Source

Merge branch 'main' into bp/openExr

pull/3096/head
Brian Popow 4 weeks ago
committed by GitHub
parent
commit
b15625e775
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 12
      tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs
  2. 65
      tests/ImageSharp.Benchmarks/Processing/ParallelProcessing.cs
  3. 1
      tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj
  4. 13
      tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs
  5. 243
      tests/ImageSharp.Tests.ProfilingSandbox/ProcessorThroughputBenchmark.cs
  6. 80
      tests/ImageSharp.Tests.ProfilingSandbox/Program.cs

12
tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs

@ -6,6 +6,7 @@ using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using ImageMagick;
using PhotoSauce.MagicScaler;
using SixLabors.ImageSharp.Formats;
@ -27,6 +28,7 @@ public enum JpegKind
Any = Baseline | Progressive
}
[SupportedOSPlatform("windows")]
public class LoadResizeSaveStressRunner
{
private const int Quality = 75;
@ -158,7 +160,7 @@ public class LoadResizeSaveStressRunner
this.outputDirectory,
Path.GetFileNameWithoutExtension(inputPath) + "-" + postfix + Path.GetExtension(inputPath));
private (int Width, int Height) ScaledSize(int inWidth, int inHeight, int outSize)
private static (int Width, int Height) ScaledSize(int inWidth, int inHeight, int outSize)
{
int width, height;
if (inWidth > inHeight)
@ -180,7 +182,7 @@ public class LoadResizeSaveStressRunner
using SystemDrawingImage image = SystemDrawingImage.FromFile(input, true);
this.LogImageProcessed(image.Width, image.Height);
(int width, int height) = this.ScaledSize(image.Width, image.Height, this.ThumbnailSize);
(int width, int height) = ScaledSize(image.Width, image.Height, this.ThumbnailSize);
Bitmap resized = new(width, height);
using Graphics graphics = Graphics.FromImage(resized);
using ImageAttributes attributes = new();
@ -282,7 +284,7 @@ public class LoadResizeSaveStressRunner
{
using SKBitmap original = SKBitmap.Decode(input);
this.LogImageProcessed(original.Width, original.Height);
(int width, int height) = this.ScaledSize(original.Width, original.Height, this.ThumbnailSize);
(int width, int height) = ScaledSize(original.Width, original.Height, this.ThumbnailSize);
using SKSurface surface = SKSurface.Create(new SKImageInfo(width, height, original.ColorType, original.AlphaType));
using SKPaint paint = new() { FilterQuality = SKFilterQuality.High };
SKCanvas canvas = surface.Canvas;
@ -300,7 +302,7 @@ public class LoadResizeSaveStressRunner
{
using SKBitmap original = SKBitmap.Decode(input);
this.LogImageProcessed(original.Width, original.Height);
(int width, int height) = this.ScaledSize(original.Width, original.Height, this.ThumbnailSize);
(int width, int height) = ScaledSize(original.Width, original.Height, this.ThumbnailSize);
using SKBitmap resized = original.Resize(new SKImageInfo(width, height), SKFilterQuality.High);
if (resized == null)
{
@ -319,7 +321,7 @@ public class LoadResizeSaveStressRunner
SKImageInfo info = codec.Info;
this.LogImageProcessed(info.Width, info.Height);
(int width, int height) = this.ScaledSize(info.Width, info.Height, this.ThumbnailSize);
(int width, int height) = ScaledSize(info.Width, info.Height, this.ThumbnailSize);
SKSizeI supportedScale = codec.GetScaledDimensions((float)width / info.Width);
using SKBitmap original = SKBitmap.Decode(codec, new SKImageInfo(supportedScale.Width, supportedScale.Height));

65
tests/ImageSharp.Benchmarks/Processing/ParallelProcessing.cs

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

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>

13
tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs

@ -3,6 +3,7 @@
using System.Diagnostics;
using System.Globalization;
using System.Runtime.Versioning;
using System.Text;
using CommandLine;
using CommandLine.Text;
@ -13,7 +14,8 @@ using SixLabors.ImageSharp.Memory.Internals;
namespace SixLabors.ImageSharp.Tests.ProfilingSandbox;
// See ImageSharp.Benchmarks/LoadResizeSave/README.md
internal class LoadResizeSaveParallelMemoryStress
[SupportedOSPlatform("windows")]
internal sealed class LoadResizeSaveParallelMemoryStress
{
private LoadResizeSaveParallelMemoryStress()
{
@ -206,14 +208,15 @@ internal class LoadResizeSaveParallelMemoryStress
StringBuilder bld = new();
bld.AppendLine($"| {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(F(nameof(this.TotalSeconds)), this.TotalSeconds);
bld.AppendFormat(CultureInfo.InvariantCulture, F(nameof(this.TotalSeconds)), this.TotalSeconds);
bld.Append(" | ");
bld.AppendFormat(F(nameof(this.MegapixelsPerSec)), this.MegapixelsPerSec);
bld.AppendFormat(CultureInfo.InvariantCulture, F(nameof(this.MegapixelsPerSec)), this.MegapixelsPerSec);
bld.Append(" | ");
bld.AppendFormat(F(nameof(this.MegapixelsPerSecPerCpu)), this.MegapixelsPerSecPerCpu);
bld.AppendFormat(CultureInfo.InvariantCulture, F(nameof(this.MegapixelsPerSecPerCpu)), this.MegapixelsPerSecPerCpu);
bld.AppendLine(" |");
return bld.ToString();
@ -223,7 +226,7 @@ internal class LoadResizeSaveParallelMemoryStress
}
}
private class CommandLineOptions
private sealed class CommandLineOptions
{
[Option('a', "async-imagesharp", Required = false, Default = false, HelpText = "Async ImageSharp without benchmark switching")]
public bool AsyncImageSharp { get; set; }

243
tests/ImageSharp.Tests.ProfilingSandbox/ProcessorThroughputBenchmark.cs

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

80
tests/ImageSharp.Tests.ProfilingSandbox/Program.cs

@ -1,76 +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 ProcessorThroughputBenchmark.RunAsync(args);
public class Program
{
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();
}
// 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