-
Notifications
You must be signed in to change notification settings - Fork 10
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 9 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,40 @@ 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; | ||
| if (TryExtractVersion(ua, versionSearchStart, out Range range)) | ||
| { | ||
| version = userAgent.AsSpan(range.Start.Value, range.End.Value - range.Start.Value).ToString(); | ||
| } | ||
|
|
||
| return (browserRule.Name, version); | ||
| } | ||
|
|
||
| return null; | ||
|
|
@@ -143,4 +174,69 @@ 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, string needle) | ||
| => TryIndexOf(haystack, needle, out _); | ||
|
|
||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| private static bool TryIndexOf(ReadOnlySpan<char> haystack, string needle, out int index) | ||
| { | ||
| index = haystack.IndexOf(needle.AsSpan(), StringComparison.OrdinalIgnoreCase); | ||
| return index >= 0; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Extracts a dotted numeric version starting at or after <paramref name="startIndex"/>. | ||
| /// 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) | ||
| { | ||
| range = default; | ||
| if ((uint)startIndex >= (uint)haystack.Length) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| // Limit search window to avoid scanning entire UA string unnecessarily | ||
| const int window = 128; | ||
| int end = Math.Min(haystack.Length, startIndex + window); | ||
| int i = startIndex; | ||
|
|
||
| // Skip separators until we hit a digit | ||
| while (i < end) | ||
| { | ||
| char c = haystack[i]; | ||
|
Contributor
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 could slice the |
||
| if ((uint)(c - '0') <= 9) | ||
|
||
| { | ||
| break; | ||
| } | ||
| i++; | ||
| } | ||
|
|
||
| if (i >= end) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| int s = i; | ||
| while (i < end) | ||
| { | ||
| char c = haystack[i]; | ||
| if (!((uint)(c - '0') <= 9 || c == '.')) | ||
| { | ||
| break; | ||
| } | ||
| i++; | ||
| } | ||
|
|
||
| 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.
The
startIndexisn't needed, as the ROShaystackcan be sliced therefore to proper start.