-
Notifications
You must be signed in to change notification settings - Fork 9.8k
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
Allow headers to match on ReferenceEquals before OrdinalIgnoreCase #9341
Allow headers to match on ReferenceEquals before OrdinalIgnoreCase #9341
Conversation
4988c1a
to
252a77c
Compare
c19add9
to
9efd8a7
Compare
9efd8a7
to
bbd1c8a
Compare
ResponseHeaderCollectionBenchmark Method | Type | Mean | Op/s | Allocated |
----------- |--------------------- |------------:|-------------:|----------:|
- SetHeaders | Common | 889.92 ns | 1,123,698.8 | 0 B |
+ SetHeaders | Common | 751.84 ns | 1,330,077.7 | 0 B |
- SetHeaders | ContentLengthString | 51.64 ns | 19,363,867.1 | 0 B |
+ SetHeaders | ContentLengthString | 51.90 ns | 19,268,524.6 | 0 B |
- SetHeaders | Plaintext | 55.45 ns | 18,034,860.0 | 0 B |
+ SetHeaders | Plaintext | 51.40 ns | 19,455,753.2 | 0 B |
- SetHeaders | Unknown | 1,539.01 ns | 649,770.4 | 0 B |
+ SetHeaders | Unknown | 1,589.23 ns | 629,237.4 | 0 B | |
cc8986d
to
bfdf578
Compare
@@ -26,7 +27,7 @@ public Task Invoke(HttpContext context) | |||
throw new InvalidOperationException("RequestId should be null here"); | |||
} | |||
|
|||
var requestId = context.Request.Headers["RequestId"]; |
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.
Note: this was "RequestId"
but HostingApplicationDiagnostics.cs
uses "Request-Id"
; now they are both using the same value via HeaderNames.RequestId
(of "Request-Id"
)
bfdf578
to
82f897c
Compare
@@ -118,74 +118,87 @@ public partial class EntityTagHeaderValue | |||
} | |||
public static partial class HeaderNames | |||
{ | |||
public const string Accept = "Accept"; |
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.
I think this is a breaking change? These can't be used in attributes anymore?
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.
Indeed. Do we have any examples where it's relevant? I have not seen these used in attributes.
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.
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.
Suppose its a choice:
-
Fast header string matching (extremely common; used in every request, 1258 uses in ASP.NET Core itself)
-
Use in attributes (extremely uncommon, 2 test uses in ASP.NET Core itself and those testing http/2 protocol psudeo headers)
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.
It’s not common no. The only other thing is that I’d like to eventually burn the headers assembly to the ground
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.
Keeping it would involve duplication, which would get pretty confusing.
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.
To caveat the scope, its source code breaking if used in attributes or other string const initalization; otherwise isn't source code breaking.
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.
Oh I didn't see the whole change. I think this change is fine then. The few cases I found on the internet were not in attribute cases where a const is required. They were just regular imperative code. Carry on.
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 use these quite a bit in case
statements for what it's worth...though I guess not anymore. Bleh.
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.
Sorry :-/
"Request-Id", | ||
"Correlation-Context", | ||
"TraceParent", | ||
"TraceState" |
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.
So I see you did come up with a few more non-HTTP/2 headers. Do these make sense to you @Tratcher?
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.
They are from hosting as it was checking each header that was the same length with IgnoreOrdinalCase for
if (!headers.TryGetValue(TraceParentHeaderName, out var correlationId))
{
headers.TryGetValue(RequestIdHeaderName, out correlationId);
etc
}} | ||
break;")} | ||
{{{Each(byLength.OrderBy(h => !h.PrimaryHeader), header => $@" | ||
if (ReferenceEquals(HeaderNames.{header.Identifier}, key)) |
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.
Is manually calling object.ReferenceEquals and then following up with string.Equals StringComparison.OrdinalIgnoreCase really faster than doing the string comparison up front? If so, shouldn't we just add an object.ReferenceEquals fast path to the start of string.Equals? Or at the very least add a helper method to Kestrel that does this?
Also, AFIACT the body of this if block is identical to the body of the string.Equals if block below. Wouldn't it be better to just add || HeaderNames.{header.Identifier}.Equals(key, StringComparison.OrdinalIgnoreCase)
to this if condition even if it isn't part of some helper method?
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.
string.Equals does have a ReferenceEquals at the start (though its uninlinable since its kinda chunky)
The two issues this resolves are
- Quick inlined reference check
- Check all the references first before checking IgnoreOrdinalCase
So if there were 3 headers of that length; and it was the 3rd header it would do
ReferenceEquals, IgnoreOrdinalCase, ReferenceEquals, IgnoreOrdinalCase, ReferenceEquals => match
With this approach it instead does
ReferenceEquals, ReferenceEquals, ReferenceEquals => match
Only if none of the ReferenceEquals match does it then start doing IgnoreOrdinalCase.
Noticed this issue after adding TraceParent
and trying to workout why ContentMD5
was being tested IgnoreOrdinalCase.
return false;")} | ||
}}")} | ||
{Each(byLength.OrderBy(h => !h.PrimaryHeader), header => $@" | ||
if (HeaderNames.{header.Identifier}.Equals(key, StringComparison.OrdinalIgnoreCase)) |
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.
Everywhere there are two if blocks in a row with identical bodies and one checking object.ReferenceEquals and the other checking string.Equals, I have the same questions I did for TryGetValueFast.
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.
As above. Checks all the ReferenceEquals first; then does OrdinalIgnoreCase; rather than interleaving the IgnoreCase checks
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.
93e7b1e
to
c9d21ce
Compare
Build: Windows x64/x86 Job failed on a YARN error (transient) |
I think @javiercn was looking at YARN reliability. |
Can you restart that job? 😉 I only have the power to restart everything, not an individual item. |
Generally I wait until all the builds are complete before triaging and restarting the failed build. I'm not sure if you can restart just the failed build before then. I'm setting a reminder to requeue once this is finished. |
Yeah, but this PR needs to be rebased on top of master. I just moved the yarn install for all projects to the beginning of the build and made them happen sequentially, that seems to have fixed the issue you are seeing. |
Doesn't our PR validation automatically rebase on the target branch? |
It doesn't AFAIK. It simply checks out the repo/branch and builds it, you still have to deal with merges/rebases. |
Looks good to go. I'll merge this tomorrow so I remember to make the breaking change announcement at the same time. |
It doesn't rebase, but it does use the "merge ref" from GitHub ( |
At a minimum, it's safe to assume the build is incorporating all changes from the most recent commit to the PR branch and all changes that were in |
Can probably close & re-open the PR to get the master ref to update and re-try? |
@davidfowl merge conflicted me |
merge conflict resolved |
Provide a performance advantage to using the
Microsoft.Net.Http.Headers.HeaderNames
constants.Change
HeaderNames
to static properties rather than string constants; so reference equality works; prior to falling back to an OrdinalIgnoreCase characters comparison. (string constants embed directly in the calling assembly so don't end up being reference equal)Use these properties in Kestrel header methods rather than its own string constants so the reference equality works there.
ResponseHeaderCollectionBenchmark (Common is the main change +18%)
The inlined string property becomes a constant mov at Jit time e.g.
becomes
For plaintext with the default builder (including host filtering)
Before
After
Background
Noticed this while looking at something else #9359
If you use the
HeaderNames
class they don't match with reference equality e.g.as they are defined in a different assembly to the string constants used in Kestrel; so it moves to an OrdinalIgnoreCase compare. Using the same
HeaderNames
header names in Kestrel means it will first match using ReferenceEquals in coreclrand won't need to fallback to the
OrdinalIgnoreCase
whenHeaderNames
is used e.g when settingas DefaultHttpResponse also uses these constants