-
Notifications
You must be signed in to change notification settings - Fork 10.3k
Avoid allocating the CandidateSet when there's a single match #9622
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System; | ||
|
@@ -16,13 +16,15 @@ internal sealed class DfaMatcher : Matcher | |
private readonly EndpointSelector _selector; | ||
private readonly DfaState[] _states; | ||
private readonly int _maxSegmentCount; | ||
private readonly bool _isDefaultEndpointSelector; | ||
|
||
public DfaMatcher(ILogger<DfaMatcher> logger, EndpointSelector selector, DfaState[] states, int maxSegmentCount) | ||
{ | ||
_logger = logger; | ||
_selector = selector; | ||
_states = states; | ||
_maxSegmentCount = maxSegmentCount; | ||
_isDefaultEndpointSelector = selector is DefaultEndpointSelector; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd feel slightly better if this was an exact type equality check. This isn't public, but that would capture the intent better. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn’t it sealed? Future proofing? |
||
} | ||
|
||
public sealed override Task MatchAsync(HttpContext httpContext, EndpointSelectorContext context) | ||
|
@@ -53,7 +55,8 @@ public sealed override Task MatchAsync(HttpContext httpContext, EndpointSelector | |
// FindCandidateSet will process the DFA and return a candidate set. This does | ||
// some preliminary matching of the URL (mostly the literal segments). | ||
var (candidates, policies) = FindCandidateSet(httpContext, path, segments); | ||
if (candidates.Length == 0) | ||
var candidateCount = candidates.Length; | ||
if (candidateCount == 0) | ||
{ | ||
if (log) | ||
{ | ||
|
@@ -68,6 +71,23 @@ public sealed override Task MatchAsync(HttpContext httpContext, EndpointSelector | |
Logger.CandidatesFound(_logger, path, candidates); | ||
} | ||
|
||
var policyCount = policies.Length; | ||
|
||
// This is a fast path for single candidate, 0 policies and default selector | ||
if (candidateCount == 1 && policyCount == 0 && _isDefaultEndpointSelector) | ||
{ | ||
ref var candidate = ref candidates[0]; | ||
|
||
// Just strict path matching | ||
if (candidate.Flags == Candidate.CandidateFlags.None) | ||
{ | ||
context.Endpoint = candidate.Endpoint; | ||
|
||
// We're done | ||
return Task.CompletedTask; | ||
} | ||
} | ||
|
||
// At this point we have a candidate set, defined as a list of endpoints in | ||
// priority order. | ||
// | ||
|
@@ -83,7 +103,7 @@ public sealed override Task MatchAsync(HttpContext httpContext, EndpointSelector | |
// `candidateSet` is the mutable state that we pass to the EndpointSelector. | ||
var candidateSet = new CandidateSet(candidates); | ||
|
||
for (var i = 0; i < candidates.Length; i++) | ||
for (var i = 0; i < candidateCount; i++) | ||
{ | ||
// PERF: using ref here to avoid copying around big structs. | ||
// | ||
|
@@ -165,7 +185,7 @@ public sealed override Task MatchAsync(HttpContext httpContext, EndpointSelector | |
} | ||
} | ||
|
||
if (policies.Length == 0) | ||
if (policyCount == 0) | ||
{ | ||
// Perf: avoid a state machine if there are no polices | ||
return _selector.SelectAsync(httpContext, context, candidateSet); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we're going to do this, we can take advantage of it in more cases, including the ones that involve cnadidate set.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is beyond the scope of this PR, but what about reusing RVD instances? It would be similar to how we're reusing HttpContext et al. It's basically a per-request allocation that is tied to the route values feature.
Reusing RVD, and the routing feature, would save allocations on every request, not just those with no route values.
Caching per-request objects on reused HttpContexts
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can just straight up do this in Kestrel I fear we won't be able to do the more generic caching for 3.0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, we can, I just wanted to minimize the code change for now. I have to look at see what things are assuming route values are never null when looking at CandidateState.RouteValues (maybe I can do it statiaclly with #enable nullable 😄 )
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CandidateSet has some special semantics around RVD that should just be removed. If treats null RVD as a shorthand for not-a-match in some APIs.