First call to Url.Action on a page is slow
Asked Answered
F

4

6

I have a performance issue with a fairly simple ASP.MVC view.

It's a log-on page that should be almost instant, but is taking about half a second.

After a lot of digging it looks like the problem is the first call the Url.Action - it's taking around 450ms (according to MiniProfiler) but that seems insanely slow.

Subsequent calls to Url.Action are taking <1ms, which is more in line with what I would expect.

This is consistent whether I use Url.Action("action", "controller") or Url.Action("action"), but doesn't seem to happen if I use Url.Content("~/controller/action"). This also happens when I call Html.BeginForm("action").

Does anyone have any idea what's causing this?

A dig into the source suggests that RouteCollection.GetVirtualPath might be the culprit, as that's common to both Url.Action and Html.BeginForm. However, surely that's used all over the place? I mean, ½ a second is far too slow.

I have 20 or so custom routes (it's a fairly large app with some legacy WebForms pages) but even then the times seem far too slow.

Any ideas how to fix it?

Fluoric answered 10/8, 2012 at 10:49 Comment(0)
F
5

Problem found, and it is with the routing tables (cheers Kirill).

Basically we have lots of routes that look something like this:

string[] controllers = GetListOfValidControllers();

routes.MapRoute(
    name: GetRouteName(),
    url: subfolder + "/{controller}/{action}/{id}",
    defaults: new { action = "Index", id = UrlParameter.Optional },
    constraints: new { controller = "(" + string.Join("|", controllers) + ")" });

It turns out that the Regex check is very slow, painfully slow. So I replaced it with an implementation of IRouteConstraint that just checks against a HashSet instead.

Then I changed the map route call:

routes.MapRoute(
    name: GetRouteName(),
    url: subfolder + "/{controller}/{action}/{id}",
    defaults: new { action = "Index", id = UrlParameter.Optional },
    constraints: new { controller = new HashSetConstraint(controllers) });

I also used the RegexConstraint mentioned in that linked article for anything more complicated - including lots of calls like this (because we have legacy WebForm pages):

routes.IgnoreRoute(
    url: "{*allaspx}", 
    constraints: new { allaspx = new RegexConstraint( @".*\.as[pmh]x(/.*)?") });

Those two simple changes completely fix the problem; Url.Action and Html.BeginForm now take a negligible amount of time (even with lots of routes).

Fluoric answered 13/8, 2012 at 15:50 Comment(0)
S
1

It seems to me that your problem is compiling of views. You need to precompile views on build and this problem will gone. details here

Sixgun answered 10/8, 2012 at 11:59 Comment(6)
That doesn't precompile views, that just compiles them after a build so you get build errors rather than run time errors. Also it didn't make any difference - I'm still seeing 450ms or so on the first Url.Action call.Fluoric
Proper precompilation is possible with ASPNet_Compiler.exe (see msdn.microsoft.com/en-us/library/ms229863(v=vs.80).aspx) but even without that the time's I'm seeing for Url.Action are crazy - it's almost like it's doing the full reflection to find the controller actions every time the page runs.Fluoric
Could you show your RegisterRoutes (from global.asax)? It can leverage on your time.Sixgun
I look in sources of framework. There are no reflection in this scenario, but there is potentially slow method GetVirtualPath.Sixgun
I'm seeing Html.BeginForm being slow in the same way, and it also calls RouteCollection.GetVirtualPath - that's hit all over the place, so I'm not sure why it's specifically slow here. I can't share the RegisterRoutes - it's a fairly large app with a lot of custom routes though.Fluoric
It was in the routes - not so much that there were a lot of them, but that we were using Regex constraints on them and these were very slow.Fluoric
A
1
    public class RegexConstraint : IRouteConstraint, IEquatable<RegexConstraint>
     {
    Regex regex;
    string pattern;

    public RegexConstraint(string pattern, RegexOptions options = RegexOptions.CultureInvariant | RegexOptions.Compiled | RegexOptions.IgnoreCase)
    {
        regex = new Regex(pattern, options);
        this.pattern = pattern;
    }

    public bool Match(System.Web.HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
    {
        object val;
        values.TryGetValue(parameterName, out val);
        string input = Convert.ToString(val, CultureInfo.InvariantCulture);
        return regex.IsMatch(input);
    }

    public string Pattern
    {
        get
        {
            return pattern;
        }
    }

    public RegexOptions RegexOptions
    {
        get
        {
            return regex.Options;
        }
    }

    private string Key
    {
        get
        {
            return regex.Options.ToString() + " | " + pattern;
        }
    }

    public override int GetHashCode()
    {
        return Key.GetHashCode();
    }

    public override bool Equals(object obj)
    {
        var other = obj as RegexConstraint;
        if (other == null) return false;
        return Key == other.Key;
    }

    public bool Equals(RegexConstraint other)
    {
        return this.Equals((object)other);
    }

    public override string ToString()
    {
        return "RegexConstraint (" + Pattern + ")";
    }
}
Aulos answered 12/9, 2012 at 8:49 Comment(1)
That's another useful implementation, I was using the one from samsaffron.com/archive/2011/10/13/…Fluoric
M
0

I've stripped it to "bare-bones"... set a single file into memory and downloading it from the action compared to downloading it from IHttpModule. IHttpModule is much faster (for small files, e.g. product list images) for some reason (probably MVC pipeline load, routing). I don't have regex used in routing (that slows it even more). In IHttpModule I am reaching the same speeds as if URL is pointing to file on a drive (of course that is if the file is on the drive but not on the drive location that URL points to).

<system.webServer>
  <modules runAllManagedModulesForAllRequests="true">
     <add name="ImagesHandler" type="NsdMupWeb.ImagesHttpModule" />
  </modules>
</system.webServer>


//Code is made for testing
public class ImagesHttpModule : IHttpModule
{
    public void Dispose()
    {
    }

    public void Init(HttpApplication context)
    {
        context.BeginRequest += Context_BeginRequest;
    }

    private void Context_BeginRequest(object sender, EventArgs e)
    {
        var app = (HttpApplication)sender;
        if (app.Request.CurrentExecutionFilePathExtension.Length > 0)
        {
            var imagePathFormated = "/image/";
            var imagesPath = app.Request.ApplicationPath.TrimEnd('/') + imagePathFormated;
            if (app.Request.CurrentExecutionFilePath.StartsWith(imagesPath))
            {
                var path = app.Request.CurrentExecutionFilePath.Remove(0, imagesPath.Length);
                var parts = path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
                if (parts.Length > 1)
                {
                    var ms = new MemoryStream();
                    Stream stream;

                    stream = System.IO.File.OpenRead(@"C:\Programming\Sindikat\Main\NsdMupWeb\Files\Cached\imageFormatProductList\1b1e2671-a365-4a87-97ba-063cf51ac34e.jpg");
                    var ctx = ((HttpApplication)sender).Context;
                    ctx.Response.ContentType = MimeMapping.GetMimeMapping(parts[1]);
                    ctx.Response.Headers.Add("last-modified", new DateTime(2000, 01, 01).ToUniversalTime().ToString("R"));

                    byte[] buffer = new byte[stream.Length / 2];
                    stream.Read(buffer, 0, buffer.Length);
                    ctx.Response.BinaryWrite(buffer);

                    buffer = new byte[stream.Length - buffer.Length];
                    stream.Read(buffer, 0, buffer.Length);
                    ctx.Response.BinaryWrite(buffer);
                    ctx.Response.End();
                }
            }
        }
    }
}
Musca answered 5/12, 2019 at 21:59 Comment(4)
Hi, thanks but this question already has an answer - we found a solution back in 2012 and it was due to the regex used to parse the routes.Fluoric
Ok, but it's even faster, it might help someone else who needs it even faster, e.g. for item list with images not residing in same URL on drive. Not even new MVC project with one route is not as fast.Musca
The accepted answer applied to the whole routing table, this one call returns a static file. Yes, for the specific case when you want to rewrite the route to a static file this is quicker than an MVC app, but my use case was an MVC app and I suspect there are much quicker methods again to route to a static file.Fluoric
I wanted to stay in MVC also but couldn't find a faster solution. MVC is an additional layer on top of this it hardly can be as fast.Musca

© 2022 - 2024 — McMap. All rights reserved.