Skip to content

Commit 0578ad4

Browse files
authored
add performance improvements and with less allocations (#15)
2 parents 301dec9 + 8724381 commit 0578ad4

File tree

12 files changed

+448
-114
lines changed

12 files changed

+448
-114
lines changed

.vscode/settings.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
// github copilot commit message instructions (preview)
3+
"github.copilot.chat.commitMessageGeneration.instructions": [
4+
{ "text": "Use conventional commit format: type(scope): description" },
5+
{ "text": "Use imperative mood: 'Add feature' not 'Added feature'" },
6+
{ "text": "Keep subject line under 50 characters" },
7+
{ "text": "Use types: feat, fix, docs, style, refactor, perf, test, chore, ci" },
8+
{ "text": "Include scope when relevant (e.g., api, ui, auth)" },
9+
{ "text": "Reference issue numbers with # prefix" }
10+
]
11+
}

Directory.Packages.props

Lines changed: 60 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,62 @@
11
<Project>
2-
<PropertyGroup>
3-
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
4-
</PropertyGroup>
5-
6-
<ItemGroup Label="Dependencies">
7-
<PackageVersion Include="Microsoft.Extensions.Primitives" Version="8.0.0" />
8-
</ItemGroup>
9-
10-
<ItemGroup Label="Benchmarks">
11-
<PackageVersion Include="BenchmarkDotNet" Version="0.14.0" />
12-
<PackageVersion Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.14.0" />
13-
</ItemGroup>
14-
15-
<ItemGroup Label="Tests">
16-
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
17-
<PackageVersion Include="Microsoft.Extensions.Diagnostics.Testing" Version="9.4.0" />
18-
<PackageVersion Include="NSubstitute" Version="5.3.0" />
19-
<PackageVersion Include="NSubstitute.Analyzers.CSharp" Version="1.0.17">
20-
<PrivateAssets>all</PrivateAssets>
21-
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
22-
</PackageVersion>
23-
<PackageVersion Include="xunit.v3" Version="2.0.1" />
24-
<PackageVersion Include="xunit.v3.extensibility.core" Version="2.0.1" />
25-
<PackageVersion Include="xunit.v3.assert" Version="2.0.1" />
26-
<PackageVersion Include="xunit.runner.console" Version="2.9.3">
27-
<PrivateAssets>all</PrivateAssets>
28-
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
29-
</PackageVersion>
30-
<PackageVersion Include="xunit.runner.visualstudio" Version="3.0.2">
31-
<PrivateAssets>all</PrivateAssets>
32-
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
33-
</PackageVersion>
34-
</ItemGroup>
35-
36-
<ItemGroup Label="Analyzers">
37-
<PackageVersion Include="Roslynator.Analyzers" Version="4.13.1">
38-
<PrivateAssets>all</PrivateAssets>
39-
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
40-
</PackageVersion>
41-
<PackageVersion Include="Roslynator.Formatting.Analyzers" Version="4.13.1">
42-
<PrivateAssets>all</PrivateAssets>
43-
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
44-
</PackageVersion>
45-
<PackageVersion Include="Roslynator.CodeAnalysis.Analyzers" Version="4.13.1">
46-
<PrivateAssets>all</PrivateAssets>
47-
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
48-
</PackageVersion>
49-
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.13.0">
50-
<PrivateAssets>all</PrivateAssets>
51-
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
52-
</PackageVersion>
53-
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.13.0">
54-
<PrivateAssets>all</PrivateAssets>
55-
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
56-
</PackageVersion>
57-
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.13.0">
58-
<PrivateAssets>all</PrivateAssets>
59-
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
60-
</PackageVersion>
61-
<PackageVersion Include="Meziantou.Analyzer" Version="2.0.195">
62-
<PrivateAssets>all</PrivateAssets>
63-
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
64-
</PackageVersion>
65-
</ItemGroup>
2+
<PropertyGroup>
3+
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
4+
</PropertyGroup>
5+
<ItemGroup Label="Dependencies">
6+
<PackageVersion Include="Microsoft.Extensions.Primitives" Version="8.0.0" />
7+
</ItemGroup>
8+
<ItemGroup Label="Benchmarks">
9+
<PackageVersion Include="BenchmarkDotNet" Version="0.15.2" />
10+
<PackageVersion Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.15.2" />
11+
</ItemGroup>
12+
<ItemGroup Label="Tests">
13+
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
14+
<PackageVersion Include="Microsoft.Extensions.Diagnostics.Testing" Version="9.4.0" />
15+
<PackageVersion Include="NSubstitute" Version="5.3.0" />
16+
<PackageVersion Include="NSubstitute.Analyzers.CSharp" Version="1.0.17">
17+
<PrivateAssets>all</PrivateAssets>
18+
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
19+
</PackageVersion>
20+
<PackageVersion Include="xunit.v3" Version="3.0.1" />
21+
<PackageVersion Include="xunit.v3.extensibility.core" Version="3.0.1" />
22+
<PackageVersion Include="xunit.v3.assert" Version="3.0.1" />
23+
<PackageVersion Include="xunit.runner.console" Version="2.9.3">
24+
<PrivateAssets>all</PrivateAssets>
25+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
26+
</PackageVersion>
27+
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.4">
28+
<PrivateAssets>all</PrivateAssets>
29+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
30+
</PackageVersion>
31+
</ItemGroup>
32+
<ItemGroup Label="Analyzers">
33+
<PackageVersion Include="Roslynator.Analyzers" Version="4.14.0">
34+
<PrivateAssets>all</PrivateAssets>
35+
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
36+
</PackageVersion>
37+
<PackageVersion Include="Roslynator.Formatting.Analyzers" Version="4.14.0">
38+
<PrivateAssets>all</PrivateAssets>
39+
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
40+
</PackageVersion>
41+
<PackageVersion Include="Roslynator.CodeAnalysis.Analyzers" Version="4.14.0">
42+
<PrivateAssets>all</PrivateAssets>
43+
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
44+
</PackageVersion>
45+
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.14.0">
46+
<PrivateAssets>all</PrivateAssets>
47+
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
48+
</PackageVersion>
49+
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.14.0">
50+
<PrivateAssets>all</PrivateAssets>
51+
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
52+
</PackageVersion>
53+
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0">
54+
<PrivateAssets>all</PrivateAssets>
55+
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
56+
</PackageVersion>
57+
<PackageVersion Include="Meziantou.Analyzer" Version="2.0.212">
58+
<PrivateAssets>all</PrivateAssets>
59+
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
60+
</PackageVersion>
61+
</ItemGroup>
6662
</Project>

Justfile

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Justfile .NET - Benjamin Abt 2025 - https://benjamin-abt.com
2+
# https://github.com/BenjaminAbt/templates/blob/main/justfile/dotnet
3+
4+
set shell := ["pwsh", "-c"]
5+
6+
# ===== Configurable defaults =====
7+
CONFIG := "Debug"
8+
TFM := "net10.0"
9+
BENCH_PRJ := "perf/HttpClientHints.Benchmarks/HttpClientHints.Benchmarks.csproj"
10+
11+
# ===== Default / Help =====
12+
default: help
13+
14+
help:
15+
# Overview:
16+
just --list
17+
# Usage:
18+
# just build
19+
# just test
20+
# just bench
21+
22+
# ===== Basic .NET Workflows =====
23+
restore:
24+
dotnet restore
25+
26+
build *ARGS:
27+
dotnet build --configuration "{{CONFIG}}" --nologo --verbosity minimal {{ARGS}}
28+
29+
rebuild *ARGS:
30+
dotnet build --configuration "{{CONFIG}}" --nologo --verbosity minimal --no-incremental {{ARGS}}
31+
32+
clean:
33+
dotnet clean --configuration "{{CONFIG}}" --nologo
34+
35+
run *ARGS:
36+
dotnet run --project --framework "{{TFM}}" --configuration "{{CONFIG}}" --no-launch-profile {{ARGS}}
37+
38+
# ===== Quality / Tests =====
39+
format:
40+
dotnet format --verbosity minimal
41+
42+
format-check:
43+
dotnet format --verify-no-changes --verbosity minimal
44+
45+
test *ARGS:
46+
dotnet test --configuration "{{CONFIG}}" --framework "{{TFM}}" --nologo --verbosity minimal {{ARGS}}
47+
48+
test-cov:
49+
dotnet test --configuration "{{CONFIG}}" --framework "{{TFM}}" --nologo --verbosity minimal /p:CollectCoverage=true /p:CoverletOutputFormat="cobertura,lcov,opencover" /p:CoverletOutput="./TestResults/coverage/coverage"
50+
51+
52+
test-filter QUERY:
53+
dotnet test --configuration "{{CONFIG}}" --framework "{{TFM}}" --nologo --verbosity minimal --filter "{{QUERY}}"
54+
55+
# ===== Packaging / Release =====
56+
pack *ARGS:
57+
dotnet pack --configuration "{{CONFIG}}" --nologo --verbosity minimal -o "./artifacts/packages" {{ARGS}}
58+
59+
publish *ARGS:
60+
dotnet publish --configuration "{{CONFIG}}" --framework "{{TFM}}" --nologo --verbosity minimal -o "./artifacts/publish/{{TFM}}" {{ARGS}}
61+
62+
publish-sc RID *ARGS:
63+
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}}
64+
65+
# ===== Benchmarks =====
66+
bench *ARGS:
67+
dotnet run --configuration Release --project "{{BENCH_PRJ}}" --framework "{{TFM}}" {{ARGS}}
68+
69+
# ===== Housekeeping =====
70+
clean-artifacts:
71+
if (Test-Path "./artifacts") { Remove-Item "./artifacts" -Recurse -Force }
72+
73+
clean-all:
74+
just clean
75+
just clean-artifacts
76+
# Optionally: git clean -xdf
77+
78+
# ===== Combined Flows =====
79+
fmt-build:
80+
just format
81+
just build
82+
83+
ci:
84+
just clean
85+
just restore
86+
just format-check
87+
just build
88+
just test-cov

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2024 MyCSharp
3+
Copyright (c) 2024-2025 MyCSharp
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

MyCSharp.HttpClientHints.sln

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_", "_", "{E23F6AE5-FF68-4E
1515
Directory.Build.props = Directory.Build.props
1616
Directory.Packages.props = Directory.Packages.props
1717
global.json = global.json
18+
Justfile = Justfile
1819
LICENSE = LICENSE
1920
NuGet.config = NuGet.config
2021
README.md = README.md
@@ -34,6 +35,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DD28
3435
EndProject
3536
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HttpClientHints.Samples.AspNetCoreMvc", "samples\HttpClientHints.Samples.AspNetCoreMvc\HttpClientHints.Samples.AspNetCoreMvc.csproj", "{2F2815DE-1730-4645-B6CF-13599BBA3B2C}"
3637
EndProject
38+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "perf", "perf", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
39+
EndProject
40+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HttpClientHints.Benchmarks", "perf\HttpClientHints.Benchmarks\HttpClientHints.Benchmarks.csproj", "{F4088F7A-51EC-4111-BBDB-CA36596B3F77}"
41+
EndProject
3742
Global
3843
GlobalSection(SolutionConfigurationPlatforms) = preSolution
3944
Debug|Any CPU = Debug|Any CPU
@@ -60,6 +65,10 @@ Global
6065
{2F2815DE-1730-4645-B6CF-13599BBA3B2C}.Debug|Any CPU.Build.0 = Debug|Any CPU
6166
{2F2815DE-1730-4645-B6CF-13599BBA3B2C}.Release|Any CPU.ActiveCfg = Release|Any CPU
6267
{2F2815DE-1730-4645-B6CF-13599BBA3B2C}.Release|Any CPU.Build.0 = Release|Any CPU
68+
{F4088F7A-51EC-4111-BBDB-CA36596B3F77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
69+
{F4088F7A-51EC-4111-BBDB-CA36596B3F77}.Debug|Any CPU.Build.0 = Debug|Any CPU
70+
{F4088F7A-51EC-4111-BBDB-CA36596B3F77}.Release|Any CPU.ActiveCfg = Release|Any CPU
71+
{F4088F7A-51EC-4111-BBDB-CA36596B3F77}.Release|Any CPU.Build.0 = Release|Any CPU
6372
EndGlobalSection
6473
GlobalSection(SolutionProperties) = preSolution
6574
HideSolutionNode = FALSE
@@ -70,5 +79,9 @@ Global
7079
{2BDA44E4-1E40-4B97-8903-B8C36C35B8C4} = {3CF449E7-2707-44A2-BACC-6F9A5C8DBA63}
7180
{E60AE9AF-09E5-4FE0-A005-5F697599E508} = {3CF449E7-2707-44A2-BACC-6F9A5C8DBA63}
7281
{2F2815DE-1730-4645-B6CF-13599BBA3B2C} = {DD28B2F1-1CF8-4070-A997-83DE356EA88C}
82+
{F4088F7A-51EC-4111-BBDB-CA36596B3F77} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
83+
EndGlobalSection
84+
GlobalSection(ExtensibilityGlobals) = postSolution
85+
SolutionGuid = {B0EDDFE8-3921-4DCE-8524-12765744AE12}
7386
EndGlobalSection
7487
EndGlobal

README.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,37 @@ HttpClientHints clientHints = HttpClientHintsHttpContextExtensions.GetClientHint
8282
clientHints.Headers.TryGetValue("Sec-CH-UA-Bitness", out StringValues bitness);
8383
```
8484

85+
## Benchmark
86+
87+
```shell
88+
BenchmarkDotNet v0.15.2, Windows 10 (10.0.19045.6216/22H2/2022Update)
89+
AMD Ryzen 9 9950X 4.30GHz, 1 CPU, 32 logical and 16 physical cores
90+
.NET SDK 10.0.100-preview.7.25380.108
91+
[Host] : .NET 10.0.0 (10.0.25.38108), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
92+
.NET 10.0 : .NET 10.0.0 (10.0.25.38108), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
93+
.NET 8.0 : .NET 8.0.19 (8.0.1925.36514), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
94+
.NET 9.0 : .NET 9.0.8 (9.0.825.36511), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
95+
96+
97+
| Method | Job | Runtime | Mean | Error | StdDev | Gen0 | Allocated |
98+
|------------------------------------------------------- |---------- |---------- |----------:|---------:|---------:|-------:|----------:|
99+
| 'View: read few properties (no alloc)' | .NET 8.0 | .NET 8.0 | 31.26 ns | 0.403 ns | 0.377 ns | - | - |
100+
| 'View: read few properties (no alloc)' | .NET 9.0 | .NET 9.0 | 34.50 ns | 0.328 ns | 0.307 ns | - | - |
101+
| 'View: read few properties (no alloc)' | .NET 10.0 | .NET 10.0 | 21.71 ns | 0.397 ns | 0.371 ns | - | - |
102+
| | | | | | | | |
103+
| 'View: BuildSnapshot (alloc 1)' | .NET 8.0 | .NET 8.0 | 70.83 ns | 1.058 ns | 0.884 ns | 0.0052 | 88 B |
104+
| 'View: BuildSnapshot (alloc 1)' | .NET 9.0 | .NET 9.0 | 67.35 ns | 0.462 ns | 0.409 ns | - | - |
105+
| 'View: BuildSnapshot (alloc 1)' | .NET 10.0 | .NET 10.0 | 44.59 ns | 0.599 ns | 0.561 ns | - | - |
106+
| | | | | | | | |
107+
| 'Extension: GetClientHints(headers) (alloc 1)' | .NET 8.0 | .NET 8.0 | 70.32 ns | 0.872 ns | 0.816 ns | 0.0052 | 88 B |
108+
| 'Extension: GetClientHints(headers) (alloc 1)' | .NET 9.0 | .NET 9.0 | 67.24 ns | 0.506 ns | 0.473 ns | - | - |
109+
| 'Extension: GetClientHints(headers) (alloc 1)' | .NET 10.0 | .NET 10.0 | 46.50 ns | 0.254 ns | 0.212 ns | - | - |
110+
| | | | | | | | |
111+
| 'Extension: GetClientHints(context) (alloc 1, cached)' | .NET 8.0 | .NET 8.0 | 111.04 ns | 0.767 ns | 0.718 ns | 0.0052 | 88 B |
112+
| 'Extension: GetClientHints(context) (alloc 1, cached)' | .NET 9.0 | .NET 9.0 | 114.79 ns | 2.304 ns | 2.155 ns | 0.0052 | 88 B |
113+
| 'Extension: GetClientHints(context) (alloc 1, cached)' | .NET 10.0 | .NET 10.0 | 98.26 ns | 1.429 ns | 1.267 ns | 0.0052 | 88 B |
114+
```
115+
85116
## Samples
86117

87118
- [ASP.NET Core MVC Sample](./samples/MyCSharp.HttpClientHints.Samples.AspNetCoreMvc/)
@@ -94,7 +125,7 @@ by [@BenjaminAbt](https://github.com/BenjaminAbt) and [@gfoidl](https://github.c
94125

95126
MIT License
96127

97-
Copyright (c) 2024 MyCSharp
128+
Copyright (c) 2024-2025 MyCSharp
98129

99130
Permission is hereby granted, free of charge, to any person obtaining a copy
100131
of this software and associated documentation files (the "Software"), to deal

0 commit comments

Comments
 (0)