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?

Keith
  • 150,284
  • 78
  • 298
  • 434

4 Answers4

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).

Keith
  • 150,284
  • 78
  • 298
  • 434
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

Kirill Bestemyanov
  • 11,946
  • 2
  • 24
  • 38
  • 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. – Keith Aug 10 '12 at 13:03
  • Proper precompilation is possible with ASPNet_Compiler.exe (see http://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. – Keith Aug 10 '12 at 13:36
  • Could you show your RegisterRoutes (from global.asax)? It can leverage on your time. – Kirill Bestemyanov Aug 10 '12 at 14:19
  • I look in sources of framework. There are no reflection in this scenario, but there is potentially slow method GetVirtualPath. – Kirill Bestemyanov Aug 10 '12 at 14:26
  • 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. – Keith Aug 13 '12 at 08:34
  • 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. – Keith Aug 13 '12 at 15:51
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 + ")";
    }
}
  • That's another useful implementation, I was using the one from http://samsaffron.com/archive/2011/10/13/optimising-asp-net-mvc3-routing – Keith Sep 13 '12 at 08:53
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();
                }
            }
        }
    }
}
Hrvoje Batrnek
  • 535
  • 1
  • 5
  • 15
  • 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. – Keith Dec 11 '19 at 09:30
  • 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. – Hrvoje Batrnek Dec 11 '19 at 13:11
  • 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. – Keith Dec 11 '19 at 19:39
  • 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. – Hrvoje Batrnek Dec 11 '19 at 23:50