-
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
Changes from all commits
9f87e8b
ef396d1
d767fb4
240783a
a23f6a2
d5ebc22
5161624
40620c6
9d9f88c
1ae2901
324f1a8
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 |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{ | ||
// github copilot commit message instructions (preview) | ||
"github.copilot.chat.commitMessageGeneration.instructions": [ | ||
{ "text": "Use conventional commit format: type(scope): description" }, | ||
{ "text": "Use imperative mood: 'Add feature' not 'Added feature'" }, | ||
{ "text": "Keep subject line under 50 characters" }, | ||
{ "text": "Use types: feat, fix, docs, style, refactor, perf, test, chore, ci" }, | ||
{ "text": "Include scope when relevant (e.g., api, ui, auth)" }, | ||
{ "text": "Reference issue numbers with # prefix" } | ||
] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
{ | ||
"version": "2.0.0", | ||
"tasks": [ | ||
{ | ||
"label": "test", | ||
"type": "shell", | ||
"command": "dotnet test --nologo", | ||
"args": [], | ||
"problemMatcher": [ | ||
"$msCompile" | ||
], | ||
"group": "build" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
# Justfile .NET - Benjamin Abt 2025 - https://benjamin-abt.com | ||
# https://github.com/BenjaminAbt/templates/blob/main/justfile/dotnet | ||
|
||
set shell := ["pwsh", "-c"] | ||
|
||
# ===== Configurable defaults ===== | ||
CONFIG := "Debug" | ||
TFM := "net10.0" | ||
BENCH_PRJ := "perf/HttpUserAgentParser.Benchmarks/HttpUserAgentParser.Benchmarks.csproj" | ||
|
||
# ===== Default / Help ===== | ||
default: help | ||
|
||
help: | ||
# Overview: | ||
just --list | ||
# Usage: | ||
# just build | ||
# just test | ||
# just bench | ||
|
||
# ===== Basic .NET Workflows ===== | ||
restore: | ||
dotnet restore | ||
|
||
build *ARGS: | ||
dotnet build --configuration "{{CONFIG}}" --nologo --verbosity minimal {{ARGS}} | ||
|
||
rebuild *ARGS: | ||
dotnet build --configuration "{{CONFIG}}" --nologo --verbosity minimal --no-incremental {{ARGS}} | ||
|
||
clean: | ||
dotnet clean --configuration "{{CONFIG}}" --nologo | ||
|
||
run *ARGS: | ||
dotnet run --project --framework "{{TFM}}" --configuration "{{CONFIG}}" --no-launch-profile {{ARGS}} | ||
|
||
# ===== Quality / Tests ===== | ||
format: | ||
dotnet format --verbosity minimal | ||
|
||
format-check: | ||
dotnet format --verify-no-changes --verbosity minimal | ||
|
||
test *ARGS: | ||
dotnet test --configuration "{{CONFIG}}" --framework "{{TFM}}" --nologo --verbosity minimal {{ARGS}} | ||
|
||
test-cov: | ||
dotnet test --configuration "{{CONFIG}}" --framework "{{TFM}}" --nologo --verbosity minimal /p:CollectCoverage=true /p:CoverletOutputFormat="cobertura,lcov,opencover" /p:CoverletOutput="./TestResults/coverage/coverage" | ||
|
||
|
||
test-filter QUERY: | ||
dotnet test --configuration "{{CONFIG}}" --framework "{{TFM}}" --nologo --verbosity minimal --filter "{{QUERY}}" | ||
|
||
# ===== Packaging / Release ===== | ||
pack *ARGS: | ||
dotnet pack --configuration "{{CONFIG}}" --nologo --verbosity minimal -o "./artifacts/packages" {{ARGS}} | ||
|
||
publish *ARGS: | ||
dotnet publish --configuration "{{CONFIG}}" --framework "{{TFM}}" --nologo --verbosity minimal -o "./artifacts/publish/{{TFM}}" {{ARGS}} | ||
|
||
publish-sc RID *ARGS: | ||
dotnet publish --configuration "{{CONFIG}}" --framework "{{TFM}}" --runtime "{{RID}}" --self-contained true -p:PublishSingleFile=true -p:PublishTrimmed=false --nologo --verbosity minimal -o "./artifacts/publish/{{TFM}}-{{RID}}" {{ARGS}} | ||
|
||
# ===== Benchmarks ===== | ||
bench *ARGS: | ||
dotnet run --configuration Release --project "{{BENCH_PRJ}}" --framework "{{TFM}}" {{ARGS}} | ||
|
||
# ===== Housekeeping ===== | ||
clean-artifacts: | ||
if (Test-Path "./artifacts") { Remove-Item "./artifacts" -Recurse -Force } | ||
|
||
clean-all: | ||
just clean | ||
just clean-artifacts | ||
# Optionally: git clean -xdf | ||
|
||
# ===== Combined Flows ===== | ||
fmt-build: | ||
just format | ||
just build | ||
|
||
ci: | ||
just clean | ||
just restore | ||
just format-check | ||
just build | ||
just test-cov |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
// Copyright © https://myCSharp.de - all rights reserved | ||
|
||
using System.Diagnostics.CodeAnalysis; | ||
using System.Text.RegularExpressions; | ||
using System.Runtime.CompilerServices; | ||
|
||
namespace MyCSharp.HttpUserAgentParser; | ||
|
||
|
@@ -48,11 +48,15 @@ public static HttpUserAgentInformation Parse(string userAgent) | |
/// </summary> | ||
public static HttpUserAgentPlatformInformation? GetPlatform(string userAgent) | ||
{ | ||
foreach (HttpUserAgentPlatformInformation item in HttpUserAgentStatics.Platforms) | ||
// Fast, allocation-free token scan (keeps public statics untouched) | ||
ReadOnlySpan<char> ua = userAgent.AsSpan(); | ||
foreach ((string Token, string Name, HttpUserAgentPlatformType PlatformType) platform in HttpUserAgentStatics.s_platformRules) | ||
{ | ||
if (item.Regex.IsMatch(userAgent)) | ||
if (ContainsIgnoreCase(ua, platform.Token)) | ||
{ | ||
return item; | ||
return new HttpUserAgentPlatformInformation( | ||
HttpUserAgentStatics.GetPlatformRegexForToken(platform.Token), | ||
platform.Name, platform.PlatformType); | ||
} | ||
} | ||
|
||
|
@@ -73,13 +77,41 @@ public static bool TryGetPlatform(string userAgent, [NotNullWhen(true)] out Http | |
/// </summary> | ||
public static (string Name, string? Version)? GetBrowser(string userAgent) | ||
{ | ||
foreach ((Regex key, string? value) in HttpUserAgentStatics.Browsers) | ||
ReadOnlySpan<char> ua = userAgent.AsSpan(); | ||
foreach ((string Name, string DetectToken, string? VersionToken) browserRule in HttpUserAgentStatics.s_browserRules) | ||
{ | ||
Match match = key.Match(userAgent); | ||
if (match.Success) | ||
if (!TryIndexOf(ua, browserRule.DetectToken, out int detectIndex)) | ||
{ | ||
return (value, match.Groups[1].Value); | ||
continue; | ||
} | ||
|
||
// Version token may differ (e.g., Safari uses "Version/") | ||
int versionSearchStart = detectIndex; | ||
if (!string.IsNullOrEmpty(browserRule.VersionToken)) | ||
{ | ||
if (TryIndexOf(ua, browserRule.VersionToken!, out int vtIndex)) | ||
{ | ||
versionSearchStart = vtIndex + browserRule.VersionToken!.Length; | ||
} | ||
else | ||
{ | ||
// If specific version token wasn't found, fall back to detect token area | ||
versionSearchStart = detectIndex + browserRule.DetectToken.Length; | ||
} | ||
} | ||
else | ||
{ | ||
versionSearchStart = detectIndex + browserRule.DetectToken.Length; | ||
} | ||
|
||
string? version = null; | ||
ua = ua.Slice(versionSearchStart); | ||
if (TryExtractVersion(ua, out Range range)) | ||
{ | ||
version = ua[range].ToString(); | ||
} | ||
|
||
return (browserRule.Name, version); | ||
} | ||
|
||
return null; | ||
|
@@ -143,4 +175,62 @@ public static bool TryGetMobileDevice(string userAgent, [NotNullWhen(true)] out | |
device = GetMobileDevice(userAgent); | ||
return device is not null; | ||
} | ||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
private static bool ContainsIgnoreCase(ReadOnlySpan<char> haystack, ReadOnlySpan<char> needle) | ||
=> TryIndexOf(haystack, needle, out _); | ||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
private static bool TryIndexOf(ReadOnlySpan<char> haystack, ReadOnlySpan<char> needle, out int index) | ||
{ | ||
index = haystack.IndexOf(needle, StringComparison.OrdinalIgnoreCase); | ||
return index >= 0; | ||
} | ||
|
||
/// <summary> | ||
/// Extracts a dotted numeric version. | ||
/// 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, out Range range) | ||
{ | ||
range = default; | ||
|
||
// Limit search window to avoid scanning entire UA string unnecessarily | ||
const int Window = 128; | ||
if (haystack.Length >= Window) | ||
{ | ||
haystack = haystack.Slice(0, Window); | ||
} | ||
|
||
int i = 0; | ||
for (; i < haystack.Length; ++i) | ||
{ | ||
char c = haystack[i]; | ||
if (char.IsBetween(c, '0', '9')) | ||
{ | ||
break; | ||
} | ||
} | ||
|
||
int s = i; | ||
haystack = haystack.Slice(i + 1); | ||
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. 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 commentThe reason will be displayed to describe this comment to others. Learn more.
We test for this UA. 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. 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 commentThe reason will be displayed to describe this comment to others. Learn more. What does the user agent
I couldn’t find any exact match online. 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. 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 commentThe 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. |
||
for (i = 0; i < haystack.Length; ++i) | ||
{ | ||
char c = haystack[i]; | ||
if (!(char.IsBetween(c, '0', '9') || c == '.')) | ||
{ | ||
break; | ||
} | ||
} | ||
i += s + 1; // shift back the previous domain | ||
|
||
if (i == s) | ||
{ | ||
return false; | ||
} | ||
|
||
range = new Range(s, i); | ||
return true; | ||
} | ||
} |
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.