Skip to content

Commit 01735f0

Browse files
committed
refactor(LinkBuilder): reduce allocations
1 parent 85bb9f4 commit 01735f0

File tree

4 files changed

+134
-12
lines changed

4 files changed

+134
-12
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
``` ini
2+
3+
BenchmarkDotNet=v0.10.10, OS=Mac OS X 10.12
4+
Processor=Intel Core i5-5257U CPU 2.70GHz (Broadwell), ProcessorCount=4
5+
.NET Core SDK=2.1.4
6+
[Host] : .NET Core 2.0.5 (Framework 4.6.0.0), 64bit RyuJIT
7+
Job-XFMVNE : .NET Core 2.0.5 (Framework 4.6.0.0), 64bit RyuJIT
8+
9+
LaunchCount=3 TargetCount=20 WarmupCount=10
10+
11+
```
12+
| Method | Mean | Error | StdDev | Gen 0 | Allocated |
13+
|--------------------------- |-----------:|----------:|----------:|-------:|----------:|
14+
| UsingSplit | 1,197.6 ns | 11.929 ns | 25.933 ns | 0.9251 | 1456 B |
15+
| UsingSpanWithStringBuilder | 1,542.0 ns | 15.249 ns | 33.792 ns | 0.9460 | 1488 B |
16+
| UsingSpanWithNoAlloc | 272.6 ns | 2.265 ns | 5.018 ns | 0.0863 | 136 B |
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
using System;
2+
using System.Diagnostics;
3+
using System.Text;
4+
using System.Threading;
5+
using BenchmarkDotNet.Attributes;
6+
using BenchmarkDotNet.Attributes.Exporters;
7+
using BenchmarkDotNet.Attributes.Jobs;
8+
using JsonApiDotNetCore.Extensions;
9+
10+
namespace Benchmarks.LinkBuilder
11+
{
12+
[MarkdownExporter, SimpleJob(launchCount : 3, warmupCount : 10, targetCount : 20), MemoryDiagnoser]
13+
public class LinkBuilder_GetNamespaceFromPath_Benchmarks
14+
{
15+
private const string PATH = "/api/some-really-long-namespace-path/resources/current/articles";
16+
private const string ENTITY_NAME = "articles";
17+
18+
[Benchmark]
19+
public void UsingSplit() => GetNamespaceFromPath_BySplitting(PATH, ENTITY_NAME);
20+
21+
[Benchmark]
22+
public void UsingSpanWithStringBuilder() => GetNamespaceFromPath_Using_Span_With_StringBuilder(PATH, ENTITY_NAME);
23+
24+
[Benchmark]
25+
public void UsingSpanWithNoAlloc() => GetNamespaceFromPath_Using_Span_No_Alloc(PATH, ENTITY_NAME);
26+
27+
public static string GetNamespaceFromPath_BySplitting(string path, string entityName)
28+
{
29+
var nSpace = string.Empty;
30+
var segments = path.Split('/');
31+
32+
for (var i = 1; i < segments.Length; i++)
33+
{
34+
if (segments[i].ToLower() == entityName)
35+
break;
36+
37+
nSpace += $"/{segments[i]}";
38+
}
39+
40+
return nSpace;
41+
}
42+
43+
public static string GetNamespaceFromPath_Using_Span_No_Alloc(string path, string entityName)
44+
{
45+
var entityNameSpan = entityName.AsSpan();
46+
var pathSpan = path.AsSpan();
47+
const char delimiter = '/';
48+
for (var i = 0; i < pathSpan.Length; i++)
49+
{
50+
if(pathSpan[i].Equals(delimiter))
51+
{
52+
var nextPosition = i+1;
53+
if(pathSpan.Length > i + entityNameSpan.Length)
54+
{
55+
var possiblePathSegment = pathSpan.Slice(nextPosition, entityNameSpan.Length);
56+
if (entityNameSpan.SequenceEqual(possiblePathSegment))
57+
{
58+
// check to see if it's the last position in the string
59+
// or if the next character is a /
60+
var lastCharacterPosition = nextPosition + entityNameSpan.Length;
61+
62+
if(lastCharacterPosition == pathSpan.Length || pathSpan.Length >= lastCharacterPosition + 2 && pathSpan[lastCharacterPosition + 1].Equals(delimiter))
63+
{
64+
return pathSpan.Slice(0, i).ToString();
65+
}
66+
}
67+
}
68+
}
69+
}
70+
71+
return string.Empty;
72+
}
73+
74+
public static string GetNamespaceFromPath_Using_Span_With_StringBuilder(string path, string entityName)
75+
{
76+
var sb = new StringBuilder();
77+
var entityNameSpan = entityName.AsSpan();
78+
var subSpans = path.SpanSplit('/');
79+
for (var i = 1; i < subSpans.Count; i++)
80+
{
81+
var span = subSpans[i];
82+
if (entityNameSpan.SequenceEqual(span))
83+
break;
84+
85+
sb.Append($"/{span.ToString()}");
86+
}
87+
return sb.ToString();
88+
}
89+
}
90+
}

benchmarks/Program.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using BenchmarkDotNet.Running;
2+
using Benchmarks.LinkBuilder;
23
using Benchmarks.Query;
34
using Benchmarks.Serialization;
45

@@ -8,7 +9,8 @@ static void Main(string[] args) {
89
var switcher = new BenchmarkSwitcher(new[] {
910
typeof(JsonApiDeserializer_Benchmarks),
1011
typeof(JsonApiSerializer_Benchmarks),
11-
typeof(QueryParser_Benchmarks)
12+
typeof(QueryParser_Benchmarks),
13+
typeof(LinkBuilder_GetNamespaceFromPath_Benchmarks)
1214
});
1315
switcher.Run(args);
1416
}

src/JsonApiDotNetCore/Builders/LinkBuilder.cs

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
using System;
2-
using System.Text;
3-
using JsonApiDotNetCore.Extensions;
4-
using JsonApiDotNetCore.Internal;
52
using JsonApiDotNetCore.Services;
63
using Microsoft.AspNetCore.Http;
74

@@ -20,22 +17,39 @@ public string GetBasePath(HttpContext context, string entityName)
2017
{
2118
var r = context.Request;
2219
return (_context.Options.RelativeLinks)
23-
? $"{GetNamespaceFromPath(r.Path, entityName)}"
20+
? GetNamespaceFromPath(r.Path, entityName)
2421
: $"{r.Scheme}://{r.Host}{GetNamespaceFromPath(r.Path, entityName)}";
2522
}
2623

2724
private static string GetNamespaceFromPath(string path, string entityName)
2825
{
29-
var sb = new StringBuilder();
3026
var entityNameSpan = entityName.AsSpan();
31-
var subSpans = path.SpanSplit('/');
32-
for (var i = 1; i < subSpans.Count; i++)
27+
var pathSpan = path.AsSpan();
28+
const char delimiter = '/';
29+
for (var i = 0; i < pathSpan.Length; i++)
3330
{
34-
var span = subSpans[i];
35-
if (entityNameSpan.SequenceEqual(span)) break;
36-
sb.Append($"/{span.ToString()}");
31+
if(pathSpan[i].Equals(delimiter))
32+
{
33+
var nextPosition = i + 1;
34+
if(pathSpan.Length > i + entityNameSpan.Length)
35+
{
36+
var possiblePathSegment = pathSpan.Slice(nextPosition, entityNameSpan.Length);
37+
if (entityNameSpan.SequenceEqual(possiblePathSegment))
38+
{
39+
// check to see if it's the last position in the string
40+
// or if the next character is a /
41+
var lastCharacterPosition = nextPosition + entityNameSpan.Length;
42+
43+
if(lastCharacterPosition == pathSpan.Length || pathSpan.Length >= lastCharacterPosition + 2 && pathSpan[lastCharacterPosition].Equals(delimiter))
44+
{
45+
return pathSpan.Slice(0, i).ToString();
46+
}
47+
}
48+
}
49+
}
3750
}
38-
return sb.ToString();
51+
52+
return string.Empty;
3953
}
4054

4155
public string GetSelfRelationLink(string parent, string parentId, string child)

0 commit comments

Comments
 (0)