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


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

Copy link
Member Author

Choose a reason for hiding this comment

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

I can't remove try {} finally {} with dispose here: it's not allowed to pass disposable variable which is under using keyword via ref parameter.
image

if (token != null)
var tokenDecodedSize = Base64Url.GetMaxDecodedLength(serializedToken.Length);

var rent = tokenDecodedSize < 256
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
var rent = tokenDecodedSize <= 256

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
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
using System.Text;
using System;
using System.Text;

<Project Path="src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test.csproj" />
</Folder>
<Folder Name="/src/Antiforgery/benchmarks/">
<Project Path="src/Antiforgery/benchmarks/Microsoft.AspNetCore.Antiforgery.Benchmarks/Microsoft.AspNetCore.Antiforgery.Benchmarks.csproj" Id="1dd9d6d8-44f5-4cc9-886d-ebfec9ec289e" />
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
<Project Path="src/Antiforgery/benchmarks/Microsoft.AspNetCore.Antiforgery.Benchmarks/Microsoft.AspNetCore.Antiforgery.Benchmarks.csproj" Id="1dd9d6d8-44f5-4cc9-886d-ebfec9ec289e" />
<Project Path="src/Antiforgery/benchmarks/Microsoft.AspNetCore.Antiforgery.Benchmarks/Microsoft.AspNetCore.Antiforgery.Benchmarks.csproj" />

@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
Copy link
Member

Choose a reason for hiding this comment

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

Could you rename this to have .MicrosoftBenchmarks at the end

<IsMicrobenchmarksProject Condition=" $(MSBuildProjectName.EndsWith('.Microbenchmarks')) ">true</IsMicrobenchmarksProject>

Comment on lines +150 to +151
var requestTokenClaimUidLength = requestToken.ClaimUid?.Length;
if (requestTokenClaimUidLength is null)
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 requestTokenClaimUidLength = requestToken.ClaimUid?.Length;
if (requestTokenClaimUidLength is null)
if (requestToken.ClaimUid is null)

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]);
var protectBuffer = new RefPooledArrayBufferWriter<byte>(stackalloc byte[256]);

Comment on lines +215 to +216
var rent = totalSize < 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 = totalSize < 256
? stackalloc byte[255]
var rent = totalSize <= 256
? stackalloc byte[256]

private static AntiforgeryToken? Deserialize(BinaryReader reader)
private static AntiforgeryToken? Deserialize(ReadOnlySpan<byte> tokenBytes)
{
var offset = 0;
Copy link
Member

Choose a reason for hiding this comment

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

I meant more like:

var embeddedVersion = tokenBytes[0];
tokenBytes = tokenBytes.Slice(1);

if (embeddedVersion != TokenVersion)
{
    return null;
}

var deserializedToken = new AntiforgeryToken();

// Read SecurityToken (16 bytes)
deserializedToken.SecurityToken = new BinaryBlob(
    AntiforgeryToken.SecurityTokenBitLength,
    tokenBytes[..AntiforgeryToken.SecurityTokenByteLength].ToArray());
tokenBytes = tokenBytes.Slice(AntiforgeryToken.SecurityTokenByteLength);

// Read IsCookieToken (1 byte)
deserializedToken.IsCookieToken = tokenBytes[0] != 0;
tokenBytes = tokenBytes.Slice(1);

: (tokenBytesRent = ArrayPool<byte>.Shared.Rent(tokenDecodedSize));
var tokenBytes = rent[..tokenDecodedSize];

var status = Base64Url.DecodeFromChars(serializedToken, tokenBytes, out int charsConsumed, out int bytesWritten);
Copy link
Member

Choose a reason for hiding this comment

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

else
{
return token;
var unprotectedBytes = _defaultCryptoSystem.Unprotect(tokenBytesDecoded.ToArray());
Copy link
Member

Choose a reason for hiding this comment

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

Do we still have good test coverage for the less optimized paths?

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