Skip to content

Commit c7365a0

Browse files
ladeakLaszlo DeakJamesNK
authored
Optimize HTTP method lookup in routing jumptable (#51803)
Co-authored-by: Laszlo Deak <ladeak87@windowslive.com> Co-authored-by: James Newton-King <james@newtonking.com>
1 parent b80fac3 commit c7365a0

File tree

7 files changed

+260
-81
lines changed

7 files changed

+260
-81
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using BenchmarkDotNet.Attributes;
5+
using Microsoft.AspNetCore.Http;
6+
7+
namespace Microsoft.AspNetCore.Routing.Matching;
8+
9+
public class HttpMethodMatcherPolicyBenchmark
10+
{
11+
private static string[] TestHttpMethods = ["*", HttpMethods.Get, HttpMethods.Connect, HttpMethods.Delete, HttpMethods.Head, HttpMethods.Options, HttpMethods.Patch, HttpMethods.Put, HttpMethods.Post, HttpMethods.Trace, "MERGE"];
12+
private HttpMethodMatcherPolicy _jumpTableBuilder = new();
13+
private List<PolicyJumpTableEdge> _edges = new();
14+
15+
[Params(3, 5, 11)]
16+
public int DestinationCount { get; set; }
17+
18+
[GlobalSetup]
19+
public void Setup()
20+
{
21+
for (int i = 0; i < DestinationCount; i++)
22+
{
23+
_edges.Add(new PolicyJumpTableEdge(new HttpMethodMatcherPolicy.EdgeKey(TestHttpMethods[i], false), i + 1));
24+
}
25+
}
26+
27+
[Benchmark]
28+
public PolicyJumpTable BuildJumpTable()
29+
{
30+
return _jumpTableBuilder.BuildJumpTable(1, _edges);
31+
}
32+
}

src/Http/Routing/perf/Microbenchmarks/Matching/HttpMethodPolicyJumpTableBenchmark.cs

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,31 +11,44 @@ public class HttpMethodPolicyJumpTableBenchmark
1111
private PolicyJumpTable _dictionaryJumptable;
1212
private PolicyJumpTable _singleEntryJumptable;
1313
private DefaultHttpContext _httpContext;
14+
private Dictionary<string, int> _destinations = new();
15+
16+
[Params("GET", "POST", "Merge")]
17+
public string TestHttpMethod { get; set; }
1418

1519
[GlobalSetup]
1620
public void Setup()
1721
{
18-
_dictionaryJumptable = new HttpMethodDictionaryPolicyJumpTable(
19-
0,
20-
new Dictionary<string, int>
21-
{
22-
[HttpMethods.Get] = 1
23-
},
24-
-1,
25-
new Dictionary<string, int>
26-
{
27-
[HttpMethods.Get] = 2
28-
});
29-
_singleEntryJumptable = new HttpMethodSingleEntryPolicyJumpTable(
30-
0,
31-
HttpMethods.Get,
32-
-1,
33-
supportsCorsPreflight: true,
34-
-1,
35-
2);
22+
_destinations.Add("MERGE", 10);
23+
var lookup = CreateLookup(_destinations);
3624

25+
_dictionaryJumptable = new HttpMethodDictionaryPolicyJumpTable(lookup, corsPreflightDestinations: null);
26+
_singleEntryJumptable = new HttpMethodSingleEntryPolicyJumpTable(0, HttpMethods.Get, -1, supportsCorsPreflight: false, -1, 2);
3727
_httpContext = new DefaultHttpContext();
38-
_httpContext.Request.Method = HttpMethods.Get;
28+
_httpContext.Request.Method = TestHttpMethod;
29+
}
30+
31+
private static HttpMethodDestinationsLookup CreateLookup(Dictionary<string, int> extra)
32+
{
33+
var destinations = new List<KeyValuePair<string, int>>
34+
{
35+
KeyValuePair.Create(HttpMethods.Connect, 1),
36+
KeyValuePair.Create(HttpMethods.Delete, 2),
37+
KeyValuePair.Create(HttpMethods.Head, 3),
38+
KeyValuePair.Create(HttpMethods.Get, 4),
39+
KeyValuePair.Create(HttpMethods.Options, 5),
40+
KeyValuePair.Create(HttpMethods.Patch, 6),
41+
KeyValuePair.Create(HttpMethods.Put, 7),
42+
KeyValuePair.Create(HttpMethods.Post, 8),
43+
KeyValuePair.Create(HttpMethods.Trace, 9)
44+
};
45+
46+
foreach (var item in extra)
47+
{
48+
destinations.Add(item);
49+
}
50+
51+
return new HttpMethodDestinationsLookup(destinations, exitDestination: 0);
3952
}
4053

4154
[Benchmark]

src/Http/Routing/perf/Microbenchmarks/readme.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
Compile the solution in Release mode (so binaries are available in release)
22

3+
Set environment variables with `. .\activate.ps1` or `activate.sh` command, which can be found at the root of the repository.
4+
35
To run a specific benchmark add it as parameter.
46
```
5-
dotnet run -c Release --framework <tfm> <benchmark_name>
7+
dotnet run -c Release --framework <tfm> --filter <benchmark_name>
68
```
79

810
To run all benchmarks use '*' as the name.
@@ -13,4 +15,4 @@ dotnet run -c Release --framework <tfm> *
1315
If you run without any parameters, you'll be offered the list of all benchmarks and get to choose.
1416
```
1517
dotnet run -c Release --framework <tfm>
16-
```
18+
```
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Http;
5+
6+
namespace Microsoft.AspNetCore.Routing.Matching;
7+
8+
internal sealed class HttpMethodDestinationsLookup
9+
{
10+
private readonly int _exitDestination;
11+
12+
private readonly int _connectDestination;
13+
private readonly int _deleteDestination;
14+
private readonly int _getDestination;
15+
private readonly int _headDestination;
16+
private readonly int _optionsDestination;
17+
private readonly int _patchDestination;
18+
private readonly int _postDestination;
19+
private readonly int _putDestination;
20+
private readonly int _traceDestination;
21+
private readonly Dictionary<string, int>? _extraDestinations;
22+
23+
public HttpMethodDestinationsLookup(List<KeyValuePair<string, int>> destinations, int exitDestination)
24+
{
25+
_exitDestination = exitDestination;
26+
27+
int? connectDestination = null;
28+
int? deleteDestination = null;
29+
int? getDestination = null;
30+
int? headDestination = null;
31+
int? optionsDestination = null;
32+
int? patchDestination = null;
33+
int? postDestination = null;
34+
int? putDestination = null;
35+
int? traceDestination = null;
36+
37+
foreach (var (method, destination) in destinations)
38+
{
39+
if (method.Length >= 3) // 3 == smallest known method
40+
{
41+
switch (method[0] | 0x20)
42+
{
43+
case 'c' when method.Equals(HttpMethods.Connect, StringComparison.OrdinalIgnoreCase):
44+
connectDestination = destination;
45+
continue;
46+
case 'd' when method.Equals(HttpMethods.Delete, StringComparison.OrdinalIgnoreCase):
47+
deleteDestination = destination;
48+
continue;
49+
case 'g' when method.Equals(HttpMethods.Get, StringComparison.OrdinalIgnoreCase):
50+
getDestination = destination;
51+
continue;
52+
case 'h' when method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase):
53+
headDestination = destination;
54+
continue;
55+
case 'o' when method.Equals(HttpMethods.Options, StringComparison.OrdinalIgnoreCase):
56+
optionsDestination = destination;
57+
continue;
58+
case 'p':
59+
if (method.Equals(HttpMethods.Put, StringComparison.OrdinalIgnoreCase))
60+
{
61+
putDestination = destination;
62+
continue;
63+
}
64+
else if (method.Equals(HttpMethods.Post, StringComparison.OrdinalIgnoreCase))
65+
{
66+
postDestination = destination;
67+
continue;
68+
}
69+
else if (method.Equals(HttpMethods.Patch, StringComparison.OrdinalIgnoreCase))
70+
{
71+
patchDestination = destination;
72+
continue;
73+
}
74+
break;
75+
case 't' when method.Equals(HttpMethods.Trace, StringComparison.OrdinalIgnoreCase):
76+
traceDestination = destination;
77+
continue;
78+
}
79+
}
80+
81+
_extraDestinations ??= new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
82+
_extraDestinations.Add(method, destination);
83+
}
84+
85+
_connectDestination = connectDestination ?? _exitDestination;
86+
_deleteDestination = deleteDestination ?? _exitDestination;
87+
_getDestination = getDestination ?? _exitDestination;
88+
_headDestination = headDestination ?? _exitDestination;
89+
_optionsDestination = optionsDestination ?? _exitDestination;
90+
_patchDestination = patchDestination ?? _exitDestination;
91+
_postDestination = postDestination ?? _exitDestination;
92+
_putDestination = putDestination ?? _exitDestination;
93+
_traceDestination = traceDestination ?? _exitDestination;
94+
}
95+
96+
public int GetDestination(string method)
97+
{
98+
// Implementation skeleton taken from https://github.com/dotnet/runtime/blob/13b43155c31beb844b1b04766fea65235ccd8363/src/libraries/System.Net.Http/src/System/Net/Http/HttpMethod.cs#L179
99+
if (method.Length >= 3) // 3 == smallest known method
100+
{
101+
(var matchedMethod, var destination) = (method[0] | 0x20) switch
102+
{
103+
'c' => (HttpMethods.Connect, _connectDestination),
104+
'd' => (HttpMethods.Delete, _deleteDestination),
105+
'g' => (HttpMethods.Get, _getDestination),
106+
'h' => (HttpMethods.Head, _headDestination),
107+
'o' => (HttpMethods.Options, _optionsDestination),
108+
'p' => method.Length switch
109+
{
110+
3 => (HttpMethods.Put, _putDestination),
111+
4 => (HttpMethods.Post, _postDestination),
112+
_ => (HttpMethods.Patch, _patchDestination),
113+
},
114+
't' => (HttpMethods.Trace, _traceDestination),
115+
_ => (null, 0),
116+
};
117+
118+
if (matchedMethod is not null && method.Equals(matchedMethod, StringComparison.OrdinalIgnoreCase))
119+
{
120+
return destination;
121+
}
122+
}
123+
124+
if (_extraDestinations != null && _extraDestinations.TryGetValue(method, out var extraDestination))
125+
{
126+
return extraDestination;
127+
}
128+
129+
return _exitDestination;
130+
}
131+
}

src/Http/Routing/src/Matching/HttpMethodDictionaryPolicyJumpTable.cs

Lines changed: 10 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,40 +7,25 @@ namespace Microsoft.AspNetCore.Routing.Matching;
77

88
internal sealed class HttpMethodDictionaryPolicyJumpTable : PolicyJumpTable
99
{
10-
private readonly int _exitDestination;
11-
private readonly Dictionary<string, int>? _destinations;
12-
private readonly int _corsPreflightExitDestination;
13-
private readonly Dictionary<string, int>? _corsPreflightDestinations;
14-
15-
private readonly bool _supportsCorsPreflight;
10+
private readonly HttpMethodDestinationsLookup _httpMethodDestinations;
11+
private readonly HttpMethodDestinationsLookup? _corsHttpMethodDestinations;
1612

1713
public HttpMethodDictionaryPolicyJumpTable(
18-
int exitDestination,
19-
Dictionary<string, int>? destinations,
20-
int corsPreflightExitDestination,
21-
Dictionary<string, int>? corsPreflightDestinations)
14+
HttpMethodDestinationsLookup destinations,
15+
HttpMethodDestinationsLookup? corsPreflightDestinations)
2216
{
23-
_exitDestination = exitDestination;
24-
_destinations = destinations;
25-
_corsPreflightExitDestination = corsPreflightExitDestination;
26-
_corsPreflightDestinations = corsPreflightDestinations;
27-
28-
_supportsCorsPreflight = _corsPreflightDestinations != null && _corsPreflightDestinations.Count > 0;
17+
_httpMethodDestinations = destinations;
18+
_corsHttpMethodDestinations = corsPreflightDestinations;
2919
}
3020

3121
public override int GetDestination(HttpContext httpContext)
3222
{
33-
int destination;
34-
3523
var httpMethod = httpContext.Request.Method;
36-
if (_supportsCorsPreflight && HttpMethodMatcherPolicy.IsCorsPreflightRequest(httpContext, httpMethod, out var accessControlRequestMethod))
24+
if (_corsHttpMethodDestinations != null && HttpMethodMatcherPolicy.IsCorsPreflightRequest(httpContext, httpMethod, out var accessControlRequestMethod))
3725
{
38-
return _corsPreflightDestinations!.TryGetValue(accessControlRequestMethod.ToString(), out destination)
39-
? destination
40-
: _corsPreflightExitDestination;
26+
var corsHttpMethod = accessControlRequestMethod.ToString();
27+
return _corsHttpMethodDestinations.GetDestination(corsHttpMethod);
4128
}
42-
43-
return _destinations != null &&
44-
_destinations.TryGetValue(httpMethod, out destination) ? destination : _exitDestination;
29+
return _httpMethodDestinations.GetDestination(httpMethod);
4530
}
4631
}

src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs

Lines changed: 27 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -310,52 +310,31 @@ public IReadOnlyList<PolicyNodeEdge> GetEdges(IReadOnlyList<Endpoint> endpoints)
310310
/// <returns></returns>
311311
public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList<PolicyJumpTableEdge> edges)
312312
{
313-
Dictionary<string, int>? destinations = null;
314-
Dictionary<string, int>? corsPreflightDestinations = null;
313+
List<KeyValuePair<string, int>>? destinations = null;
314+
List<KeyValuePair<string, int>>? corsPreflightDestinations = null;
315+
var corsPreflightExitDestination = exitDestination;
316+
315317
for (var i = 0; i < edges.Count; i++)
316318
{
317319
// We create this data, so it's safe to cast it.
318320
var key = (EdgeKey)edges[i].State;
321+
var destination = edges[i].Destination;
322+
319323
if (key.IsCorsPreflightRequest)
320324
{
321-
if (corsPreflightDestinations == null)
322-
{
323-
corsPreflightDestinations = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
324-
}
325-
326-
corsPreflightDestinations.Add(key.HttpMethod, edges[i].Destination);
325+
ProcessEdge(key.HttpMethod, destination, ref corsPreflightExitDestination, ref corsPreflightDestinations);
327326
}
328327
else
329328
{
330-
if (destinations == null)
331-
{
332-
destinations = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
333-
}
334-
335-
destinations.Add(key.HttpMethod, edges[i].Destination);
329+
ProcessEdge(key.HttpMethod, destination, ref exitDestination, ref destinations);
336330
}
337331
}
338332

339-
int corsPreflightExitDestination = exitDestination;
340-
if (corsPreflightDestinations != null && corsPreflightDestinations.TryGetValue(AnyMethod, out var matchesAnyVerb))
341-
{
342-
// If we have endpoints that match any HTTP method, use that as the exit.
343-
corsPreflightExitDestination = matchesAnyVerb;
344-
corsPreflightDestinations.Remove(AnyMethod);
345-
}
346-
347-
if (destinations != null && destinations.TryGetValue(AnyMethod, out matchesAnyVerb))
348-
{
349-
// If we have endpoints that match any HTTP method, use that as the exit.
350-
exitDestination = matchesAnyVerb;
351-
destinations.Remove(AnyMethod);
352-
}
353-
354333
if (destinations?.Count == 1)
355334
{
356335
// If there is only a single valid HTTP method then use an optimized jump table.
357336
// It avoids unnecessary dictionary lookups with the method name.
358-
var httpMethodDestination = destinations.Single();
337+
var httpMethodDestination = destinations[0];
359338
var method = httpMethodDestination.Key;
360339
var destination = httpMethodDestination.Value;
361340
var supportsCorsPreflight = false;
@@ -364,7 +343,7 @@ public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList<PolicyJ
364343
if (corsPreflightDestinations?.Count > 0)
365344
{
366345
supportsCorsPreflight = true;
367-
corsPreflightDestination = corsPreflightDestinations.Single().Value;
346+
corsPreflightDestination = corsPreflightDestinations[0].Value;
368347
}
369348

370349
return new HttpMethodSingleEntryPolicyJumpTable(
@@ -378,10 +357,23 @@ public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList<PolicyJ
378357
else
379358
{
380359
return new HttpMethodDictionaryPolicyJumpTable(
381-
exitDestination,
382-
destinations,
383-
corsPreflightExitDestination,
384-
corsPreflightDestinations);
360+
new HttpMethodDestinationsLookup(destinations ?? new(), exitDestination),
361+
corsPreflightDestinations != null ? new HttpMethodDestinationsLookup(corsPreflightDestinations, corsPreflightExitDestination) : null);
362+
}
363+
364+
static void ProcessEdge(string httpMethod, int destination, ref int exitDestination, ref List<KeyValuePair<string, int>>? destinations)
365+
{
366+
// If we have endpoints that match any HTTP method, use that as the exit.
367+
if (string.Equals(httpMethod, AnyMethod, StringComparison.OrdinalIgnoreCase))
368+
{
369+
exitDestination = destination;
370+
}
371+
else
372+
{
373+
374+
destinations ??= new();
375+
destinations.Add(KeyValuePair.Create(httpMethod, destination));
376+
}
385377
}
386378
}
387379

0 commit comments

Comments
 (0)