From b2799ef90147f125061919bc3d462750bfcbfa4e Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Sun, 29 Mar 2020 22:35:52 +0200 Subject: [PATCH] AB#5820 - Added simple controller and view to test the miniprofiler --- .../Controllers/BackOfficeController.cs | 14 +++ .../AspNetCore/AspNetCoreBackOfficeInfo.cs | 2 +- .../UmbracoCoreServiceCollectionExtensions.cs | 2 +- ...racoRequestApplicationBuilderExtensions.cs | 5 +- .../Runtime/Profiler/WebProfiler.cs | 104 +++++++++++---- .../Runtime/Profiler/WebProfilerComponent.cs | 3 + .../Runtime/Profiler/WebProfilerProvider.cs | 119 ------------------ .../Properties/launchSettings.json | 11 +- src/Umbraco.Web.UI.NetCore/Startup.cs | 45 ++++++- .../Views/BackOffice/Index.cshtml | 20 +++ .../Editors/BackOfficeController.cs | 7 +- 11 files changed, 176 insertions(+), 156 deletions(-) create mode 100644 src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs delete mode 100644 src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerProvider.cs create mode 100644 src/Umbraco.Web.UI.NetCore/Views/BackOffice/Index.cshtml diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs new file mode 100644 index 0000000000..83f6eda24a --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Mvc; +using Umbraco.Composing; + +namespace Umbraco.Web.BackOffice.Controllers +{ + public class BackOfficeController : Controller + { + // GET + public IActionResult Index() + { + return View(); + } + } +} diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreBackOfficeInfo.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreBackOfficeInfo.cs index 375028132e..39b1d7ff4e 100644 --- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreBackOfficeInfo.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreBackOfficeInfo.cs @@ -10,7 +10,7 @@ namespace Umbraco.Web.Common.AspNetCore GetAbsoluteUrl = globalSettings.UmbracoPath; } - public string GetAbsoluteUrl { get; } + public string GetAbsoluteUrl { get; } // TODO make absolute } } diff --git a/src/Umbraco.Web.Common/AspNetCore/UmbracoCoreServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/AspNetCore/UmbracoCoreServiceCollectionExtensions.cs index eb320c459f..99a274364b 100644 --- a/src/Umbraco.Web.Common/AspNetCore/UmbracoCoreServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/AspNetCore/UmbracoCoreServiceCollectionExtensions.cs @@ -169,7 +169,7 @@ namespace Umbraco.Web.Common.AspNetCore } var webProfiler = new WebProfiler(httpContextAccessor); - webProfiler.Start(); + webProfiler.StartBoot(); return webProfiler; } diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoRequestApplicationBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoRequestApplicationBuilderExtensions.cs index 110c09e1a9..8278c99bf5 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoRequestApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/UmbracoRequestApplicationBuilderExtensions.cs @@ -1,5 +1,6 @@ using System; using Microsoft.AspNetCore.Builder; +using StackExchange.Profiling; using Umbraco.Web.Common.Middleware; namespace Umbraco.Web.Common.Extensions @@ -13,9 +14,11 @@ namespace Umbraco.Web.Common.Extensions throw new ArgumentNullException(nameof(app)); } - app.UseMiddleware(); + app.UseMiddleware(); + app.UseMiddleware(); return app; } } + } diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs index 8baf8790c3..a05483633d 100644 --- a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs +++ b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs @@ -1,76 +1,118 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using StackExchange.Profiling; +using StackExchange.Profiling.Internal; using Umbraco.Core; -using Umbraco.Core.Cache; using Umbraco.Core.Logging; -using Umbraco.Web.Common.Middleware; namespace Umbraco.Web.Common.Runtime.Profiler { public class WebProfiler : IProfiler { + private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(); + private volatile BootPhase _bootPhase; + private MiniProfiler _startupProfiler; + + private const string BootRequestItemKey = "Umbraco.Core.Logging.WebProfiler__isBootRequest"; private readonly IHttpContextAccessor _httpContextAccessor; - private readonly WebProfilerProvider _provider; private int _first; public WebProfiler(IHttpContextAccessor httpContextAccessor) { // create our own provider, which can provide a profiler even during boot - _provider = new WebProfilerProvider(); _httpContextAccessor = httpContextAccessor; - - MiniProfiler.DefaultOptions.ProfilerProvider = _provider; + _bootPhase = BootPhase.Boot; } + /// + /// + /// + /// + /// Normally we would call MiniProfiler.Current.RenderIncludes(...), but because the requeststate is not set, this method does not work. + /// We fake the requestIds from the RequestState here. + /// public string Render() { - return MiniProfiler.Current - .RenderIncludes(_httpContextAccessor.HttpContext, RenderPosition.Right).ToString(); + + var profiler = MiniProfiler.Current; + if (profiler == null) return string.Empty; + + var context = _httpContextAccessor.HttpContext; + + var path = (profiler.Options as MiniProfilerOptions)?.RouteBasePath.Value.EnsureTrailingSlash(); + + var result = StackExchange.Profiling.Internal.Render.Includes( + profiler, + path: context.Request.PathBase + path, + isAuthorized: true, + requestIDs: new List{ profiler.Id }, + position: RenderPosition.Right, + showTrivial: profiler.Options.PopupShowTrivial, + showTimeWithChildren: profiler.Options.PopupShowTimeWithChildren, + maxTracesToShow: profiler.Options.PopupMaxTracesToShow, + showControls:profiler.Options.ShowControls, + startHidden: profiler.Options.PopupStartHidden); + + return result; } - public IDisposable Step(string name) => MiniProfiler.Current?.Step(name); + public IDisposable Step(string name) + { + return MiniProfiler.Current?.Step(name); + } public void Start() { MiniProfiler.StartNew(); } + public void StartBoot() + { + _startupProfiler = MiniProfiler.StartNew("Startup Profiler"); + } + + public void StopBoot() + { + _startupProfiler.Stop(); + } + public void Stop(bool discardResults = false) { MiniProfiler.Current?.Stop(discardResults); } + public void UmbracoApplicationBeginRequest(HttpContext context) { - // if this is the first request, notify our own provider that this request is the boot request - var first = Interlocked.Exchange(ref _first, 1) == 0; - if (first) + if (ShouldProfile(context.Request)) { - _provider.BeginBootRequest(); - context.Items[BootRequestItemKey] = true; - // and no need to start anything, profiler is already there - } - // else start a profiler, the normal way - else if (ShouldProfile(context.Request)) Start(); + } } public void UmbracoApplicationEndRequest(HttpContext context) { - // if this is the boot request, or if we should profile this request, stop - // (the boot request is always profiled, no matter what) - if (context.Items.TryGetValue(BootRequestItemKey, out var isBoot) && isBoot.Equals(true)) - { - _provider.EndBootRequest(); - Stop(); - } - else if (ShouldProfile(context.Request)) + if (ShouldProfile(context.Request)) { Stop(); + + // if this is the first request, append the startup profiler + var first = Interlocked.Exchange(ref _first, 1) == 0; + if (first) + { + + var startupDuration = _startupProfiler.Root.DurationMilliseconds.GetValueOrDefault(); + MiniProfiler.Current.DurationMilliseconds += startupDuration; + MiniProfiler.Current.GetTimingHierarchy().First().DurationMilliseconds += startupDuration; + MiniProfiler.Current.Root.AddChild(_startupProfiler.Root); + + _startupProfiler = null; + } } } @@ -82,5 +124,15 @@ namespace Umbraco.Web.Common.Runtime.Profiler if (bool.TryParse(request.Cookies["UMB-DEBUG"], out var cUmbDebug)) return cUmbDebug; return false; } + + /// + /// Indicates the boot phase. + /// + private enum BootPhase + { + Boot = 0, // boot phase (before the 1st request even begins) + BootRequest = 1, // request boot phase (during the 1st request) + Booted = 2 // done booting + } } } diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComponent.cs b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComponent.cs index 9a656ae4f3..a36753e634 100644 --- a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComponent.cs +++ b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComponent.cs @@ -51,6 +51,9 @@ namespace Umbraco.Web.Common.Runtime.Profiler _umbracoRequestLifetime.RequestStart += (sender, context) => _profiler.UmbracoApplicationBeginRequest(context); _umbracoRequestLifetime.RequestEnd += (sender, context) => _profiler.UmbracoApplicationEndRequest(context); + + // Stop the profiling of the booting process + _profiler.StopBoot(); } } } diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerProvider.cs b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerProvider.cs deleted file mode 100644 index 99ad6f724c..0000000000 --- a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerProvider.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System; -using System.Threading; -using StackExchange.Profiling; -using StackExchange.Profiling.Internal; -using Umbraco.Core.Cache; - -namespace Umbraco.Web.Common.Runtime.Profiler -{ - public class WebProfilerProvider : DefaultProfilerProvider - { - private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(); - private volatile BootPhase _bootPhase; - private int _first; - private MiniProfiler _startupProfiler; - - public WebProfilerProvider() - { - // booting... - _bootPhase = BootPhase.Boot; - } - - - /// - /// Gets the current profiler. - /// - /// - /// If the boot phase is not Booted, then this will return the startup profiler (this), otherwise - /// returns the base class - /// - public override MiniProfiler CurrentProfiler - { - get - { - // if not booting then just use base (fast) - // no lock, _bootPhase is volatile - if (_bootPhase == BootPhase.Booted) - return base.CurrentProfiler; - - // else - try - { - var current = base.CurrentProfiler; - return current ?? _startupProfiler; - } - catch - { - return _startupProfiler; - } - } - } - - public void BeginBootRequest() - { - _locker.EnterWriteLock(); - try - { - if (_bootPhase != BootPhase.Boot) - throw new InvalidOperationException("Invalid boot phase."); - _bootPhase = BootPhase.BootRequest; - - // assign the profiler to be the current MiniProfiler for the request - // is's already active, starting and all - CurrentProfiler = _startupProfiler; - } - finally - { - _locker.ExitWriteLock(); - } - } - - public void EndBootRequest() - { - _locker.EnterWriteLock(); - try - { - if (_bootPhase != BootPhase.BootRequest) - throw new InvalidOperationException("Invalid boot phase."); - _bootPhase = BootPhase.Booted; - - _startupProfiler = null; - } - finally - { - _locker.ExitWriteLock(); - } - } - - /// - /// Starts a new MiniProfiler. - /// - /// - /// - /// This is called when WebProfiler calls MiniProfiler.Start() so, - /// - as a result of WebRuntime starting the WebProfiler, and - /// - assuming profiling is enabled, on every BeginRequest that should be profiled, - /// - except for the very first one which is the boot request. - /// - /// - public override MiniProfiler Start(string profilerName, MiniProfilerBaseOptions options) - { - var first = Interlocked.Exchange(ref _first, 1) == 0; - if (first == false) return base.Start(profilerName, options); - - _startupProfiler = new MiniProfiler("StartupProfiler", options); - CurrentProfiler = _startupProfiler; - return _startupProfiler; - } - - /// - /// Indicates the boot phase. - /// - private enum BootPhase - { - Boot = 0, // boot phase (before the 1st request even begins) - BootRequest = 1, // request boot phase (during the 1st request) - Booted = 2 // done booting - } - } -} diff --git a/src/Umbraco.Web.UI.NetCore/Properties/launchSettings.json b/src/Umbraco.Web.UI.NetCore/Properties/launchSettings.json index b145249bb5..27f36121ac 100644 --- a/src/Umbraco.Web.UI.NetCore/Properties/launchSettings.json +++ b/src/Umbraco.Web.UI.NetCore/Properties/launchSettings.json @@ -1,7 +1,7 @@ { "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, + "windowsAuthentication": false, + "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:36804", "sslPort": 44354 @@ -15,10 +15,11 @@ "ASPNETCORE_ENVIRONMENT": "Development" } }, - "Umbraco.Web.UI.BackOffice": { + "Umbraco.Web.UI.NetCore": { "commandName": "Project", - "launchBrowser": true, - "applicationUrl": "https://localhost:5001;http://localhost:5000", + "launchBrowser": false, + "applicationUrl": "https://localhost:44354;http://localhost:9000", + "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index b38333bb9a..e0440e39ea 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -5,13 +5,19 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using StackExchange.Profiling; using Umbraco.Composing; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.IO; using Umbraco.Web.BackOffice.AspNetCore; using Umbraco.Web.Common.AspNetCore; using Umbraco.Web.Common.Extensions; +using Umbraco.Web.Common.Runtime.Profiler; using Umbraco.Web.Website.AspNetCore; @@ -43,11 +49,19 @@ namespace Umbraco.Web.UI.BackOffice services.AddUmbracoConfiguration(_config); services.AddUmbracoCore(_webHostEnvironment); services.AddUmbracoWebsite(); + + services.AddMvc(); + services.AddMiniProfiler(options => + { + options.ShouldProfile = request => false; // WebProfiler determine and start profiling. We should not use the MiniProfilerMiddleware to also profile + }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { + + // app.UseMiniProfiler(); app.UseUmbracoRequest(); if (env.IsDevelopment()) { @@ -59,9 +73,17 @@ namespace Umbraco.Web.UI.BackOffice app.UseUmbracoBackOffice(); app.UseRouting(); - app.UseEndpoints(endpoints => { + endpoints.MapControllerRoute( + "backoffice-spa", + "/umbraco", + new + { + Controller = "BackOffice", + Action = "Index" + } + ); endpoints.MapGet("/", async context => { await context.Response.WriteAsync($"Hello World!{Current.Profiler.Render()}"); @@ -69,4 +91,25 @@ namespace Umbraco.Web.UI.BackOffice }); } } + + public class UmbracoMiniProfilerOptions : MiniProfilerOptions + { + public UmbracoMiniProfilerOptions() + { + } + + public new MiniProfiler StartProfiler(string profilerName = null) + { + return base.StartProfiler(); + } + + public new Func ShouldProfile => (request) => + { + if (new Uri(request.GetEncodedUrl(), UriKind.RelativeOrAbsolute).IsClientSideRequest()) return false; + if (bool.TryParse(request.Query["umbDebug"], out var umbDebug)) return umbDebug; + if (bool.TryParse(request.Headers["X-UMB-DEBUG"], out var xUmbDebug)) return xUmbDebug; + if (bool.TryParse(request.Cookies["UMB-DEBUG"], out var cUmbDebug)) return cUmbDebug; + return false; + }; + } } diff --git a/src/Umbraco.Web.UI.NetCore/Views/BackOffice/Index.cshtml b/src/Umbraco.Web.UI.NetCore/Views/BackOffice/Index.cshtml new file mode 100644 index 0000000000..1b5007c005 --- /dev/null +++ b/src/Umbraco.Web.UI.NetCore/Views/BackOffice/Index.cshtml @@ -0,0 +1,20 @@ +@using Umbraco.Composing + +@{ + Layout = null; +} + + + + + + title + + +
+ Hello World + @Html.Raw(Current.Profiler.Render()) +
+ + + diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index e39d1658a2..09076400de 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -104,8 +104,11 @@ namespace Umbraco.Web.Editors public async Task Default() { return await RenderDefaultOrProcessExternalLoginAsync( - () => View(_ioHelper.BackOfficePath.EnsureEndsWith('/') + "Views/Default.cshtml", new BackOfficeModel(_features, GlobalSettings, _umbracoVersion, _contentSettings,_ioHelper, _treeCollection, _httpContextAccessor, _hostingEnvironment, _runtimeSettings, _securitySettings)), - () => View(_ioHelper.BackOfficePath.EnsureEndsWith('/') + "Views/Default.cshtml", new BackOfficeModel(_features, GlobalSettings, _umbracoVersion, _contentSettings, _ioHelper, _treeCollection, _httpContextAccessor, _hostingEnvironment, _runtimeSettings, _securitySettings))); + () => + View(_ioHelper.BackOfficePath.EnsureEndsWith('/') + "Views/Default.cshtml", new BackOfficeModel(_features, GlobalSettings, _umbracoVersion, _contentSettings,_ioHelper, _treeCollection, _httpContextAccessor, _hostingEnvironment, _runtimeSettings, _securitySettings)), + () => + View(_ioHelper.BackOfficePath.EnsureEndsWith('/') + "Views/Default.cshtml", new BackOfficeModel(_features, GlobalSettings, _umbracoVersion, _contentSettings, _ioHelper, _treeCollection, _httpContextAccessor, _hostingEnvironment, _runtimeSettings, _securitySettings)) + ); } [HttpGet]