File: ResponseCachingBenchmark.cs
Web Access
Project: src\src\Middleware\perf\ResponseCaching.Microbenchmarks\Microsoft.AspNetCore.ResponseCaching.Microbenchmarks.csproj (Microsoft.AspNetCore.ResponseCaching.Microbenchmarks)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.IO.Pipelines;
using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.ResponseCaching;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
 
namespace Microsoft.AspNetCore.WebSockets.Microbenchmarks;
 
public class ResponseCachingBenchmark
{
    private static readonly string _cacheControl = $"{CacheControlHeaderValue.PublicString}, {CacheControlHeaderValue.MaxAgeString}={int.MaxValue}";
 
    private ResponseCachingMiddleware _middleware;
    private readonly byte[] _data = new byte[1 * 1024 * 1024];
 
    [Params(
        100,
        64 * 1024,
        1 * 1024 * 1024
    )]
    public int Size { get; set; }
 
    [GlobalSetup]
    public void Setup()
    {
        _middleware = new ResponseCachingMiddleware(
                async context =>
                {
                    context.Response.Headers.CacheControl = _cacheControl;
                    await context.Response.BodyWriter.WriteAsync(new ReadOnlyMemory<byte>(_data, 0, Size));
                },
                Options.Create(new ResponseCachingOptions
                {
                    SizeLimit = int.MaxValue, // ~2GB
                    MaximumBodySize = 1 * 1024 * 1024,
                }),
                NullLoggerFactory.Instance,
                new DefaultObjectPoolProvider()
            );
 
        // no need to actually cache as there is a warm-up fase
    }
 
    [Benchmark]
    public async Task Cache()
    {
        var pipe = new Pipe();
        var consumer = ConsumeAsync(pipe.Reader, CancellationToken.None);
        DefaultHttpContext context = CreateHttpContext(pipe);
        context.Request.Method = HttpMethods.Get;
        context.Request.Path = "/a";
 
        // don't serve from cache but store result
        context.Request.Headers.CacheControl = CacheControlHeaderValue.NoCacheString;
 
        await _middleware.Invoke(context);
 
        await pipe.Writer.CompleteAsync();
        await consumer;
    }
 
    [Benchmark]
    public async Task ServeFromCache()
    {
        var pipe = new Pipe();
        var consumer = ConsumeAsync(pipe.Reader, CancellationToken.None);
        DefaultHttpContext context = CreateHttpContext(pipe);
        context.Request.Method = HttpMethods.Get;
        context.Request.Path = "/b";
 
        await _middleware.Invoke(context);
 
        await pipe.Writer.CompleteAsync();
        await consumer;
    }
 
    private static DefaultHttpContext CreateHttpContext(Pipe pipe)
    {
        var features = new FeatureCollection();
        features.Set<IHttpRequestFeature>(new HttpRequestFeature());
        features.Set<IHttpResponseFeature>(new HttpResponseFeature());
        features.Set<IHttpResponseBodyFeature>(new PipeResponseBodyFeature(pipe.Writer));
        var context = new DefaultHttpContext(features);
        return context;
    }
 
    private async ValueTask ConsumeAsync(PipeReader reader, CancellationToken cancellationToken)
    {
        while (true)
        {
            var result = await reader.ReadAsync(cancellationToken);
            var buffer = result.Buffer;
 
            reader.AdvanceTo(buffer.End, buffer.End);
 
            if (result.IsCompleted)
            {
                break;
            }
        }
 
        await reader.CompleteAsync();
    }
 
    private sealed class PipeResponseBodyFeature : IHttpResponseBodyFeature
    {
        public PipeResponseBodyFeature(PipeWriter pipeWriter)
        {
            Writer = pipeWriter;
        }
 
        public Stream Stream => Writer.AsStream();
 
        public PipeWriter Writer { get; }
 
        public Task CompleteAsync() => Writer.CompleteAsync().AsTask();
 
        public void DisableBuffering()
        {
            throw new NotImplementedException();
        }
 
        public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellationToken = default)
        {
            throw new NotImplementedException();
        }
 
        public Task StartAsync(CancellationToken cancellationToken = default)
        {
            throw new NotImplementedException();
        }
    }
}