Skip to content

ASP.NET Core 3.x: use Diagnostic bits #475

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/Releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ This page tracks major changes included in any update starting with version 4.0.
- 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))
- (**.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))
- **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))
- (**Major**) Drops IE 11 support
- Fix for missing `IMemoryCache` depending on config ([#440](https://github.com/MiniProfiler/dotnet/pull/440))

- MySQL Storage:
- Updates `MySqlConnector` to 0.60.1 for misc fixes ([#432](https://github.com/MiniProfiler/dotnet/pull/432)) (thanks [@bgrainger](https://github.com/bgrainger)!)
- Redis Storage
Expand Down
183 changes: 183 additions & 0 deletions src/MiniProfiler.AspNetCore.Mvc/MvcDiagnosticListener.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
#if NETCOREAPP3_0
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Diagnostics;
using Microsoft.AspNetCore.Mvc.Filters;
using StackExchange.Profiling.Internal;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;

namespace StackExchange.Profiling.Data
{
/// <summary>
/// Diagnostic listener for Microsoft.AspNetCore.Mvc.* events
/// </summary>
/// <remarks>
/// See:
/// - https://github.com/dotnet/aspnetcore/blob/v3.0.0/src/Mvc/Mvc.Core/src/MvcCoreDiagnosticListenerExtensions.cs
/// - https://github.com/dotnet/aspnetcore/blob/v3.0.0/src/Mvc/Mvc.Core/src/Diagnostics/MvcDiagnostics.cs
/// - https://github.com/dotnet/aspnetcore/blob/v3.0.0/src/Mvc/Mvc.Razor/src/Diagnostics/MvcDiagnostics.cs
/// </remarks>
public class MvcDiagnosticListener : IMiniProfilerDiagnosticListener
{
/// <summary>
/// Diagnostic Listener name to handle.
/// </summary>
public string ListenerName => "Microsoft.AspNetCore";

/// <summary>
/// Notifies the observer that the provider has finished sending push-based notifications.
/// </summary>
public void OnCompleted() { }

/// <summary>
/// Notifies the observer that the provider has experienced an error condition.
/// </summary>
/// <param name="error">An object that provides additional information about the error.</param>
public void OnError(Exception error) => Trace.WriteLine(error);

// So we don't keep allocating strings for the same actions over and over
private readonly ConcurrentDictionary<(string, string), string> _descriptorNameCache = new ConcurrentDictionary<(string, string), string>();

private string GetName(string label, ActionDescriptor descriptor)
{
var key = (label, descriptor.DisplayName);
if (!_descriptorNameCache.TryGetValue(key, out var result))
{
// For the "Samples.AspNetCore.Controllers.HomeController.Index (Samples.AspNetCore3)" format,
// ...trim off the assembly on the end.
var assemblyNamePos = descriptor.DisplayName.IndexOf(" (");
if (assemblyNamePos > 0)
{
result = string.Concat(label, ": ", descriptor.DisplayName.AsSpan().Slice(0, assemblyNamePos));
}
else
{
result = label + ": " + descriptor.DisplayName;
}
_descriptorNameCache[key] = result;
}
return result;
}

private static string GetName(IActionResult result) => result switch
{
ViewResult vr => vr.ViewName.HasValue() ? "View: " + vr.ViewName : nameof(ViewResult),
ContentResult cr => cr.ContentType.HasValue() ? "Content: " + cr.ContentType : nameof(ContentResult),
ObjectResult or => or.DeclaredType != null ? "Object: " + or.DeclaredType.Name : nameof(ObjectResult),
StatusCodeResult scr => scr.StatusCode > 0 ? "Status Code: " + scr.StatusCode.ToString() : nameof(StatusCodeResult),
JsonResult jr => jr.ContentType.HasValue() ? "JSON: " + jr.ContentType : nameof(JsonResult),
_ => "Result: " + result.GetType().Name
};

private static string GetName(IFilterMetadata filter) => filter.GetType().Name;

/// <summary>
/// Stores the current timing in the tree, on each request.
/// </summary>
private readonly AsyncLocal<(object State, Timing Timing)> CurrentTiming = new AsyncLocal<(object, Timing)>();

private object Start<T>(T state, string stepName) where T : class
{
CurrentTiming.Value = (state, MiniProfiler.Current.Step(stepName));
return null;
}

private object Complete<T>(T state) where T : class
{
if (CurrentTiming.Value.State is T currentState && currentState == state)
{
using (CurrentTiming.Value.Timing) { }
}
return null;
}

/// <summary>
/// Provides the observer with new data.
/// </summary>
/// <param name="kv">The current notification information.</param>
public void OnNext(KeyValuePair<string, object> kv) => _ = kv.Value switch
{
// 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),
// ControllerActionMethod
BeforeControllerActionMethodEventData data => Start(data.ActionContext.ActionDescriptor, GetName("Controller Action", data.ActionContext.ActionDescriptor)),
AfterControllerActionMethodEventData data => Complete(data.ActionContext.ActionDescriptor),
// ActionResultEvent
BeforeActionResultEventData data => Start(data.Result, GetName(data.Result)),
AfterActionResultEventData data => Complete(data.Result),

// AuthorizationFilterOnAuthorization
BeforeAuthorizationFilterOnAuthorizationEventData data => Start(data.Filter, "Auth Filter: " + GetName(data.Filter)),
AfterAuthorizationFilterOnAuthorizationEventData data => Complete(data.Filter),

// ResourceFilterOnResourceExecution
BeforeResourceFilterOnResourceExecutionEventData data => Start(data.Filter, "Resource Filter (Exec): " + GetName(data.Filter)),
AfterResourceFilterOnResourceExecutionEventData data => Complete(data.Filter),
// ResourceFilterOnResourceExecuting
BeforeResourceFilterOnResourceExecutingEventData data => Start(data.Filter, "Resource Filter (Execing): " + GetName(data.Filter)),
AfterResourceFilterOnResourceExecutingEventData data => Complete(data.Filter),
// ResourceFilterOnResourceExecuted
BeforeResourceFilterOnResourceExecutedEventData data => Start(data.Filter, "Resource Filter (Execed): " + GetName(data.Filter)),
AfterResourceFilterOnResourceExecutedEventData data => Complete(data.Filter),

// ExceptionFilterOnException
BeforeExceptionFilterOnException data => Start(data.Filter, "Exception Filter: " + GetName(data.Filter)),
AfterExceptionFilterOnExceptionEventData data => Complete(data.Filter),

// ActionFilterOnActionExecution
BeforeActionFilterOnActionExecutionEventData data => Start(data.Filter, "Action Filter (Exec): " + GetName(data.Filter)),
AfterActionFilterOnActionExecutionEventData data => Complete(data.Filter),
// ActionFilterOnActionExecuting
BeforeActionFilterOnActionExecutingEventData data => Start(data.Filter, "Action Filter (Execing): " + GetName(data.Filter)),
AfterActionFilterOnActionExecutingEventData data => Complete(data.Filter),
// ActionFilterOnActionExecuted
BeforeActionFilterOnActionExecutedEventData data => Start(data.Filter, "Action Filter (Execed): " + GetName(data.Filter)),
AfterActionFilterOnActionExecutedEventData data => Complete(data.Filter),

// ResultFilterOnResultExecution
BeforeResultFilterOnResultExecutionEventData data => Start(data.Filter, "Result Filter (Exec): " + GetName(data.Filter)),
AfterResultFilterOnResultExecutionEventData data => Complete(data.Filter),
// ResultFilterOnResultExecuting
BeforeResultFilterOnResultExecutingEventData data => Start(data.Filter, "Result Filter (Execing): " + GetName(data.Filter)),
AfterResultFilterOnResultExecutingEventData data => Complete(data.Filter),
// ResultFilterOnResultExecuted
BeforeResultFilterOnResultExecutedEventData data => Start(data.Filter, "Result Filter (Execed): " + GetName(data.Filter)),
AfterResultFilterOnResultExecutedEventData data => Complete(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),

// 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),

// PageFilterOnPageHandlerExecution
BeforePageFilterOnPageHandlerExecutionEventData data => Start(data.Filter, "Filter (Exec): " + GetName(data.Filter)),
AfterPageFilterOnPageHandlerExecutionEventData data => Complete(data.Filter),
// PageFilterOnPageHandlerExecuting
BeforePageFilterOnPageHandlerExecutingEventData data => Start(data.Filter, "Filter (Execing): " + GetName(data.Filter)),
AfterPageFilterOnPageHandlerExecutingEventData data => Complete(data.Filter),
// PageFilterOnPageHandlerExecuted
BeforePageFilterOnPageHandlerExecutedEventData data => Start(data.Filter, "Filter (Execed): " + GetName(data.Filter)),
AfterPageFilterOnPageHandlerExecutedEventData data => Complete(data.Filter),

// PageFilterOnPageHandlerSelection
BeforePageFilterOnPageHandlerSelectionEventData data => Start(data.Filter, "Filter (Selection): " + GetName(data.Filter)),
AfterPageFilterOnPageHandlerSelectionEventData data => Complete(data.Filter),
// PageFilterOnPageHandlerSelected
BeforePageFilterOnPageHandlerSelectedEventData data => Start(data.Filter, "Filter (Selected): " + GetName(data.Filter)),
AfterPageFilterOnPageHandlerSelectedEventData data => Complete(data.Filter),
_ => null
};
}
}
#endif
27 changes: 17 additions & 10 deletions src/MiniProfiler.AspNetCore.Mvc/MvcExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using StackExchange.Profiling;
using StackExchange.Profiling.Internal;
using StackExchange.Profiling.Storage;
using System;

#if NETCOREAPP3_0
using StackExchange.Profiling.Data;
#else
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewComponents;
#endif

namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
Expand All @@ -23,22 +28,22 @@ public static IMiniProfilerBuilder AddMiniProfiler(this IServiceCollection servi
{
services.AddMemoryCache(); // Unconditionally register an IMemoryCache since it's the most common and default case
services.AddSingleton<IConfigureOptions<MiniProfilerOptions>, MiniProfilerOptionsDefaults>();
services.AddSingleton<DiagnosticInitializer>(); // For any IMiniProfilerDiagnosticListener registration
if (configureOptions != null)
{
services.Configure(configureOptions);
}
// Set background statics
services.Configure<MiniProfilerOptions>(o => MiniProfiler.Configure(o));
services.AddSingleton<DiagnosticInitializer>(); // For any IMiniProfilerDiagnosticListener registration

// See https://github.com/MiniProfiler/dotnet/issues/162 for plans
// Blocked on https://github.com/aspnet/Mvc/issues/6222
//services.AddSingleton<IMiniProfilerDiagnosticListener, MvcViewDiagnosticListener>();
#if NETCOREAPP3_0
services.AddSingleton<IMiniProfilerDiagnosticListener, MvcDiagnosticListener>(); // For view and action profiling
#else
services.AddTransient<IConfigureOptions<MvcOptions>, MiniProfilerSetup>()
.AddTransient<IConfigureOptions<MvcViewOptions>, MiniProfilerSetup>();
#if !NETCOREAPP3_0
services.AddTransient<IViewComponentInvokerFactory, ProfilingViewComponentInvokerFactory>();
.AddTransient<IConfigureOptions<MvcViewOptions>, MiniProfilerSetup>()
.AddTransient<IViewComponentInvokerFactory, ProfilingViewComponentInvokerFactory>();
#endif

return new MiniProfilerBuilder(services);
}
}
Expand All @@ -60,6 +65,7 @@ public void Configure(MiniProfilerOptions options)
}
}

#if !NETCOREAPP3_0
internal class MiniProfilerSetup : IConfigureOptions<MvcViewOptions>, IConfigureOptions<MvcOptions>
{
public void Configure(MvcViewOptions options)
Expand All @@ -75,4 +81,5 @@ public void Configure(MvcOptions options)
options.Filters.Add(new ProfilingActionFilter());
}
}
#endif
}
5 changes: 4 additions & 1 deletion src/MiniProfiler.Shared/ui/includes.css
Original file line number Diff line number Diff line change
Expand Up @@ -650,7 +650,10 @@
padding-bottom: 3px;
}
.mp-results .mp-popup .mp-timings .mp-label {
max-width: 275px;
max-width: 350px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.mp-results .mp-queries {
display: none;
Expand Down
5 changes: 4 additions & 1 deletion src/MiniProfiler.Shared/ui/includes.less
Original file line number Diff line number Diff line change
Expand Up @@ -724,7 +724,10 @@
}

.mp-label {
max-width: 275px;
max-width: 350px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
}
}
Expand Down
Loading