Skip to content

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

Merged
merged 3 commits into from
Apr 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/Http/Routing/src/EndpointSelectorContext.cs
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;
Expand All @@ -24,7 +24,7 @@ public sealed class EndpointSelectorContext : IEndpointFeature, IRouteValuesFeat
/// </summary>
public RouteValueDictionary RouteValues
{
get => _routeValues;
get => _routeValues ?? (_routeValues = new RouteValueDictionary());
Copy link
Member

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.

Copy link
Member

@JamesNK JamesNK Apr 21, 2019

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

Copy link
Member Author

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

Copy link
Member Author

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.

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 😄 )

Copy link
Member

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.

set
{
_routeValues = value;
Expand Down Expand Up @@ -67,4 +67,4 @@ RouteData IRoutingFeature.RouteData
set => throw new NotSupportedException();
}
}
}
}
28 changes: 24 additions & 4 deletions src/Http/Routing/src/Matching/DfaMatcher.cs
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;
Expand All @@ -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;
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The 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)
Expand Down Expand Up @@ -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)
{
Expand All @@ -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.
//
Expand All @@ -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.
//
Expand Down Expand Up @@ -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);
Expand Down