-
Notifications
You must be signed in to change notification settings - Fork 9
add performance enhancements #72
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
Conversation
feat(solution): add NestedProjects section to solution file chore: add commit message instructions to settings chore: create Justfile for build and benchmark commands chore: add Directory.Build.props for benchmark project
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.
Pull Request Overview
This PR implements significant performance enhancements to the HTTP User Agent Parser by replacing regex-based parsing with faster Span-based token matching for platform and browser detection. The changes demonstrate substantial performance improvements with up to 90% reduction in memory allocations.
- Replace regex-based platform and browser detection with optimized Span token matching
- Add fast-path token rules arrays for zero-allocation lookups while maintaining regex fallback compatibility
- Update copyright years across all license files and add development tooling configuration
Reviewed Changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 4 comments.
Show a summary per file
File | Description |
---|---|
src/HttpUserAgentParser/HttpUserAgentParser.cs | Core optimization implementing Span-based parsing methods with aggressive inlining |
src/HttpUserAgentParser/HttpUserAgentStatics.cs | Added fast-path token rule arrays and precompiled regex mappings for performance |
src/HttpUserAgentParser/Providers/HttpUserAgentParserDefaultProvider.cs | Updated XML documentation reference to match method signature |
LICENSE files | Updated copyright year from 2023 to 2025 |
Benchmark/tooling files | Added development environment configuration and namespace corrections |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
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.
Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.
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.
Some notes, and I'll push two commits for these (which you can revert, squash, etc. however you want).
]; | ||
|
||
// Precompiled platform regex map to attach to PlatformInformation without per-call allocations | ||
private static readonly Dictionary<string, Regex> s_platformRegexMap = s_platformRules |
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.
Could use FrozenDictionary
on targets that support it.
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 totally forgot FrozenDictionary
// Skip separators until we hit a digit | ||
while (i < end) | ||
{ | ||
char c = haystack[i]; |
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 could slice the haystack
to avoid some bound checks here and below.
while (i < end) | ||
{ | ||
char c = haystack[i]; | ||
if ((uint)(c - '0') <= 9) |
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.
Maybe it's worth to use a vectorized search here too?
IndexOf will be a method call, so potentially slower, but a simple self coded search for one vector length could be faster. Maybe in combination with a SWAR search.
I can try this later (September). But we cache that info later on, so is it worth it?
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.
Maybe it's worth to use a vectorized search here too?
Yes, but tbh vectorized search is way over my casual knowledge ☹
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 we merge this and create a new PR end sept?
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.
Yep, I'll file an issue and assign to me so I can't forget it.
Edit: #73
/// Accepts digits and dots; skips common separators ('/', ' ', ':', '=') until first digit. | ||
/// Returns false if no version-like token is found. | ||
/// </summary> | ||
private static bool TryExtractVersion(ReadOnlySpan<char> haystack, int startIndex, out Range range) |
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.
The startIndex
isn't needed, as the ROS haystack
can be sliced therefore to proper start.
With your changes BenchmarkDotNet v0.14.0, Windows 10 (10.0.19045.6216/22H2/2022Update)
AMD Ryzen 9 9950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 10.0.100-preview.7.25380.108
[Host] : .NET 10.0.0 (10.0.25.38108), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
ShortRun : .NET 10.0.0 (10.0.25.38108), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
Job=ShortRun IterationCount=3 LaunchCount=1
WarmupCount=3
| Method | Categories | Data | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Gen2 | Allocated | Alloc Ratio |
|------------------- |----------- |------------- |----------------:|-----------------:|---------------:|----------:|---------:|---------:|---------:|---------:|-----------:|------------:|
| MyCSharp | Basic | Chrome Win10 | 867.96 ns | 67.366 ns | 3.693 ns | 1.00 | 0.01 | 0.0029 | - | - | 48 B | 1.00 |
| UAParser | Basic | Chrome Win10 | 8,915,044.79 ns | 619,314.743 ns | 33,946.731 ns | 10,271.42 | 50.86 | 656.2500 | 546.8750 | 109.3750 | 11523315 B | 240,069.06 |
| DeviceDetector.NET | Basic | Chrome Win10 | 5,314,358.33 ns | 7,521,345.077 ns | 412,270.305 ns | 6,122.91 | 411.98 | 296.8750 | 125.0000 | 31.2500 | 5002235 B | 104,213.23 |
| | | | | | | | | | | | | |
| MyCSharp | Basic | Google-Bot | 161.00 ns | 10.338 ns | 0.567 ns | 1.00 | 0.00 | - | - | - | - | NA |
| UAParser | Basic | Google-Bot | 9,464,732.81 ns | 4,726,277.375 ns | 259,063.212 ns | 58,789.19 | 1,404.99 | 671.8750 | 656.2500 | 109.3750 | 11877003 B | NA |
| DeviceDetector.NET | Basic | Google-Bot | 5,999,877.08 ns | 1,009,150.081 ns | 55,314.921 ns | 37,267.61 | 318.42 | 500.0000 | 62.5000 | - | 8817022 B | NA |
| | | | | | | | | | | | | |
| MyCSharp | Cached | Chrome Win10 | 24.86 ns | 2.335 ns | 0.128 ns | 1.00 | 0.01 | - | - | - | - | NA |
| UAParser | Cached | Chrome Win10 | 173,556.74 ns | 6,719.752 ns | 368.332 ns | 6,982.49 | 33.76 | 2.1973 | - | - | 37489 B | NA |
| | | | | | | | | | | | | |
| MyCSharp | Cached | Google-Bot | 17.48 ns | 1.317 ns | 0.072 ns | 1.00 | 0.01 | - | - | - | - | NA |
| UAParser | Cached | Google-Bot | 130,208.42 ns | 30,606.978 ns | 1,677.672 ns | 7,449.30 | 87.27 | 2.6855 | - | - | 45857 B | NA | |
} | ||
|
||
int s = i; | ||
haystack = haystack.Slice(i + 1); |
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 have a test for an invalid useragent. With this code it throws an ArgumentOutOfRangeException here
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.
Mozilla/ (iPhone; U; CPU iPhone OS like Mac OS X) AppleWebKit/ (KHTML, like Gecko) Version/ Mobile/ Safari/
We test for this UA.
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.
Wanted to add a test and create a pr by myself. But couldn't get the solution to build. (Have not installed .net10 preview on my machine)
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.
What does the user agent
Mozilla/ (iPhone; U; CPU iPhone OS like Mac OS X) AppleWebKit/ (KHTML, like Gecko) Version/ Mobile/ Safari/
stand for?
I couldn’t find any exact match online.
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.
Yeah that is a invalid one.
We have a test which tries to parse this one (can not remember how we came up with this one...)
We use it to test that we do not proceed when the ua is invalid.
I am ok to change my test, but the parser did not throw before and now throws an exception 🤷
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.
Okay, a invalid one.
I will take a look. It should not break tests.
I saw we're not using Span enough, so I made a test
I added span
out of interest I used span in public apis (I was aware of ToString)
so I used string again in public apis
results:
~90% less allocations (if not zero anyway)
~20% better performance