An interesting side effect of compiled views

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

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:

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

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:

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?

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.


Editied to add: Two 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.

Advertisements

2 thoughts on “An interesting side effect of compiled views

  1. 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….

    • Yes – well remembered – thanks. That is indeed the one that reminded me. (And thanks Chris, for writing it)

      The “clear out temporary ASP.Net files” suggestion is interesting – I’ll put testing that on my task list, and see if that makes any difference. I need to spend some more time on this work…

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.