// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using System.Text.Encodings.Web; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Razor.Hosting; using Microsoft.AspNetCore.Routing; using Microsoft.CodeAnalysis; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Mvc.Performance { public class RuntimePerformanceBenchmarkBase { private class NullLoggerFactory : ILoggerFactory, ILogger { void ILoggerFactory.AddProvider(ILoggerProvider provider) {} ILogger ILoggerFactory.CreateLogger(string categoryName) => this; void IDisposable.Dispose() {} IDisposable ILogger.BeginScope(TState state) => null; bool ILogger.IsEnabled(LogLevel logLevel) => false; void ILogger.Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) {} } private class BenchmarkViewExecutor : ViewExecutor { public BenchmarkViewExecutor(IOptions viewOptions, IHttpResponseStreamWriterFactory writerFactory, ICompositeViewEngine viewEngine, ITempDataDictionaryFactory tempDataFactory, DiagnosticListener diagnosticListener, IModelMetadataProvider modelMetadataProvider) : base(viewOptions, writerFactory, viewEngine, tempDataFactory, diagnosticListener, modelMetadataProvider) { } public StringBuilder StringBuilder { get; } = new StringBuilder(); public override async Task ExecuteAsync( ActionContext actionContext, IView view, ViewDataDictionary viewData, ITempDataDictionary tempData, string contentType, int? statusCode) { using (var stringWriter = new StringWriter(StringBuilder)) { var viewContext = new ViewContext( actionContext, view, viewData, tempData, stringWriter, ViewOptions.HtmlHelperOptions); await ExecuteAsync(viewContext, contentType, statusCode); await stringWriter.FlushAsync(); } } } private class BenchmarkHostingEnvironment : IHostingEnvironment { public BenchmarkHostingEnvironment() { ApplicationName = typeof(ViewAssemblyMarker).Assembly.FullName; WebRootFileProvider = new NullFileProvider(); ContentRootFileProvider = new NullFileProvider(); ContentRootPath = AppContext.BaseDirectory; WebRootPath = AppContext.BaseDirectory; } public string EnvironmentName { get; set; } public string ApplicationName { get; set; } public string WebRootPath { get; set; } public IFileProvider WebRootFileProvider { get; set; } public string ContentRootPath { get; set; } public IFileProvider ContentRootFileProvider { get; set; } } protected RuntimePerformanceBenchmarkBase(params string[] viewPaths) { ViewPaths = viewPaths; } public virtual string[] ViewPaths { get; private set; } [ParamsSource(nameof(ViewPaths))] public string ViewPath; protected IView View; private ServiceProvider _serviceProvider; private RouteData _routeData; private ActionDescriptor _actionDescriptor; private IServiceScope _requestScope; private ICompositeViewEngine _viewEngine; private BenchmarkViewExecutor _executor; private ViewEngineResult _viewEngineResult; private ActionContext _actionContext; private ViewDataDictionary _viewDataDictionary; private ITempDataDictionaryFactory _tempDataDictionaryFactory; private ITempDataDictionary _tempData; // runs once for every Document value [GlobalSetup] public void GlobalSetup() { var loader = new RazorCompiledItemLoader(); var viewsDll = Path.ChangeExtension(typeof(ViewAssemblyMarker).Assembly.Location, "Views.dll"); var viewsAssembly = Assembly.Load(File.ReadAllBytes(viewsDll)); var services = new ServiceCollection(); var listener = new DiagnosticListener(GetType().Assembly.FullName); var partManager = new ApplicationPartManager(); partManager.ApplicationParts.Add(CompiledRazorAssemblyApplicationPartFactory.GetDefaultApplicationParts(viewsAssembly).Single()); var builder = services .AddSingleton() .AddSingleton() .AddSingleton(listener) .AddSingleton(listener) .AddSingleton() .AddSingleton(partManager) .AddScoped() .AddMvc(); _serviceProvider = services.BuildServiceProvider(); _routeData = new RouteData(); _actionDescriptor = new ActionDescriptor(); _tempDataDictionaryFactory = _serviceProvider.GetRequiredService(); _viewEngine = _serviceProvider.GetRequiredService(); } [GlobalCleanup] public void GlobalCleanup() { _serviceProvider.Dispose(); } [IterationSetup] public virtual void IterationSetup() { _requestScope = _serviceProvider.CreateScope(); _viewEngineResult = _viewEngine.GetView(null, ViewPath, true); _viewEngineResult.EnsureSuccessful(null); _actionContext = new ActionContext( new DefaultHttpContext() { RequestServices = _requestScope.ServiceProvider }, _routeData, _actionDescriptor); _tempData = _tempDataDictionaryFactory.GetTempData(_actionContext.HttpContext); _viewDataDictionary = new ViewDataDictionary( _requestScope.ServiceProvider.GetRequiredService(), _actionContext.ModelState); _viewDataDictionary.Model = Model; _executor = _requestScope.ServiceProvider.GetRequiredService(); } [IterationCleanup] public virtual void IterationCleanup() { if (_viewEngineResult.View is IDisposable d) { d.Dispose(); } _requestScope.Dispose(); } protected virtual object Model { get; } = null; [Benchmark] public async Task RenderView() { await _executor.ExecuteAsync( _actionContext, _viewEngineResult.View, _viewDataDictionary, _tempData, "text/html", 200); return _executor.StringBuilder.ToString(); } } }