Skip to content

Commit 5a41ba7

Browse files
Add optimized variants and backwards compatible version
1 parent a867c94 commit 5a41ba7

8 files changed

Lines changed: 294 additions & 23 deletions

File tree

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System;
5+
using System.Buffers;
6+
using System.Runtime.CompilerServices;
7+
using Microsoft.AspNetCore.Http;
8+
9+
namespace SixLabors.ImageSharp.Web.Caching
10+
{
11+
/// <summary>
12+
/// Optimized helper methods for generating cache keys from URI components. Much of this code has been adapted from the MIT licensed .NET runtime.
13+
/// </summary>
14+
internal static class CacheKeyHelper
15+
{
16+
private static readonly SpanAction<char, (bool LowerInvariant, string Host, string PathBase, string Path, string Query)> InitializeAbsoluteUriStringSpanAction = new(InitializeAbsoluteUriString);
17+
18+
public enum CaseHandling
19+
{
20+
None,
21+
LowerInvariant
22+
}
23+
24+
/// <summary>
25+
/// Combines the given URI components into a string that is properly encoded for use in HTTP headers.
26+
/// </summary>
27+
/// <param name="handling">Determines case handling for the result. <paramref name="query"/> is always converted to invariant lowercase.</param>
28+
/// <param name="pathBase">The first portion of the request path associated with application root.</param>
29+
/// <param name="path">The portion of the request path that identifies the requested resource.</param>
30+
/// <param name="query">The query, if any.</param>
31+
/// <returns>The combined URI components, properly encoded for use in HTTP headers.</returns>
32+
public static string BuildRelativeKey(
33+
CaseHandling handling,
34+
PathString pathBase = default,
35+
PathString path = default,
36+
QueryString query = default)
37+
38+
// Take any potential performance hit vs concatination for code reading sanity.
39+
=> BuildAbsoluteKey(handling, default, pathBase, path, query);
40+
41+
/// <summary>
42+
/// Combines the given URI components into a string that is properly encoded for use in HTTP headers.
43+
/// Note that unicode in the HostString will be encoded as punycode.
44+
/// </summary>
45+
/// <param name="handling">Determines case handling for the result. <paramref name="query"/> is always converted to invariant lowercase.</param>
46+
/// <param name="host">The host portion of the uri normally included in the Host header. This may include the port.</param>
47+
/// <param name="pathBase">The first portion of the request path associated with application root.</param>
48+
/// <param name="path">The portion of the request path that identifies the requested resource.</param>
49+
/// <param name="query">The query, if any.</param>
50+
/// <returns>The combined URI components, properly encoded for use in HTTP headers.</returns>
51+
public static string BuildAbsoluteKey(
52+
CaseHandling handling,
53+
HostString host,
54+
PathString pathBase = default,
55+
PathString path = default,
56+
QueryString query = default)
57+
{
58+
string hostText = host.ToString();
59+
string pathBaseText = pathBase.ToString();
60+
string pathText = path.ToString();
61+
string queryText = query.ToString();
62+
63+
// PERF: Calculate string length to allocate correct buffer size for string.Create.
64+
int length =
65+
hostText.Length +
66+
pathBaseText.Length +
67+
pathText.Length +
68+
queryText.Length;
69+
70+
if (string.IsNullOrEmpty(pathText))
71+
{
72+
if (string.IsNullOrEmpty(pathBaseText))
73+
{
74+
pathText = "/";
75+
length++;
76+
}
77+
}
78+
else if (pathBaseText.EndsWith('/'))
79+
{
80+
// If the path string has a trailing slash and the other string has a leading slash, we need
81+
// to trim one of them.
82+
// Just decrement the total length, for now.
83+
length--;
84+
}
85+
86+
return string.Create(length, (handling == CaseHandling.LowerInvariant, hostText, pathBaseText, pathText, queryText), InitializeAbsoluteUriStringSpanAction);
87+
}
88+
89+
/// <summary>
90+
/// Copies the specified <paramref name="text"/> to the specified <paramref name="buffer"/> starting at the specified <paramref name="index"/>.
91+
/// </summary>
92+
/// <param name="buffer">The buffer to copy text to.</param>
93+
/// <param name="index">The buffer start index.</param>
94+
/// <param name="text">The text to copy.</param>
95+
/// <returns>The <see cref="int"/> representing the combined text length.</returns>
96+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
97+
private static int CopyTextToBuffer(Span<char> buffer, int index, ReadOnlySpan<char> text)
98+
{
99+
text.CopyTo(buffer.Slice(index, text.Length));
100+
return index + text.Length;
101+
}
102+
103+
/// <summary>
104+
/// Copies the specified <paramref name="text"/> to the specified <paramref name="buffer"/> starting at the specified <paramref name="index"/>
105+
/// converting each character to lowercase, using the casing rules of the invariant culture.
106+
/// </summary>
107+
/// <param name="buffer">The buffer to copy text to.</param>
108+
/// <param name="index">The buffer start index.</param>
109+
/// <param name="text">The text to copy.</param>
110+
/// <returns>The <see cref="int"/> representing the combined text length.</returns>
111+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
112+
private static int CopyTextToBufferLowerInvariant(Span<char> buffer, int index, ReadOnlySpan<char> text)
113+
=> index + text.ToLowerInvariant(buffer.Slice(index, text.Length));
114+
115+
/// <summary>
116+
/// Initializes the URI <see cref="string"/> for <see cref="BuildAbsoluteKey(CaseHandling, HostString, PathString, PathString, QueryString)"/>.
117+
/// </summary>
118+
/// <param name="buffer">The URI <see cref="string"/>'s <see cref="char"/> buffer.</param>
119+
/// <param name="uriParts">The URI parts.</param>
120+
private static void InitializeAbsoluteUriString(Span<char> buffer, (bool Lower, string Host, string PathBase, string Path, string Query) uriParts)
121+
{
122+
int index = 0;
123+
ReadOnlySpan<char> pathBaseSpan = uriParts.PathBase.AsSpan();
124+
125+
if (uriParts.Path.Length > 0 && pathBaseSpan.Length > 0 && pathBaseSpan[pathBaseSpan.Length - 1] == '/')
126+
{
127+
// If the path string has a trailing slash and the other string has a leading slash, we need
128+
// to trim one of them.
129+
// Trim the last slash from pathBase. The total length was decremented before the call to string.Create.
130+
pathBaseSpan = pathBaseSpan.Slice(0, pathBaseSpan.Length - 1);
131+
}
132+
133+
if (uriParts.Lower)
134+
{
135+
index = CopyTextToBufferLowerInvariant(buffer, index, uriParts.Host.AsSpan());
136+
index = CopyTextToBufferLowerInvariant(buffer, index, pathBaseSpan);
137+
index = CopyTextToBufferLowerInvariant(buffer, index, uriParts.Path.AsSpan());
138+
}
139+
else
140+
{
141+
index = CopyTextToBuffer(buffer, index, uriParts.Host.AsSpan());
142+
index = CopyTextToBuffer(buffer, index, pathBaseSpan);
143+
index = CopyTextToBuffer(buffer, index, uriParts.Path.AsSpan());
144+
}
145+
146+
// Querystring is always copied as lower invariant.
147+
_ = CopyTextToBufferLowerInvariant(buffer, index, uriParts.Query.AsSpan());
148+
}
149+
}
150+
}

tests/ImageSharp.Web.Benchmarks/Caching/CacheKeyBaseline.cs renamed to src/ImageSharp.Web/Caching/LegacyV1CacheKey.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@
33

44
using System.Text;
55
using Microsoft.AspNetCore.Http;
6-
using SixLabors.ImageSharp.Web.Caching;
76
using SixLabors.ImageSharp.Web.Commands;
87

9-
namespace SixLabors.ImageSharp.Web.Benchmarks.Caching
8+
namespace SixLabors.ImageSharp.Web.Caching
109
{
1110
/// <summary>
12-
/// Original implementation of cache key creation.
11+
/// Maintained for compatibility purposes only this cache key implementation generates the same
12+
/// out as the V1 middleware. If possible, it is recommended to use the <see cref="UriRelativeCacheKey"/>.
1313
/// </summary>
14-
public class CacheKeyBaseline : ICacheKey
14+
public class LegacyV1CacheKey : ICacheKey
1515
{
1616
/// <inheritdoc/>
1717
public string Create(HttpContext context, CommandCollection commands)

src/ImageSharp.Web/Caching/UriAbsoluteCacheKey.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// Licensed under the Apache License, Version 2.0.
33

44
using Microsoft.AspNetCore.Http;
5-
using Microsoft.AspNetCore.Http.Extensions;
65
using SixLabors.ImageSharp.Web.Commands;
76

87
namespace SixLabors.ImageSharp.Web.Caching
@@ -14,6 +13,6 @@ public class UriAbsoluteCacheKey : ICacheKey
1413
{
1514
/// <inheritdoc/>
1615
public string Create(HttpContext context, CommandCollection commands)
17-
=> UriHelper.BuildAbsolute(context.Request.Scheme, context.Request.Host, context.Request.PathBase, context.Request.Path, QueryString.Create(commands)).ToLowerInvariant();
16+
=> CacheKeyHelper.BuildAbsoluteKey(CacheKeyHelper.CaseHandling.None, context.Request.Host, context.Request.PathBase, context.Request.Path, QueryString.Create(commands));
1817
}
1918
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using Microsoft.AspNetCore.Http;
5+
using SixLabors.ImageSharp.Web.Commands;
6+
7+
namespace SixLabors.ImageSharp.Web.Caching
8+
{
9+
/// <summary>
10+
/// Creates a case insensitive cache key based on the request scheme, host, path and commands.
11+
/// </summary>
12+
public class UriAbsoluteCaseInsensitiveCacheKey : ICacheKey
13+
{
14+
/// <inheritdoc/>
15+
public string Create(HttpContext context, CommandCollection commands)
16+
=> CacheKeyHelper.BuildAbsoluteKey(CacheKeyHelper.CaseHandling.LowerInvariant, context.Request.Host, context.Request.PathBase, context.Request.Path, QueryString.Create(commands));
17+
}
18+
}

src/ImageSharp.Web/Caching/UriRelativeCacheKey.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// Licensed under the Apache License, Version 2.0.
33

44
using Microsoft.AspNetCore.Http;
5-
using Microsoft.AspNetCore.Http.Extensions;
65
using SixLabors.ImageSharp.Web.Commands;
76

87
namespace SixLabors.ImageSharp.Web.Caching
@@ -14,6 +13,6 @@ public class UriRelativeCacheKey : ICacheKey
1413
{
1514
/// <inheritdoc/>
1615
public string Create(HttpContext context, CommandCollection commands)
17-
=> UriHelper.BuildRelative(context.Request.PathBase, context.Request.Path, QueryString.Create(commands)).ToLowerInvariant();
16+
=> CacheKeyHelper.BuildRelativeKey(CacheKeyHelper.CaseHandling.None, context.Request.PathBase, context.Request.Path, QueryString.Create(commands));
1817
}
1918
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using Microsoft.AspNetCore.Http;
5+
using SixLabors.ImageSharp.Web.Commands;
6+
7+
namespace SixLabors.ImageSharp.Web.Caching
8+
{
9+
/// <summary>
10+
/// Creates a case insensitive cache key based on the request path and commands.
11+
/// </summary>
12+
public class UriRelativeCaseInsensitiveCacheKey : ICacheKey
13+
{
14+
/// <inheritdoc/>
15+
public string Create(HttpContext context, CommandCollection commands)
16+
=> CacheKeyHelper.BuildRelativeKey(CacheKeyHelper.CaseHandling.LowerInvariant, context.Request.PathBase, context.Request.Path, QueryString.Create(commands));
17+
}
18+
}

tests/ImageSharp.Web.Benchmarks/Caching/CacheKeyBenchmarks.cs

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,32 +18,42 @@ public class CacheKeyBenchmarks
1818
{ "width", "400" }
1919
};
2020

21-
private static readonly ICacheKey CacheKeyBaseline = new CacheKeyBaseline();
21+
private static readonly ICacheKey LegacyV1CacheKey = new LegacyV1CacheKey();
2222
private static readonly ICacheKey UriRelativeCacheKey = new UriRelativeCacheKey();
2323
private static readonly ICacheKey UriAbsoluteCacheKey = new UriAbsoluteCacheKey();
24+
private static readonly ICacheKey UriRelativeCaseInsensitiveCacheKey = new UriRelativeCaseInsensitiveCacheKey();
25+
private static readonly ICacheKey UriAbsoluteCaseInsensitiveCacheKey = new UriAbsoluteCaseInsensitiveCacheKey();
2426

25-
[Benchmark(Baseline = true, Description = "Baseline")]
26-
public string CreateUsingBaseline() => CacheKeyBaseline.Create(Context, Commands);
27+
[Benchmark(Baseline = true, Description = nameof(LegacyV1CacheKey))]
28+
public string CreateUsingBaseline() => LegacyV1CacheKey.Create(Context, Commands);
2729

28-
[Benchmark(Description = "UriRelativeCacheKey")]
30+
[Benchmark(Description = nameof(UriRelativeCacheKey))]
2931
public string CreateUsingUriRelativeCacheKey() => UriRelativeCacheKey.Create(Context, Commands);
3032

31-
[Benchmark(Description = "UriAbsoluteCacheKey")]
33+
[Benchmark(Description = nameof(UriRelativeCaseInsensitiveCacheKey))]
34+
public string CreateUsingUriRelativeCaseInsensitiveCacheKey() => UriRelativeCacheKey.Create(Context, Commands);
35+
36+
[Benchmark(Description = nameof(UriAbsoluteCacheKey))]
3237
public string CreateUsingUriAbsoluteCacheKey() => UriAbsoluteCacheKey.Create(Context, Commands);
3338

39+
[Benchmark(Description = nameof(UriAbsoluteCaseInsensitiveCacheKey))]
40+
public string CreateUsingUriAbsoluteCaseInsensitiveCacheKey() => UriAbsoluteCacheKey.Create(Context, Commands);
41+
3442
/*
3543
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000
36-
Intel Core i7-10750H CPU 2.60GHz, 1 CPU, 12 logical and 6 physical cores
44+
Intel Core i7-8650U CPU 1.90GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores
3745
.NET SDK=6.0.101
3846
[Host] : .NET Core 3.1.22 (CoreCLR 4.700.21.56803, CoreFX 4.700.21.57101), X64 RyuJIT
3947
DefaultJob : .NET Core 3.1.22 (CoreCLR 4.700.21.56803, CoreFX 4.700.21.57101), X64 RyuJIT
4048
4149
42-
| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Allocated |
43-
|-------------------- |---------:|--------:|--------:|------:|--------:|-------:|----------:|
44-
| Baseline | 497.4 ns | 8.26 ns | 7.33 ns | 1.00 | 0.00 | 0.1106 | 696 B |
45-
| UriRelativeCacheKey | 270.9 ns | 5.47 ns | 7.11 ns | 0.55 | 0.02 | 0.0587 | 368 B |
46-
| UriAbsoluteCacheKey | 447.4 ns | 3.38 ns | 3.16 ns | 0.90 | 0.01 | 0.0939 | 592 B |
50+
| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Allocated |
51+
|----------------------------------- |---------:|---------:|---------:|------:|--------:|-------:|----------:|
52+
| LegacyV1CacheKey | 666.8 ns | 13.22 ns | 14.14 ns | 1.00 | 0.00 | 0.1659 | 696 B |
53+
| UriRelativeCacheKey | 358.0 ns | 1.98 ns | 1.65 ns | 0.54 | 0.01 | 0.0706 | 296 B |
54+
| UriRelativeCaseInsensitiveCacheKey | 363.1 ns | 7.20 ns | 11.21 ns | 0.55 | 0.02 | 0.0706 | 296 B |
55+
| UriAbsoluteCacheKey | 490.3 ns | 5.14 ns | 4.80 ns | 0.74 | 0.02 | 0.0763 | 320 B |
56+
| UriAbsoluteCaseInsensitiveCacheKey | 475.4 ns | 4.18 ns | 3.71 ns | 0.71 | 0.02 | 0.0763 | 320 B |
4757
*/
4858

4959
private static HttpContext CreateContext()

0 commit comments

Comments
 (0)