Thursday, April 23, 2009 11:22 PM
Kazi Manzur Rashid
ASP.NET MVC View Location and Performance Issue
Recently there has been quite some talk on HtmlHelper.RenderPartial() in ASP.NET MVC space which you will find over here and here. After reading those posts, I did an in depth analysis of the view location part, I am sharing the details with you so that it can help both of us better understanding the internal details of the ASP.NET MVC framework. But before that I would like to suggest that the on going discussions should not be on partial view specific as there is no difference between the logic finding partial or regular view of default the WebFormViewEngine.
The default WebFormViewEngine is inherited from the base VirtualPathProviderViewEngine which in turns uses DefaultViewLocationCache for caching the location when running in web server in release mode. The DefaultViewLocationCache uses the ASP.NET built-in cache to cache the location. The following shows the code of DefaultViewLocationCache(I have excluded the other parts of the codes that are irrelevant in this discussion):
public class DefaultViewLocationCache : IViewLocationCache
{
private static readonly TimeSpan _defaultTimeSpan = new TimeSpan(0, 15, 0);
public static readonly IViewLocationCache Null = new NullViewLocationCache();
public DefaultViewLocationCache() : this(_defaultTimeSpan)
{
}
public void InsertViewLocation(HttpContextBase httpContext, string key, string virtualPath)
{
if (httpContext == null)
{
throw new ArgumentNullException("httpContext");
}
httpContext.Cache.Insert(key, virtualPath, null /* dependencies */, Cache.NoAbsoluteExpiration, TimeSpan);
}
}
The first thing which I like to point is the default cache duration is 15 minutes(line 3 & 7) and this duration is never overridden in the ASP.NET MVC framework. Next, when caching it uses the relative expiration which means if the view is not accessed in this duration, the cache will become invalid (line 18). So there should be a file exists check for each inactive view after the 15 minutes, so the maximum (15 x 4 x 24) = 1440 number of file checks in a day assuming that the view is accessed just after the cache becomes invalid. I did a small benchmark with the following code(FileExists method is exactly taken from the WebFormViewEngine) and it took around 8.5 seconds for 1440 checks to complete.
public ActionResult Index()
{
Stopwatch watch = new Stopwatch();
watch.Start();
for (int i = 1; i <= 1440; i++)
{
FileExists(ControllerContext, "~/Views/Home/TopMenu.ascx"); //Does not exist
FileExists(ControllerContext, "~/Views/Shared/TopMenu.ascx");
}
watch.Stop();
TimeSpan elapsed = watch.Elapsed;
ViewData["elapsed"] = elapsed;
return View();
}
private static bool FileExists(ControllerContext controllerContext, string virtualPath)
{
try
{
object viewInstance = BuildManager.CreateInstanceFromVirtualPath(virtualPath, typeof(object));
return viewInstance != null;
}
catch (HttpException he)
{
if (he.GetHttpCode() == (int)HttpStatusCode.NotFound)
{
// If BuildManager returns a 404 (Not Found) that means the file did not exist
return false;
}
else
{
// All other error codes imply other errors such as compilation or parsing errors
throw;
}
}
catch
{
return false;
}
}
I don’t think 8.5 seconds for 1440 checks is a performance issue. But anyway, if you are wondering how to reduce the number of checks, here is the code that you can put in your global.asax or bootstrapper.
foreach (var viewEngine in ViewEngines.Engines.OfType<VirtualPathProviderViewEngine>())
{
viewEngine.ViewLocationCache = new DefaultViewLocationCache(TimeSpan.FromHours(24));
}
The above code will cache the location for 24 hours that means there will be only one check for an inactive view in a day.
So if you are wondering why this post shows so many exceptions, the reason is as Simone pointed out that it was running in debug mode which means the path is never cached. There might be also some exceptions generated (which is gracefully handled by the ASP.NET MVC framework) when a view is accessed for the first time even running in release mode. The reason behind it, the view itself or the part of the view(partial view) is located under the shared folder instead of the controller specific view folder, the framework first tries to find the view in the controller view folder, when it does not find the view, it throws an exception, which the framework handles itself(line 31- 35 in the above FileExists method), then it again tries the shared folder and finds the view and caches the file path, so the next request for the same view in that cached period returns the path from the cache, instead of generating the exception in finding the view.
I hope the above will clarify the confusion related with view location performance concerns.
Before ending this post I would like to comment that the reason behind making the above thing bit complex(caching, handling exception internally etc etc) is due to a missing public method of asp.net BuildManager. It is obvious that the BuildManager must have the virtual path existence checking as well as file location caching and once again the Reflector proves that it does have these methods.
The GetVPathBuildResultInternal is called by the CreateInstanceFromVirtualPath method which is used by the WebFormViewEngine. Next, the caching, by default the ASP.NET comes with two built-in internal VirtualPathProvider: 1. MapPathBasedVirtualPathProvider and 2. ClientVirtualPathProvider. The following shows that the ClientVirtualPathProvider itself maintaining a cache:
So the question remain does the ClientVirtualPathProvider comes into action when resolving the virtual path which I would like to left for the ASP.NET Team to answer.
Filed under: Asp.net, MVC, ASPNETMVC, ASP.NET MVC