Tips & Tricks: Improving the performance of your website–Pt. 2
Continuing my previous article on website performance tuning, today I'll talk about how you can achieve better performance at the server side of things. This part will be reflected in the time it takes to create the html that is sent in response to the initial http request. If this takes too long, all the previous optimizations with regards to reducing scripts, images, styles, etc., will seem irrelevant. The final goal with this whole exercise is to serve the page and have it be ready for the user to operate before he really notices the lag, becoming effectively instant. I personally try to aim for a 300ms load time for the html page itself; this seems to give a very smooth experience.
You should start out by setting a goal yourself, so you won't be spending time optimizing pages that are actually fast enough. It might be a good idea to distinguish between read-only requests and requests that alter the state of the server. The latter type of request will often require a lot of extra validation and generally do not happen as often. Depending on the standard workflows in your system, you might want to have different goals for these types of requests.
Application caching
In the previous article, I showed some code that I mentioned would be explained today. This is a caching mechanism I've been using in web applications for a while. There are near infinite ways to perform caching in your application, but this is my take. The purpose is to avoid database queries as much as possible, keeping results in memory and invalidating them when they grow stale.
My implementation is a wrapper over the ASP.NET caching mechanism but the implementation details can be swapped as needed without changing the interface. For ease of use, I prefer to have a switch in the web.config to toggle whether the feature is enabled. You can see the complete cache provider below.
- public static class CacheProvider
- {
- private static bool _enableCache;
- static CacheProvider()
- {
- // Implement as you like
- _enableCache = CacheConfiguration.Enabled;
- }
- /// <summary>
- /// A cache duration of one year.
- /// </summary>
- public const int Forever = 31556926;
- public static TQuery CacheQuery<TQuery>(Func<TQuery> performQuery, string cacheKey, object cacheLockObject, int duration)
- where TQuery : class
- {
- if (!_enableCache)
- return performQuery();
- var result = HttpRuntime.Cache[cacheKey] as TQuery;
- if (result == null)
- {
- lock (cacheLockObject)
- {
- result = HttpRuntime.Cache[cacheKey] as TQuery;
- if (result == null)
- {
- result = performQuery();
- HttpRuntime.Cache.Insert(cacheKey, result, null, DateTime.Now.AddSeconds(duration), Cache.NoSlidingExpiration);
- }
- }
- }
- return result;
- }
- public static void InvalidateQueryCache(string cacheKey, object cacheLockObject)
- {
- if (!_enableCache)
- return;
- var result = HttpRuntime.Cache[cacheKey];
- if (result != null)
- {
- lock (cacheLockObject)
- {
- result = HttpRuntime.Cache[cacheKey];
- if (result != null)
- {
- HttpRuntime.Cache.Remove(cacheKey);
- }
- }
- }
- }
- }
As shown in the last code listing in the first article, the following line of code is an example of how to use the provider.
- return CacheProvider.CacheQuery(query, cacheKey, _cacheLockObject_Src, CacheProvider.Forever);
The cache looks if there is an entry stored for the specified key. If there is, it is returned, and if not, the supplied lambda query is executed. A simple Action lambda to represent queries allows us to cache any type of operation, making it very flexible. Removing the dependency on the ASP.NET cache would even allow it to be used for caching algorithmic computations in a desktop application.
What I mainly use the cache for, is as an intermediary layer over my service layer, making both sides unaware of the caching by using inheritance and virtual methods. Steve Smith has a couple of posts where he describes the Cached Repository pattern that has inspired my own implementations.
Compiled queries
When using the Entity Framework ORM for more complex systems, I have seen many cases where parsing the LINQ query and building the SQL query takes far longer than executing the query itself. Using compiled queries, you can reduce this problem by storing the query in memory after it has been executed the first time, though at the price of a slightly longer compilation time than normal. The syntax is not exactly elegant, so I prefer to isolate each compiled query in its own class. This also yields another advantage, as I will describe in the tip on application warmup.
First, I've defined some base classes for making it easier to create new query types. Note that the base types define how many arguments the query takes, as generic arguments. Thus, separate bases classes are needed for no-argument queries, single-argument queries, etc.
- public abstract class CompiledQueryBase
- {
- protected MyEntityContext GetContext()
- {
- var context = new MyEntityContext();
- context.ContextOptions.LazyLoadingEnabled = false;
- return context;
- }
- }
- public abstract class CompiledQuery<T1, TResult> : CompiledQueryBase
- {
- protected MyEntityContext Context { get; private set; }
- protected abstract Func<MyEntityContext , T1, TResult> Query { get; }
- public CompiledQuery(MyEntityContext context)
- {
- Context = context;
- }
- public TResult Execute(T1 arg1)
- {
- return Query(Context, arg1);
- }
- }
- public abstract class CompiledCollectionQuery<T1, TResult> : CompiledQueryBase
- {
- protected MyEntityContext Context { get; private set; }
- protected abstract Func<MyEntityContext , T1, IQueryable<TResult>> Query { get; }
- public CompiledCollectionQuery(MyEntityContext context)
- {
- Context = context;
- }
- public IQueryable<TResult> Execute(T1 arg1)
- {
- return Query(Context, arg1);
- }
- }
As a side note, I always disable lazy loading on my context as this will prevent the whole category of n+1 issues associated with ORMs.
As a sample on how to implement a concrete compiled query, take this code from a project I'm working on:
- public class ImprovementDeadlinesCompiledQuery : CompiledCollectionQuery<int, int, string, Event>
- {
- protected override Func<QmsContext, int, int, string, IQueryable<Event>> Query { get { return _getImprovementDeadlines; } }
- private static readonly Func<QmsContext, int, int, string, IQueryable<Event>> _getImprovementDeadlines = CompiledQuery.Compile(
- (QmsContext context, int month, int year, string username) =>
- from i in context.Improvements
- where !i.IsClosed &&
- !i.Recipients.Any(r => r.RecipientUser.UserName == username && r.IsCheckedOut) &&
- (i.Recipients.Any(r => r.RecipientUser.UserName == username && !r.IsCheckedOut) || i.Recipients.Any(r => r.RecipientGroup.Members.Any(u => u.UserName == username)))
- where i.DueBy.Year == year && i.DueBy.Month == month
- group i by i.DueBy.Day into g
- select new Event
- {
- Day = g.Key,
- Entries = g.Select(s => new EventEntry
- {
- Title = s.Subject,
- ActionId = s.Id
- })
- }
- );
- public ImprovementDeadlinesCompiledQuery(QmsContext context)
- : base(context)
- { }
- }
- //Usage
- var items = new ImprovementDeadlinesCompiledQuery(context).Execute(month, year, username);
While the performance improvements can be quite significant with this technique, keep in mind that the first user loading a page with a compiled query will still get the slow performance. Luckily, there is a fix for this as described in the following tip. Oh, and another minor detail – in the upcoming version of EF, all queries will be compiled by default.
Application warmup
A common problem I see is that while caching and other techniques can make warm page requests very fast, invariably, some poor user has to pay the tax for hitting a cold page. Like most other problems, this one can be worked around, and to do this, we need to rely on a little bit of IIS functionality. As it turns out, IIS has a feature that allows you to automatically start the site and run a method for accepting http requests. In this method, you can preload caches and perform other long-running tasks. Scott Gu has a nice article on the subject, but the main point is that you have a class that implements the System.Web.Hosting.IProcessHostPreloadClient interface and update the applicationHost.config file to point to the namespace of the class that implements it.
One of the things I use this technique for, is to preload all the compiled queries I have created. I simply iterate over all the types in my assembly and look for classes implementing an interface called ICompiledQuery with the single method void Preload(). To support this, I update the compiled query base classes I showed in the previous tip, like this:
- public abstract class CompiledQueryBase : ICompiledQuery
- {
- public abstract void Preload();
- protected MyEntityContext GetContext()
- {
- var context = new MyEntityContext();
- context.ContextOptions.LazyLoadingEnabled = false;
- return context;
- }
- }
- public abstract class CompiledQuery<T1, TResult> : CompiledQueryBase
- {
- protected MyEntityContext Context { get; private set; }
- protected abstract Func<MyEntityContext , T1, TResult> Query { get; }
- public CompiledQuery(MyEntityContext context)
- {
- Context = context;
- }
- public TResult Execute(T1 arg1)
- {
- return Query(Context, arg1);
- }
- public override void Preload()
- {
- try { Query(GetContext(), default(T1)); }
- catch { };
- }
- }
This allows me to do the following in the warmup module:
- public class Warmup : IProcessHostPreloadClient
- {
- public void Preload(string[] parameters)
- {
- //Preload all compiled queries
- FindAndLoadCompiledQueries();
- }
- public static void FindAndLoadCompiledQueries()
- {
- var compiledQueries = typeof(CompiledQueryBase).Assembly.GetTypes().
- Where(t =>
- typeof(ICompiledQuery).IsAssignableFrom(t) &&
- t.IsClass &&
- !t.IsAbstract
- );
- foreach (var queryType in compiledQueries)
- {
- var query = (ICompiledQuery)Activator.CreateInstance(queryType, (QmsContext)null);
- query.Preload();
- }
- }
- }
Micro ORMs
Sometimes compiled queries are simply not enough for meeting those performance requirements and you have to turn to more drastic means. I recently discovered a new category of tools for just this occasion – the micro ORMs. These tools are a breed of ORMs that I've seen cropping up recently and there are a number of them to choose from. Essentially, they're a thin wrapper over plain SQL that makes it easier to build queries and serialize the result into objects. Their claim to fame is naturally their performance, and generally they seem to add only a few percent of overhead to vanilla SQL.
Sam Saffron helped develop the Dapper ORM for Stack Overflow and they use it to replace Linq 2 SQL code in select places where performance is not satisfactory. Stack Overflow is a very high-traffic site, so the fact that they need tools such as this does not necessarily mean it makes sense for you. But it's good to know that the tools are out there if you need them. Head over and read the post on Sam's blog for more background on the Dapper project and what they use it for.
Remove unused view engines
A simple thing, but it makes rendering of views a bit faster. By default, MVC registers view engines for both the new Razor views and the old Web Forms views. This means that the types of files that will be searched for in the view folder includes extensions that are irrelevant to you unless you use both types of views in the same project. To fix this, go into the Global.asax file and add the following code in the OnApplicationStarted method:
- ViewEngines.Engines.Clear();
- ViewEngines.Engines.Add(new RazorViewEngine());
MVC Mini Profiler
A nifty little tool, from the same people that brought you the Dapper micro ORM, is the MVC Mini Profiler. It's a light-weight profiler that is designed to remain in the running code even after deploying to production. This allows developers to go in and get detailed performance metrics in a running system. Each time a new page request is made, including Ajax calls, a small box appears, allowing you to see the result of the profiling session directly in your browser. This is immensely useful, helping you to detect problems even when you aren't looking for them. Even better, if you hook it up to your database, it will profile SQL queries as well, highlighting duplicate queries and separating the time spent in the database from that spent in the application code. Yes, it even shows you the actual SQL queries that were executed against the database.
This great little tool has become a cornerstone in my performance optimization work. All you have to do to get started is to install the Nuget package for MVC 3 project integration and fill out the blanks. Get it while it's hot.
That's it for now. There are a million other ways to optimize your websites but I find these interesting and not immediately obvious. So there you go.
Comments