3

I try to precompile my Azure WebApplication, so the first hit doesn't take multiple seconds for every first hit of a page.

I have the following code in my WebRole.Run():

using ( var server_manager = new ServerManager() )
{
    var main_site               = server_manager.Sites[RoleEnvironment.CurrentRoleInstance.Id + "_Web"];
    var main_application        = main_site.Applications["/"];
    var main_application_pool   = server_manager.ApplicationPools[main_application.ApplicationPoolName];

    string physical_path = main_application.VirtualDirectories["/"].PhysicalPath;

    main_application["preloadEnabled"] = true;
    main_application_pool["autoStart"] = true;
    main_application_pool["startMode"] = "AlwaysRunning";

    server_manager.CommitChanges();

    Log.Info( "Building Razor Pages", "WebRole" );

    var build_manager = new ClientBuildManager( "/", physical_path );
    build_manager.PrecompileApplication();

    Log.Info( "Building Razor Pages: Done", "WebRole" );
}

It doesn't seem to throw any Exceptions and when I look into the log it takes about 55 seconds to do build_manager.PrecompileApplication().

Seems about correct to me.

Except that when I try to load the pages the first time it still takes a hit. If I look into the MiniProfiler I see specificly that the Find part takes long. I still suspect it is the compilation, because 1.5 seconds just to find a file seems a bit long to me.

enter image description here

Is there anything wrong with my approach above? Is there a way to check if the page is really compiled? And in the case it is compiled, what else could it be? And why o why is stuff so complicated..

Dirk Boer
  • 8,522
  • 13
  • 63
  • 111
  • Could you try the code from this question http://stackoverflow.com/q/10830044/57428 and see if it makes a difference? – sharptooth Jan 23 '14 at 06:32
  • The code in the questions seems to be the same, assuming the VirtualPath is "/". Or do you mean adding a third 'null' parameter, or extra ClientBuildManagerParameters? – Dirk Boer Jan 23 '14 at 09:36
  • No, I mean just enumerate all the `VirtualDirectories` of the application and use paths from them. There may be some stupid subtle differences compared to passing `/` directly. – sharptooth Jan 23 '14 at 09:44
  • Ahh ok! Good point, I'm going to give it a try. I'll let you know after my next upload. Thanks! – Dirk Boer Jan 23 '14 at 09:54
  • Doesn't seem to make a difference :/ in my log I see the only VirtualDirectory passing by is '/'. If you look at the MiniProfiler statistics, how big do you think the chance is that this is really caused by compilation, and not by other stuff like maybe (very) slow view finding that is cached the second time? – Dirk Boer Jan 23 '14 at 12:19
  • 1
    I'd say that about 1,5 seconds for compiling a view can be realistic (the precompiler is very slow) and wasting that much on just finding a file is not realistic. May it be that something causes precompiled views invalidation? Like perhaps their date-time is changed or something? Also is the site called "Web" in the service definition? – sharptooth Jan 23 '14 at 12:37
  • Good point about the 'Web'. To be sure I made it that now it just loops through serverManager.Sites.Applications.VirtualDirectories. Still the same problem though. Do you know any other ways to help me 'debug' this behaviour? – Dirk Boer Jan 23 '14 at 13:31
  • 1
    Maybe it's worth trying to rename the "site" in the service definition so that it isn't called "Web" because otherwise the site payload is duplicated (http://stackoverflow.com/q/11897132/57428) and that maybe causes the problem you observe. As a bonus your package gets smaller which makes uploads faster. – sharptooth Jan 23 '14 at 13:44
  • Thanks for that tip. The upload *does* seem to go a lot faster. Still the same problem though :/ do you know if the swapping of staging and production in a way could 'invalidate' the precompilation? – Dirk Boer Jan 23 '14 at 16:08
  • "swap VIP" is invisible to the deployment so it should not invalidate precompilation. Btw what if the latency you observe is because of JIT compilation and similar stuff? Will the first page request latency increase or stay the same if you remove the precompilation code? – sharptooth Jan 24 '14 at 06:20
  • To be sure I'll try it again today, but I think it is pretty identical. As you can see in the screenshot most of the performance gets lost in the Find View parts (about 6 seconds in total). The other parts also look a bit slower than normal (guess that's the JIT?), but that is still acceptable for me (1300ms instead of 7100ms) – Dirk Boer Jan 24 '14 at 07:17
  • Btw thanks a *lot* for all the help and time you've put in. I think the chances are pretty slim, but I *hope* I can give some help back one day ;) – Dirk Boer Jan 24 '14 at 07:21
  • LOL. From my perspective it looks like this: there's someone else (you) who does the same thing as I do and it works for me but not for him which means it can stop working for me at some moment. When you put it this way you immediately see it as not absolutely altruistic. – sharptooth Jan 24 '14 at 07:50
  • Hmm... I see one more difference compared to what I have: you have autostart enabled and it is being committed before precompilation which means the application pool will start immediately and before precompilation. What if you precompile first and then change and commit the configuration changes? – sharptooth Jan 24 '14 at 08:07
  • Ha! True altruistic people don't want to take credit for what they do - like you ;) but good point about the order, I thought it shouldn't make a difference as long as you don't hit it before it's compiled, but maybe it does in some strange way. Do you btw also do the compilation in you WebRole.cs, or in globals.asax? – Dirk Boer Jan 24 '14 at 08:28
  • I do it in the role `OnStart()`. – sharptooth Jan 24 '14 at 08:34
  • Hey sharptooth, I never got it too work. Must be some weird setting somewhere in my config. I posted my workaround hack in the answers. Thanks for all your help, I did learn quite a bit from it :) – Dirk Boer Jan 26 '14 at 13:37

2 Answers2

2

I never got it too work. Probably some weird Web.config / Azure / project setting that seems to be off, but I never found it.

Before you read further I recommend trying this approach first for your project: https://stackoverflow.com/a/15902360/647845. (because it's not dependent on a hack)

My workaround is relatively straightforward, I search for all Razor Views in the application, and than I just try to render them with a mocked ControllerContext, dismissing the results. Allmost all Views will throw an Exception, because of a nonvalid ViewModel, but that doesn't matter as long as the view got compiled in the first place.

private void PrecompileViews()
{
    string search_path = GetSearchPath();

    // Create a dummy ControllerContext - using the HomeController, but could be any controller
    var http_context = new HttpContext( new HttpRequest("", "http://dummy", ""), new HttpResponse(new StringWriter()) );
    http_context.Request.RequestContext.RouteData.Values.Add( "controller", "Home" );
    var controller_context = new ControllerContext( new HttpContextWrapper(http_context), http_context.Request.RequestContext.RouteData, new HomeController() );

    // loop through all views, and try to render them with the dummy ControllerContext
    foreach ( var file in Directory.GetFiles(search_path, "*.cshtml", SearchOption.AllDirectories) )
    {
        string relative_view_path = "~" + file.Replace( search_path, "", StringComparison.InvariantCultureIgnoreCase );

        var view_result = new ViewResult { ViewName = relative_view_path };

        try
        {
            view_result.ExecuteResult( controller_context );
        }
        catch (Exception)
        {
            // Almost all views will throw exceptions, because of a non valid ViewModel, 
            // but that doesn't matter because the view stills got compiled
        }
    }
}

GetSearchPath returns the root directory of your application:

private string GetSearchPath()
{
    string bin_path = Path.GetDirectoryName( Assembly.GetExecutingAssembly().GetName().CodeBase );
    string local_path = new Uri(bin_path).LocalPath;
    return Path.GetFullPath( Path.Combine( local_path, ".." ) );
}

And I call it all at the end of my Global.asax:

protected void Application_Start()
{
     [...]
     PrecompileViews();
 }
Community
  • 1
  • 1
Dirk Boer
  • 8,522
  • 13
  • 63
  • 111
  • `Application_Start()` is run every time the application pool recycles which is every 27 hours or something and also after something like 20 minutes of inactivity. – sharptooth Jan 27 '14 at 06:23
  • I have disabled the timeout (see: http://blog.smarx.com/posts/controlling-application-pool-idle-timeouts-in-windows-azure). The application pool recycle I have to look into, thanks! – Dirk Boer Jan 27 '14 at 08:41
  • There's a better way to run this code - craft a controller-action pair and invoke that by sending an HTTP request to self from inside role OnStart(). Check `HttpRequest.IsLocal` inside the action so that external folks can't trigger it. – sharptooth Jan 27 '14 at 09:02
  • Hi, sorry I just recently saw this comment. I tried to do that at first, but I couldn't figure out a way to invoke an url request on 'localhost'. As I want this to work off course on the staging environment, (and make it as resilient as possible) I don't want to do it on some defined IP/domain. http://localhost/trigger didn't seem to work. – Dirk Boer Feb 02 '14 at 10:21
  • The `Site` object (obtained via `ServerManager`) will have `Bindings` - you can enumerate them and find the one which has `Port` set to 80 and find the IP address under `BindingInformation` of that binding - that IP address is mapped onto localhost and can be used to make HTTP requests (as in `http://1.2.3.4/Magic`). – sharptooth Feb 03 '14 at 04:25
1

The razor generator can compile your views at save or build time. This way you upload your compiled views in an Assembly to azure.

http://razorgenerator.codeplex.com/

AAD
  • 380
  • 3
  • 9