Jeremy Davis
Jeremy Davis
Sitecore, C# and web development
Article printed from: https://blog.jermdavis.dev/posts/2018/an-interesting-side-effect-of-compiled-views

An interesting side effect of compiled views

Published 02 April 2018
Updated 30 August 2019

I read a blog post earlier this week that talked about the benefits of compiling your View files to increase performance in Sitecore applications. Reading that post (which I stupidly failed to keep track of the link to, so can't reference it now the comments pointed me back to) reminded me of an interesting issue that came up on a project I was looking at recently. If you're interested in the raw performance of your Sitecore sites, you might want to consider this as well when you're planning your views:

The issue url copied!

[A few updates to this article at the end – please read those too]

A client was running some detailed performance testing on a site. Along with some other issues, they noticed that their performance trace data showed a surprising amount of time spent in RazorGenerator.Mvc.PrecompiledMvcEngine.FileExists method. For simple page requests they would see trace data including lines like this:

File Exists Trace

On some pages about 200ms of the overall execution time was accounted for by calls to this method, which seems a really large amount of time for framework code like this. The code was running on servers with SSD drives, so it was unlikely to be a physical disk access issue. Since they were quite focused on the performance of their pages, the client was interested in what was happening to cause this, and what could be done about it.

Investigating in more detail url copied!

The first thing I looked into was what that method was. A bit of time with Google brought up the source for the RazorGenerator project. Sitecore have added this to the product in order to allow views to be pre-compiled at build time, to reduce site startup time. The `FileExists` method is checking to see if a view exists, but it can also make a test to see if a cshtml file is newer than the pre-compiled view in an assembly:
protected override bool FileExists(ControllerContext controllerContext, string virtualPath)
{
    virtualPath = PrecompiledMvcEngine.EnsureVirtualPathPrefix(virtualPath);

    ViewMapping mapping;
    if (!_mappings.TryGetValue(virtualPath, out mapping))
    {
        return false;
    }

    if (mapping.ViewAssembly.UsePhysicalViewsIfNewer && mapping.ViewAssembly.IsPhysicalFileNewer(virtualPath))
    {
        // If the physical file on disk is newer and the user's opted in this behavior, serve it instead.
        return false;
    }
    return Exists(virtualPath);
}

					

If the setting for UsePhysicalViewsIfNewer is true and the view file is newer then the logic will make use of the cshtml file instead of the data in the assembly when the view file is newer. The test for the age of the cshtml file takes place in this method:

internal static bool IsPhysicalFileNewer(string virtualPath, string baseVirtualPath, Lazy<DateTime> assemblyLastWriteTime)
{
    if (virtualPath.StartsWith(baseVirtualPath ?? String.Empty, StringComparison.OrdinalIgnoreCase))
    {
        // If a base virtual path is specified, we should remove it as a prefix. Everything that follows should map to a view file on disk.
        if (!String.IsNullOrEmpty(baseVirtualPath))
        {
            virtualPath = "~/" + virtualPath.Substring(baseVirtualPath.Length);
        }

        string path = HostingEnvironment.MapPath(virtualPath);
        return File.Exists(path) && File.GetLastWriteTimeUtc(path) > assemblyLastWriteTime.Value;
    }
    return false;
}

					

And when looking at the detailed trace data the client had captured, the server in question was spending a considerable amount of time in the File.Exists and File.GetLastWriteTimeUtc methods that this calls. No individual call took much time, but there were a lot of them.

Looking at the views in the client's site, it was clear that they were not pre-compiling their views, and that their pages were broken up into a large number of nested view files that were largely included as partials. I wanted to try and determine if this issue was specific to the client's code or whether it was a more general issue, so I spent some time attempting to recreate the issue in a blank instance of Sitecore 8.2 with just a couple of view files added.

The simplest model I could come up with was to have a View Rendering which called HTML.Partial() in a loop, to simulate a complex set of nested partial views. With this as a parent view:

<h2>Using Partials</h2>

@for (int i = 0; i < 100; i++)
{
    @Html.Partial("~/views/ViewPerformanceTest/SimpleSubView.cshtml")
}

					

and this as the child partial view:

<div>View: @DateTime.Now.Ticks</div>

					

In order to verify that the issue was not specific to binding the child views as partials, I also tried a variation of the parent view that bound the children with Sitecore's ViewRendering() method:

@using Sitecore.Mvc
<h2>Using ViewRenderings</h2>

@for (int i = 0; i < 100; i++)
{
	@Html.Sitecore().ViewRendering("/views/ViewPerformanceTest/SimpleSubView.cshtml")
}

					

And with these deployed to a copy of the appropriate version of Sitecore, I tried to see if I could replicate the client's results. Which I could:

Views Trace

I tried a variety of tests, with compiled views present and not present, with UsePhysicalViewsIfNewer enabled and disabled, with view files present and not present, and using both Partial and ViewRendering bindings. Under v8.2 all of these were showing similar behaviour where > 40% of the time for each request was spent in this bit of RazorGenerator. As a comparison I also tried running the same views under a copy of V8.1 – where (unsurprisingly) these overheads were not visible.

So what can we learn from this? url copied!

To be honest, this confused me a bit, as my reading understanding of how this feature worked (based largely on the code above and Kam's post), suggested that the `UsePhysicalViewsIfNewer` switch should change the results. But having discussed this with Sitecore Support, their response was mostly focused on the comments below and "talk to the RazorGenerator developers" – so perhaps my understanding wasn't right...

Overall, what I've learned here is to remember that the startup-time improvements that RazorGenerator brings to v8.2 can come with a trade-off. You get rid of the compilation time overhead that you would have seen on startup in v8.1, but you get a small chunk of time spent testing the state of the cshtml file each time you reference a view.

From the perspective of the RazorGenerator project (which is aimed at "normal" MVC) this probably isn't a big thing. Most MVC pages don't include that many views, so the overhead isn't a major issue. But given that Sitecore pages can include many controllers and views for the individual UI components, and these can in turn include further child views, the small overhead per view can mount up. And it seems some projects with complex pages can add sufficient overhead for it to get noticeable.

So what can we do about this? Probably two key things:

  • Firstly, think about how you break your UI down into views. You may find that fewer view files offer a performance improvement as a trade-off against maintainability.
  • Secondly, make sure you make use of ouput caching wherever you can. When you bind a view using Sitecore's API methods rather than `Html.Partial()` you can avoid calling RazorGenerator at all if the output of the view has been cached.
Kamruz Jaman commented:

I think the article you are referring to is probably this: https://chrisperks.co/2018/03/22/hundreds-of-renderings-your-first-page-load-could-be-sloooow/

I've used RazorGenerator.MVC on several projects and have never noticed any issues (but we use Sitecore caching, so it's entirely possible that we just did not notice). The above article uses a different pre-compiler so wonder if the results would be different? One advantages of using a precompiler is compile-time errors being thrown instead of run-time errors (for example, if you deleted a property in a model that was still referenced in a view), so there are other benefits IMO (you can set MvcBuildViews=true but this massively increased build times for us).

I wonder what your overall execution time would be using Pre-compiled vs non-compiled views would be in **your project**, i.e. add the RazorGenerator.MVC nuget package to your solution, would the overall time to compiling your views less than the impact of the check. You should also try clearing out the Temporary ASP.NET Files folder in case that was causing issues with you being able to disable “UsePhysicalViewsIfNewer”. Just some thoughts anyway….

Editied to add: Two Three things:

Firstly, thank you Kamruz, the post that I was referring to was indeed this one.

Secondly I got asked if I was saying "don't use compiled views" – to which the answer is a resounding no. The points made by Chris in the post that jogged my memory (and Kam in his article on the topic) are perfectly valid. I'm discussing a specific edge case that you may see with complex sites that don't output cache much.

And more recently: In old comments, Ian points out that Sitecore Support have a patch for v9 that may help resolve problems related to this. If you're having issues, that may help...

↑ Back to top