File: EndToEndBenchmarks.cs
Web Access
Project: src\src\Middleware\OutputCaching\perf\Microbenchmarks\Microsoft.AspNetCore.OutputCaching.Microbenchmarks.csproj (Microsoft.AspNetCore.OutputCaching.Microbenchmarks)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#nullable enable
 
using System.Buffers;
using System.IO.Pipelines;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Net.Http.Headers;
 
namespace Microsoft.AspNetCore.OutputCaching.Microbenchmarks;
 
[MemoryDiagnoser, GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory), CategoriesColumn]
public class EndToEndBenchmarks
{
    [Params(10, 1000, (64 * 1024) + 17, (256 * 1024) + 17)]
    public int PayloadLength { get; set; } = 1024; // default for simple runs
 
    private byte[] _payloadOversized = Array.Empty<byte>();
    private string Key = "";
    private IOutputCacheStore _store = null!;
 
    private static readonly OutputCacheOptions _options = new();
    private static readonly Action _noop = () => { };
 
    private static readonly HashSet<string> _tags = new();
    private static IHeaderDictionary _headers = null!;
 
    private ReadOnlyMemory<byte> Payload => new(_payloadOversized, 0, PayloadLength);
 
    [GlobalCleanup]
    public void Cleanup()
    {
        var arr = _payloadOversized;
        _payloadOversized = Array.Empty<byte>();
        if (arr.Length != 0)
        {
            ArrayPool<byte>.Shared.Return(arr);
        }
        _store = null!;
        _headers = null!;
    }
 
    [GlobalSetup]
    public async Task InitAsync()
    {
        Key = Guid.NewGuid().ToString();
        _store = new DummyStore(Key);
        _payloadOversized = ArrayPool<byte>.Shared.Rent(PayloadLength);
        Random.Shared.NextBytes(_payloadOversized);
        // some random headers from ms.com
        _headers = new HeaderDictionary
        {
            ContentLength = PayloadLength,
            ["X-Rtag"] = "AEM_PROD_Marketing",
            ["X-Vhost"] = "publish_microsoft_s",
        };
        _headers.ContentType = "text/html;charset=utf-8";
        _headers.Vary = "Accept-Encoding";
        _headers.XContentTypeOptions = "nosniff";
        _headers.XFrameOptions = "SAMEORIGIN";
        _headers.RequestId = Key;
 
        // store, fetch, validate (for each impl)
        await StreamSync();
        await ReadAsync(true);
 
        await StreamAsync();
        await ReadAsync(true);
 
        await WriterAsync();
        await ReadAsync(true);
    }
 
    static void WriteInRandomChunks(ReadOnlySpan<byte> value, Stream destination)
    {
        var rand = Random.Shared;
        while (!value.IsEmpty)
        {
            var bytes = Math.Min(rand.Next(4, 1024), value.Length);
            destination.Write(value.Slice(0, bytes));
            value = value.Slice(bytes);
        }
        destination.Flush();
    }
 
    static Task WriteInRandomChunks(ReadOnlyMemory<byte> source, PipeWriter destination, CancellationToken cancellationToken)
    {
        var value = source.Span;
        var rand = Random.Shared;
        while (!value.IsEmpty)
        {
            var bytes = Math.Min(rand.Next(4, 1024), value.Length);
            var span = destination.GetSpan(bytes);
            bytes = Math.Min(bytes, span.Length);
            value.Slice(0, bytes).CopyTo(span);
            destination.Advance(bytes);
            value = value.Slice(bytes);
        }
        return destination.FlushAsync(cancellationToken).AsTask();
    }
 
    static async Task WriteInRandomChunksAsync(ReadOnlyMemory<byte> value, Stream destination, CancellationToken cancellationToken)
    {
        var rand = Random.Shared;
        while (!value.IsEmpty)
        {
            var bytes = Math.Min(rand.Next(4, 1024), value.Length);
            await destination.WriteAsync(value.Slice(0, bytes), cancellationToken);
            value = value.Slice(bytes);
        }
        await destination.FlushAsync(cancellationToken);
    }
 
    [Benchmark(Description = "StreamSync"), BenchmarkCategory("Write")]
    public async Task StreamSync()
    {
        ReadOnlySequence<byte> body;
        using (var oc = new OutputCacheStream(Stream.Null, _options.MaximumBodySize, StreamUtilities.BodySegmentSize, _noop))
        {
            WriteInRandomChunks(Payload.Span, oc);
            body = oc.GetCachedResponseBody();
        }
        var entry = new OutputCacheEntry(DateTimeOffset.UtcNow, StatusCodes.Status200OK)
            .CopyHeadersFrom(_headers);
        entry.SetBody(body, recycleBuffers: true);
        await OutputCacheEntryFormatter.StoreAsync(Key, entry, _tags, _options.DefaultExpirationTimeSpan, _store, NullLogger.Instance, CancellationToken.None);
        entry.Dispose();
    }
 
    [Benchmark(Description = "StreamAsync"), BenchmarkCategory("Write")]
    public async Task StreamAsync()
    {
        ReadOnlySequence<byte> body;
        using (var oc = new OutputCacheStream(Stream.Null, _options.MaximumBodySize, StreamUtilities.BodySegmentSize, _noop))
        {
            await WriteInRandomChunksAsync(Payload, oc, CancellationToken.None);
            body = oc.GetCachedResponseBody();
        }
        var entry = new OutputCacheEntry(DateTimeOffset.UtcNow, StatusCodes.Status200OK)
            .CopyHeadersFrom(_headers);
        entry.SetBody(body, recycleBuffers: true);
        await OutputCacheEntryFormatter.StoreAsync(Key, entry, _tags, _options.DefaultExpirationTimeSpan, _store, NullLogger.Instance, CancellationToken.None);
        entry.Dispose();
    }
 
    [Benchmark(Description = "BodyWriter"), BenchmarkCategory("Write")]
    public async Task WriterAsync()
    {
        ReadOnlySequence<byte> body;
        using (var oc = new OutputCacheStream(Stream.Null, _options.MaximumBodySize, StreamUtilities.BodySegmentSize, _noop))
        {
            var pipe = PipeWriter.Create(oc, new StreamPipeWriterOptions(leaveOpen: true));
            await WriteInRandomChunks(Payload, pipe, CancellationToken.None);
            body = oc.GetCachedResponseBody();
        }
        var entry = new OutputCacheEntry(DateTimeOffset.UtcNow, StatusCodes.Status200OK)
            .CopyHeadersFrom(_headers);
        entry.SetBody(body, recycleBuffers: true);
        await OutputCacheEntryFormatter.StoreAsync(Key, entry, _tags, _options.DefaultExpirationTimeSpan, _store, NullLogger.Instance, CancellationToken.None);
        entry.Dispose();
    }
 
    [Benchmark, BenchmarkCategory("Read")]
    public Task ReadAsync() => ReadAsync(false);
 
    private async Task ReadAsync(bool validate)
    {
        static void ThrowNotFound() => throw new KeyNotFoundException();
 
        var entry = await OutputCacheEntryFormatter.GetAsync(Key, _store, CancellationToken.None);
        if (validate)
        {
            Validate(entry!);
        }
        if (entry is null)
        {
            ThrowNotFound();
        }
        else
        {
            entry.Dispose();
        }
    }
 
    private void Validate(OutputCacheEntry value)
    {
        ArgumentNullException.ThrowIfNull(value);
        var body = value.Body;
        if (body.Length != PayloadLength)
        {
            throw new InvalidOperationException("Invalid payload length");
        }
 
        if (body.IsSingleSegment)
        {
            if (!Payload.Span.SequenceEqual(body.FirstSpan))
            {
                throw new InvalidOperationException("Invalid payload");
            }
        }
        else
        {
            var oversized = ArrayPool<byte>.Shared.Rent(PayloadLength);
            value.Body.CopyTo(oversized);
            if (!Payload.Span.SequenceEqual(new(oversized, 0, PayloadLength)))
            {
                throw new InvalidOperationException("Invalid payload");
            }
 
            ArrayPool<byte>.Shared.Return(oversized);
        }
 
        if (value.Headers.Length != _headers.Count - 2)
        {
            throw new InvalidOperationException("Incorrect header count");
        }
        foreach (var header in _headers)
        {
            if (header.Key == HeaderNames.ContentLength || header.Key == HeaderNames.RequestId)
            {
                // not stored
                continue;
            }
            if (!value.TryFindHeader(header.Key, out var vals) || vals != header.Value)
            {
                throw new InvalidOperationException("Invalid header: " + header.Key);
            }
        }
    }
 
    sealed class DummyStore : IOutputCacheStore
    {
        private readonly string _key;
        private byte[]? _payload;
        public DummyStore(string key) => _key = key;
 
        ValueTask IOutputCacheStore.EvictByTagAsync(string tag, CancellationToken cancellationToken) => default;
 
        ValueTask<byte[]?> IOutputCacheStore.GetAsync(string key, CancellationToken cancellationToken)
        {
            if (key != _key)
            {
                Throw();
            }
            return new(_payload);
        }
 
        ValueTask IOutputCacheStore.SetAsync(string key, byte[]? value, string[]? tags, TimeSpan validFor, CancellationToken cancellationToken)
        {
            if (key != _key)
            {
                Throw();
            }
            _payload = value;
            return default;
        }
 
        static void Throw() => throw new InvalidOperationException("Incorrect key");
    }
 
    internal sealed class NullPipeWriter : PipeWriter, IDisposable
    {
        public void Dispose()
        {
            var arr = _buffer;
            _buffer = null!;
            if (arr is not null)
            {
                ArrayPool<byte>.Shared.Return(arr);
            }
        }
        byte[] _buffer;
        public NullPipeWriter(int size) => _buffer = ArrayPool<byte>.Shared.Rent(size);
        public override void Advance(int bytes) { }
        public override Span<byte> GetSpan(int sizeHint = 0) => _buffer;
        public override Memory<byte> GetMemory(int sizeHint = 0) => _buffer;
        public override void Complete(Exception? exception = null) { }
        public override void CancelPendingFlush() { }
        public override ValueTask CompleteAsync(Exception? exception = null) => default;
        public override ValueTask<FlushResult> FlushAsync(CancellationToken cancellationToken = default) => default;
    }
}