File: PreserveComponentStateBenchmark.cs
Web Access
Project: src\src\Mvc\perf\Microbenchmarks\Microsoft.AspNetCore.Mvc\Microsoft.AspNetCore.Mvc.Microbenchmarks.csproj (Microsoft.AspNetCore.Mvc.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.Globalization;
using System.Security.Cryptography;
using System.Text.Encodings.Web;
using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.TagHelpers;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
 
namespace Microsoft.AspNetCore.Mvc.Microbenchmarks;
 
public class PreserveComponentStateBenchmark
{
    private readonly PersistComponentStateTagHelper _tagHelper = new()
    {
        PersistenceMode = PersistenceMode.WebAssembly
    };
 
    TagHelperAttributeList _attributes = new();
 
    private TagHelperContext _context;
    private Func<bool, HtmlEncoder, Task<TagHelperContent>> _childContent =
        (_, __) => Task.FromResult(new DefaultTagHelperContent() as TagHelperContent);
    private IServiceProvider _serviceProvider;
    private IServiceScope _serviceScope;
    private TagHelperOutput _output;
    private Dictionary<string, byte[]> _entries = new();
 
    private byte[] _entryValue;
 
    public PreserveComponentStateBenchmark()
    {
        _context = new TagHelperContext(_attributes, new Dictionary<object, object>(), "asdf");
        _serviceProvider = new ServiceCollection()
            .AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance)
            .AddScoped(typeof(ILogger<>), typeof(NullLogger<>))
            .AddSingleton<IConfiguration>(new ConfigurationBuilder().Build())
            .AddSingleton<IWebHostEnvironment>(new BenchmarkWebHostEnvironment())
            .AddMvc().Services.BuildServiceProvider();
    }
 
    private sealed class BenchmarkWebHostEnvironment : IWebHostEnvironment
    {
        public string EnvironmentName { get; set; } = "Production";
        public string ApplicationName { get; set; } = typeof(PreserveComponentStateBenchmark).Assembly.GetName().Name!;
        public string WebRootPath { get; set; } = AppContext.BaseDirectory;
        public IFileProvider WebRootFileProvider { get; set; } = new NullFileProvider();
        public string ContentRootPath { get; set; } = AppContext.BaseDirectory;
        public IFileProvider ContentRootFileProvider { get; set; } = new NullFileProvider();
    }
 
    // From 30 entries of about 100 bytes (~3K) to 100 entries with 100K per entry (~10MB)
    // Sending 10MB of prerendered state is too much, and only used as a way to "stress" the system.
    // In general, so long as entries don't exceed the buffer limits we are ok.
    // 300 Kb is the upper limit of a reasonable payload for prerendered state
    // The 8386 was selected by serializing 100 weather forecast records as a reference
    // For regular runs we only enable by default 30 entries and 8386 bytes per entry, which is about 250K of serialized
    // state on the limit of the accepted payload size budget for critical resources served from a page.
    [Params(30 /*, 100*/)]
    public int Entries;
 
    [Params(/*100,*/ 8386/*, 100_000*/)]
    public int EntrySize;
 
    [GlobalSetup]
    public void Setup()
    {
        _entryValue = new byte[EntrySize];
        RandomNumberGenerator.Fill(_entryValue);
        for (int i = 0; i < Entries; i++)
        {
            _entries.Add(i.ToString(CultureInfo.InvariantCulture), _entryValue);
        }
    }
 
    [Benchmark(Description = "Persist component state tag helper webassembly")]
    public async Task PersistComponentStateTagHelperWebAssemblyAsync()
    {
        _tagHelper.ViewContext = GetViewContext();
        var state = _tagHelper.ViewContext.HttpContext.RequestServices.GetRequiredService<PersistentComponentState>();
        state.RegisterOnPersisting(() =>
        {
            foreach (var (key, value) in _entries)
            {
                state.PersistAsJson(key, value);
            }
 
            return Task.CompletedTask;
        });
 
        _output = new TagHelperOutput("persist-component-state", _attributes, _childContent);
        _output.Content = new DefaultTagHelperContent();
        await _tagHelper.ProcessAsync(_context, _output);
        _output.Content.WriteTo(StreamWriter.Null, NullHtmlEncoder.Default);
        _serviceScope.Dispose();
    }
 
    private ViewContext GetViewContext()
    {
        _serviceScope = _serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope();
        var httpContext = new DefaultHttpContext
        {
            RequestServices = _serviceScope.ServiceProvider
        };
 
        return new ViewContext
        {
            HttpContext = httpContext,
        };
    }
}