Skip to content

Commit a6de45e

Browse files
committed
benchmark parallel efficiency and overhead
1 parent 920b798 commit a6de45e

File tree

7 files changed

+346
-12
lines changed

7 files changed

+346
-12
lines changed

ImageSharp.Drawing.sln

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharp.Drawing.WebGPU",
333333
EndProject
334334
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharp.Drawing.WebGPU.ShaderGen", "src\ImageSharp.Drawing.WebGPU.ShaderGen\ImageSharp.Drawing.WebGPU.ShaderGen.csproj", "{C7606104-5D58-4670-912C-3F336606B02D}"
335335
EndProject
336+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharp.Drawing.ManualBenchmarks", "tests\ImageSharp.Drawing.ManualBenchmarks\ImageSharp.Drawing.ManualBenchmarks.csproj", "{70193989-E587-451A-AF34-4C74FEE5DA5A}"
337+
EndProject
336338
Global
337339
GlobalSection(SolutionConfigurationPlatforms) = preSolution
338340
Debug|Any CPU = Debug|Any CPU
@@ -403,6 +405,18 @@ Global
403405
{C7606104-5D58-4670-912C-3F336606B02D}.Release|x64.Build.0 = Release|Any CPU
404406
{C7606104-5D58-4670-912C-3F336606B02D}.Release|x86.ActiveCfg = Release|Any CPU
405407
{C7606104-5D58-4670-912C-3F336606B02D}.Release|x86.Build.0 = Release|Any CPU
408+
{70193989-E587-451A-AF34-4C74FEE5DA5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
409+
{70193989-E587-451A-AF34-4C74FEE5DA5A}.Debug|Any CPU.Build.0 = Debug|Any CPU
410+
{70193989-E587-451A-AF34-4C74FEE5DA5A}.Debug|x64.ActiveCfg = Debug|Any CPU
411+
{70193989-E587-451A-AF34-4C74FEE5DA5A}.Debug|x64.Build.0 = Debug|Any CPU
412+
{70193989-E587-451A-AF34-4C74FEE5DA5A}.Debug|x86.ActiveCfg = Debug|Any CPU
413+
{70193989-E587-451A-AF34-4C74FEE5DA5A}.Debug|x86.Build.0 = Debug|Any CPU
414+
{70193989-E587-451A-AF34-4C74FEE5DA5A}.Release|Any CPU.ActiveCfg = Release|Any CPU
415+
{70193989-E587-451A-AF34-4C74FEE5DA5A}.Release|Any CPU.Build.0 = Release|Any CPU
416+
{70193989-E587-451A-AF34-4C74FEE5DA5A}.Release|x64.ActiveCfg = Release|Any CPU
417+
{70193989-E587-451A-AF34-4C74FEE5DA5A}.Release|x64.Build.0 = Release|Any CPU
418+
{70193989-E587-451A-AF34-4C74FEE5DA5A}.Release|x86.ActiveCfg = Release|Any CPU
419+
{70193989-E587-451A-AF34-4C74FEE5DA5A}.Release|x86.Build.0 = Release|Any CPU
406420
EndGlobalSection
407421
GlobalSection(SolutionProperties) = preSolution
408422
HideSolutionNode = FALSE
@@ -431,6 +445,7 @@ Global
431445
{23859314-5693-4E6C-BE5C-80A433439D2A} = {1799C43E-5C54-4A8F-8D64-B1475241DB0D}
432446
{061582C2-658F-40AE-A978-7D74A4EB2C0A} = {815C0625-CD3D-440F-9F80-2D83856AB7AE}
433447
{C7606104-5D58-4670-912C-3F336606B02D} = {815C0625-CD3D-440F-9F80-2D83856AB7AE}
448+
{70193989-E587-451A-AF34-4C74FEE5DA5A} = {56801022-D71A-4FBE-BC5B-CBA08E2284EC}
434449
EndGlobalSection
435450
GlobalSection(ExtensibilityGlobals) = postSolution
436451
SolutionGuid = {5F8B9D1F-CD8B-4CC5-8216-D531E25BD795}

src/Directory.Build.props

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,13 @@
2424
<PropertyGroup>
2525
<UseImageSharp>true</UseImageSharp>
2626
</PropertyGroup>
27-
27+
2828
<ItemGroup>
2929
<!-- DynamicProxyGenAssembly2 is needed so Moq can use our internals -->
3030
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" Key="0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7" />
3131
<InternalsVisibleTo Include="SixLabors.ImageSharp.Tests" Key="$(SixLaborsPublicKey)" />
3232
<InternalsVisibleTo Include="ImageSharp.Drawing.Benchmarks" Key="$(SixLaborsPublicKey)" />
33+
<InternalsVisibleTo Include="ImageSharp.Drawing.ManualBenchmarks" Key="$(SixLaborsPublicKey)" />
3334
</ItemGroup>
3435

3536
</Project>

tests/ImageSharp.Drawing.Benchmarks/Drawing/FillPolygon.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,20 @@ public void ImageSharp()
108108
}
109109
}));
110110

111+
[Benchmark]
112+
public void ImageSharp_SingleThreaded()
113+
{
114+
Configuration configuration = this.image.Configuration.Clone();
115+
configuration.MaxDegreeOfParallelism = 1;
116+
this.image.Mutate(configuration, c => c.ProcessWithCanvas(canvas =>
117+
{
118+
foreach (Polygon polygon in this.polygons)
119+
{
120+
canvas.Fill(Processing.Brushes.Solid(Color.White), polygon);
121+
}
122+
}));
123+
}
124+
111125
[Benchmark(Baseline = true)]
112126
public void SkiaSharp()
113127
{

tests/ImageSharp.Drawing.Benchmarks/Drawing/FillTiger.cs

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,6 @@ namespace SixLabors.ImageSharp.Drawing.Benchmarks.Drawing;
2222
/// </summary>
2323
public class FillTiger
2424
{
25-
private const float Scale = 4f;
26-
private const int Width = 800;
27-
private const int Height = 800;
28-
2925
private static readonly string SvgFilePath =
3026
TestFile.GetInputFileFullPath(TestImages.Svg.GhostscriptTiger);
3127

@@ -41,28 +37,35 @@ public class FillTiger
4137

4238
private WebGPURenderTarget<Rgba32> webGpuTarget;
4339

40+
[Params(1000, 100)]
41+
public int Dimensions { get; set; }
42+
4443
[GlobalSetup]
4544
public void Setup()
4645
{
46+
int width = this.Dimensions;
47+
int height = this.Dimensions;
48+
float scale = this.Dimensions / 200f;
49+
4750
ThreadPool.GetMinThreads(out int minWorkerThreads, out int minCompletionPortThreads);
4851
int desiredWorkerThreads = Math.Max(minWorkerThreads, Environment.ProcessorCount);
4952
ThreadPool.SetMinThreads(desiredWorkerThreads, minCompletionPortThreads);
5053
Parallel.For(0, desiredWorkerThreads, static _ => { });
5154

5255
List<SvgBenchmarkHelper.SvgElement> elements = SvgBenchmarkHelper.ParseSvg(SvgFilePath);
5356

54-
this.skSurface = SKSurface.Create(new SKImageInfo(Width, Height));
55-
this.skElements = SvgBenchmarkHelper.BuildSkiaElements(elements, Scale);
57+
this.skSurface = SKSurface.Create(new SKImageInfo(width, height));
58+
this.skElements = SvgBenchmarkHelper.BuildSkiaElements(elements, scale);
5659

57-
this.sdBitmap = new Bitmap(Width, Height);
60+
this.sdBitmap = new Bitmap(width, height);
5861
this.sdGraphics = Graphics.FromImage(this.sdBitmap);
5962
this.sdGraphics.SmoothingMode = SmoothingMode.AntiAlias;
60-
this.sdElements = SvgBenchmarkHelper.BuildSystemDrawingElements(elements, Scale);
63+
this.sdElements = SvgBenchmarkHelper.BuildSystemDrawingElements(elements, scale);
6164

62-
this.image = new Image<Rgba32>(Width, Height);
63-
this.isElements = SvgBenchmarkHelper.BuildImageSharpElements(elements, Scale);
65+
this.image = new Image<Rgba32>(width, height);
66+
this.isElements = SvgBenchmarkHelper.BuildImageSharpElements(elements, scale);
6467

65-
this.webGpuTarget = new WebGPURenderTarget<Rgba32>(Width, Height);
68+
this.webGpuTarget = new WebGPURenderTarget<Rgba32>(width, height);
6669
}
6770

6871
[IterationSetup]
@@ -152,6 +155,28 @@ public void ImageSharp()
152155
}
153156
}));
154157

158+
[Benchmark]
159+
public void ImageSharp_SingleThreaded()
160+
{
161+
Configuration configuration = this.image.Configuration.Clone();
162+
configuration.MaxDegreeOfParallelism = 1;
163+
this.image.Mutate(configuration, c => c.ProcessWithCanvas(canvas =>
164+
{
165+
foreach ((IPath path, Processing.SolidBrush fill, SolidPen stroke) in this.isElements)
166+
{
167+
if (fill is not null)
168+
{
169+
canvas.Fill(fill, path);
170+
}
171+
172+
if (stroke is not null)
173+
{
174+
canvas.Draw(stroke, path);
175+
}
176+
}
177+
}));
178+
}
179+
155180
[Benchmark]
156181
public void ImageSharpWebGPU()
157182
{
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using System.Diagnostics;
5+
using CommandLine;
6+
using CommandLine.Text;
7+
using SixLabors.ImageSharp;
8+
using SixLabors.ImageSharp.Drawing;
9+
using SixLabors.ImageSharp.Drawing.Processing;
10+
using SixLabors.ImageSharp.Drawing.Tests;
11+
using SixLabors.ImageSharp.PixelFormats;
12+
using SixLabors.ImageSharp.Processing;
13+
14+
public sealed class DrawingThroughputBenchmark
15+
{
16+
private readonly CommandLineOptions options;
17+
private readonly Configuration configuration;
18+
private readonly List<(IPath Path, SolidBrush Fill, SolidPen Stroke)> elements;
19+
private ulong totalProcessedPixels;
20+
21+
private DrawingThroughputBenchmark(CommandLineOptions options)
22+
{
23+
this.options = options;
24+
this.configuration = Configuration.Default.Clone();
25+
this.configuration.MaxDegreeOfParallelism = options.ProcessorParallelism > 0
26+
? options.ProcessorParallelism
27+
: Environment.ProcessorCount;
28+
List<SvgBenchmarkHelper.SvgElement> elements = SvgBenchmarkHelper.ParseSvg(
29+
TestFile.GetInputFileFullPath(TestImages.Svg.GhostscriptTiger));
30+
float size = (options.Width + options.Height) * 0.5f;
31+
this.elements = SvgBenchmarkHelper.BuildImageSharpElements(elements, size / 200f);
32+
}
33+
34+
public static Task RunAsync(string[] args)
35+
{
36+
CommandLineOptions? options = null;
37+
if (args.Length > 0)
38+
{
39+
options = CommandLineOptions.Parse(args);
40+
if (options == null)
41+
{
42+
return Task.CompletedTask;
43+
}
44+
}
45+
46+
options ??= new CommandLineOptions();
47+
return new DrawingThroughputBenchmark(options.Normalize())
48+
.RunAsync();
49+
}
50+
51+
private async Task RunAsync()
52+
{
53+
SemaphoreSlim semaphore = new(this.options.ConcurrentRequests);
54+
Console.WriteLine(this.options.Method);
55+
Func<int> action = this.options.Method switch
56+
{
57+
Method.Tiger => this.Tiger,
58+
_ => throw new NotImplementedException(),
59+
};
60+
61+
Console.WriteLine(this.options);
62+
Console.WriteLine($"Running {this.options.Method} for {this.options.Seconds} seconds ...");
63+
TimeSpan runFor = TimeSpan.FromSeconds(this.options.Seconds);
64+
65+
// inFlight starts at 1 to represent the dispatch loop itself
66+
int inFlight = 1;
67+
TaskCompletionSource drainTcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
68+
69+
Stopwatch stopwatch = Stopwatch.StartNew();
70+
while (stopwatch.Elapsed < runFor && !drainTcs.Task.IsCompleted)
71+
{
72+
await semaphore.WaitAsync();
73+
74+
if (stopwatch.Elapsed >= runFor)
75+
{
76+
semaphore.Release();
77+
break;
78+
}
79+
80+
Interlocked.Increment(ref inFlight);
81+
82+
_ = ProcessImage();
83+
84+
async Task ProcessImage()
85+
{
86+
try
87+
{
88+
if (stopwatch.Elapsed >= runFor || drainTcs.Task.IsCompleted)
89+
{
90+
return;
91+
}
92+
93+
await Task.Yield(); // "emulate IO", i.e., make sure the processing code is async
94+
ulong pixels = (ulong)action();
95+
Interlocked.Add(ref this.totalProcessedPixels, pixels);
96+
}
97+
catch (Exception ex)
98+
{
99+
Console.WriteLine(ex);
100+
drainTcs.TrySetException(ex);
101+
}
102+
finally
103+
{
104+
semaphore.Release();
105+
if (Interlocked.Decrement(ref inFlight) == 0)
106+
{
107+
drainTcs.TrySetResult();
108+
}
109+
}
110+
}
111+
}
112+
113+
// Release the dispatch loop's own count; if no work is in flight, this completes immediately
114+
if (Interlocked.Decrement(ref inFlight) == 0)
115+
{
116+
drainTcs.TrySetResult();
117+
}
118+
119+
await drainTcs.Task;
120+
stopwatch.Stop();
121+
122+
double totalMegaPixels = this.totalProcessedPixels / 1_000_000.0;
123+
double totalSeconds = stopwatch.ElapsedMilliseconds / 1000.0;
124+
double megapixelsPerSec = totalMegaPixels / totalSeconds;
125+
Console.WriteLine($"TotalSeconds: {totalSeconds:F2}");
126+
Console.WriteLine($"MegaPixelsPerSec: {megapixelsPerSec:F2}");
127+
}
128+
129+
private int Tiger()
130+
{
131+
using Image<Rgba32> image = new(this.options.Width, this.options.Height);
132+
image.Mutate(this.configuration, c => c.ProcessWithCanvas(canvas =>
133+
{
134+
foreach ((IPath path, SolidBrush fill, SolidPen stroke) in this.elements)
135+
{
136+
if (fill is not null)
137+
{
138+
canvas.Fill(fill, path);
139+
}
140+
141+
if (stroke is not null)
142+
{
143+
canvas.Draw(stroke, path);
144+
}
145+
}
146+
}));
147+
return image.Width * image.Height;
148+
}
149+
150+
private enum Method
151+
{
152+
Tiger,
153+
}
154+
155+
private sealed class CommandLineOptions
156+
{
157+
private const int DefaultSize = 2000;
158+
159+
[Option('m', "method", Required = false, Default = Method.Tiger, HelpText = "The stress test method to run (Edges, Crop)")]
160+
public Method Method { get; set; } = Method.Tiger;
161+
162+
[Option('p', "drawing-parallelism", Required = false, Default = -1, HelpText = "Level of parallelism for the image processor")]
163+
public int ProcessorParallelism { get; set; } = -1;
164+
165+
[Option('c', "concurrent-requests", Required = false, Default = -1, HelpText = "Number of concurrent in-flight requests")]
166+
public int ConcurrentRequests { get; set; } = -1;
167+
168+
[Option('w', "width", Required = false, Default = DefaultSize, HelpText = "Width of the test image")]
169+
public int Width { get; set; } = DefaultSize;
170+
171+
[Option('h', "height", Required = false, Default = DefaultSize, HelpText = "Height of the test image")]
172+
public int Height { get; set; } = DefaultSize;
173+
174+
[Option('s', "seconds", Required = false, Default = 5, HelpText = "Duration of the stress test in seconds")]
175+
public int Seconds { get; set; } = 5;
176+
177+
public override string ToString() => string.Join(
178+
"|",
179+
$"method: {this.Method}",
180+
$"processor-parallelism: {this.ProcessorParallelism}",
181+
$"concurrent-requests: {this.ConcurrentRequests}",
182+
$"width: {this.Width}",
183+
$"height: {this.Height}",
184+
$"seconds: {this.Seconds}");
185+
186+
public CommandLineOptions Normalize()
187+
{
188+
if (this.ProcessorParallelism < 0)
189+
{
190+
this.ProcessorParallelism = Environment.ProcessorCount;
191+
}
192+
193+
if (this.ConcurrentRequests < 0)
194+
{
195+
this.ConcurrentRequests = Environment.ProcessorCount;
196+
}
197+
198+
return this;
199+
}
200+
201+
public static CommandLineOptions? Parse(string[] args)
202+
{
203+
CommandLineOptions? result = null;
204+
using Parser parser = new(settings => settings.CaseInsensitiveEnumValues = true);
205+
ParserResult<CommandLineOptions> parserResult = parser.ParseArguments<CommandLineOptions>(args).WithParsed(o =>
206+
{
207+
result = o;
208+
});
209+
210+
if (result == null)
211+
{
212+
Console.WriteLine(HelpText.RenderUsageText(parserResult));
213+
}
214+
215+
return result;
216+
}
217+
}
218+
}

0 commit comments

Comments
 (0)