Skip to content

Commit 84a49cd

Browse files
committed
port SVG parsing fix and add the rest of the benchmarks
1 parent 1eeb158 commit 84a49cd

File tree

8 files changed

+311
-8
lines changed

8 files changed

+311
-8
lines changed

ImageSharp.Drawing.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{
337337
.github\workflows\build-and-test.yml = .github\workflows\build-and-test.yml
338338
EndProjectSection
339339
EndProject
340+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharp.Drawing.ManualBenchmarks", "tests\ImageSharp.Drawing.ManualBenchmarks\ImageSharp.Drawing.ManualBenchmarks.csproj", "{EB9C10E0-59B7-4620-B8EB-220E8FFC73E6}"
341+
EndProject
340342
Global
341343
GlobalSection(SolutionConfigurationPlatforms) = preSolution
342344
Debug|Any CPU = Debug|Any CPU
@@ -359,6 +361,10 @@ Global
359361
{5493F024-0A3F-420C-AC2D-05B77A36025B}.Debug|Any CPU.Build.0 = Debug|Any CPU
360362
{5493F024-0A3F-420C-AC2D-05B77A36025B}.Release|Any CPU.ActiveCfg = Release|Any CPU
361363
{5493F024-0A3F-420C-AC2D-05B77A36025B}.Release|Any CPU.Build.0 = Release|Any CPU
364+
{EB9C10E0-59B7-4620-B8EB-220E8FFC73E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
365+
{EB9C10E0-59B7-4620-B8EB-220E8FFC73E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
366+
{EB9C10E0-59B7-4620-B8EB-220E8FFC73E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
367+
{EB9C10E0-59B7-4620-B8EB-220E8FFC73E6}.Release|Any CPU.Build.0 = Release|Any CPU
362368
EndGlobalSection
363369
GlobalSection(SolutionProperties) = preSolution
364370
HideSolutionNode = FALSE
@@ -386,6 +392,7 @@ Global
386392
{68A8CC40-6AED-4E96-B524-31B1158FDEEA} = {815C0625-CD3D-440F-9F80-2D83856AB7AE}
387393
{5493F024-0A3F-420C-AC2D-05B77A36025B} = {528610AC-7C0C-46E8-9A2D-D46FD92FEE29}
388394
{23859314-5693-4E6C-BE5C-80A433439D2A} = {1799C43E-5C54-4A8F-8D64-B1475241DB0D}
395+
{EB9C10E0-59B7-4620-B8EB-220E8FFC73E6} = {56801022-D71A-4FBE-BC5B-CBA08E2284EC}
389396
EndGlobalSection
390397
GlobalSection(ExtensibilityGlobals) = postSolution
391398
SolutionGuid = {5F8B9D1F-CD8B-4CC5-8216-D531E25BD795}

src/ImageSharp.Drawing/Shapes/Path.cs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ SegmentInfo IPathInternals.PointAlongPath(float distance)
145145
public static bool TryParseSvgPath(string svgPath, [NotNullWhen(true)] out IPath? value)
146146
=> TryParseSvgPath(svgPath.AsSpan(), out value);
147147

148-
/// <summary>
148+
/// <summary>
149149
/// Converts an SVG path string into an <see cref="IPath"/>.
150150
/// </summary>
151151
/// <param name="svgPath">The string containing the SVG path data.</param>
@@ -381,10 +381,42 @@ private static ReadOnlySpan<char> FindScaler(ReadOnlySpan<char> str, out float s
381381
str = TrimSeparator(str);
382382
scaler = 0;
383383

384+
bool hasDot = false;
384385
for (int i = 0; i < str.Length; i++)
385386
{
386-
if (IsSeparator(str[i]) || i == str.Length)
387+
char ch = str[i];
388+
389+
if (IsSeparator(ch))
390+
{
391+
scaler = ParseFloat(str[..i]);
392+
return str[i..];
393+
}
394+
395+
if (ch == '.')
396+
{
397+
if (hasDot)
398+
{
399+
// Second decimal point starts a new number.
400+
scaler = ParseFloat(str[..i]);
401+
return str[i..];
402+
}
403+
404+
hasDot = true;
405+
}
406+
else if ((ch is '-' or '+') && i > 0)
407+
{
408+
// A sign character mid-number starts a new number,
409+
// unless it follows an exponent indicator.
410+
char prev = str[i - 1];
411+
if (prev is not 'e' and not 'E')
412+
{
413+
scaler = ParseFloat(str[..i]);
414+
return str[i..];
415+
}
416+
}
417+
else if (char.IsLetter(ch))
387418
{
419+
// Hit a command letter; end this number.
388420
scaler = ParseFloat(str[..i]);
389421
return str[i..];
390422
}
@@ -395,7 +427,7 @@ private static ReadOnlySpan<char> FindScaler(ReadOnlySpan<char> str, out float s
395427
scaler = ParseFloat(str);
396428
}
397429

398-
return ReadOnlySpan<char>.Empty;
430+
return Array.Empty<char>();
399431
}
400432

401433
private static bool IsSeparator(char ch)

tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
<IsPackable>false</IsPackable>
77
<GenerateProgramFile>false</GenerateProgramFile>
88
<IsTestProject>false</IsTestProject>
9+
<LangVersion>latest</LangVersion>
910
</PropertyGroup>
1011

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

tests/ImageSharp.Drawing.Tests/Drawing/SvgTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ public class SvgTests
1111
{
1212
[Theory]
1313
[WithBlankImage(200, 200, PixelTypes.Rgba32, 1f)]
14+
[WithBlankImage(1000, 1000, PixelTypes.Rgba32, 5f)]
1415
public void Tiger<TPixel>(TestImageProvider<TPixel> provider, float scale)
1516
where TPixel : unmanaged, IPixel<TPixel>
1617
{
@@ -32,5 +33,6 @@ public void Tiger<TPixel>(TestImageProvider<TPixel> provider, float scale)
3233
}
3334
}
3435
});
36+
image.DebugSave(provider, $"s{scale}");
3537
}
3638
}

tests/ImageSharp.Drawing.Tests/TestUtilities/PolygonFactory.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,7 @@ public static PointF[][] GetGeoJsonPoints(string geoJsonContent) =>
7474
GetGeoJsonPoints(geoJsonContent, Matrix3x2.Identity);
7575

7676
public static Polygon CreatePolygon(params (float X, float Y)[] coords)
77-
=> new(new LinearLineSegment(CreatePointArray(coords)))
78-
{
79-
// The default epsilon is too large for test code, we prefer the vertices not to be changed
80-
RemoveCloseAndCollinearPoints = false
81-
};
77+
=> new(new LinearLineSegment(CreatePointArray(coords)));
8278

8379
public static (PointF Start, PointF End) CreateHorizontalLine(float y)
8480
=> (new PointF(-Inf, y), new PointF(Inf, y));

0 commit comments

Comments
 (0)