Skip to content

Instantly share code, notes, and snippets.

@MihaZupan
Created June 1, 2022 17:44
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