Skip to content

Conversation

@DeagleGross
Copy link
Member

@DeagleGross DeagleGross commented Dec 12, 2025

#44758 introduced new DataProtection API, and in this PR I am changing Antiforgery implementation to use a high-performance DataProtection API. Moreover, going through the implementation, I've refactored to improve performance here and there. The results are listed below.

The main idea of the improvement is to use the DataProtection API, which does not make us allocate any buffer. This is a hot-path in Antiforgery, because token serialization and deserialization may happen multiple times for different tokens per single request.

Before, we had to allocate a buffer, fill it in with the token data, pass it into the dataProtector and later we were using streams API, which also allocate and may be doing extra job instead of writing to the destination buffer. There are less of extra types used today, which should give some benefit to the usage.

I have tested the changes via crank, but unfortunately I dont see a heavy improvement in RPS speed (around 4% RPS improvement only). I suspect those improvements mostly focus on allocation reduction, meaning in the long run it will give more RPS.

IAntiforgeryTokenSerializer

branch Method Mean Error StdDev Op/s Gen 0 Gen 1 Gen 2 Allocated
main Serialize 2.221 us 0.0245 us 0.0229 us 450,239.5 0.0076 - - 872 B
main Deserialize 2.436 us 0.0463 us 0.1036 us 410,492.5 0.0076 - - 632 B
change Serialize 1.932 us 0.0110 us 0.0098 us 517,688.9 0.0038 - - 544 B
change Deserialize 1.927 us 0.0107 us 0.0100 us 519,058.2 0.0038 - - 344 B

IAntiforgeryTokenGenerator

branch Method Mean Error StdDev Op/s Gen 0 Gen 1 Gen 2 Allocated
main GenerateRequestToken_Anonymous 11.0555 ns 0.1203 ns 0.1066 ns 90,452,434.9 0.0007 - - 56 B
main GenerateRequestToken_Authenticated 401.2545 ns 7.1693 ns 6.3554 ns 2,492,184.2 0.0076 - - 592 B
main TryValidateTokenSet_Anonymous 6.7227 ns 0.0357 ns 0.0316 ns 148,750,552.9 - - - -
main TryValidateTokenSet_Authenticated 508.1742 ns 4.4728 ns 3.7350 ns 1,967,829.1 0.0095 - - 760 B
main TryValidateTokenSet_ClaimsBased 308.4674 ns 3.3256 ns 3.1108 ns 3,241,833.1 0.0038 - - 312 B
change GenerateRequestToken_Anonymous 11.190 ns 0.2428 ns 0.6046 ns 89,364,681.9 0.0007 - - 56 B
change GenerateRequestToken_Authenticated 338.056 ns 6.7313 ns 14.9161 ns 2,958,092.2 0.0052 - - 424 B
change TryValidateTokenSet_Anonymous 7.966 ns 0.1616 ns 0.2915 ns 125,531,038.3 - - - -
change TryValidateTokenSet_Authenticated 13.386 ns 0.2476 ns 0.3550 ns 74,707,554.5 - - - -
change TryValidateTokenSet_ClaimsBased 220.111 ns 4.2723 ns 5.7034 ns 4,543,156.3 0.0014 - - 120 B

IAntiforgery

branch Method Mean Error StdDev Op/s Gen 0 Gen 1 Gen 2 Allocated
main GetAndStoreTokens 59.56 us 2.482 us 7.082 us 16,789.6 - - - 5 KB
main ValidateRequestAsync 50.60 us 2.150 us 6.167 us 19,764.1 - - - 4 KB
change GetAndStoreTokens 49.62 us 1.386 us 3.954 us 20,153.9 - - - 3 KB
change ValidateRequestAsync 43.67 us 1.541 us 4.471 us 22,900.6 - - - 3 KB

Relates to #50065

@DeagleGross DeagleGross self-assigned this Dec 12, 2025
@DeagleGross DeagleGross added area-perf Performance infrastructure issues feature-antiforgery labels Dec 12, 2025
@DeagleGross DeagleGross marked this pull request as ready for review December 12, 2025 16:10
Copilot AI review requested due to automatic review settings December 12, 2025 16:10
Copy link
Contributor

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 introduces significant performance improvements to the Antiforgery system by leveraging new span-based DataProtection APIs introduced in #44758. The changes eliminate buffer allocations and stream-based serialization in favor of direct span operations, resulting in substantial improvements across all measured scenarios: 13-15% reduction in allocations and 15-20% improvement in throughput.

Key Changes:

  • Span-based DataProtection: Replaces byte array allocations with ISpanDataProtector for zero-copy data protection operations
  • 7-bit encoding utilities: Adds shared utilities for efficient length-prefixed string serialization using span-based APIs
  • Removed object pooling: Eliminates AntiforgerySerializationContext pooling infrastructure now that allocations are minimal

Reviewed changes

Copilot reviewed 24 out of 24 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/Shared/Encoding/Int7BitEncodingUtils.cs Adds span-based methods for reading/writing 7-bit encoded integers and strings
src/Shared/test/Shared.Tests/Encoding/Int7BitEncodingUtilsTests.cs Comprehensive test coverage for new encoding utilities
src/Shared/WebEncoders/WebEncoders.cs Adds Base64Url decoding directly to span for .NET 9+
src/Antiforgery/src/Internal/DefaultClaimUidExtractor.cs Converts to span-based claim UID extraction using stack allocation and direct SHA256 hashing
src/Antiforgery/src/Internal/DefaultAntiforgeryTokenSerializer.cs Converts to span-based serialization using ISpanDataProtector API
src/Antiforgery/src/Internal/DefaultAntiforgeryTokenGenerator.cs Updates to use span-based claim UID comparison
src/Antiforgery/src/Internal/AntiforgerySerializationContext.cs Removed - no longer needed with span-based approach
src/Antiforgery/src/Internal/AntiforgerySerializationContextPooledObjectPolicy.cs Removed - pooling infrastructure no longer needed
src/Antiforgery/src/AntiforgeryServiceCollectionExtensions.cs Removes object pool registration
src/Antiforgery/test/DefaultClaimUidExtractorTest.cs Updates tests for new span-based API signature
src/Antiforgery/test/DefaultAntiforgeryTokenSerializerTest.cs Adds TestSpanDataProtector mock implementation
src/Antiforgery/test/DefaultAntiforgeryTokenGeneratorTest.cs Adds DummyClaimUidExtractor helper for testing
src/Antiforgery/benchmarks/Microsoft.AspNetCore.Antiforgery.Benchmarks/* New benchmark project with comprehensive performance tests

public void SetupGetAndStoreTokens()
{
// Create a fresh context for each iteration to simulate real-world usage
_getAndStoreTokensContext = CreateHttpContext();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this gets excluded from the measurement, but it does create GC pressure. Could we get more accurate numbers by resetting the HttpContext between iterations?

throw new FormatException("Failed to decode token as Base64 char sequence.");
}

var tokenBytesDecoded = tokenBytes.Slice(0, bytesWritten);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: any reason for using Slice over [..bytesWriten]?

private static BinaryBlob? GetClaimUidBlob(string? base64ClaimUid)
{
if (base64ClaimUid == null)
bool AreIdenticalClaimUids(Span<byte> claimUidBytes)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

static

{
return !extractedClaimUidBytes;
}
if (requestToken.ClaimUid.Length != claimUidBytes.Length)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (requestToken.ClaimUid.Length != claimUidBytes.Length)
if (requestToken.ClaimUid.Length != claimUidBytes.Length)

{
return false;
}
return requestToken.ClaimUid.GetData().SequenceEqual(claimUidBytes);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return requestToken.ClaimUid.GetData().SequenceEqual(claimUidBytes);
return requestToken.ClaimUid.GetData().SequenceEqual(claimUidBytes);

Comment on lines +35 to +36
var rent = tokenDecodedSize < 256
? stackalloc byte[255]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var rent = tokenDecodedSize < 256
? stackalloc byte[255]
var rent = tokenDecodedSize <= 256
? stackalloc byte[256]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer using powers of 2, looks more elegant and might be more beneficial for byte alignment.


if (_perfCryptoSystem is not null)
{
var protectBuffer = new RefPooledArrayBufferWriter<byte>(stackalloc byte[255]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var protectBuffer = new RefPooledArrayBufferWriter<byte>(stackalloc byte[255]);
using var protectBuffer = new RefPooledArrayBufferWriter<byte>(stackalloc byte[256]);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can remove try as well

}
else
{
var usernameByteCount = System.Text.Encoding.UTF8.GetByteCount(token.Username!);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using System.Text;?

var protectBuffer = new RefPooledArrayBufferWriter<byte>(stackalloc byte[255]);
try
{
_perfCryptoSystem!.Protect(tokenBytes, ref protectBuffer);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
_perfCryptoSystem!.Protect(tokenBytes, ref protectBuffer);
_perfCryptoSystem.Protect(tokenBytes, ref protectBuffer);

return new string(chars, startIndex: 0, length: outputLength);
if (_perfCryptoSystem is not null)
{
var protectBuffer = new RefPooledArrayBufferWriter<byte>(stackalloc byte[255]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var protectBuffer = new RefPooledArrayBufferWriter<byte>(stackalloc byte[255]);
using var protectBuffer = new RefPooledArrayBufferWriter<byte>(stackalloc byte[255]);

}

private byte[] ComputeSha256(IEnumerable<string> parameters)
private static void ComputeSha256(IList<string> parameters, Span<byte> destination)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private static void ComputeSha256(IList<string> parameters, Span<byte> destination)
private static void ComputeSha256(List<string> parameters, Span<byte> destination)

Use concrete types for internal methods.

var serializationContext = _pool.Get();

try
// Calculate total size needed for serialization
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Calculate total size needed for serialization
Debug.Assert(destination.Length >= SHA256.HashSizeInBytes);
// Calculate total size needed for serialization

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-perf Performance infrastructure issues feature-antiforgery

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants