/HttpClientBenchmarks.cs Secret
Created
June 1, 2022 17:44
This file contains 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
using BenchmarkDotNet.Attributes; | |
using BenchmarkDotNet.Running; | |
using System.Diagnostics; | |
using System.Net; | |
using System.Net.Http.Headers; | |
using System.Reflection; | |
using System.Text; | |
//BenchmarkSwitcher.FromAssembly(typeof(HttpClientBenchmarks).Assembly).Run(args); | |
RunForProfiler(); | |
static void RunForProfiler() | |
{ | |
const int NumWorkers = 6; | |
var benchmark = new HttpClientBenchmarks | |
{ | |
RequestHeaders = 0, | |
ConcurrencyPerHandler = NumWorkers, | |
}; | |
benchmark.Setup(); | |
Task.Run(async () => | |
{ | |
Stopwatch s = Stopwatch.StartNew(); | |
while (true) | |
{ | |
await Task.Delay(1000); | |
long numRequests = Interlocked.Exchange(ref Stats.CombinedRequests, 0); | |
Console.Title = $"{(int)(numRequests / s.Elapsed.TotalSeconds / 1000)} k/s"; | |
s.Restart(); | |
} | |
}); | |
for (int i = 1; i < NumWorkers; i++) | |
{ | |
new Thread(() => Worker(benchmark, $"{(char)(i + 'A')}")) { IsBackground = true }.Start(); | |
} | |
Worker(benchmark, "A"); | |
static void Worker(HttpClientBenchmarks benchmark, string name) | |
{ | |
const int LoopIterations = 1_000; | |
Stopwatch s = Stopwatch.StartNew(); | |
while (true) | |
{ | |
for (int i = 0; i < LoopIterations; i++) | |
{ | |
WorkLoop(benchmark, LoopIterations); | |
Interlocked.Add(ref Stats.CombinedRequests, LoopIterations); | |
} | |
Console.WriteLine($"{name}: {(int)((LoopIterations * LoopIterations) / s.Elapsed.TotalSeconds / 1000)} k/s"); | |
s.Restart(); | |
} | |
static void WorkLoop(HttpClientBenchmarks benchmark, int iterations) | |
{ | |
for (int i = 0; i < iterations; i++) | |
{ | |
benchmark.SendAsync(); | |
} | |
} | |
} | |
} | |
public static class Stats | |
{ | |
public static long CombinedRequests = 0; | |
} | |
//[MemoryDiagnoser] | |
//[LongRunJob] | |
public class HttpClientBenchmarks | |
{ | |
private static readonly Uri _requestUri = new Uri("http://10.0.0.100:8080/plaintext"); | |
private HttpMessageInvoker _messageInvoker = null!; | |
private string[] _requestHeaders = null!; | |
private HttpRequestMessage? _request; | |
public int ContentLength = 0; | |
//[Params( | |
// 0, 1, 2, 3, 4, 5, 6, 7, 8 | |
// , 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 22, 24, 26, 28, 30, 32, 40, 48, 56, 64, 100 | |
// )] | |
[Params(0, 1, 4, 8, 16)] | |
public int RequestHeaders = 0; | |
public bool UsePreparedRequest = false; | |
public int ConcurrencyPerHandler = 1; | |
private HttpRequestMessage CreateRequest() | |
{ | |
var request = new HttpRequestMessage(HttpMethod.Get, _requestUri) | |
{ | |
Version = HttpVersion.Version11, | |
VersionPolicy = HttpVersionPolicy.RequestVersionExact | |
}; | |
foreach (string headerName in _requestHeaders) | |
{ | |
request.Headers.TryAddWithoutValidation(headerName, "foo-bar-123"); | |
} | |
return request; | |
} | |
[GlobalSetup] | |
public void Setup() | |
{ | |
byte[] responseBytes = Encoding.ASCII.GetBytes( | |
"HTTP/1.1 200 OK\r\n" + | |
//"Date: Sun, 12 Dec 2021 22:23:40 GMT\r\n" + | |
//"Server: Kestrel\r\n" + | |
//"Content-type: text/html\r\n" + | |
$"Content-Length: {ContentLength}\r\n" + | |
"\r\n" + | |
new string('a', ContentLength)); | |
TaskCompletionSource connectCallbackLock = new(TaskCreationOptions.RunContinuationsAsynchronously); | |
_messageInvoker = new(new SocketsHttpHandler | |
{ | |
UseProxy = false, | |
AllowAutoRedirect = false, | |
AutomaticDecompression = DecompressionMethods.None, | |
UseCookies = false, | |
ActivityHeadersPropagator = null, | |
PooledConnectionIdleTimeout = TimeSpan.FromDays(10), // Avoid the cleaning timer executing during the benchmark | |
ConnectCallback = async (context, cancellation) => | |
{ | |
await connectCallbackLock.Task; | |
return new ResponseStream(responseBytes); | |
} | |
}); | |
_requestHeaders = HttpHeadersBenchmarks.CreateHeaderNames(RequestHeaders); | |
_request = CreateRequest(); | |
var tasks = new Task[ConcurrencyPerHandler]; | |
for (int i = 0; i < tasks.Length; i++) | |
{ | |
tasks[i] = WarmupAsync(); | |
async Task WarmupAsync() | |
{ | |
using HttpResponseMessage response = await _messageInvoker.SendAsync(_request, CancellationToken.None); | |
await response.Content.CopyToAsync(Stream.Null); | |
} | |
} | |
// Wait until all the tasks are blocked on the ConnectCallback | |
Thread.Sleep(100); | |
connectCallbackLock.SetResult(); | |
Task.WaitAll(tasks); | |
if (!UsePreparedRequest) | |
{ | |
_request = null; | |
} | |
} | |
[Benchmark] | |
public void SendAsync() | |
{ | |
HttpRequestMessage request = _request ?? CreateRequest(); | |
Task<HttpResponseMessage> responseTask = _messageInvoker.SendAsync(request, CancellationToken.None); | |
if (!responseTask.IsCompletedSuccessfully) | |
{ | |
throw new Exception(); | |
} | |
using HttpResponseMessage response = responseTask.Result; | |
if (ContentLength > 0) | |
{ | |
Task copyToTask = response.Content.CopyToAsync(Stream.Null); | |
if (!copyToTask.IsCompletedSuccessfully) | |
{ | |
throw new Exception(); | |
} | |
copyToTask.GetAwaiter().GetResult(); | |
} | |
} | |
} | |
public sealed class ResponseStream : Stream | |
{ | |
private TaskCompletionSource<int> _writeTcs = new(TaskCreationOptions.RunContinuationsAsynchronously); | |
private bool _writeCompleted; | |
private bool _readStarted; | |
private readonly byte[] _responseData; | |
public ResponseStream(byte[] responseData) | |
{ | |
_responseData = responseData; | |
} | |
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) | |
{ | |
_responseData.CopyTo(buffer.Span); | |
lock (this) | |
{ | |
if (_writeCompleted) | |
{ | |
_writeCompleted = false; | |
return new ValueTask<int>(_responseData.Length); | |
} | |
else | |
{ | |
_readStarted = true; | |
return new ValueTask<int>(_writeTcs.Task); | |
} | |
} | |
} | |
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) | |
{ | |
lock (this) | |
{ | |
if (_readStarted) | |
{ | |
_readStarted = false; | |
_writeTcs.SetResult(_responseData.Length); | |
_writeTcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously); | |
} | |
else | |
{ | |
_writeCompleted = true; | |
} | |
} | |
return default; | |
} | |
public override Task FlushAsync(CancellationToken cancellationToken) | |
{ | |
return Task.CompletedTask; | |
} | |
public override void Flush() => throw new InvalidOperationException(); | |
public override int Read(byte[] buffer, int offset, int count) => throw new InvalidOperationException(); | |
public override long Seek(long offset, SeekOrigin origin) => throw new InvalidOperationException(); | |
public override void SetLength(long value) => throw new InvalidOperationException(); | |
public override void Write(byte[] buffer, int offset, int count) => throw new InvalidOperationException(); | |
public override bool CanRead => true; | |
public override bool CanSeek => false; | |
public override bool CanWrite => true; | |
public override long Length => throw new InvalidOperationException(); | |
public override long Position { get => throw new InvalidOperationException(); set => throw new InvalidOperationException(); } | |
} | |
[MemoryDiagnoser] | |
public class HttpHeadersBenchmarks | |
{ | |
private static readonly Action<HttpHeaders, HttpHeaders> _addHeadersMethod = | |
typeof(HttpHeaders).GetMethod("AddHeaders", BindingFlags.NonPublic | BindingFlags.Instance)! | |
.CreateDelegate<Action<HttpHeaders, HttpHeaders>>(); | |
[Params(2, 4, 6, 8, 12, 16, 32)] | |
public int RequestHeaders; | |
private string[] _headerNames = null!; | |
private const string HeaderValue = "foo-bar-123"; | |
private HttpHeaders _headers = null!; | |
[GlobalSetup] | |
public void Setup() | |
{ | |
_headerNames = CreateHeaderNames(RequestHeaders); | |
var request = new HttpRequestMessage(); | |
foreach (var header in _headerNames) | |
{ | |
request.Headers.TryAddWithoutValidation(header, HeaderValue); | |
} | |
_headers = request.Headers; | |
} | |
public static string[] CreateHeaderNames(int numberOfHeaders) | |
{ | |
var headerNames = new List<string>(); | |
// Known headers without specific value format requirements | |
headerNames.Add("Cookie"); | |
headerNames.Add("Set-Cookie"); | |
headerNames.Add("Age"); | |
headerNames.Add("Origin"); | |
headerNames.Add("ETag"); | |
headerNames.Add("Server"); | |
headerNames.Add("TE"); | |
headerNames.Add("X-Request-ID"); | |
var buffer = new byte[64]; | |
var rng = new Random(42); | |
while (headerNames.Count < numberOfHeaders) | |
{ | |
rng.NextBytes(buffer); | |
string base64 = Convert.ToBase64String(buffer).TrimEnd('=').Replace("+", "").Replace("/", ""); | |
int length = rng.Next(8, 40); | |
string name = base64.Substring(0, length); | |
if (!headerNames.Contains(name, StringComparer.OrdinalIgnoreCase)) | |
{ | |
headerNames.Add(name); | |
} | |
} | |
foreach (var headerName in headerNames) | |
{ | |
var request = new HttpRequestMessage(); | |
try | |
{ | |
request.Headers.Add(headerName, HeaderValue); | |
} | |
catch | |
{ | |
throw new Exception($"Invalid header '{headerName}: {HeaderValue}'"); | |
} | |
} | |
return headerNames.ToArray().AsSpan(0, numberOfHeaders).ToArray(); | |
} | |
//[Benchmark] | |
public void AddHeaders() | |
{ | |
var request = new HttpRequestMessage(); | |
_addHeadersMethod(request.Headers, _headers); | |
} | |
//[Benchmark] | |
public HttpRequestMessage NonValidatedAdd() | |
{ | |
var request = new HttpRequestMessage(); | |
HttpRequestHeaders headers = request.Headers; | |
foreach (string name in _headerNames) | |
{ | |
headers.TryAddWithoutValidation(name, HeaderValue); | |
} | |
return request; | |
} | |
//[Benchmark] | |
public HttpRequestMessage ValidatedAdd() | |
{ | |
var request = new HttpRequestMessage(); | |
HttpRequestHeaders headers = request.Headers; | |
foreach (string name in _headerNames) | |
{ | |
headers.Add(name, HeaderValue); | |
} | |
return request; | |
} | |
[Benchmark] | |
public int NonValidatedAdd_NonValidatedEnumerate() | |
{ | |
var request = new HttpRequestMessage(); | |
HttpRequestHeaders headers = request.Headers; | |
foreach (string name in _headerNames) | |
{ | |
headers.TryAddWithoutValidation(name, HeaderValue); | |
} | |
int count = 0; | |
foreach (KeyValuePair<string, HeaderStringValues> header in headers.NonValidated) | |
{ | |
count++; | |
} | |
return count; | |
} | |
//[Benchmark] | |
public int NonValidatedAdd_ValidatedEnumerate() | |
{ | |
var request = new HttpRequestMessage(); | |
HttpRequestHeaders headers = request.Headers; | |
foreach (string name in _headerNames) | |
{ | |
headers.TryAddWithoutValidation(name, HeaderValue); | |
} | |
int count = 0; | |
foreach (KeyValuePair<string, IEnumerable<string>> header in headers) | |
{ | |
count++; | |
} | |
return count; | |
} | |
//[Benchmark] | |
public int ValidatedAdd_NonValidatedEnumerate() | |
{ | |
var request = new HttpRequestMessage(); | |
HttpRequestHeaders headers = request.Headers; | |
foreach (string name in _headerNames) | |
{ | |
headers.Add(name, HeaderValue); | |
} | |
int count = 0; | |
foreach (KeyValuePair<string, HeaderStringValues> header in headers.NonValidated) | |
{ | |
count++; | |
} | |
return count; | |
} | |
//[Benchmark] | |
public int ValidatedAdd_ValidatedEnumerate() | |
{ | |
var request = new HttpRequestMessage(); | |
HttpRequestHeaders headers = request.Headers; | |
foreach (string name in _headerNames) | |
{ | |
headers.Add(name, HeaderValue); | |
} | |
int count = 0; | |
foreach (KeyValuePair<string, IEnumerable<string>> header in headers) | |
{ | |
count++; | |
} | |
return count; | |
} | |
} | |
public class HttpHeadersWorstCase | |
{ | |
[Params(16, 32, 64, 128)] | |
public int ResponseHeadersLengthKb; | |
[Params(64, 128, 256, 512, 1024)] | |
public int NumberOfHeaders; | |
private string[] _headerNames = null!; | |
[GlobalSetup] | |
public void Setup() | |
{ | |
int responseLength = ResponseHeadersLengthKb * 1024; | |
int maxBytesPerHeader = responseLength / NumberOfHeaders; | |
int maxBytesPerName = maxBytesPerHeader - 4; // ':', ' ', '\r', '\n' | |
var nameBytes = new char[maxBytesPerName]; | |
Array.Fill(nameBytes, 'a'); | |
var headerNames = new string[NumberOfHeaders]; | |
for (int i = 0; i < headerNames.Length; i++) | |
{ | |
Span<char> uniqueSuffix = nameBytes.AsSpan(nameBytes.Length - 4); | |
uniqueSuffix.Fill('a'); | |
if (i < 10) uniqueSuffix = uniqueSuffix.Slice(3); | |
else if (i < 100) uniqueSuffix = uniqueSuffix.Slice(2); | |
else if (i < 1000) uniqueSuffix = uniqueSuffix.Slice(1); | |
i.TryFormat(uniqueSuffix, out _); | |
headerNames[i] = new string(nameBytes); | |
} | |
_headerNames = headerNames; | |
} | |
[Benchmark] | |
public void Add() | |
{ | |
var request = new HttpRequestMessage(); | |
HttpRequestHeaders headers = request.Headers; | |
foreach (string name in _headerNames) | |
{ | |
headers.TryAddWithoutValidation(name, ""); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment