-
Notifications
You must be signed in to change notification settings - Fork 10.5k
Antiforgery perf improvements (includes new DataProtection API usage) #64751
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
Open
DeagleGross
wants to merge
13
commits into
main
Choose a base branch
from
dmkorolev/antiforgery-perf
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,250
−440
Open
Changes from 7 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
f8b6df6
some impl of deserialize?
DeagleGross 12790ce
serialize!
DeagleGross 47cc61f
workaround one of byte[] allocations
DeagleGross 2b428da
add micro benchmarks proj
DeagleGross f30bbe4
some reworks
DeagleGross a05bc26
rerun benchmarks, simplify token generation
DeagleGross da95fcb
rollback interface
DeagleGross 9e79105
copilot feedback
DeagleGross c4bd92f
cleanup comments
DeagleGross c218d52
try fix for non-netcore
DeagleGross b51f8c1
address PR comments 1
DeagleGross 4f9e35c
address PR comments 2
DeagleGross c76b051
benchmarks changes
DeagleGross File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,11 @@ | ||
| { | ||
| { | ||
| "solution": { | ||
| "path": "..\\..\\AspNetCore.slnx", | ||
| "projects": [ | ||
| "src\\Antiforgery\\benchmarks\\Microsoft.AspNetCore.Antiforgery.Benchmarks\\Microsoft.AspNetCore.Antiforgery.Benchmarks.csproj", | ||
| "src\\Antiforgery\\samples\\MinimalFormSample\\MinimalFormSample.csproj", | ||
| "src\\Antiforgery\\src\\Microsoft.AspNetCore.Antiforgery.csproj", | ||
| "src\\Antiforgery\\test\\Microsoft.AspNetCore.Antiforgery.Test.csproj" | ||
| ] | ||
| } | ||
| } | ||
| } |
159 changes: 159 additions & 0 deletions
159
...benchmarks/Microsoft.AspNetCore.Antiforgery.Benchmarks/Benchmarks/AntifogeryBenchmarks.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.Security.Claims; | ||
| using BenchmarkDotNet.Attributes; | ||
| using Microsoft.AspNetCore.Http; | ||
| using Microsoft.AspNetCore.Http.Features; | ||
| using Microsoft.Extensions.DependencyInjection; | ||
| using Microsoft.Extensions.Options; | ||
| using Microsoft.Extensions.Primitives; | ||
|
|
||
| namespace Microsoft.AspNetCore.Antiforgery.Benchmarks.Benchmarks; | ||
|
|
||
| /* | ||
|
|
||
| main branch: | ||
| | Method | Mean | Error | StdDev | Op/s | Gen 0 | Gen 1 | Gen 2 | Allocated | | ||
| |--------------------- |---------:|---------:|---------:|---------:|------:|------:|------:|----------:| | ||
| | GetAndStoreTokens | 59.56 us | 2.482 us | 7.082 us | 16,789.6 | - | - | - | 5 KB | | ||
| | ValidateRequestAsync | 50.60 us | 2.150 us | 6.167 us | 19,764.1 | - | - | - | 4 KB | | ||
|
|
||
| this PR: | ||
| | Method | Mean | Error | StdDev | Op/s | Gen 0 | Gen 1 | Gen 2 | Allocated | | ||
| |--------------------- |---------:|---------:|---------:|---------:|------:|------:|------:|----------:| | ||
| | GetAndStoreTokens | 49.62 us | 1.386 us | 3.954 us | 20,153.9 | - | - | - | 3 KB | | ||
| | ValidateRequestAsync | 43.67 us | 1.541 us | 4.471 us | 22,900.6 | - | - | - | 3 KB | | ||
|
|
||
| */ | ||
|
|
||
| [AspNetCoreBenchmark] | ||
| public class AntiforgeryBenchmarks | ||
| { | ||
| private IServiceProvider _serviceProvider = null!; | ||
| private IAntiforgery _antiforgery = null!; | ||
| private string _cookieName = null!; | ||
| private string _formFieldName = null!; | ||
|
|
||
| // For GetAndStoreTokens - fresh context each time | ||
| private HttpContext _getAndStoreTokensContext = null!; | ||
|
|
||
| // For ValidateRequestAsync - context with valid tokens | ||
| private HttpContext _validateRequestContext = null!; | ||
| private string _cookieToken = null!; | ||
| private string _requestToken = null!; | ||
|
|
||
| [GlobalSetup] | ||
| public void Setup() | ||
| { | ||
| var serviceCollection = new ServiceCollection(); | ||
| serviceCollection.AddAntiforgery(); | ||
| serviceCollection.AddLogging(); | ||
| _serviceProvider = serviceCollection.BuildServiceProvider(); | ||
|
|
||
| _antiforgery = _serviceProvider.GetRequiredService<IAntiforgery>(); | ||
|
|
||
| // Get the actual cookie and form field names from options | ||
| var options = _serviceProvider.GetRequiredService<IOptions<AntiforgeryOptions>>().Value; | ||
| _cookieName = options.Cookie.Name!; | ||
| _formFieldName = options.FormFieldName; | ||
|
|
||
| // Setup context for GetAndStoreTokens (no existing tokens) | ||
| _getAndStoreTokensContext = CreateHttpContext(); | ||
|
|
||
| // Generate tokens for validation benchmark | ||
| var tokenContext = CreateHttpContext(); | ||
| var tokenSet = _antiforgery.GetAndStoreTokens(tokenContext); | ||
| _cookieToken = tokenSet.CookieToken!; | ||
| _requestToken = tokenSet.RequestToken!; | ||
|
|
||
| // Setup context for ValidateRequestAsync (with valid tokens) | ||
| _validateRequestContext = CreateHttpContextWithTokens(_cookieToken, _requestToken); | ||
| } | ||
|
|
||
| [IterationSetup(Target = nameof(GetAndStoreTokens))] | ||
| public void SetupGetAndStoreTokens() | ||
| { | ||
| // Create a fresh context for each iteration to simulate real-world usage | ||
| _getAndStoreTokensContext = CreateHttpContext(); | ||
| } | ||
|
|
||
| [IterationSetup(Target = nameof(ValidateRequestAsync))] | ||
| public void SetupValidateRequest() | ||
| { | ||
| // Create a fresh context with tokens for each iteration | ||
| _validateRequestContext = CreateHttpContextWithTokens(_cookieToken, _requestToken); | ||
| } | ||
|
|
||
| [Benchmark] | ||
| public AntiforgeryTokenSet GetAndStoreTokens() | ||
| { | ||
| return _antiforgery.GetAndStoreTokens(_getAndStoreTokensContext); | ||
| } | ||
|
|
||
| [Benchmark] | ||
| public Task ValidateRequestAsync() | ||
| { | ||
| return _antiforgery.ValidateRequestAsync(_validateRequestContext); | ||
| } | ||
|
|
||
| private HttpContext CreateHttpContext() | ||
| { | ||
| var context = new DefaultHttpContext(); | ||
| context.RequestServices = _serviceProvider; | ||
|
|
||
| // Create an authenticated identity with a Name claim (required by antiforgery) | ||
| var identity = new ClaimsIdentity( | ||
| [new Claim(ClaimsIdentity.DefaultNameClaimType, "testuser@example.com")], | ||
| "TestAuth"); | ||
| context.User = new ClaimsPrincipal(identity); | ||
|
|
||
| context.Request.Method = "POST"; | ||
| context.Request.ContentType = "application/x-www-form-urlencoded"; | ||
|
|
||
| // Setup response features to allow cookie writing | ||
| var responseFeature = new TestHttpResponseFeature(); | ||
| context.Features.Set<IHttpResponseFeature>(responseFeature); | ||
| context.Features.Set<IHttpResponseBodyFeature>(new StreamResponseBodyFeature(Stream.Null)); | ||
|
|
||
| return context; | ||
| } | ||
|
|
||
| private HttpContext CreateHttpContextWithTokens(string cookieToken, string requestToken) | ||
| { | ||
| var context = new DefaultHttpContext(); | ||
| context.RequestServices = _serviceProvider; | ||
|
|
||
| // Create an authenticated identity with a Name claim (required by antiforgery) | ||
| var identity = new ClaimsIdentity( | ||
| [new Claim(ClaimsIdentity.DefaultNameClaimType, "testuser@example.com")], | ||
| "TestAuth"); | ||
| context.User = new ClaimsPrincipal(identity); | ||
|
|
||
| context.Request.Method = "POST"; | ||
| context.Request.ContentType = "application/x-www-form-urlencoded"; | ||
|
|
||
| // Set the cookie token using the actual cookie name from options | ||
| context.Request.Headers.Cookie = $"{_cookieName}={cookieToken}"; | ||
|
|
||
| // Set the request token in form using the actual form field name | ||
| context.Request.Form = new FormCollection(new Dictionary<string, StringValues> | ||
| { | ||
| { _formFieldName, requestToken } | ||
| }); | ||
|
|
||
| return context; | ||
| } | ||
|
|
||
| private sealed class TestHttpResponseFeature : IHttpResponseFeature | ||
| { | ||
| public int StatusCode { get; set; } = 200; | ||
| public string? ReasonPhrase { get; set; } | ||
| public IHeaderDictionary Headers { get; set; } = new HeaderDictionary(); | ||
| public Stream Body { get; set; } = Stream.Null; | ||
| public bool HasStarted => false; | ||
|
|
||
| public void OnStarting(Func<object, Task> callback, object state) { } | ||
| public void OnCompleted(Func<object, Task> callback, object state) { } | ||
| } | ||
| } |
153 changes: 153 additions & 0 deletions
153
...osoft.AspNetCore.Antiforgery.Benchmarks/Benchmarks/AntiforgeryTokenGeneratorBenchmarks.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.Security.Claims; | ||
| using BenchmarkDotNet.Attributes; | ||
| using Microsoft.AspNetCore.Http; | ||
| using Microsoft.Extensions.DependencyInjection; | ||
|
|
||
| namespace Microsoft.AspNetCore.Antiforgery.Benchmarks.Benchmarks; | ||
|
|
||
| /* | ||
| main branch: | ||
| | Method | Mean | Error | StdDev | Op/s | Gen 0 | Gen 1 | Gen 2 | Allocated | | ||
| |----------------------------------- |------------:|----------:|----------:|----------------:|-------:|------:|------:|----------:| | ||
| | GenerateRequestToken_Anonymous | 11.0555 ns | 0.1203 ns | 0.1066 ns | 90,452,434.9 | 0.0007 | - | - | 56 B | | ||
| | GenerateRequestToken_Authenticated | 401.2545 ns | 7.1693 ns | 6.3554 ns | 2,492,184.2 | 0.0076 | - | - | 592 B | | ||
| | TryValidateTokenSet_Anonymous | 6.7227 ns | 0.0357 ns | 0.0316 ns | 148,750,552.9 | - | - | - | - | | ||
| | TryValidateTokenSet_Authenticated | 508.1742 ns | 4.4728 ns | 3.7350 ns | 1,967,829.1 | 0.0095 | - | - | 760 B | | ||
| | TryValidateTokenSet_ClaimsBased | 308.4674 ns | 3.3256 ns | 3.1108 ns | 3,241,833.1 | 0.0038 | - | - | 312 B | | ||
|
|
||
| this PR: | ||
| | Method | Mean | Error | StdDev | Op/s | Gen 0 | Gen 1 | Gen 2 | Allocated | | ||
| |----------------------------------- |------------:|----------:|-----------:|--------------:|-------:|------:|------:|----------:| | ||
| | GenerateRequestToken_Anonymous | 11.190 ns | 0.2428 ns | 0.6046 ns | 89,364,681.9 | 0.0007 | - | - | 56 B | | ||
| | GenerateRequestToken_Authenticated | 338.056 ns | 6.7313 ns | 14.9161 ns | 2,958,092.2 | 0.0052 | - | - | 424 B | | ||
| | TryValidateTokenSet_Anonymous | 7.966 ns | 0.1616 ns | 0.2915 ns | 125,531,038.3 | - | - | - | - | | ||
| | TryValidateTokenSet_Authenticated | 13.386 ns | 0.2476 ns | 0.3550 ns | 74,707,554.5 | - | - | - | - | | ||
| | TryValidateTokenSet_ClaimsBased | 220.111 ns | 4.2723 ns | 5.7034 ns | 4,543,156.3 | 0.0014 | - | - | 120 B | | ||
|
|
||
| */ | ||
|
|
||
| [AspNetCoreBenchmark] | ||
| public class AntiforgeryTokenGeneratorBenchmarks | ||
| { | ||
| private IAntiforgeryTokenGenerator _tokenGenerator = null!; | ||
|
|
||
| // Anonymous user scenario | ||
| private HttpContext _anonymousHttpContext = null!; | ||
| private AntiforgeryToken _anonymousCookieToken = null!; | ||
| private AntiforgeryToken _anonymousRequestToken = null!; | ||
|
|
||
| // Authenticated user with username scenario | ||
| private HttpContext _authenticatedHttpContext = null!; | ||
| private AntiforgeryToken _authenticatedCookieToken = null!; | ||
| private AntiforgeryToken _authenticatedRequestToken = null!; | ||
|
|
||
| // Claims-based user scenario | ||
| private HttpContext _claimsHttpContext = null!; | ||
| private AntiforgeryToken _claimsCookieToken = null!; | ||
| private AntiforgeryToken _claimsRequestToken = null!; | ||
|
|
||
| [GlobalSetup] | ||
| public void Setup() | ||
| { | ||
| var serviceCollection = new ServiceCollection(); | ||
| serviceCollection.AddAntiforgery(); | ||
| var serviceProvider = serviceCollection.BuildServiceProvider(); | ||
|
|
||
| _tokenGenerator = serviceProvider.GetRequiredService<IAntiforgeryTokenGenerator>(); | ||
|
|
||
| // Setup anonymous user scenario | ||
| _anonymousHttpContext = new DefaultHttpContext(); | ||
| _anonymousHttpContext.User = new ClaimsPrincipal(new ClaimsIdentity()); | ||
|
|
||
| _anonymousCookieToken = new AntiforgeryToken { IsCookieToken = true }; | ||
| _anonymousRequestToken = new AntiforgeryToken | ||
| { | ||
| IsCookieToken = false, | ||
| SecurityToken = _anonymousCookieToken.SecurityToken, | ||
| Username = string.Empty | ||
| }; | ||
|
|
||
| // Setup authenticated user with username scenario | ||
| _authenticatedHttpContext = new DefaultHttpContext(); | ||
| var authenticatedIdentity = new ClaimsIdentity( | ||
| [new Claim(ClaimsIdentity.DefaultNameClaimType, "testuser@example.com")], | ||
| "TestAuthentication"); | ||
| _authenticatedHttpContext.User = new ClaimsPrincipal(authenticatedIdentity); | ||
|
|
||
| _authenticatedCookieToken = new AntiforgeryToken { IsCookieToken = true }; | ||
| _authenticatedRequestToken = new AntiforgeryToken | ||
| { | ||
| IsCookieToken = false, | ||
| SecurityToken = _authenticatedCookieToken.SecurityToken, | ||
| Username = "testuser@example.com" | ||
| }; | ||
|
|
||
| // Setup claims-based user scenario | ||
| _claimsHttpContext = new DefaultHttpContext(); | ||
| var claimsIdentity = new ClaimsIdentity( | ||
| [ | ||
| new Claim(ClaimsIdentity.DefaultNameClaimType, "claimsuser@example.com"), | ||
| new Claim("sub", "user-id-12345"), | ||
| new Claim(ClaimTypes.NameIdentifier, "unique-id") | ||
| ], | ||
| "ClaimsAuthentication"); | ||
| _claimsHttpContext.User = new ClaimsPrincipal(claimsIdentity); | ||
|
|
||
| _claimsCookieToken = new AntiforgeryToken { IsCookieToken = true }; | ||
|
|
||
| // For claims-based users, we need to extract the ClaimUid | ||
| var claimUid = new byte[32]; | ||
| _ = new DefaultClaimUidExtractor().TryExtractClaimUidBytes(_claimsHttpContext.User, claimUid); | ||
| _claimsRequestToken = new AntiforgeryToken | ||
| { | ||
| IsCookieToken = false, | ||
| SecurityToken = _claimsCookieToken.SecurityToken, | ||
| ClaimUid = claimUid is not null ? new BinaryBlob(256, claimUid) : null | ||
| }; | ||
| } | ||
|
|
||
| [Benchmark] | ||
| public object GenerateRequestToken_Anonymous() | ||
| { | ||
| return _tokenGenerator.GenerateRequestToken(_anonymousHttpContext, _anonymousCookieToken); | ||
| } | ||
|
|
||
| [Benchmark] | ||
| public object GenerateRequestToken_Authenticated() | ||
| { | ||
| return _tokenGenerator.GenerateRequestToken(_authenticatedHttpContext, _authenticatedCookieToken); | ||
| } | ||
|
|
||
| [Benchmark] | ||
| public bool TryValidateTokenSet_Anonymous() | ||
| { | ||
| return _tokenGenerator.TryValidateTokenSet( | ||
| _anonymousHttpContext, | ||
| _anonymousCookieToken, | ||
| _anonymousRequestToken, | ||
| out _); | ||
| } | ||
|
|
||
| [Benchmark] | ||
| public bool TryValidateTokenSet_Authenticated() | ||
| { | ||
| return _tokenGenerator.TryValidateTokenSet( | ||
| _authenticatedHttpContext, | ||
| _authenticatedCookieToken, | ||
| _authenticatedRequestToken, | ||
| out _); | ||
| } | ||
|
|
||
| [Benchmark] | ||
| public bool TryValidateTokenSet_ClaimsBased() | ||
| { | ||
| return _tokenGenerator.TryValidateTokenSet( | ||
| _claimsHttpContext, | ||
| _claimsCookieToken, | ||
| _claimsRequestToken, | ||
| out _); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.