diff --git a/docs/AspDotNetCore.md b/docs/AspDotNetCore.md index e3b3d964a..334f3b9b0 100644 --- a/docs/AspDotNetCore.md +++ b/docs/AspDotNetCore.md @@ -64,6 +64,27 @@ public void ConfigureServices(IServiceCollection services) // (Optional) Use something other than the "light" color scheme. // (defaults to "light") options.ColorScheme = StackExchange.Profiling.ColorScheme.Auto; + + // The below are newer options, available in .NET Core 3.0 and above: + + // (Optional) You can disable MVC filter profiling + // (defaults to true, and filters are profiled) + options.EnableMvcFilterProfiling = true; + // ...or only save filters that take over a certain millisecond duration (including their children) + // (defaults to null, and all filters are profiled) + // options.MvcFilterMinimumSaveMs = 1.0m; + + // (Optional) You can disable MVC view profiling + // (defaults to true, and views are profiled) + options.EnableMvcFilterProfiling = true; + // ...or only save views that take over a certain millisecond duration (including their children) + // (defaults to null, and all views are profiled) + // options.MvcViewMinimumSaveMs = 1.0m; + + // (Optional - not recommended) You can enable a heavy debug mode with stacks and tooltips when using memory storage + // It has a lot of overhead vs. normal profiling and should only be used with that in mind + // (defaults to false, debug/heavy mode is off) + //options.EnableDebugMode = true; }); } ``` diff --git a/docs/Releases.md b/docs/Releases.md index ee1189603..48f4314a4 100644 --- a/docs/Releases.md +++ b/docs/Releases.md @@ -11,8 +11,10 @@ This page tracks major changes included in any update starting with version 4.0. - Added dark and "auto" (system preference decides) color themes, total is "Light", "Dark", and "Auto" ([#451](https://github.com/MiniProfiler/dotnet/pull/451)) - Generally moves to CSS 3 variables, for easier custom themes as well ([#451](https://github.com/MiniProfiler/dotnet/pull/451)) - Added `SqlServerFormatter.IncludeParameterValues` for excluding actual values in output if desired ([#463](https://github.com/MiniProfiler/dotnet/pull/463)) + - New "debug" mode (via `.EnableDebugMode`) that outputs stack dumps for every timing (expensive/heavy, and not intended for normal operation - [#482](https://github.com/MiniProfiler/dotnet/pull/482)) - (**.NET Core only**) Added `MiniProfilerOptions.ResultsAuthorizeAsync` and `MiniProfiler.ResultsAuthorizeListAsync` ([#472](https://github.com/MiniProfiler/dotnet/pull/472)) - - (**.NET Core only**) Added profiling to all diagnostic events (views, filters, etc. - [#475](https://github.com/MiniProfiler/dotnet/pull/475)) + - (**.NET Core only**) Added profiling to all diagnostic events (views, filters, etc. - [#475](https://github.com/MiniProfiler/dotnet/pull/475) & [#482](https://github.com/MiniProfiler/dotnet/pull/482)) + - New options around this are in the ASP.NET Core docs on the left. - **Fixes/Changes**: - Fix for ['i.Started.toUTCString is not a function'](https://github.com/MiniProfiler/dotnet/pull/462) when global serializer options are changed. - Removed jQuery (built-in) dependency ([#442](https://github.com/MiniProfiler/dotnet/pull/442)) diff --git a/samples/Samples.AspNetCore3/Controllers/HomeController.cs b/samples/Samples.AspNetCore3/Controllers/HomeController.cs index 25e20c9ce..ce49a9edc 100644 --- a/samples/Samples.AspNetCore3/Controllers/HomeController.cs +++ b/samples/Samples.AspNetCore3/Controllers/HomeController.cs @@ -3,8 +3,12 @@ namespace Samples.AspNetCore.Controllers { + [ExampleActionFilter] public class HomeController : Controller { + [ExampleLongActionFilter] + //[ExampleActionFilter] + [ExampleAsyncActionFilter] public IActionResult Index() { using (MiniProfiler.Current.Step("Example Step")) diff --git a/samples/Samples.AspNetCore3/Helpers/ExampleActionFilter.cs b/samples/Samples.AspNetCore3/Helpers/ExampleActionFilter.cs new file mode 100644 index 000000000..0c08a6b8d --- /dev/null +++ b/samples/Samples.AspNetCore3/Helpers/ExampleActionFilter.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Samples.AspNetCore +{ + public class ExampleActionFilterAttribute : ActionFilterAttribute + { + public override void OnActionExecuting(ActionExecutingContext context) => Thread.Sleep(100); + } + + public class ExampleLongActionFilterAttribute : ActionFilterAttribute + { + public override void OnActionExecuting(ActionExecutingContext context) => Thread.Sleep(200); + } + + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true)] + public class ExampleAsyncActionFilterAttribute : Attribute, IAsyncActionFilter + { + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + await Task.Delay(300); + await next(); + } + } +} diff --git a/samples/Samples.AspNetCore3/Startup.cs b/samples/Samples.AspNetCore3/Startup.cs index 456bff246..02272ffbb 100644 --- a/samples/Samples.AspNetCore3/Startup.cs +++ b/samples/Samples.AspNetCore3/Startup.cs @@ -83,6 +83,20 @@ public void ConfigureServices(IServiceCollection services) // Enabled sending the Server-Timing header on responses options.EnableServerTimingHeader = true; + // Optionally disable MVC filter profiling + //options.EnableMvcFilterProfiling = false; + // Or only save filters that take over a certain millisecond duration (including their children) + //options.MvcFilterMinimumSaveMs = 1.0m; + + // Optionally disable MVC view profiling + //options.EnableMvcFilterProfiling = false; + // Or only save views that take over a certain millisecond duration (including their children) + //options.MvcViewMinimumSaveMs = 1.0m; + + // This enables debug mode with stacks and tooltips when using memory storage + // It has a lot of overhead vs. normal profiling and should only be used with that in mind + //options.EnableDebugMode = true; + options.IgnoredPaths.Add("/lib"); options.IgnoredPaths.Add("/css"); options.IgnoredPaths.Add("/js"); diff --git a/samples/Samples.AspNetCore3/web.config b/samples/Samples.AspNetCore3/web.config index ccdd0e200..d195283ab 100644 --- a/samples/Samples.AspNetCore3/web.config +++ b/samples/Samples.AspNetCore3/web.config @@ -7,7 +7,7 @@ - + diff --git a/src/MiniProfiler.AspNetCore.Mvc/MvcDiagnosticListener.cs b/src/MiniProfiler.AspNetCore.Mvc/MvcDiagnosticListener.cs index 5c65400a5..77c092307 100644 --- a/src/MiniProfiler.AspNetCore.Mvc/MvcDiagnosticListener.cs +++ b/src/MiniProfiler.AspNetCore.Mvc/MvcDiagnosticListener.cs @@ -1,4 +1,5 @@ #if NETCOREAPP3_0 +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Diagnostics; @@ -8,7 +9,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; -using System.Threading; namespace StackExchange.Profiling.Data { @@ -75,22 +75,62 @@ private string GetName(string label, ActionDescriptor descriptor) private static string GetName(IFilterMetadata filter) => filter.GetType().Name; + private const string ContextItemsKey = "MiniProfiler-DiagnosticStack"; /// - /// Stores the current timing in the tree, on each request. + /// Gets the timing stack for a given request. /// - private readonly AsyncLocal<(object State, Timing Timing)> CurrentTiming = new AsyncLocal<(object, Timing)>(); + private Stack<(object State, Timing Timing)> GetStack(HttpContext context) + { + if (context == null) + { + return null; + } + if (context?.Items[ContextItemsKey] is Stack<(object State, Timing Timing)> stack) + { + return stack; + } + stack = new Stack<(object State, Timing Timing)>(); + context.Items[ContextItemsKey] = stack; + return stack; + } - private object Start(T state, string stepName) where T : class + private object StartFilter(FilterContext context, T state, string stepName) where T : class { - CurrentTiming.Value = (state, MiniProfiler.Current.Step(stepName)); + var profiler = MiniProfiler.Current; + if (profiler?.Options is MiniProfilerOptions opts && opts.EnableMvcFilterProfiling) + { + GetStack(context.HttpContext).Push((state, new Timing(profiler, profiler.Head, stepName, opts.MvcFilterMinimumSaveMs, true, debugStackShave: 4))); + } return null; } - private object Complete(T state) where T : class + private object StartView(HttpContext context, T state, string stepName) where T : class + { + var profiler = MiniProfiler.Current; + if (profiler?.Options is MiniProfilerOptions opts && opts.EnableMvcViewProfiling) + { + GetStack(context).Push((state, new Timing(profiler, profiler.Head, stepName, opts.MvcViewMinimumSaveMs, true, debugStackShave: 4))); + } + return null; + } + + private object Start(HttpContext context, T state, string stepName) where T : class + { + var profiler = MiniProfiler.Current; + GetStack(context).Push((state, profiler != null ? new Timing(profiler, profiler.Head, stepName, null, null, debugStackShave: 4) : null)); + return null; + } + + private object Complete(FilterContext context, T state) where T : class => Complete(context.HttpContext, state); + + private object Complete(HttpContext context, T state) where T : class { - if (CurrentTiming.Value.State is T currentState && currentState == state) + var stack = GetStack(context); + var top = stack.Pop(); + if (top.State is T currentState && currentState == state) { - using (CurrentTiming.Value.Timing) { } + // Set the previous timing explicitly to the stack parent for this context + using (top.Timing) { } } return null; } @@ -103,79 +143,79 @@ private string GetName(string label, ActionDescriptor descriptor) { // MVC Bits: https://github.com/dotnet/aspnetcore/blob/v3.0.0/src/Mvc/Mvc.Core/src/Diagnostics/MvcDiagnostics.cs // ActionEvent - BeforeActionEventData data => Start(data.ActionDescriptor, GetName("Action", data.ActionDescriptor)), - AfterActionEventData data => Complete(data.ActionDescriptor), + BeforeActionEventData data => Start(data.HttpContext, data.ActionDescriptor, GetName("Action", data.ActionDescriptor)), + AfterActionEventData data => Complete(data.HttpContext, data.ActionDescriptor), // ControllerActionMethod - BeforeControllerActionMethodEventData data => Start(data.ActionContext.ActionDescriptor, GetName("Controller Action", data.ActionContext.ActionDescriptor)), - AfterControllerActionMethodEventData data => Complete(data.ActionContext.ActionDescriptor), + BeforeControllerActionMethodEventData data => Start(data.ActionContext.HttpContext, data.ActionContext.ActionDescriptor, GetName("Controller Action", data.ActionContext.ActionDescriptor)), + AfterControllerActionMethodEventData data => Complete(data.ActionContext.HttpContext, data.ActionContext.ActionDescriptor), // ActionResultEvent - BeforeActionResultEventData data => Start(data.Result, GetName(data.Result)), - AfterActionResultEventData data => Complete(data.Result), + BeforeActionResultEventData data => Start(data.ActionContext.HttpContext, data.Result, GetName(data.Result)), + AfterActionResultEventData data => Complete(data.ActionContext.HttpContext, data.Result), // AuthorizationFilterOnAuthorization - BeforeAuthorizationFilterOnAuthorizationEventData data => Start(data.Filter, "Auth Filter: " + GetName(data.Filter)), - AfterAuthorizationFilterOnAuthorizationEventData data => Complete(data.Filter), + BeforeAuthorizationFilterOnAuthorizationEventData data => StartFilter(data.AuthorizationContext, data.Filter, "Auth Filter: " + GetName(data.Filter)), + AfterAuthorizationFilterOnAuthorizationEventData data => Complete(data.AuthorizationContext.HttpContext, data.Filter), // ResourceFilterOnResourceExecution - BeforeResourceFilterOnResourceExecutionEventData data => Start(data.Filter, "Resource Filter (Exec): " + GetName(data.Filter)), - AfterResourceFilterOnResourceExecutionEventData data => Complete(data.Filter), + BeforeResourceFilterOnResourceExecutionEventData data => StartFilter(data.ResourceExecutingContext, data.Filter, "Resource Filter (Exec): " + GetName(data.Filter)), + AfterResourceFilterOnResourceExecutionEventData data => Complete(data.ResourceExecutedContext.HttpContext, data.Filter), // ResourceFilterOnResourceExecuting - BeforeResourceFilterOnResourceExecutingEventData data => Start(data.Filter, "Resource Filter (Execing): " + GetName(data.Filter)), - AfterResourceFilterOnResourceExecutingEventData data => Complete(data.Filter), + BeforeResourceFilterOnResourceExecutingEventData data => StartFilter(data.ResourceExecutingContext, data.Filter, "Resource Filter (Execing): " + GetName(data.Filter)), + AfterResourceFilterOnResourceExecutingEventData data => Complete(data.ResourceExecutingContext.HttpContext, data.Filter), // ResourceFilterOnResourceExecuted - BeforeResourceFilterOnResourceExecutedEventData data => Start(data.Filter, "Resource Filter (Execed): " + GetName(data.Filter)), - AfterResourceFilterOnResourceExecutedEventData data => Complete(data.Filter), + BeforeResourceFilterOnResourceExecutedEventData data => StartFilter(data.ResourceExecutedContext, data.Filter, "Resource Filter (Execed): " + GetName(data.Filter)), + AfterResourceFilterOnResourceExecutedEventData data => Complete(data.ResourceExecutedContext, data.Filter), // ExceptionFilterOnException - BeforeExceptionFilterOnException data => Start(data.Filter, "Exception Filter: " + GetName(data.Filter)), - AfterExceptionFilterOnExceptionEventData data => Complete(data.Filter), + BeforeExceptionFilterOnException data => StartFilter(data.ExceptionContext, data.Filter, "Exception Filter: " + GetName(data.Filter)), + AfterExceptionFilterOnExceptionEventData data => Complete(data.ExceptionContext, data.Filter), // ActionFilterOnActionExecution - BeforeActionFilterOnActionExecutionEventData data => Start(data.Filter, "Action Filter (Exec): " + GetName(data.Filter)), - AfterActionFilterOnActionExecutionEventData data => Complete(data.Filter), + BeforeActionFilterOnActionExecutionEventData data => StartFilter(data.ActionExecutingContext, data.Filter, "Action Filter (Exec): " + GetName(data.Filter)), + AfterActionFilterOnActionExecutionEventData data => Complete(data.ActionExecutedContext, data.Filter), // ActionFilterOnActionExecuting - BeforeActionFilterOnActionExecutingEventData data => Start(data.Filter, "Action Filter (Execing): " + GetName(data.Filter)), - AfterActionFilterOnActionExecutingEventData data => Complete(data.Filter), + BeforeActionFilterOnActionExecutingEventData data => StartFilter(data.ActionExecutingContext, data.Filter, "Action Filter (Execing): " + GetName(data.Filter)), + AfterActionFilterOnActionExecutingEventData data => Complete(data.ActionExecutingContext, data.Filter), // ActionFilterOnActionExecuted - BeforeActionFilterOnActionExecutedEventData data => Start(data.Filter, "Action Filter (Execed): " + GetName(data.Filter)), - AfterActionFilterOnActionExecutedEventData data => Complete(data.Filter), + BeforeActionFilterOnActionExecutedEventData data => StartFilter(data.ActionExecutedContext, data.Filter, "Action Filter (Execed): " + GetName(data.Filter)), + AfterActionFilterOnActionExecutedEventData data => Complete(data.ActionExecutedContext, data.Filter), // ResultFilterOnResultExecution - BeforeResultFilterOnResultExecutionEventData data => Start(data.Filter, "Result Filter (Exec): " + GetName(data.Filter)), - AfterResultFilterOnResultExecutionEventData data => Complete(data.Filter), + BeforeResultFilterOnResultExecutionEventData data => StartFilter(data.ResultExecutingContext, data.Filter, "Result Filter (Exec): " + GetName(data.Filter)), + AfterResultFilterOnResultExecutionEventData data => Complete(data.ResultExecutedContext, data.Filter), // ResultFilterOnResultExecuting - BeforeResultFilterOnResultExecutingEventData data => Start(data.Filter, "Result Filter (Execing): " + GetName(data.Filter)), - AfterResultFilterOnResultExecutingEventData data => Complete(data.Filter), + BeforeResultFilterOnResultExecutingEventData data => StartFilter(data.ResultExecutingContext, data.Filter, "Result Filter (Execing): " + GetName(data.Filter)), + AfterResultFilterOnResultExecutingEventData data => Complete(data.ResultExecutingContext, data.Filter), // ResultFilterOnResultExecuted - BeforeResultFilterOnResultExecutedEventData data => Start(data.Filter, "Result Filter (Execed): " + GetName(data.Filter)), - AfterResultFilterOnResultExecutedEventData data => Complete(data.Filter), + BeforeResultFilterOnResultExecutedEventData data => StartFilter(data.ResultExecutedContext, data.Filter, "Result Filter (Execed): " + GetName(data.Filter)), + AfterResultFilterOnResultExecutedEventData data => Complete(data.ResultExecutedContext, data.Filter), // Razor Bits: https://github.com/dotnet/aspnetcore/blob/v3.0.0/src/Mvc/Mvc.Razor/src/Diagnostics/MvcDiagnostics.cs // ViewPage - BeforeViewPageEventData data => Start(data.Page, "View: " + data.Page.Path), - AfterViewPageEventData data => Complete(data.Page), + BeforeViewPageEventData data => StartView(data.HttpContext, data.Page, "View: " + data.Page.Path), + AfterViewPageEventData data => Complete(data.HttpContext, data.Page), // RazorPage Bits: https://github.com/dotnet/aspnetcore/blob/v3.0.0/src/Mvc/Mvc.RazorPages/src/Diagnostics/MvcDiagnostics.cs // HandlerMethod - BeforeHandlerMethodEventData data => Start(data.Instance, "Handler: " + data.HandlerMethodDescriptor.Name), - AfterHandlerMethodEventData data => Complete(data.Instance), + BeforeHandlerMethodEventData data => Start(data.ActionContext.HttpContext, data.Instance, "Handler: " + data.HandlerMethodDescriptor.Name), + AfterHandlerMethodEventData data => Complete(data.ActionContext.HttpContext, data.Instance), // PageFilterOnPageHandlerExecution - BeforePageFilterOnPageHandlerExecutionEventData data => Start(data.Filter, "Filter (Exec): " + GetName(data.Filter)), - AfterPageFilterOnPageHandlerExecutionEventData data => Complete(data.Filter), + BeforePageFilterOnPageHandlerExecutionEventData data => StartFilter(data.HandlerExecutionContext, data.Filter, "Filter (Exec): " + GetName(data.Filter)), + AfterPageFilterOnPageHandlerExecutionEventData data => Complete(data.HandlerExecutedContext, data.Filter), // PageFilterOnPageHandlerExecuting - BeforePageFilterOnPageHandlerExecutingEventData data => Start(data.Filter, "Filter (Execing): " + GetName(data.Filter)), - AfterPageFilterOnPageHandlerExecutingEventData data => Complete(data.Filter), + BeforePageFilterOnPageHandlerExecutingEventData data => StartFilter(data.HandlerExecutingContext, data.Filter, "Filter (Execing): " + GetName(data.Filter)), + AfterPageFilterOnPageHandlerExecutingEventData data => Complete(data.HandlerExecutingContext, data.Filter), // PageFilterOnPageHandlerExecuted - BeforePageFilterOnPageHandlerExecutedEventData data => Start(data.Filter, "Filter (Execed): " + GetName(data.Filter)), - AfterPageFilterOnPageHandlerExecutedEventData data => Complete(data.Filter), + BeforePageFilterOnPageHandlerExecutedEventData data => StartFilter(data.HandlerExecutedContext, data.Filter, "Filter (Execed): " + GetName(data.Filter)), + AfterPageFilterOnPageHandlerExecutedEventData data => Complete(data.HandlerExecutedContext, data.Filter), // PageFilterOnPageHandlerSelection - BeforePageFilterOnPageHandlerSelectionEventData data => Start(data.Filter, "Filter (Selection): " + GetName(data.Filter)), - AfterPageFilterOnPageHandlerSelectionEventData data => Complete(data.Filter), + BeforePageFilterOnPageHandlerSelectionEventData data => StartFilter(data.HandlerSelectedContext, data.Filter, "Filter (Selection): " + GetName(data.Filter)), + AfterPageFilterOnPageHandlerSelectionEventData data => Complete(data.HandlerSelectedContext, data.Filter), // PageFilterOnPageHandlerSelected - BeforePageFilterOnPageHandlerSelectedEventData data => Start(data.Filter, "Filter (Selected): " + GetName(data.Filter)), - AfterPageFilterOnPageHandlerSelectedEventData data => Complete(data.Filter), + BeforePageFilterOnPageHandlerSelectedEventData data => StartFilter(data.HandlerSelectedContext, data.Filter, "Filter (Selected): " + GetName(data.Filter)), + AfterPageFilterOnPageHandlerSelectedEventData data => Complete(data.HandlerSelectedContext, data.Filter), _ => null }; } diff --git a/src/MiniProfiler.AspNetCore/MiniProfilerOptions.cs b/src/MiniProfiler.AspNetCore/MiniProfilerOptions.cs index a0117ccbc..71dc1d2d3 100644 --- a/src/MiniProfiler.AspNetCore/MiniProfilerOptions.cs +++ b/src/MiniProfiler.AspNetCore/MiniProfilerOptions.cs @@ -60,6 +60,26 @@ public class MiniProfilerOptions : MiniProfilerBaseOptions /// Whether to add a Server-Timing header after profiling a request. Only supported in .NET Core 3.0 and higher. /// public bool EnableServerTimingHeader { get; set; } + + /// + /// Whether to profile MVC filters as individual steps. + /// + public bool EnableMvcFilterProfiling { get; set; } = true; + + /// + /// The minimum duration to record for MVC filter timings, anything below this will be discarded as noise. + /// + public decimal? MvcFilterMinimumSaveMs { get; set; } + + /// + /// Whether to profile MVC views as individual steps. + /// + public bool EnableMvcViewProfiling { get; set; } = true; + + /// + /// The minimum duration to record for MVC view timings, anything below this will be discarded as noise. + /// + public decimal? MvcViewMinimumSaveMs { get; set; } #endif } } diff --git a/src/MiniProfiler.Shared/Helpers/StackTraceSnippet.cs b/src/MiniProfiler.Shared/Helpers/StackTraceSnippet.cs index eaa1a794d..4a7ad29be 100644 --- a/src/MiniProfiler.Shared/Helpers/StackTraceSnippet.cs +++ b/src/MiniProfiler.Shared/Helpers/StackTraceSnippet.cs @@ -1,6 +1,11 @@ using StackExchange.Profiling.Internal; +using System; +using System.Collections.Generic; using System.Diagnostics; +using System.Net; using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; namespace StackExchange.Profiling.Helpers { @@ -86,4 +91,476 @@ bool ShouldExcludeType(MethodBase method) return sb.ToStringRecycle(); } } + + /// + /// StackTrace utilities, from Exceptional + /// + /// + /// ...need to make this a source package... + /// + internal static class StackTraceUtils + { + // Inspired by StackTraceParser by Atif Aziz, project home: https://github.com/atifaziz/StackTraceParser + internal const string Space = @"[\x20\t]", + NoSpace = @"[^\x20\t]"; + private static class Groups + { + public const string LeadIn = nameof(LeadIn); + public const string Frame = nameof(Frame); + public const string Type = nameof(Type); + public const string AsyncMethod = nameof(AsyncMethod); + public const string Method = nameof(Method); + public const string Params = nameof(Params); + public const string ParamType = nameof(ParamType); + public const string ParamName = nameof(ParamName); + public const string SourceInfo = nameof(SourceInfo); + public const string Path = nameof(Path); + public const string LinePrefix = nameof(LinePrefix); + public const string Line = nameof(Line); + } + + private static readonly char[] NewLine_CarriageReturn = { '\n', '\r' }; + + private const string EndStack = "--- End of stack trace from previous location where exception was thrown ---"; + + // TODO: Patterns, or a bunch of these... + private static readonly HashSet _asyncFrames = new HashSet() + { + // 3.1 Stacks + "System.Runtime.CompilerServices.AsyncTaskMethodBuilder.SetException(Exception exception)", + "System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder`1.SetException(Exception exception)", + "System.Threading.Tasks.AwaitTaskContinuation.RunOrScheduleAction(IAsyncStateMachineBox box, Boolean allowInlining)", + "System.Threading.Tasks.Task.FinishSlow(Boolean userDelegateExecute)", + "System.Threading.Tasks.Task.TrySetException(Object exceptionObject)", + + // 3.0 Stacks + "System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)", + "System.Threading.Tasks.Task.RunContinuations(Object continuationObject)", + "System.Threading.Tasks.Task`1.TrySetResult(TResult result)", + "System.Threading.Tasks.AwaitTaskContinuation.RunOrScheduleAction(Action action, Boolean allowInlining)", + "System.Threading.Tasks.Task.CancellationCleanupLogic()", + "System.Threading.Tasks.Task.TrySetCanceled(CancellationToken tokenToRecord, Object cancellationException)", + "System.Threading.Tasks.Task.FinishContinuations()", + + "System.Runtime.CompilerServices.AsyncMethodBuilderCore.ContinuationWrapper.Invoke()", + "System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.SetExistingTaskResult(TResult result)", + "System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.AsyncStateMachineBox`1.ExecutionContextCallback(Object s)", + "System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.AsyncStateMachineBox`1.MoveNext(Thread threadPoolThread)", + "System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.AsyncStateMachineBox`1.MoveNext()", + "System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.SetException(Exception exception)", + "System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.SetResult(TResult result)", + "System.Runtime.CompilerServices.AsyncTaskMethodBuilder.SetResult()", + "System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder`1.SetResult(TResult result)", + "System.Runtime.CompilerServices.TaskAwaiter.<>c.b__12_0(Action innerContinuation, Task innerTask)", + + // < .NET Core 3.0 stacks + "System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()", + "System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)", + "System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)", + "System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd(Task task)", + "System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()", + "System.Runtime.CompilerServices.ConfiguredTaskAwaitable.ConfiguredTaskAwaiter.GetResult()", + "System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()", + "System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)", + "System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()", + "Microsoft.Extensions.Internal.ObjectMethodExecutorAwaitable.Awaiter.GetResult()", + EndStack + }; + + // TODO: Adjust for URLs instead of files + private static readonly Regex _regex = new Regex($@" + ^(?<{Groups.LeadIn}>{Space}*\w+{Space}+) + (?<{Groups.Frame}> + (?<{Groups.Type}>({NoSpace}+(<(?<{Groups.AsyncMethod}>\w+)>d__[0-9]+))|{NoSpace}+)\. + (?<{Groups.Method}>{NoSpace}+?){Space}* + (?<{Groups.Params}>\(({Space}*\) + |(?<{Groups.ParamType}>.+?){Space}+(?<{Groups.ParamName}>.+?) + (,{Space}*(?<{Groups.ParamType}>.+?){Space}+(?<{Groups.ParamName}>.+?))*\)) + ) + ({Space}+ + (\w+{Space}+ + (?<{Groups.SourceInfo}> + (?<{Groups.Path}>([a-z]\:.+?|(\b(https?|ftp|file)://)?[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|])) + (?<{Groups.LinePrefix}>\:\w+{Space}+) + (?<{Groups.Line}>[0-9]+)\p{{P}}? + |\[0x[0-9a-f]+\]{Space}+\w+{Space}+<(?<{Groups.Path}>[^>]+)>(?<{Groups.LinePrefix}>:)(?<{Groups.Line}>[0-9]+)) + ) + )? + )\s*$", + RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.Multiline + | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace, + TimeSpan.FromSeconds(2)); + + /// + /// Converts a stack trace to formatted HTML with styling and linkifiation. + /// + /// The stack trace to HTMLify. + /// The frame index to start marking as common (e.g. to grey out beneath). + /// An HTML-pretty version of the stack trace. + internal static string HtmlPrettify(string stackTrace, int? commonStart = null) + { + string GetBetween(Capture prev, Capture next) => + stackTrace.Substring(prev.Index + prev.Length, next.Index - (prev.Index + prev.Length)); + + int pos = 0; + var sb = StringBuilderCache.Get(); + var matches = _regex.Matches(stackTrace); + for (var mi = 0; mi < matches.Count; mi++) + { + Match m = matches[mi]; + Group leadIn = m.Groups[Groups.LeadIn], + frame = m.Groups[Groups.Frame], + type = m.Groups[Groups.Type], + asyncMethod = m.Groups[Groups.AsyncMethod], + method = m.Groups[Groups.Method], + allParams = m.Groups[Groups.Params], + sourceInfo = m.Groups[Groups.SourceInfo], + path = m.Groups[Groups.Path], + linePrefix = m.Groups[Groups.LinePrefix], + line = m.Groups[Groups.Line]; + CaptureCollection paramTypes = m.Groups[Groups.ParamType].Captures, + paramNames = m.Groups[Groups.ParamName].Captures; + bool nextIsAsync = false; + if (mi < matches.Count - 1) + { + Group nextFrame = matches[mi + 1].Groups[Groups.Frame]; + nextIsAsync = _asyncFrames.Contains(nextFrame.Value); + } + + var isAsync = _asyncFrames.Contains(frame.Value); + + // The initial message may be above an async frame + if (sb.Length == 0 && isAsync && leadIn.Index > pos) + { + sb.Append("") + .Append("") + .AppendHtmlEncode(stackTrace.Substring(pos, leadIn.Index - pos).Trim(NewLine_CarriageReturn)) + .Append("") + .Append(""); + pos += sb.Length; + } + + sb.Append("= commonStart) + { + sb.Append(" common"); + } + sb.Append("\">"); + + if (leadIn.Index > pos) + { + var miscContent = stackTrace.Substring(pos, leadIn.Index - pos); + if (miscContent.Contains(EndStack)) + { + // Handle end-of-stack removals and redundant multilines remaining + miscContent = miscContent.Replace(EndStack, "") + .Replace("\r\n\r\n", "\r\n") + .Replace("\n\n", "\n\n"); + } + + sb.Append("") + .AppendHtmlEncode(miscContent) + .Append(""); + } + sb.Append("") + .AppendHtmlEncode(leadIn.Value) + .Append(""); + + // Check if the next line is the end of an async hand-off + var nextEndStack = stackTrace.IndexOf(EndStack, m.Index + m.Length); + if ((nextEndStack > -1 && nextEndStack < m.Index + m.Length + 3) || (!isAsync && nextIsAsync)) + { + sb.Append("async "); + } + + if (asyncMethod.Success) + { + sb.Append("") + .AppendGenericsHtml(GetBetween(leadIn, asyncMethod)) + .Append("") + .Append("") + .AppendHtmlEncode(asyncMethod.Value) + .Append("") + .Append("") + .AppendGenericsHtml(GetBetween(asyncMethod, method)); + sb.Append(""); + } + else + { + sb.Append("") + .AppendGenericsHtml(type.Value) + .Append("") + .AppendHtmlEncode(GetBetween(type, method)) // "." + .Append("") + .Append(""); + } + sb.Append("") + .Append("") + .AppendHtmlEncode(NormalizeMethodName(method.Value)) + .Append(""); + + if (paramTypes.Count > 0) + { + sb.Append("") + .Append(GetBetween(method, paramTypes[0])) + .Append(""); + for (var i = 0; i < paramTypes.Count; i++) + { + if (i > 0) + { + sb.Append("") + .AppendHtmlEncode(GetBetween(paramNames[i - 1], paramTypes[i])) // ", " + .Append(""); + } + sb.Append("") + .AppendGenericsHtml(paramTypes[i].Value) + .Append("") + .AppendHtmlEncode(GetBetween(paramTypes[i], paramNames[i])) // " " + .Append("") + .AppendHtmlEncode(paramNames[i].Value) + .Append(""); + } + var last = paramNames[paramTypes.Count - 1]; + sb.Append("") + .AppendHtmlEncode(allParams.Value.Substring(last.Index + last.Length - allParams.Index)) + .Append(""); + } + else + { + sb.Append("") + .AppendHtmlEncode(allParams.Value) // "()" + .Append(""); + } + sb.Append(""); // method-section for table layout + + if (sourceInfo.Value.HasValue()) + { + sb.Append(""); + + var curPath = sourceInfo.Value; + if (curPath != sourceInfo.Value) + { + sb.Append("") + .AppendHtmlEncode(GetBetween(allParams, sourceInfo)) + .Append("") + .Append(curPath); + } + else if (path.Value.HasValue()) + { + var subPath = GetSubPath(path.Value, type.Value); + + sb.Append("") + .AppendHtmlEncode(GetBetween(allParams, path)) + .Append("") + .Append("") + .AppendHtmlEncode(subPath) + .Append("") + .AppendHtmlEncode(GetBetween(path, linePrefix)) + .Append("") + .AppendHtmlEncode(linePrefix.Value) + .Append("") + .Append("") + .AppendHtmlEncode(line.Value) + .Append(""); + } + sb.Append(""); + } + + sb.Append(""); + + pos = frame.Index + frame.Length; + } + + // append anything left + sb.Append(""); + var tailLength = stackTrace.Length - pos; + if (tailLength > 0) + { + sb.AppendHtmlEncode(stackTrace.Substring(pos, tailLength)); + } + sb.Append(""); + + return sb.ToStringRecycle(); + } + + private static char[] Backslash { get; } = new[] { '\\' }; + + private static string GetSubPath(string sourcePath, string type) + { + //C:\git\NickCraver\StackExchange.Exceptional\src\StackExchange.Exceptional.Shared\Utils.Test.cs + int pathPos = 0; + foreach (var path in sourcePath.Split(Backslash)) + { + pathPos += (path.Length + 1); + if (type.StartsWith(path)) + { + return sourcePath.Substring(pathPos); + } + } + return sourcePath; + } + + /// + /// .NET Core changes methods so generics render as as Method[T], this normalizes it. + /// + private static string NormalizeMethodName(string method) + { + return method?.Replace("[", "<").Replace("]", ">"); + } + } + + internal static class StackTraceExtensions + { + private static readonly char[] _dot = new char[] { '.' }; + private static readonly Regex _genericTypeRegex = new Regex($@"(?{StackTraceUtils.NoSpace}+)`(?\d+)"); + private static readonly string[] _singleT = new[] { "T" }; + + private static readonly Dictionary _commonGenerics = new Dictionary + { + ["Microsoft.CodeAnalysis.SymbolVisitor`1"] = new[] { "TResult" }, + ["Microsoft.CodeAnalysis.Diagnostics.CodeBlockStartAnalysisContext`1"] = new[] { "TLanguageKindEnum" }, + ["Microsoft.CodeAnalysis.Diagnostics.SourceTextValueProvider`1"] = new[] { "TValue" }, + ["Microsoft.CodeAnalysis.Diagnostics.SyntaxTreeValueProvider`1"] = new[] { "TValue" }, + ["Microsoft.CodeAnalysis.Semantics.OperationVisitor`2"] = new[] { "TArgument", "TResult" }, + ["System.Converter`2"] = new[] { "TInput", "TOutput" }, + ["System.EventHandler`1"] = new[] { "TEventArgs" }, + ["System.Func`1"] = new[] { "TResult" }, + ["System.Func`2"] = new[] { "T", "TResult" }, + ["System.Func`3"] = new[] { "T1", "T2", "TResult" }, + ["System.Func`4"] = new[] { "T1", "T2", "T3", "TResult" }, + ["System.Func`5"] = new[] { "T1", "T2", "T3", "T4", "TResult" }, + ["System.Func`6"] = new[] { "T1", "T2", "T3", "T4", "T5", "TResult" }, + ["System.Func`7"] = new[] { "T1", "T2", "T3", "T4", "T5", "T6", "TResult" }, + ["System.Func`8"] = new[] { "T1", "T2", "T3", "T4", "T5", "T6", "T7", "TResult" }, + ["System.Func`9"] = new[] { "T1", "T2", "T3", "T4", "T5", "T6", "T7", "T8", "TResult" }, + ["System.Func`10"] = new[] { "T1", "T2", "T3", "T4", "T5", "T6", "T7", "T8", "T9", "TResult" }, + ["System.Func`11"] = new[] { "T1", "T2", "T3", "T4", "T5", "T6", "T7", "T8", "T9", "T10", "TResult" }, + ["System.Func`12"] = new[] { "T1", "T2", "T3", "T4", "T5", "T6", "T7", "T8", "T9", "T10", "T11", "TResult" }, + ["System.Func`13"] = new[] { "T1", "T2", "T3", "T4", "T5", "T6", "T7", "T8", "T9", "T10", "T11", "T12", "TResult" }, + ["System.Func`14"] = new[] { "T1", "T2", "T3", "T4", "T5", "T6", "T7", "T8", "T9", "T10", "T11", "T12", "T13", "TResult" }, + ["System.Func`15"] = new[] { "T1", "T2", "T3", "T4", "T5", "T6", "T7", "T8", "T9", "T10", "T11", "T12", "T13", "T14", "TResult" }, + ["System.Func`16"] = new[] { "T1", "T2", "T3", "T4", "T5", "T6", "T7", "T8", "T9", "T10", "T11", "T12", "T13", "T14", "T15", "TResult" }, + ["System.Func`17"] = new[] { "T1", "T2", "T3", "T4", "T5", "T6", "T7", "T8", "T9", "T10", "T11", "T12", "T13", "T14", "T15", "T16", "TWhatTheHellAreYouDoing" }, + ["System.Tuple`8"] = new[] { "T1", "T2", "T3", "T4", "T5", "T6", "T7", "TRest" }, + ["System.Collections.Concurrent.ConcurrentDictionary`2"] = new[] { "TKey", "TValue" }, + ["System.Collections.Concurrent.OrderablePartitioner`1"] = new[] { "TSource" }, + ["System.Collections.Concurrent.Partitioner`1"] = new[] { "TSource" }, + ["System.Collections.Generic.Dictionary`2"] = new[] { "TKey", "TValue" }, + ["System.Collections.Generic.SortedDictionary`2"] = new[] { "TKey", "TValue" }, + ["System.Collections.Generic.SortedList`2"] = new[] { "TKey", "TValue" }, + ["System.Collections.Immutable.ImmutableDictionary`2"] = new[] { "TKey", "TValue" }, + ["System.Collections.Immutable.ImmutableSortedDictionary`2"] = new[] { "TKey", "TValue" }, + ["System.Collections.ObjectModel.KeyedCollection`2"] = new[] { "TKey", "TItem" }, + ["System.Collections.ObjectModel.ReadOnlyDictionary`2"] = new[] { "TKey", "TValue" }, + ["System.Data.Common.CommandTrees.DbExpressionVisitor`1"] = new[] { "TResultType" }, + ["System.Data.Linq.EntitySet`1"] = new[] { "TEntity" }, + ["System.Data.Linq.Table`1"] = new[] { "TEntity" }, + ["System.Data.Linq.Mapping.MetaAccessor`2"] = new[] { "TEntity", "TMember" }, + ["System.Data.Linq.SqlClient.Implementation.ObjectMaterializer`1"] = new[] { "TDataReader" }, + ["System.Data.Objects.ObjectSet`1"] = new[] { "TEntity" }, + ["System.Data.Objects.DataClasses.EntityCollection`1"] = new[] { "TEntity" }, + ["System.Data.Objects.DataClasses.EntityReference`1"] = new[] { "TEntity" }, + ["System.Linq.Lookup`2"] = new[] { "TKey", "TElement" }, + ["System.Linq.OrderedParallelQuery`1"] = new[] { "TSource" }, + ["System.Linq.ParallelQuery`1"] = new[] { "TSource" }, + ["System.Linq.Expressions.Expression`1"] = new[] { "TDelegate" }, + ["System.Runtime.CompilerServices.ConditionalWeakTable`2"] = new[] { "TKey", "TValue" }, + ["System.Threading.Tasks.Task`1"] = new[] { "TResult" }, + ["System.Threading.Tasks.TaskCompletionSource`1"] = new[] { "TResult" }, + ["System.Threading.Tasks.TaskFactory`1"] = new[] { "TResult" }, + ["System.Web.ModelBinding.ArrayModelBinder`1"] = new[] { "TElement" }, + ["System.Web.ModelBinding.CollectionModelBinder`1"] = new[] { "TElement" }, + ["System.Web.ModelBinding.DataAnnotationsModelValidator`1"] = new[] { "TAttribute" }, + ["System.Web.ModelBinding.DictionaryModelBinder`2"] = new[] { "TKey", "TValue" }, + ["System.Web.ModelBinding.DictionaryValueProvider`1"] = new[] { "TValue" }, + ["System.Web.ModelBinding.KeyValuePairModelBinder`2"] = new[] { "TKey", "TValue" }, + ["System.Windows.WeakEventManager`2"] = new[] { "TEventSource", "TEventArgs" }, + ["System.Windows.Documents.TextElementCollection`1"] = new[] { "TextElementType" }, + ["System.Windows.Threading.DispatcherOperation`1"] = new[] { "TResult" }, + ["System.Xaml.Schema.XamlValueConverter`1"] = new[] { "TConverterBase" }, + }; + + internal static StringBuilder AppendHtmlEncode(this StringBuilder sb, string s) => sb.Append(WebUtility.HtmlEncode(s)); + + internal static StringBuilder AppendGenericsHtml(this StringBuilder sb, string typeOrMethod) + { + const string _dotSpan = "."; + // Check the common framework list above + _commonGenerics.TryGetValue(typeOrMethod, out string[] args); + + // Break each type down by namespace and class (remember, we *could* have nested generic classes) + var classes = typeOrMethod.Split(_dot); + // Loop through each dot component of the type, e.g. "System", "Collections", "Generics" + for (var i = 0; i < classes.Length; i++) + { + if (i > 0) + { + sb.Append(_dotSpan); + } + var match = _genericTypeRegex.Match(classes[i]); + if (match.Success) + { + // If arguments aren't known, get the defaults + if (args == null && int.TryParse(match.Groups["ArgCount"].Value, out int count)) + { + if (count == 1) + { + args = _singleT; + } + else + { + args = new string[count]; + for (var j = 0; j < count; j++) + { + args[j] = "T" + (j + 1).ToString(); // , or + } + } + } + // In the known case, BaseClass is "System.Collections.Generic.Dictionary" + // In the unknown case, we're hitting here at "Class" only + sb.AppendHtmlEncode(match.Groups["BaseClass"].Value); + AppendArgs(args); + } + else + { + sb.AppendHtmlEncode(classes[i]); + } + } + return sb; + + void AppendArgs(string[] tArgs) + { + sb.Append("<"); + // Don't put crazy amounts of arguments in here + if (tArgs.Length > 5) + { + sb.Append("").Append(tArgs[0]).Append("") + .Append(",") + .Append("").Append(tArgs[1]).Append("") + .Append(",") + .Append("").Append(tArgs[2]).Append("") + .Append("…") + .Append("").Append(tArgs[tArgs.Length - 1]).Append(""); + } + else + { + for (int i = 0; i < tArgs.Length; i++) + { + if (i > 0) + { + sb.Append(","); + } + sb.Append(""); + sb.Append(tArgs[i]) + .Append(""); + } + } + sb.Append(">"); + } + } + } } diff --git a/src/MiniProfiler.Shared/Internal/MiniProfilerBaseOptions.cs b/src/MiniProfiler.Shared/Internal/MiniProfilerBaseOptions.cs index b3566c8fb..9bc2200bf 100644 --- a/src/MiniProfiler.Shared/Internal/MiniProfilerBaseOptions.cs +++ b/src/MiniProfiler.Shared/Internal/MiniProfilerBaseOptions.cs @@ -23,6 +23,11 @@ public class MiniProfilerBaseOptions /// public virtual string VersionHash { get; set; } = typeof(MiniProfilerBaseOptions).GetTypeInfo().Assembly.GetCustomAttribute()?.InformationalVersion ?? Version.ToString(); + /// + /// Whether to enable verbose diagnostics mode in MiniProfiler. + /// + public bool EnableDebugMode { get; set; } + /// /// Assemblies to exclude from the stack trace report. /// diff --git a/src/MiniProfiler.Shared/MiniProfiler.cs b/src/MiniProfiler.Shared/MiniProfiler.cs index 4f5495a82..e3f9997d6 100644 --- a/src/MiniProfiler.Shared/MiniProfiler.cs +++ b/src/MiniProfiler.Shared/MiniProfiler.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Threading; using System.Threading.Tasks; @@ -333,6 +334,7 @@ public MiniProfiler Clone() } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal Timing StepImpl(string name, decimal? minSaveMs = null, bool? includeChildrenWithMinSave = false) { return new Timing(this, Head, name, minSaveMs, includeChildrenWithMinSave); diff --git a/src/MiniProfiler.Shared/MiniProfilerExtensions.cs b/src/MiniProfiler.Shared/MiniProfilerExtensions.cs index cd468f9ab..3cd25d17b 100644 --- a/src/MiniProfiler.Shared/MiniProfilerExtensions.cs +++ b/src/MiniProfiler.Shared/MiniProfilerExtensions.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using System.Runtime.CompilerServices; namespace StackExchange.Profiling { @@ -36,6 +37,7 @@ public static T Inline(this MiniProfiler profiler, Func selector, string n /// The current profiling session or null. /// A descriptive name for the code that is encapsulated by the resulting Timing's lifetime. /// the profile step + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Timing Step(this MiniProfiler profiler, string name) => profiler?.StepImpl(name); /// @@ -50,6 +52,7 @@ public static T Inline(this MiniProfiler profiler, Func selector, string n /// /// If is set to true and a child is removed due to its use of StepIf, then the /// time spent in that time will also not count for the current StepIf calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Timing StepIf(this MiniProfiler profiler, string name, decimal minSaveMs, bool includeChildren = false) { return profiler?.StepImpl(name, minSaveMs, includeChildren); @@ -67,6 +70,7 @@ public static Timing StepIf(this MiniProfiler profiler, string name, decimal min /// /// Should be used like the extension method /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static CustomTiming CustomTiming(this MiniProfiler profiler, string category, string commandString, string executeType = null, bool includeStackTrace = true) { return CustomTimingIf(profiler, category, commandString, 0, executeType: executeType, includeStackTrace: includeStackTrace); diff --git a/src/MiniProfiler.Shared/Timing.cs b/src/MiniProfiler.Shared/Timing.cs index 793018242..eeaae9192 100644 --- a/src/MiniProfiler.Shared/Timing.cs +++ b/src/MiniProfiler.Shared/Timing.cs @@ -35,13 +35,32 @@ public class Timing : IDisposable /// The name of this timing. /// (Optional) The minimum threshold (in milliseconds) for saving this timing. /// (Optional) Whether the children are included when comparing to the threshold. - public Timing(MiniProfiler profiler, Timing parent, string name, decimal? minSaveMs = null, bool? includeChildrenWithMinSave = false) + public Timing(MiniProfiler profiler, Timing parent, string name, decimal? minSaveMs = null, bool? includeChildrenWithMinSave = false) : + this(profiler, parent, name, minSaveMs, includeChildrenWithMinSave, 0) + { } + + /// + /// Creates a new Timing named 'name' in the 'profiler's session, with 'parent' as this Timing's immediate ancestor. + /// + /// The this belongs to. + /// The this is a child of. + /// The name of this timing. + /// (Optional) The minimum threshold (in milliseconds) for saving this timing. + /// (Optional) Whether the children are included when comparing to the threshold. + /// The number of frames to shave off the debug stack. + public Timing(MiniProfiler profiler, Timing parent, string name, decimal? minSaveMs, bool? includeChildrenWithMinSave, int debugStackShave) { Id = Guid.NewGuid(); Profiler = profiler; Profiler.Head = this; // root will have no parent + // Also, due to stack unwinding for minimal frame depth in MVC and such, we may need to traverse up when the + // AsyncLocal head is not reset properly in the context we expect (it was reset lower down) + while (parent?.DurationMilliseconds.HasValue == true) + { + parent = parent.ParentTiming; + } parent?.AddChild(this); Name = name; @@ -50,6 +69,11 @@ public Timing(MiniProfiler profiler, Timing parent, string name, decimal? minSav _minSaveMs = minSaveMs; _includeChildrenWithMinSave = includeChildrenWithMinSave == true; StartMilliseconds = profiler.GetRoundedMilliseconds(_startTicks); + + if (profiler.Options.EnableDebugMode) + { + DebugInfo = new TimingDebugInfo(this, debugStackShave); + } } /// @@ -107,6 +131,12 @@ public List Children [DataMember(Order = 6)] public Dictionary> CustomTimings { get; set; } + /// + /// Present only when EnableDebugMode is true, additional step info in-memory only. + /// + [DataMember(Order = 7)] + public TimingDebugInfo DebugInfo { get; set; } + /// /// JSON representing the Custom Timings associated with this timing. /// diff --git a/src/MiniProfiler.Shared/TimingDebugInfo.cs b/src/MiniProfiler.Shared/TimingDebugInfo.cs new file mode 100644 index 000000000..68dde9fd5 --- /dev/null +++ b/src/MiniProfiler.Shared/TimingDebugInfo.cs @@ -0,0 +1,59 @@ +using System.Diagnostics; +using System.Runtime.Serialization; +using StackExchange.Profiling.Helpers; + +namespace StackExchange.Profiling +{ + /// + /// Debug info for a timing, only present when EnableDebugMode is set in options + /// + [DataContract] + public class TimingDebugInfo + { + /// + /// An (already-encoded) HTML representation of the call stack. + /// + /// + /// Repetitive, but pays the prettification cost on fetch. + /// We'll want to do diff with the parent timing here in highlight or something. + /// + [DataMember(Order = 1)] + public string RichHtmlStack => StackTraceUtils.HtmlPrettify(RawStack.ToString(), CommonStackStart); + + /// + /// The index of the stack frame that common frames with parent start at (e.g. happened in the parent timing, before this). + /// + [DataMember(Order = 2)] + public int? CommonStackStart { get; } + + private Timing ParentTiming { get; } + private StackTrace RawStack { get; } + + internal TimingDebugInfo(Timing parent, int debugStackShave = 0) + { + ParentTiming = parent; + RawStack = new StackTrace(4 + debugStackShave, true); + + if (parent.ParentTiming?.DebugInfo?.RawStack is StackTrace parentStack) + { + // Seek a common end in frames + int myIndex, parentIndex; + for (myIndex = RawStack.FrameCount - 1, parentIndex = parentStack.FrameCount - 1; + myIndex >= 0 && parentIndex >= 0; + myIndex--, parentIndex--) + { + StackFrame myFrame = RawStack.GetFrame(myIndex), + parentFrame = parentStack.GetFrame(parentIndex); + if (myFrame.GetILOffset() == parentFrame.GetILOffset() && myFrame.GetMethod() == parentFrame.GetMethod()) + { + CommonStackStart = myIndex; + } + else + { + break; + } + } + } + } + } +} diff --git a/src/MiniProfiler.Shared/ui/includes.css b/src/MiniProfiler.Shared/ui/includes.css index baa36e955..3a87fe480 100644 --- a/src/MiniProfiler.Shared/ui/includes.css +++ b/src/MiniProfiler.Shared/ui/includes.css @@ -218,6 +218,87 @@ .mp-result table.mp-client-timings .t-paint div { background: var(--mp-timing-paint-color); } +.mp-result .mp-debug { + position: relative; + cursor: pointer; +} +.mp-result .mp-debug .mp-debug-tooltip { + display: none; + position: fixed; + background-color: var(--mp-main-bg-color); + box-shadow: var(--mp-popup-shadow); + right: 100px; + max-width: 90%; + border-radius: 9px; + padding: 12px; + margin-top: 18px; + opacity: 0.95; + backdrop-filter: blur(4px); +} +.mp-result .mp-debug .mp-nested-timing { + margin-left: 16px; +} +.mp-result .mp-debug td:first-child:hover .mp-debug-tooltip { + display: block; +} +.mp-result .mp-debug td:first-child:hover > span { + text-shadow: var(--mp-button-active-text-color) 0 0 4px; +} +.mp-result .mp-stack-trace { + color: var(--mp-muted-color); + margin-bottom: 5px; + font-family: Consolas, Monaco, monospace; + overflow-x: hidden; + overflow-y: auto; + white-space: pre-line; + max-height: 200px; +} +.mp-result .mp-stack-trace .stack-row { + white-space: nowrap; +} +.mp-result .mp-stack-trace .stack-row.common { + filter: grayscale(80%); +} +.mp-result .mp-stack-trace .stack.async { + opacity: 0.6; + display: none; +} +.mp-result .mp-stack-trace .stack.leadin, +.mp-result .mp-stack-trace .stack.file, +.mp-result .mp-stack-trace .stack.line-prefix, +.mp-result .mp-stack-trace .stack.path, +.mp-result .mp-stack-trace .stack.dot { + color: var(--mp-muted-color); +} +.mp-result .mp-stack-trace .stack.async-tag { + color: var(--mp-highlight-attribute-color); + font-weight: bold; +} +.mp-result .mp-stack-trace .stack.async-tag:before { + content: "("; +} +.mp-result .mp-stack-trace .stack.async-tag:after { + content: ")"; +} +.mp-result .mp-stack-trace .stack.type { + color: var(--mp-highlight-keyword-color); +} +.mp-result .mp-stack-trace .stack.generic-type { + color: var(--mp-highlight-fade-color); +} +.mp-result .mp-stack-trace .stack.misc, +.mp-result .mp-stack-trace .stack.parens { + color: var(--mp-highlight-comment-color); +} +.mp-result .mp-stack-trace .stack.method { + color: var(--mp-highlight-string-color); +} +.mp-result .mp-stack-trace .stack.paramType { + color: var(--mp-highlight-attribute-color); +} +.mp-result .mp-stack-trace .stack.paramName { + color: var(--mp-highlight-variable-color); +} .mp-result .mp-label { color: var(--mp-label-color); overflow: hidden; @@ -232,8 +313,8 @@ .mp-result .mp-trivial { display: none; } -.mp-result .mp-trivial td, -.mp-result .mp-trivial td * { +.mp-result .mp-trivial td:not(:first-child), +.mp-result .mp-trivial td:not(:first-child) * { color: var(--mp-muted-color) !important; } .mp-result .mp-number { diff --git a/src/MiniProfiler.Shared/ui/includes.less b/src/MiniProfiler.Shared/ui/includes.less index 0c62e9bf1..243aa1f2e 100644 --- a/src/MiniProfiler.Shared/ui/includes.less +++ b/src/MiniProfiler.Shared/ui/includes.less @@ -206,6 +206,105 @@ } } + .mp-debug { + position: relative; + cursor: pointer; + + .mp-debug-tooltip { + display: none; + position: fixed; + background-color: var(--mp-main-bg-color); + box-shadow: var(--mp-popup-shadow); + right: 100px; + max-width: 90%; + border-radius: 9px; + padding: 12px; + margin-top: 18px; + opacity: 0.95; + backdrop-filter: blur(4px); + } + + .mp-nested-timing { + margin-left: 16px; + } + + td:first-child:hover { + .mp-debug-tooltip { + display: block; + } + + & > span { + text-shadow: var(--mp-button-active-text-color) 0 0 4px; + } + } + } + + .mp-stack-trace { + color: var(--mp-muted-color); + margin-bottom: 5px; + font-family: Consolas, Monaco, monospace; + overflow-x: hidden; + overflow-y: auto; + white-space: pre-line; + max-height: 200px; + + .stack-row { + white-space: nowrap; + + &.common { + filter: grayscale(80%); + } + } + + .stack { + &.async { + opacity: 0.6; + display: none; + } + + &.leadin, &.file, &.line-prefix, &.path, &.dot { + color: var(--mp-muted-color); + } + + &.async-tag { + color: var(--mp-highlight-attribute-color); + font-weight: bold; + + &:before { + content: "("; + } + + &:after { + content: ")"; + } + } + + &.type { + color: var(--mp-highlight-keyword-color); + } + + &.generic-type { + color: var(--mp-highlight-fade-color); + } + + &.misc, &.parens { + color: var(--mp-highlight-comment-color); + } + + &.method { + color: var(--mp-highlight-string-color); + } + + &.paramType { + color: var(--mp-highlight-attribute-color); + } + + &.paramName { + color: var(--mp-highlight-variable-color); + } + } + } + .mp-label { color: var(--mp-label-color); overflow: hidden; @@ -223,7 +322,7 @@ .mp-trivial { display: none; - td, td * { + td:not(:first-child), td:not(:first-child) * { color: var(--mp-muted-color) !important; } } diff --git a/src/MiniProfiler.Shared/ui/includes.min.css b/src/MiniProfiler.Shared/ui/includes.min.css index ad74a0d91..5f9e1663c 100644 --- a/src/MiniProfiler.Shared/ui/includes.min.css +++ b/src/MiniProfiler.Shared/ui/includes.min.css @@ -1 +1 @@ -:root{--mp-main-bg-color:#fff;--mp-button-active-bg-color:#207ab7;--mp-button-active-text-color:#fff;--mp-button-warning-bg-color:#c91d2e;--mp-button-warning-text-color:#fff;--mp-duration-color:#111;--mp-warning-color:#c91d2e;--mp-critical-color:#f00;--mp-alt-row-color:#f5f5f5;--mp-muted-color:#aaa;--mp-link-color:#07c;--mp-label-color:#555;--mp-gap-font-color:#444;--mp-gap-bg-color:#e1e5ed;--mp-overlay-bg-color:#000;--mp-info-border-color:#ddd;--mp-query-border-color:#efefef;--mp-timing-unknown-color:#80ccda;--mp-timing-dns-color:#8baad1;--mp-timing-connect-color:#8cbb4e;--mp-timing-ssl-color:#795892;--mp-timing-request-color:#0099b2;--mp-timing-response-color:#d99d35;--mp-timing-dom-color:#bd4d32;--mp-timing-domcontent-color:#dd4678;--mp-timing-load-color:#1d72aa;--mp-timing-paint-color:#bc41e3;--mp-highlight-fade-color:#ffb;--mp-highlight-default-color:#000;--mp-highlight-string-color:#756bb1;--mp-highlight-comment-color:#636363;--mp-highlight-literal-color:#31a354;--mp-highlight-variable-color:#88f;--mp-highlight-keyword-color:#3182bd;--mp-highlight-attribute-color:#e6550d;--mp-result-border:solid .5px #ababab;--mp-result-border-radius:10px;--mp-popup-shadow:0 1px 5px #55555555;}@media(prefers-color-scheme:dark){.mp-scheme-auto{--mp-main-bg-color:#222;--mp-duration-color:#eee;--mp-alt-row-color:#333;--mp-muted-color:#888;--mp-label-color:#ccc;--mp-gap-font-color:#d6d6d6;--mp-gap-bg-color:#424448;--mp-link-color:#4db5ff;--mp-query-border-color:#575757;--mp-highlight-fade-color:#884;--mp-highlight-default-color:#eee;--mp-highlight-literal-color:#3fca6a;--mp-highlight-keyword-color:#36a1ef;--mp-result-border:solid .5px #575757;}.mp-scheme-auto body{background-color:var(--mp-main-bg-color);}}.mp-scheme-dark{--mp-main-bg-color:#222;--mp-duration-color:#eee;--mp-alt-row-color:#333;--mp-muted-color:#888;--mp-label-color:#ccc;--mp-gap-font-color:#d6d6d6;--mp-gap-bg-color:#424448;--mp-link-color:#4db5ff;--mp-query-border-color:#575757;--mp-highlight-fade-color:#884;--mp-highlight-default-color:#eee;--mp-highlight-literal-color:#3fca6a;--mp-highlight-keyword-color:#36a1ef;--mp-result-border:solid .5px #575757;}.mp-scheme-dark body{background-color:var(--mp-main-bg-color);}.mp-result,.mp-queries{color:var(--mp-label-color);line-height:1;font-size:12px;}.mp-result pre,.mp-queries pre,.mp-result code,.mp-queries code,.mp-result label,.mp-queries label,.mp-result table,.mp-queries table,.mp-result tbody,.mp-queries tbody,.mp-result thead,.mp-queries thead,.mp-result tfoot,.mp-queries tfoot,.mp-result tr,.mp-queries tr,.mp-result th,.mp-queries th,.mp-result td,.mp-queries td{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline;background-color:transparent;overflow:visible;max-height:none;}.mp-result pre,.mp-queries pre,.mp-result code,.mp-queries code{font-family:Fira Code,Consolas,monospace,serif;font-variant-ligatures:none;}.mp-result table,.mp-queries table{color:var(--mp-label-color);border-collapse:collapse;border-spacing:0;width:100%;}.mp-result a,.mp-queries a{cursor:pointer;color:var(--mp-link-color);text-decoration:none;}.mp-result a:hover,.mp-queries a:hover{text-decoration:underline;}.mp-result{font-family:sans-serif;}.mp-result.show-columns th.mp-more-columns,.mp-result.show-columns td.mp-more-columns{display:table-cell !important;}.mp-result.show-columns a.mp-more-columns{display:inline-block;}.mp-result.show-columns .mp-links span a:nth-child(1){display:none;}.mp-result.show-trivial tr.mp-trivial{display:table-row !important;}.mp-result.show-trivial a.mp-trivial{display:inline-block;}.mp-result.show-trivial .mp-links span a:nth-child(3){display:none;}.mp-result table.mp-client-timings{margin-top:10px;}.mp-result table.mp-client-timings td:nth-child(2){width:100%;padding:0;}.mp-result table.mp-client-timings td div{height:13px;min-width:1px;}.mp-result table.mp-client-timings .t-point div{height:4px;border-radius:8px;margin-bottom:4px;box-shadow:0 0 2px;}.mp-result table.mp-client-timings .t-unknown div{background:var(--mp-timing-unknown-color);}.mp-result table.mp-client-timings .t-dns div{background:var(--mp-timing-dns-color);}.mp-result table.mp-client-timings .t-connect div{background:var(--mp-timing-connect-color);}.mp-result table.mp-client-timings .t-ssl div{background:var(--mp-timing-ssl-color);}.mp-result table.mp-client-timings .t-request div{background:var(--mp-timing-request-color);}.mp-result table.mp-client-timings .t-response div{background:var(--mp-timing-response-color);}.mp-result table.mp-client-timings .t-dom div{background:var(--mp-timing-dom-color);}.mp-result table.mp-client-timings .t-domcontent div{background:var(--mp-timing-domcontent-color);}.mp-result table.mp-client-timings .t-load div{background:var(--mp-timing-load-color);}.mp-result table.mp-client-timings .t-paint div{background:var(--mp-timing-paint-color);}.mp-result .mp-label{color:var(--mp-label-color);overflow:hidden;text-overflow:ellipsis;}.mp-result .mp-unit{color:var(--mp-muted-color);}.mp-result .mp-more-columns{display:none;}.mp-result .mp-trivial{display:none;}.mp-result .mp-trivial td,.mp-result .mp-trivial td *{color:var(--mp-muted-color) !important;}.mp-result .mp-number{color:var(--mp-duration-color);}.mp-result .mp-info>div{white-space:nowrap;overflow:hidden;}.mp-result .mp-info>div>div{display:inline-block;}.mp-result .mp-info .mp-name{display:inline-block;font-weight:bold;float:left;}.mp-result .mp-info .mp-machine-name,.mp-result .mp-info .mp-started{text-align:right;float:right;}.mp-result .mp-info .mp-server-time{white-space:nowrap;}.mp-result .mp-timings th{background-color:var(--mp-main-bg-color);color:var(--mp-muted-color);text-align:right;}.mp-result .mp-timings th,.mp-result .mp-timings td{white-space:nowrap;}.mp-result .mp-timings .mp-show-more{display:none;}.mp-result .mp-timings .mp-duration{font-family:Consolas,monospace,serif;color:var(--mp-duration-color);text-align:right;}.mp-result .mp-timings .mp-indent{letter-spacing:4px;}.mp-result .mp-timings .mp-queries-show .mp-number,.mp-result .mp-timings .mp-queries-show .mp-unit{color:var(--mp-link-color);}.mp-result .mp-timings .mp-queries-show.mp-queries-warning{color:var(--mp-warning-color);font-weight:bold;}.mp-result .mp-timings .mp-queries-duration{padding-left:6px;}.mp-result .mp-timings:not(.mp-client-timings) .mp-trivial:first-child{display:table-row;}.mp-result .mp-custom-timing-overview{float:right;margin:10px 0;width:auto;}.mp-result .mp-custom-timing-overview td{white-space:nowrap;text-align:right;}.mp-result .mp-custom-timing-overview td:last-child{padding-left:8px;}.mp-result .mp-links{margin-top:10px;clear:both;}.mp-result .mp-links span{float:right;}.mp-result .mp-links a{font-size:95%;margin-left:12px;}.mp-result .mp-links a:first-child{margin-left:0;}.mp-warning{color:var(--mp-critical-color);}@keyframes mp-fadeIn{from{background-color:var(--mp-highlight-fade-color);}to{background-color:none;}}.mp-overlay{z-index:2147483642;}.mp-overlay .mp-overlay-bg{z-index:2147483643;background:var(--mp-overlay-bg-color);opacity:.5;top:0;left:0;min-width:100%;position:fixed;height:calc(100%);}.mp-overlay .mp-queries{display:block;z-index:2147483644;top:25px;left:25px;right:25px;position:fixed;max-height:calc(100vh - 50px);background-color:var(--mp-main-bg-color);border-radius:2px;overflow-y:auto;overflow-x:auto;}.mp-queries{font-family:sans-serif;}.mp-queries pre{white-space:pre-wrap;}.mp-queries div{text-align:left;}.mp-queries table{table-layout:fixed;}.mp-queries td,.mp-queries th{padding:8px;border-bottom:solid 1px var(--mp-query-border-color);}.mp-queries td:first-child,.mp-queries th:first-child{width:250px;text-align:right;}.mp-queries td:first-child div,.mp-queries th:first-child div{text-align:right;margin-bottom:5px;word-break:break-word;}.mp-queries td:last-child,.mp-queries th:last-child{text-align:left;padding-right:15px;}.mp-queries .highlight{animation:mp-fadeIn 2s 1;}.mp-queries .mp-odd{background-color:var(--mp-alt-row-color);}.mp-queries .mp-stack-trace{padding-bottom:1px;color:var(--mp-muted-color);margin-bottom:5px;}.mp-queries .mp-since-start,.mp-queries .mp-duration{text-align:right;}.mp-queries.show-trivial .mp-gap-info.mp-trivial-gap{display:table-row;}.mp-queries .mp-gap-info{background-color:var(--mp-gap-bg-color);opacity:.8;color:var(--mp-gap-font-color);}.mp-queries .mp-gap-info .query{word-break:break-all;}.mp-queries .mp-gap-info .mp-unit{color:var(--mp-muted-color);}.mp-queries .mp-gap-info.mp-trivial-gap{display:none;}.mp-queries .mp-trivial-gap-container{text-align:center;margin:8px 0;}.mp-queries .mp-call-type{font-weight:bold;}.mp-queries .hljs{display:block;overflow-x:auto;padding:.5em;}.mp-queries .hljs,.mp-queries .hljs-subst{color:var(--mp-highlight-default-color);}.mp-queries .hljs-string,.mp-queries .hljs-meta,.mp-queries .hljs-symbol,.mp-queries .hljs-template-tag,.mp-queries .hljs-template-variable,.mp-queries .hljs-addition{color:var(--mp-highlight-string-color);}.mp-queries .hljs-comment,.mp-queries .hljs-quote{color:var(--mp-highlight-comment-color);}.mp-queries .hljs-number,.mp-queries .hljs-regexp,.mp-queries .hljs-literal,.mp-queries .hljs-bullet,.mp-queries .hljs-link{color:var(--mp-highlight-literal-color);}.mp-queries .hljs-deletion,.mp-queries .hljs-variable{color:var(--mp-highlight-variable-color);}.mp-queries .hljs-keyword,.mp-queries .hljs-selector-tag,.mp-queries .hljs-title,.mp-queries .hljs-section,.mp-queries .hljs-built_in,.mp-queries .hljs-doctag,.mp-queries .hljs-type,.mp-queries .hljs-tag,.mp-queries .hljs-name,.mp-queries .hljs-selector-id,.mp-queries .hljs-selector-class,.mp-queries .hljs-strong{color:var(--mp-highlight-keyword-color);}.mp-queries .hljs-emphasis{font-style:italic;}.mp-queries .hljs-attribute{color:var(--mp-highlight-attribute-color);}.mp-results{z-index:2147483641;position:fixed;top:0;}.mp-results.mp-left,.mp-results.mp-bottomleft{left:0;}.mp-results.mp-left .mp-controls,.mp-results.mp-left.mp-no-controls .mp-result:last-child .mp-button{border-bottom-right-radius:var(--mp-result-border-radius);}.mp-results.mp-left .mp-button,.mp-results.mp-left .mp-controls{border-right:var(--mp-result-border);}.mp-results.mp-right{right:0;}.mp-results.mp-right .mp-controls,.mp-results.mp-right.mp-no-controls .mp-result:last-child .mp-button{border-bottom-left-radius:var(--mp-result-border-radius);}.mp-results.mp-right .mp-button,.mp-results.mp-right .mp-controls{border-left:var(--mp-result-border);}.mp-results.mp-bottomleft{top:inherit;bottom:0;}.mp-results.mp-bottomleft .mp-result:first-child .mp-button{border-top-right-radius:var(--mp-result-border-radius);}.mp-results.mp-bottomleft .mp-button,.mp-results.mp-bottomleft .mp-controls{border-right:var(--mp-result-border);}.mp-results.mp-bottomleft .mp-result .mp-button,.mp-results.mp-bottomleft .mp-controls{border-bottom:0;border-top:var(--mp-result-border);}.mp-results.mp-bottomright{top:inherit;bottom:0;right:0;}.mp-results.mp-bottomright .mp-result:first-child .mp-button{border-top-left-radius:var(--mp-result-border-radius);}.mp-results.mp-bottomright .mp-button,.mp-results.mp-bottomright .mp-controls{border-left:var(--mp-result-border);}.mp-results.mp-bottomright .mp-result .mp-button,.mp-results.mp-bottomright .mp-controls{border-bottom:0;border-top:var(--mp-result-border);}.mp-results .mp-button{user-select:none;}.mp-results .mp-button.mp-button-warning{font-weight:bold;background-color:var(--mp-button-warning-bg-color);color:var(--mp-button-warning-text-color);}.mp-results .mp-button.mp-button-warning .mp-number,.mp-results .mp-button.mp-button-warning .mp-unit,.mp-results .mp-button.mp-button-warning .mp-warning{color:var(--mp-button-warning-text-color);}.mp-results .mp-button .mp-warning{font-weight:bold;}.mp-results .mp-button>.mp-number{font-family:Consolas,monospace,serif;}.mp-results .mp-controls{display:none;}.mp-results .mp-button,.mp-results .mp-controls{z-index:2147483640;border-bottom:var(--mp-result-border);background-color:var(--mp-main-bg-color);padding:4px 8px;text-align:right;cursor:pointer;}.mp-results .mp-result{position:relative;}.mp-results .mp-result.active .mp-button{background-color:var(--mp-button-active-bg-color);animation:none;border-radius:0 !important;}.mp-results .mp-result.active .mp-button.mp-button-warning{background-color:var(--mp-button-warning-bg-color);}.mp-results .mp-result.active .mp-button .mp-number,.mp-results .mp-result.active .mp-button .mp-warning{color:var(--mp-button-active-text-color);font-weight:bold;}.mp-results .mp-result.active .mp-button .mp-unit{color:var(--mp-button-active-text-color);font-weight:normal;}.mp-results .mp-result.active .mp-popup{display:block;}.mp-results.new .mp-button{animation:mp-fadeIn 2s 1;}.mp-results .mp-controls{display:block;font-size:12px;font-family:Consolas,monospace,serif;cursor:default;text-align:center;}.mp-results .mp-controls span{color:var(--mp-muted-color);border-right:1px solid var(--mp-muted-color);padding-right:5px;margin-right:5px;cursor:pointer;}.mp-results .mp-controls span:last-child{border-right:none;}.mp-results .mp-popup{display:none;z-index:2147483641;position:absolute;background-color:var(--mp-main-bg-color);padding:5px 10px;text-align:left;line-height:18px;overflow:auto;box-shadow:var(--mp-popup-shadow);border-radius:2px;}.mp-results .mp-popup .mp-info{margin-bottom:3px;padding-bottom:2px;border-bottom:1px solid var(--mp-info-border-color);}.mp-results .mp-popup .mp-info .mp-name{font-size:1.1em;}.mp-results .mp-popup .mp-info .mp-overall-duration{color:var(--mp-muted-color);}.mp-results .mp-popup .mp-timings th,.mp-results .mp-popup .mp-timings td{padding:0 6px;}.mp-results .mp-popup .mp-timings th{font-size:95%;padding-bottom:3px;}.mp-results .mp-popup .mp-timings .mp-label{max-width:350px;text-overflow:ellipsis;white-space:nowrap;overflow:hidden;}.mp-results .mp-queries{display:none;}.mp-results.mp-min .mp-result{display:none;}.mp-results.mp-min .mp-controls span{display:none;}.mp-results.mp-min .mp-controls .mp-min-max{border-right:none;padding:0;margin:0;}.mp-results.mp-min:hover .mp-controls .mp-min-max{display:inline;}.mp-result-full .mp-result{width:950px;margin:25px auto;}.mp-result-full .mp-result .mp-button{display:none;}.mp-result-full .mp-result .mp-popup .mp-info{font-size:18px;border-bottom:1px solid var(--mp-muted-color);padding-bottom:3px;margin-bottom:10px;}.mp-result-full .mp-result .mp-popup .mp-info .mp-name{margin-bottom:5px;}.mp-result-full .mp-result .mp-popup .mp-info .mp-overall-duration,.mp-result-full .mp-result .mp-popup .mp-info .mp-started{font-size:80%;color:var(--mp-muted-color);}.mp-result-full .mp-result .mp-popup .mp-info .mp-overall-duration{padding-right:20px;}.mp-result-full .mp-result .mp-popup .mp-timings td,.mp-result-full .mp-result .mp-popup .mp-timings th{font-size:13px;padding-left:8px;padding-right:8px;}.mp-result-full .mp-result .mp-popup .mp-timings th{padding-bottom:7px;}.mp-result-full .mp-result .mp-popup .mp-timings td{padding-bottom:4px;}.mp-result-full .mp-result .mp-popup .mp-timings td:first-child{padding-left:10px;}.mp-result-full .mp-result .mp-popup .mp-timings .mp-label{max-width:550px;}.mp-result-full .mp-result .mp-queries{margin:20px 0;}.mp-result-full .mp-result .mp-queries th{font-size:16px;}.mp-result-full .mp-share-mp-results{display:none;}table.mp-results-index{border:0;border-spacing:0;font-size:12px;font-family:Arial;}table.mp-results-index a{color:var(--mp-link-color);text-decoration:none;}table.mp-results-index tbody{color:var(--mp-label-color);}table.mp-results-index tbody tr:nth-child(odd){background-color:var(--mp-alt-row-color);}table.mp-results-index tbody tr:nth-child(even){background-color:var(--mp-main-bg-color);}table.mp-results-index tbody td{text-align:center;}table.mp-results-index tbody td:first-child{text-align:left;}table.mp-results-index tbody td[colspan="3"]{color:var(--mp-muted-color);}table.mp-results-index thead tr{color:var(--mp-label-color);}table.mp-results-index thead tr th{padding:5px 15px;}table.mp-results-index td{padding:8px;} \ No newline at end of file +:root{--mp-main-bg-color:#fff;--mp-button-active-bg-color:#207ab7;--mp-button-active-text-color:#fff;--mp-button-warning-bg-color:#c91d2e;--mp-button-warning-text-color:#fff;--mp-duration-color:#111;--mp-warning-color:#c91d2e;--mp-critical-color:#f00;--mp-alt-row-color:#f5f5f5;--mp-muted-color:#aaa;--mp-link-color:#07c;--mp-label-color:#555;--mp-gap-font-color:#444;--mp-gap-bg-color:#e1e5ed;--mp-overlay-bg-color:#000;--mp-info-border-color:#ddd;--mp-query-border-color:#efefef;--mp-timing-unknown-color:#80ccda;--mp-timing-dns-color:#8baad1;--mp-timing-connect-color:#8cbb4e;--mp-timing-ssl-color:#795892;--mp-timing-request-color:#0099b2;--mp-timing-response-color:#d99d35;--mp-timing-dom-color:#bd4d32;--mp-timing-domcontent-color:#dd4678;--mp-timing-load-color:#1d72aa;--mp-timing-paint-color:#bc41e3;--mp-highlight-fade-color:#ffb;--mp-highlight-default-color:#000;--mp-highlight-string-color:#756bb1;--mp-highlight-comment-color:#636363;--mp-highlight-literal-color:#31a354;--mp-highlight-variable-color:#88f;--mp-highlight-keyword-color:#3182bd;--mp-highlight-attribute-color:#e6550d;--mp-result-border:solid .5px #ababab;--mp-result-border-radius:10px;--mp-popup-shadow:0 1px 5px #55555555;}@media(prefers-color-scheme:dark){.mp-scheme-auto{--mp-main-bg-color:#222;--mp-duration-color:#eee;--mp-alt-row-color:#333;--mp-muted-color:#888;--mp-label-color:#ccc;--mp-gap-font-color:#d6d6d6;--mp-gap-bg-color:#424448;--mp-link-color:#4db5ff;--mp-query-border-color:#575757;--mp-highlight-fade-color:#884;--mp-highlight-default-color:#eee;--mp-highlight-literal-color:#3fca6a;--mp-highlight-keyword-color:#36a1ef;--mp-result-border:solid .5px #575757;}.mp-scheme-auto body{background-color:var(--mp-main-bg-color);}}.mp-scheme-dark{--mp-main-bg-color:#222;--mp-duration-color:#eee;--mp-alt-row-color:#333;--mp-muted-color:#888;--mp-label-color:#ccc;--mp-gap-font-color:#d6d6d6;--mp-gap-bg-color:#424448;--mp-link-color:#4db5ff;--mp-query-border-color:#575757;--mp-highlight-fade-color:#884;--mp-highlight-default-color:#eee;--mp-highlight-literal-color:#3fca6a;--mp-highlight-keyword-color:#36a1ef;--mp-result-border:solid .5px #575757;}.mp-scheme-dark body{background-color:var(--mp-main-bg-color);}.mp-result,.mp-queries{color:var(--mp-label-color);line-height:1;font-size:12px;}.mp-result pre,.mp-queries pre,.mp-result code,.mp-queries code,.mp-result label,.mp-queries label,.mp-result table,.mp-queries table,.mp-result tbody,.mp-queries tbody,.mp-result thead,.mp-queries thead,.mp-result tfoot,.mp-queries tfoot,.mp-result tr,.mp-queries tr,.mp-result th,.mp-queries th,.mp-result td,.mp-queries td{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline;background-color:transparent;overflow:visible;max-height:none;}.mp-result pre,.mp-queries pre,.mp-result code,.mp-queries code{font-family:Fira Code,Consolas,monospace,serif;font-variant-ligatures:none;}.mp-result table,.mp-queries table{color:var(--mp-label-color);border-collapse:collapse;border-spacing:0;width:100%;}.mp-result a,.mp-queries a{cursor:pointer;color:var(--mp-link-color);text-decoration:none;}.mp-result a:hover,.mp-queries a:hover{text-decoration:underline;}.mp-result{font-family:sans-serif;}.mp-result.show-columns th.mp-more-columns,.mp-result.show-columns td.mp-more-columns{display:table-cell !important;}.mp-result.show-columns a.mp-more-columns{display:inline-block;}.mp-result.show-columns .mp-links span a:nth-child(1){display:none;}.mp-result.show-trivial tr.mp-trivial{display:table-row !important;}.mp-result.show-trivial a.mp-trivial{display:inline-block;}.mp-result.show-trivial .mp-links span a:nth-child(3){display:none;}.mp-result table.mp-client-timings{margin-top:10px;}.mp-result table.mp-client-timings td:nth-child(2){width:100%;padding:0;}.mp-result table.mp-client-timings td div{height:13px;min-width:1px;}.mp-result table.mp-client-timings .t-point div{height:4px;border-radius:8px;margin-bottom:4px;box-shadow:0 0 2px;}.mp-result table.mp-client-timings .t-unknown div{background:var(--mp-timing-unknown-color);}.mp-result table.mp-client-timings .t-dns div{background:var(--mp-timing-dns-color);}.mp-result table.mp-client-timings .t-connect div{background:var(--mp-timing-connect-color);}.mp-result table.mp-client-timings .t-ssl div{background:var(--mp-timing-ssl-color);}.mp-result table.mp-client-timings .t-request div{background:var(--mp-timing-request-color);}.mp-result table.mp-client-timings .t-response div{background:var(--mp-timing-response-color);}.mp-result table.mp-client-timings .t-dom div{background:var(--mp-timing-dom-color);}.mp-result table.mp-client-timings .t-domcontent div{background:var(--mp-timing-domcontent-color);}.mp-result table.mp-client-timings .t-load div{background:var(--mp-timing-load-color);}.mp-result table.mp-client-timings .t-paint div{background:var(--mp-timing-paint-color);}.mp-result .mp-debug{position:relative;cursor:pointer;}.mp-result .mp-debug .mp-debug-tooltip{display:none;position:fixed;background-color:var(--mp-main-bg-color);box-shadow:var(--mp-popup-shadow);right:100px;max-width:90%;border-radius:9px;padding:12px;margin-top:18px;opacity:.95;backdrop-filter:blur(4px);}.mp-result .mp-debug .mp-nested-timing{margin-left:16px;}.mp-result .mp-debug td:first-child:hover .mp-debug-tooltip{display:block;}.mp-result .mp-debug td:first-child:hover>span{text-shadow:var(--mp-button-active-text-color) 0 0 4px;}.mp-result .mp-stack-trace{color:var(--mp-muted-color);margin-bottom:5px;font-family:Consolas,Monaco,monospace;overflow-x:hidden;overflow-y:auto;white-space:pre-line;max-height:200px;}.mp-result .mp-stack-trace .stack-row{white-space:nowrap;}.mp-result .mp-stack-trace .stack-row.common{filter:grayscale(80%);}.mp-result .mp-stack-trace .stack.async{opacity:.6;display:none;}.mp-result .mp-stack-trace .stack.leadin,.mp-result .mp-stack-trace .stack.file,.mp-result .mp-stack-trace .stack.line-prefix,.mp-result .mp-stack-trace .stack.path,.mp-result .mp-stack-trace .stack.dot{color:var(--mp-muted-color);}.mp-result .mp-stack-trace .stack.async-tag{color:var(--mp-highlight-attribute-color);font-weight:bold;}.mp-result .mp-stack-trace .stack.async-tag:before{content:"(";}.mp-result .mp-stack-trace .stack.async-tag:after{content:")";}.mp-result .mp-stack-trace .stack.type{color:var(--mp-highlight-keyword-color);}.mp-result .mp-stack-trace .stack.generic-type{color:var(--mp-highlight-fade-color);}.mp-result .mp-stack-trace .stack.misc,.mp-result .mp-stack-trace .stack.parens{color:var(--mp-highlight-comment-color);}.mp-result .mp-stack-trace .stack.method{color:var(--mp-highlight-string-color);}.mp-result .mp-stack-trace .stack.paramType{color:var(--mp-highlight-attribute-color);}.mp-result .mp-stack-trace .stack.paramName{color:var(--mp-highlight-variable-color);}.mp-result .mp-label{color:var(--mp-label-color);overflow:hidden;text-overflow:ellipsis;}.mp-result .mp-unit{color:var(--mp-muted-color);}.mp-result .mp-more-columns{display:none;}.mp-result .mp-trivial{display:none;}.mp-result .mp-trivial td:not(:first-child),.mp-result .mp-trivial td:not(:first-child) *{color:var(--mp-muted-color) !important;}.mp-result .mp-number{color:var(--mp-duration-color);}.mp-result .mp-info>div{white-space:nowrap;overflow:hidden;}.mp-result .mp-info>div>div{display:inline-block;}.mp-result .mp-info .mp-name{display:inline-block;font-weight:bold;float:left;}.mp-result .mp-info .mp-machine-name,.mp-result .mp-info .mp-started{text-align:right;float:right;}.mp-result .mp-info .mp-server-time{white-space:nowrap;}.mp-result .mp-timings th{background-color:var(--mp-main-bg-color);color:var(--mp-muted-color);text-align:right;}.mp-result .mp-timings th,.mp-result .mp-timings td{white-space:nowrap;}.mp-result .mp-timings .mp-show-more{display:none;}.mp-result .mp-timings .mp-duration{font-family:Consolas,monospace,serif;color:var(--mp-duration-color);text-align:right;}.mp-result .mp-timings .mp-indent{letter-spacing:4px;}.mp-result .mp-timings .mp-queries-show .mp-number,.mp-result .mp-timings .mp-queries-show .mp-unit{color:var(--mp-link-color);}.mp-result .mp-timings .mp-queries-show.mp-queries-warning{color:var(--mp-warning-color);font-weight:bold;}.mp-result .mp-timings .mp-queries-duration{padding-left:6px;}.mp-result .mp-timings:not(.mp-client-timings) .mp-trivial:first-child{display:table-row;}.mp-result .mp-custom-timing-overview{float:right;margin:10px 0;width:auto;}.mp-result .mp-custom-timing-overview td{white-space:nowrap;text-align:right;}.mp-result .mp-custom-timing-overview td:last-child{padding-left:8px;}.mp-result .mp-links{margin-top:10px;clear:both;}.mp-result .mp-links span{float:right;}.mp-result .mp-links a{font-size:95%;margin-left:12px;}.mp-result .mp-links a:first-child{margin-left:0;}.mp-warning{color:var(--mp-critical-color);}@keyframes mp-fadeIn{from{background-color:var(--mp-highlight-fade-color);}to{background-color:none;}}.mp-overlay{z-index:2147483642;}.mp-overlay .mp-overlay-bg{z-index:2147483643;background:var(--mp-overlay-bg-color);opacity:.5;top:0;left:0;min-width:100%;position:fixed;height:calc(100%);}.mp-overlay .mp-queries{display:block;z-index:2147483644;top:25px;left:25px;right:25px;position:fixed;max-height:calc(100vh - 50px);background-color:var(--mp-main-bg-color);border-radius:2px;overflow-y:auto;overflow-x:auto;}.mp-queries{font-family:sans-serif;}.mp-queries pre{white-space:pre-wrap;}.mp-queries div{text-align:left;}.mp-queries table{table-layout:fixed;}.mp-queries td,.mp-queries th{padding:8px;border-bottom:solid 1px var(--mp-query-border-color);}.mp-queries td:first-child,.mp-queries th:first-child{width:250px;text-align:right;}.mp-queries td:first-child div,.mp-queries th:first-child div{text-align:right;margin-bottom:5px;word-break:break-word;}.mp-queries td:last-child,.mp-queries th:last-child{text-align:left;padding-right:15px;}.mp-queries .highlight{animation:mp-fadeIn 2s 1;}.mp-queries .mp-odd{background-color:var(--mp-alt-row-color);}.mp-queries .mp-stack-trace{padding-bottom:1px;color:var(--mp-muted-color);margin-bottom:5px;}.mp-queries .mp-since-start,.mp-queries .mp-duration{text-align:right;}.mp-queries.show-trivial .mp-gap-info.mp-trivial-gap{display:table-row;}.mp-queries .mp-gap-info{background-color:var(--mp-gap-bg-color);opacity:.8;color:var(--mp-gap-font-color);}.mp-queries .mp-gap-info .query{word-break:break-all;}.mp-queries .mp-gap-info .mp-unit{color:var(--mp-muted-color);}.mp-queries .mp-gap-info.mp-trivial-gap{display:none;}.mp-queries .mp-trivial-gap-container{text-align:center;margin:8px 0;}.mp-queries .mp-call-type{font-weight:bold;}.mp-queries .hljs{display:block;overflow-x:auto;padding:.5em;}.mp-queries .hljs,.mp-queries .hljs-subst{color:var(--mp-highlight-default-color);}.mp-queries .hljs-string,.mp-queries .hljs-meta,.mp-queries .hljs-symbol,.mp-queries .hljs-template-tag,.mp-queries .hljs-template-variable,.mp-queries .hljs-addition{color:var(--mp-highlight-string-color);}.mp-queries .hljs-comment,.mp-queries .hljs-quote{color:var(--mp-highlight-comment-color);}.mp-queries .hljs-number,.mp-queries .hljs-regexp,.mp-queries .hljs-literal,.mp-queries .hljs-bullet,.mp-queries .hljs-link{color:var(--mp-highlight-literal-color);}.mp-queries .hljs-deletion,.mp-queries .hljs-variable{color:var(--mp-highlight-variable-color);}.mp-queries .hljs-keyword,.mp-queries .hljs-selector-tag,.mp-queries .hljs-title,.mp-queries .hljs-section,.mp-queries .hljs-built_in,.mp-queries .hljs-doctag,.mp-queries .hljs-type,.mp-queries .hljs-tag,.mp-queries .hljs-name,.mp-queries .hljs-selector-id,.mp-queries .hljs-selector-class,.mp-queries .hljs-strong{color:var(--mp-highlight-keyword-color);}.mp-queries .hljs-emphasis{font-style:italic;}.mp-queries .hljs-attribute{color:var(--mp-highlight-attribute-color);}.mp-results{z-index:2147483641;position:fixed;top:0;}.mp-results.mp-left,.mp-results.mp-bottomleft{left:0;}.mp-results.mp-left .mp-controls,.mp-results.mp-left.mp-no-controls .mp-result:last-child .mp-button{border-bottom-right-radius:var(--mp-result-border-radius);}.mp-results.mp-left .mp-button,.mp-results.mp-left .mp-controls{border-right:var(--mp-result-border);}.mp-results.mp-right{right:0;}.mp-results.mp-right .mp-controls,.mp-results.mp-right.mp-no-controls .mp-result:last-child .mp-button{border-bottom-left-radius:var(--mp-result-border-radius);}.mp-results.mp-right .mp-button,.mp-results.mp-right .mp-controls{border-left:var(--mp-result-border);}.mp-results.mp-bottomleft{top:inherit;bottom:0;}.mp-results.mp-bottomleft .mp-result:first-child .mp-button{border-top-right-radius:var(--mp-result-border-radius);}.mp-results.mp-bottomleft .mp-button,.mp-results.mp-bottomleft .mp-controls{border-right:var(--mp-result-border);}.mp-results.mp-bottomleft .mp-result .mp-button,.mp-results.mp-bottomleft .mp-controls{border-bottom:0;border-top:var(--mp-result-border);}.mp-results.mp-bottomright{top:inherit;bottom:0;right:0;}.mp-results.mp-bottomright .mp-result:first-child .mp-button{border-top-left-radius:var(--mp-result-border-radius);}.mp-results.mp-bottomright .mp-button,.mp-results.mp-bottomright .mp-controls{border-left:var(--mp-result-border);}.mp-results.mp-bottomright .mp-result .mp-button,.mp-results.mp-bottomright .mp-controls{border-bottom:0;border-top:var(--mp-result-border);}.mp-results .mp-button{user-select:none;}.mp-results .mp-button.mp-button-warning{font-weight:bold;background-color:var(--mp-button-warning-bg-color);color:var(--mp-button-warning-text-color);}.mp-results .mp-button.mp-button-warning .mp-number,.mp-results .mp-button.mp-button-warning .mp-unit,.mp-results .mp-button.mp-button-warning .mp-warning{color:var(--mp-button-warning-text-color);}.mp-results .mp-button .mp-warning{font-weight:bold;}.mp-results .mp-button>.mp-number{font-family:Consolas,monospace,serif;}.mp-results .mp-controls{display:none;}.mp-results .mp-button,.mp-results .mp-controls{z-index:2147483640;border-bottom:var(--mp-result-border);background-color:var(--mp-main-bg-color);padding:4px 8px;text-align:right;cursor:pointer;}.mp-results .mp-result{position:relative;}.mp-results .mp-result.active .mp-button{background-color:var(--mp-button-active-bg-color);animation:none;border-radius:0 !important;}.mp-results .mp-result.active .mp-button.mp-button-warning{background-color:var(--mp-button-warning-bg-color);}.mp-results .mp-result.active .mp-button .mp-number,.mp-results .mp-result.active .mp-button .mp-warning{color:var(--mp-button-active-text-color);font-weight:bold;}.mp-results .mp-result.active .mp-button .mp-unit{color:var(--mp-button-active-text-color);font-weight:normal;}.mp-results .mp-result.active .mp-popup{display:block;}.mp-results.new .mp-button{animation:mp-fadeIn 2s 1;}.mp-results .mp-controls{display:block;font-size:12px;font-family:Consolas,monospace,serif;cursor:default;text-align:center;}.mp-results .mp-controls span{color:var(--mp-muted-color);border-right:1px solid var(--mp-muted-color);padding-right:5px;margin-right:5px;cursor:pointer;}.mp-results .mp-controls span:last-child{border-right:none;}.mp-results .mp-popup{display:none;z-index:2147483641;position:absolute;background-color:var(--mp-main-bg-color);padding:5px 10px;text-align:left;line-height:18px;overflow:auto;box-shadow:var(--mp-popup-shadow);border-radius:2px;}.mp-results .mp-popup .mp-info{margin-bottom:3px;padding-bottom:2px;border-bottom:1px solid var(--mp-info-border-color);}.mp-results .mp-popup .mp-info .mp-name{font-size:1.1em;}.mp-results .mp-popup .mp-info .mp-overall-duration{color:var(--mp-muted-color);}.mp-results .mp-popup .mp-timings th,.mp-results .mp-popup .mp-timings td{padding:0 6px;}.mp-results .mp-popup .mp-timings th{font-size:95%;padding-bottom:3px;}.mp-results .mp-popup .mp-timings .mp-label{max-width:350px;text-overflow:ellipsis;white-space:nowrap;overflow:hidden;}.mp-results .mp-queries{display:none;}.mp-results.mp-min .mp-result{display:none;}.mp-results.mp-min .mp-controls span{display:none;}.mp-results.mp-min .mp-controls .mp-min-max{border-right:none;padding:0;margin:0;}.mp-results.mp-min:hover .mp-controls .mp-min-max{display:inline;}.mp-result-full .mp-result{width:950px;margin:25px auto;}.mp-result-full .mp-result .mp-button{display:none;}.mp-result-full .mp-result .mp-popup .mp-info{font-size:18px;border-bottom:1px solid var(--mp-muted-color);padding-bottom:3px;margin-bottom:10px;}.mp-result-full .mp-result .mp-popup .mp-info .mp-name{margin-bottom:5px;}.mp-result-full .mp-result .mp-popup .mp-info .mp-overall-duration,.mp-result-full .mp-result .mp-popup .mp-info .mp-started{font-size:80%;color:var(--mp-muted-color);}.mp-result-full .mp-result .mp-popup .mp-info .mp-overall-duration{padding-right:20px;}.mp-result-full .mp-result .mp-popup .mp-timings td,.mp-result-full .mp-result .mp-popup .mp-timings th{font-size:13px;padding-left:8px;padding-right:8px;}.mp-result-full .mp-result .mp-popup .mp-timings th{padding-bottom:7px;}.mp-result-full .mp-result .mp-popup .mp-timings td{padding-bottom:4px;}.mp-result-full .mp-result .mp-popup .mp-timings td:first-child{padding-left:10px;}.mp-result-full .mp-result .mp-popup .mp-timings .mp-label{max-width:550px;}.mp-result-full .mp-result .mp-queries{margin:20px 0;}.mp-result-full .mp-result .mp-queries th{font-size:16px;}.mp-result-full .mp-share-mp-results{display:none;}table.mp-results-index{border:0;border-spacing:0;font-size:12px;font-family:Arial;}table.mp-results-index a{color:var(--mp-link-color);text-decoration:none;}table.mp-results-index tbody{color:var(--mp-label-color);}table.mp-results-index tbody tr:nth-child(odd){background-color:var(--mp-alt-row-color);}table.mp-results-index tbody tr:nth-child(even){background-color:var(--mp-main-bg-color);}table.mp-results-index tbody td{text-align:center;}table.mp-results-index tbody td:first-child{text-align:left;}table.mp-results-index tbody td[colspan="3"]{color:var(--mp-muted-color);}table.mp-results-index thead tr{color:var(--mp-label-color);}table.mp-results-index thead tr th{padding:5px 15px;}table.mp-results-index td{padding:8px;} \ No newline at end of file diff --git a/src/MiniProfiler.Shared/ui/lib/MiniProfiler.ts b/src/MiniProfiler.Shared/ui/lib/MiniProfiler.ts index 7fe57b155..bec8e43fe 100644 --- a/src/MiniProfiler.Shared/ui/lib/MiniProfiler.ts +++ b/src/MiniProfiler.Shared/ui/lib/MiniProfiler.ts @@ -56,6 +56,7 @@ namespace StackExchange.Profiling { // additive on client side CustomTimingStats: { [id: string]: ICustomTimingStat }; DurationWithoutChildrenMilliseconds: number; + DurationOfChildrenMilliseconds: number; Depth: number; HasCustomTimings: boolean; HasDuplicateCustomTimings: { [id: string]: boolean }; @@ -64,6 +65,12 @@ namespace StackExchange.Profiling { Parent: ITiming; // added for gaps (TODO: change all this) richTiming: IGapTiming[]; + // In debug mode only + DebugInfo: ITimingDebugInfo; + } + + interface ITimingDebugInfo { + RichHtmlStack: string; } interface ICustomTiming { @@ -478,6 +485,7 @@ namespace StackExchange.Profiling { function processTiming(timing: ITiming, parent: ITiming, depth: number) { timing.DurationWithoutChildrenMilliseconds = timing.DurationMilliseconds; + timing.DurationOfChildrenMilliseconds = 0; timing.Parent = parent; timing.Depth = depth; timing.HasDuplicateCustomTimings = {}; @@ -486,6 +494,7 @@ namespace StackExchange.Profiling { for (const child of timing.Children || []) { processTiming(child, timing, depth + 1); timing.DurationWithoutChildrenMilliseconds -= child.DurationMilliseconds; + timing.DurationOfChildrenMilliseconds += child.DurationMilliseconds; } // do this after subtracting child durations @@ -673,11 +682,41 @@ namespace StackExchange.Profiling { } return (milliseconds || 0).toFixed(decimalPlaces === undefined ? 1 : decimalPlaces); }; + const renderDebugInfo = (timing: ITiming) => { + if (timing.DebugInfo) { + const customTimings = (p.CustomTimingStats ? Object.keys(p.CustomTimingStats) : []).map((tk) => timing.CustomTimings[tk] ? ` +
+ ${timing.CustomTimingStats[tk].Count} ${encode(tk)} call${timing.CustomTimingStats[tk].Count == 1 ? '' : 's'} + totalling ${duration(timing.CustomTimingStats[tk].Duration)} ms + ${((timing.HasDuplicateCustomTimings[tk] || timing.HasWarnings[tk]) ? '(duplicates deletected)' : '')} +
` : '').join(''); + return ` +
+
Detailed info for ${encode(timing.Name)}
+
Starts at: ${duration(timing.StartMilliseconds)} ms
+
+ Overall duration (with children): ${duration(timing.DurationMilliseconds)} ms +
+ Self duration: ${duration(timing.DurationWithoutChildrenMilliseconds)} ms + ${customTimings} +
+
+ Children (${timing.Children ? timing.Children.length : '0'}) duration: ${duration(timing.DurationOfChildrenMilliseconds)} ms +
+
+
Stack:
+
${timing.DebugInfo.RichHtmlStack}
+
+ 🔍`; + } + return ''; + }; const renderTiming = (timing: ITiming) => { const customTimingTypes = p.CustomTimingStats ? Object.keys(p.CustomTimingStats) : []; let str = ` - + + ${renderDebugInfo(timing)} 0 ? ` style="padding-left:${timing.Depth * 11}px;"` : ''}> ${encode(timing.Name)} @@ -709,7 +748,7 @@ namespace StackExchange.Profiling { - + @@ -721,7 +760,7 @@ namespace StackExchange.Profiling { - +
duration (ms) with children (ms) from start (ms)