Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

BenjaminAbt
Copy link
Member

@BenjaminAbt BenjaminAbt commented Aug 23, 2025

I saw we're not using Span enough, so I made a test

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 |     1,072.10 ns |        71.749 ns |       3.933 ns |      1.00 |    0.00 |   0.0248 |        - |        - |      432 B |        1.00 |
| UAParser           | Basic      | Chrome Win10 | 8,718,519.27 ns | 1,792,549.083 ns |  98,255.664 ns |  8,132.26 |   83.48 | 656.2500 | 546.8750 | 109.3750 | 11523310 B |   26,674.33 |
| DeviceDetector.NET | Basic      | Chrome Win10 | 5,081,545.57 ns | 3,716,802.892 ns | 203,730.509 ns |  4,739.85 |  165.26 | 296.8750 | 125.0000 |  31.2500 |  5002235 B |   11,579.25 |
|                    |            |              |                 |                  |                |           |         |          |          |          |            |             |
| MyCSharp           | Basic      | Google-Bot   |       163.33 ns |        37.544 ns |       2.058 ns |      1.00 |    0.02 |        - |        - |        - |          - |          NA |
| UAParser           | Basic      | Google-Bot   | 9,188,011.98 ns | 1,811,775.356 ns |  99,309.521 ns | 56,259.76 |  806.07 | 671.8750 | 656.2500 | 109.3750 | 11877003 B |          NA |
| DeviceDetector.NET | Basic      | Google-Bot   | 6,215,080.21 ns |   983,887.715 ns |  53,930.206 ns | 38,055.99 |  502.18 | 539.0625 | 117.1875 |  23.4375 |  8817033 B |          NA |
|                    |            |              |                 |                  |                |           |         |          |          |          |            |             |
| MyCSharp           | Cached     | Chrome Win10 |        27.07 ns |         1.813 ns |       0.099 ns |      1.00 |    0.00 |        - |        - |        - |          - |          NA |
| UAParser           | Cached     | Chrome Win10 |   173,383.01 ns |    27,961.935 ns |   1,532.688 ns |  6,403.90 |   53.09 |   2.1973 |        - |        - |    37489 B |          NA |
|                    |            |              |                 |                  |                |           |         |          |          |          |            |             |
| MyCSharp           | Cached     | Google-Bot   |        17.21 ns |         0.959 ns |       0.053 ns |      1.00 |    0.00 |        - |        - |        - |          - |          NA |
| UAParser           | Cached     | Google-Bot   |   131,931.93 ns |    16,721.894 ns |     916.583 ns |  7,666.68 |   50.38 |   2.6855 |        - |        - |    45857 B |          NA |

I added span

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 |       896.87 ns |        59.474 ns |       3.260 ns |      1.00 |    0.00 |   0.0029 |        - |        - |       48 B |        1.00 |
| UAParser           | Basic      | Chrome Win10 | 8,891,271.35 ns | 1,961,890.839 ns | 107,537.857 ns |  9,913.76 |  108.41 | 656.2500 | 578.1250 | 109.3750 | 11523315 B |  240,069.06 |
| DeviceDetector.NET | Basic      | Chrome Win10 | 5,049,973.05 ns | 3,628,352.419 ns | 198,882.240 ns |  5,630.71 |  192.86 | 304.6875 | 140.6250 |  31.2500 |  5002215 B |  104,212.81 |
|                    |            |              |                 |                  |                |           |         |          |          |          |            |             |
| MyCSharp           | Basic      | Google-Bot   |       160.96 ns |        25.669 ns |       1.407 ns |      1.00 |    0.01 |        - |        - |        - |          - |          NA |
| UAParser           | Basic      | Google-Bot   | 9,093,382.81 ns |   103,238.454 ns |   5,658.848 ns | 56,496.64 |  430.13 | 671.8750 | 656.2500 | 109.3750 | 11877003 B |          NA |
| DeviceDetector.NET | Basic      | Google-Bot   | 6,053,134.64 ns | 1,350,870.084 ns |  74,045.748 ns | 37,607.76 |  490.21 | 539.0625 | 117.1875 |  23.4375 |  8817071 B |          NA |
|                    |            |              |                 |                  |                |           |         |          |          |          |            |             |
| MyCSharp           | Cached     | Chrome Win10 |        24.82 ns |         3.461 ns |       0.190 ns |      1.00 |    0.01 |        - |        - |        - |          - |          NA |
| UAParser           | Cached     | Chrome Win10 |   175,061.44 ns |    11,772.936 ns |     645.314 ns |  7,052.21 |   51.65 |   2.1973 |        - |        - |    37489 B |          NA |
|                    |            |              |                 |                  |                |           |         |          |          |          |            |             |
| MyCSharp           | Cached     | Google-Bot   |        17.43 ns |         2.840 ns |       0.156 ns |      1.00 |    0.01 |        - |        - |        - |          - |          NA |
| UAParser           | Cached     | Google-Bot   |   129,204.69 ns |     1,676.334 ns |      91.886 ns |  7,412.38 |   57.21 |   2.6855 |        - |        - |    45856 B |          NA |

out of interest I used span in public apis (I was aware of ToString)

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 |       778.69 ns |       101.196 ns |       5.547 ns |      1.00 |    0.01 |   0.0305 |        - |        - |      512 B |        1.00 |
| UAParser           | Basic      | Chrome Win10 | 8,829,695.83 ns | 2,751,222.776 ns | 150,803.805 ns | 11,339.59 |  181.74 | 656.2500 | 546.8750 | 109.3750 | 11523315 B |   22,506.47 |
| DeviceDetector.NET | Basic      | Chrome Win10 | 4,917,648.57 ns | 2,897,262.318 ns | 158,808.725 ns |  6,315.52 |  180.88 | 304.6875 | 140.6250 |  31.2500 |  5002215 B |    9,769.95 |
|                    |            |              |                 |                  |                |           |         |          |          |          |            |             |
| MyCSharp           | Basic      | Google-Bot   |       147.14 ns |        27.115 ns |       1.486 ns |      1.00 |    0.01 |   0.0100 |        - |        - |      168 B |        1.00 |
| UAParser           | Basic      | Google-Bot   | 9,217,306.77 ns |   655,208.858 ns |  35,914.209 ns | 62,647.74 |  585.41 | 671.8750 | 656.2500 | 109.3750 | 11877003 B |   70,696.45 |
| DeviceDetector.NET | Basic      | Google-Bot   | 6,085,734.11 ns | 1,591,622.638 ns |  87,242.208 ns | 41,363.22 |  627.40 | 539.0625 | 117.1875 |  23.4375 |  8817041 B |   52,482.39 |
|                    |            |              |                 |                  |                |           |         |          |          |          |            |             |
| MyCSharp           | Cached     | Chrome Win10 |        24.33 ns |         1.835 ns |       0.101 ns |      1.00 |    0.01 |        - |        - |        - |          - |          NA |
| UAParser           | Cached     | Chrome Win10 |   171,740.26 ns |    20,385.847 ns |   1,117.417 ns |  7,060.28 |   47.11 |   2.1973 |        - |        - |    37489 B |          NA |
|                    |            |              |                 |                  |                |           |         |          |          |          |            |             |
| MyCSharp           | Cached     | Google-Bot   |        18.39 ns |         2.174 ns |       0.119 ns |      1.00 |    0.01 |        - |        - |        - |          - |          NA |
| UAParser           | Cached     | Google-Bot   |   130,348.41 ns |    31,922.790 ns |   1,749.796 ns |  7,089.65 |   91.46 |   2.6855 |        - |        - |    45856 B |          NA |

so I used string again in public apis

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 |       936.44 ns |       131.253 ns |       7.194 ns |      1.00 |    0.01 |   0.0029 |        - |        - |       48 B |        1.00 |
| UAParser           | Basic      | Chrome Win10 | 9,512,347.40 ns | 3,961,045.109 ns | 217,118.249 ns | 10,158.42 |  211.89 | 656.2500 | 546.8750 | 109.3750 | 11523315 B |  240,069.06 |
| DeviceDetector.NET | Basic      | Chrome Win10 | 5,428,530.73 ns | 5,276,988.556 ns | 289,249.550 ns |  5,797.23 |  270.29 | 296.8750 | 125.0000 |  31.2500 |  5002239 B |  104,213.31 |
|                    |            |              |                 |                  |                |           |         |          |          |          |            |             |
| MyCSharp           | Basic      | Google-Bot   |       165.66 ns |        21.926 ns |       1.202 ns |      1.00 |    0.01 |        - |        - |        - |          - |          NA |
| UAParser           | Basic      | Google-Bot   | 9,737,403.12 ns | 2,336,698.462 ns | 128,082.328 ns | 58,781.92 |  764.74 | 671.8750 | 656.2500 | 109.3750 | 11877003 B |          NA |
| DeviceDetector.NET | Basic      | Google-Bot   | 6,331,960.42 ns | 1,602,716.199 ns |  87,850.283 ns | 38,224.23 |  518.30 | 500.0000 |  62.5000 |        - |  8817013 B |          NA |
|                    |            |              |                 |                  |                |           |         |          |          |          |            |             |
| MyCSharp           | Cached     | Chrome Win10 |        26.75 ns |         3.749 ns |       0.205 ns |      1.00 |    0.01 |        - |        - |        - |          - |          NA |
| UAParser           | Cached     | Chrome Win10 |   250,039.55 ns |     6,502.182 ns |     356.407 ns |  9,346.54 |   63.39 |   2.1973 |        - |        - |    37488 B |          NA |
|                    |            |              |                 |                  |                |           |         |          |          |          |            |             |
| MyCSharp           | Cached     | Google-Bot   |        19.66 ns |         4.312 ns |       0.236 ns |      1.00 |    0.01 |        - |        - |        - |          - |          NA |
| UAParser           | Cached     | Google-Bot   |   184,991.85 ns |    46,235.986 ns |   2,534.350 ns |  9,408.77 |  148.82 |   2.6855 |        - |        - |    45857 B |          NA |

results:
~90% less allocations (if not zero anyway)
~20% better performance

@BenjaminAbt BenjaminAbt requested review from gfoidl and Copilot August 23, 2025 09:24
Copy link

@Copilot Copilot AI left a 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.

Copy link

@Copilot Copilot AI left a 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.

Copy link
Contributor

@gfoidl gfoidl left a 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
Copy link
Contributor

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.

Copy link
Member Author

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];
Copy link
Contributor

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)
Copy link
Contributor

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?

Copy link
Member Author

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 ☹

Copy link
Member Author

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?

Copy link
Contributor

@gfoidl gfoidl Aug 23, 2025

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)
Copy link
Contributor

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.

@BenjaminAbt
Copy link
Member Author

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 |

@BenjaminAbt BenjaminAbt merged commit f6d5f63 into main Aug 23, 2025
2 checks passed
@BenjaminAbt BenjaminAbt deleted the feature/enhancements branch August 23, 2025 17:51
}

int s = i;
haystack = haystack.Slice(i + 1);

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

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.

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)

Copy link
Member Author

@BenjaminAbt BenjaminAbt Aug 24, 2025

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.

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 🤷

Copy link
Member Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants