Automatically instrumenting an ASP.NET MVC3 app
over 13 years ago
Update: There is now a nuget package that will take care of wiring this up automatically see: NuGet Gallery | MiniProfiler.MVC3 2.0.2
MiniProfiler seems to have upset quite a few people. One major complain we got when Scott Hanselman blogged about it, goes:
Why do you expect me to sprinkle all these ugly
using (MiniProfiler.Current.Step("stuff"))
throughout my code. This is ugly and wrong, profilers should never require you to alter your code base. FAIL."
Our usual retort is that you are able to properly control the granularity of your profiling this way. Sure you can introduce aspects using PostSharp or your favorite IOC container, however this method may also force you to refactor your code to accommodate profiling.
That said, I agree that sometimes it may seem tedious and unnecessary to chuck using statements where the framework can do that for us.
Here is a quick document on how you can get far more granular timings out-of-the-box without altering any of your controllers or actions.
###Profiling Controllers
Automatically wrapping your controllers with MVC3 is actually quite trivial, it can be done with a GlobalActionFilter
For example:
public class ProfilingActionFilter : ActionFilterAttribute
{
const string stackKey = "ProfilingActionFilterStack";
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var mp = MiniProfiler.Current;
if (mp != null)
{
var stack = HttpContext.Current.Items[stackKey] as Stack<IDisposable>;
if (stack == null)
{
stack = new Stack<IDisposable>();
HttpContext.Current.Items[stackKey] = stack;
}
var prof = MiniProfiler.Current.Step("Controller: " + filterContext.Controller.ToString() + "." + filterContext.ActionDescriptor.ActionName);
stack.Push(prof);
}
base.OnActionExecuting(filterContext);
}
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
base.OnActionExecuted(filterContext);
var stack = HttpContext.Current.Items[stackKey] as Stack<IDisposable>;
if (stack != null && stack.Count > 0)
{
stack.Pop().Dispose();
}
}
}
You wire this up in Global.asax.cs
protected void Application_Start()
{
// other stuff ...
GlobalFilters.Filters.Add(new ProfilingActionFilter());
RegisterGlobalFilters(GlobalFilters.Filters);
/// more stuff
}
###Profiling Views
Views on the other hand a bit more tricky, what you can do is register a special “profiling” view engine that takes care of instrumentation:
public class ProfilingViewEngine : IViewEngine
{
class WrappedView : IView
{
IView wrapped;
string name;
bool isPartial;
public WrappedView(IView wrapped, string name, bool isPartial)
{
this.wrapped = wrapped;
this.name = name;
this.isPartial = isPartial;
}
public void Render(ViewContext viewContext, System.IO.TextWriter writer)
{
using (MiniProfiler.Current.Step("Render " + (isPartial?"partial":"") + ": " + name))
{
wrapped.Render(viewContext, writer);
}
}
}
IViewEngine wrapped;
public ProfilingViewEngine(IViewEngine wrapped)
{
this.wrapped = wrapped;
}
public ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
{
var found = wrapped.FindPartialView(controllerContext, partialViewName, useCache);
if (found != null && found.View != null)
{
found = new ViewEngineResult(new WrappedView(found.View, partialViewName, isPartial: true), this);
}
return found;
}
public ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
var found = wrapped.FindView(controllerContext, viewName, masterName, useCache);
if (found != null && found.View != null)
{
found = new ViewEngineResult(new WrappedView(found.View, viewName, isPartial: false), this);
}
return found;
}
public void ReleaseView(ControllerContext controllerContext, IView view)
{
wrapped.ReleaseView(controllerContext, view);
}
}
You wire this up in Global.asax.cs:
protected void Application_Start()
{
// stuff
var copy = ViewEngines.Engines.ToList();
ViewEngines.Engines.Clear();
foreach (var item in copy)
{
ViewEngines.Engines.Add(new ProfilingViewEngine(item));
}
// more stuff
}
The trick here is that we return a special view engine that simply wraps the parent and intercepts calls to Render.
The results speak for themselves:
###Before
###After
There you go, with minimal changes to your app you are now able to get a fair amount of instrumentation. My controller in the above sample had a System.Threading.Thread.Sleep(50);
call which is really easy to spot now. My slow view had a System.Threading.Thread.Sleep(20);
which is also easy to spot.
The caveat of this approach is that we now have another layer of indirection which has a minimal effect on performance. We will look at porting in these helpers to MiniProfiler in the near future.
Happy profiling, Sam
Awesome!