The other day I succumbed to the urge to upgrade my customized version of MVC Music store from ASP.Net MVC3 to MVC4. I had just finished watching Steve Sanderson’s excellent TechDays 2012 session, C#5, ASP.NET MVC 4, and asynchronous Web applications (if you’re doing ASP.Net MVC and haven’t watched it, queue it up, awesome presentation). During the presentation, Steve used Apache Bench to show the improvements as he upgraded his small project from synchronous MVC4 to asynchronous MVC4, which got me wondering what effect MVC4 and Asynchronous MVC4 would show during the wcat load test step in my MVC Music Store build process.

The Upgrade to MVC4

Upgrading from MVC3 to MVC4 was easy. I followed the instructions from the MVC4 release notes and committed my changes.

I installed Visual Studio 2012 on my build server (including .Net 4.5) so the build would run. Installing a copy on build servers is allowed under visual studio licensing models and while some people try to create a ‘pure’ non-VS build experience, I think the only advantage one gets from this model is ensuring the production application is compiled in a different manner from the one the developers built and tested under.

VS upgraded, .Net upgraded, MVC upgraded, time to commit and push.

Build running, deploying for smoke test, failed. Page Won’t Load.

Still Copying MVC3 DLLs to Build Output

The CI build step runs the build and creates a publishable package that will be archived for all later build stages and final deployment. The last step of the CI stage deploys the package to a VM and does an HTTP GET on the front page, just to ensure it’s deployable. Turns out the site deployed fine, but the front page refused to display.

Project foldersDeploying MVC3 early on took advantage of a special folder named _bin_deployableAssemblies, so MSBuild was overwriting the MVC4 assemblies with the contents of this folder (MVC3) and then giving me errors on the razor stack when I tried to actually load the page. The build server didn’t mind because it was using either GAC’d or Reference Assemblies instead of the local DLLs.

I initially delete the folder to fix this, which turned out to be overkill because I also had SQL Server CE assemblies in there. The final result was to clear out just the MVC3 assemblies from the folder, leaving the SQL Server CE ones.

I ran several builds through the load test step, with interesting results that I’ll show at the bottom.

The Upgrade to Asynchronous

I’ll be the first to call my changes hacky. In a prior post, I switched SQL CE for full SQL Server to fix a performance bottleneck. With the known bottleneck removed, I wasn’t sure what the current one was or if Asynchronous MVC would improve things further, but I had high hopes.

You can see the final result on BitBucket, but here’s a sample of the quick and dirty surgery:

MVCMusicStore.Main / MvcMusicStore / Controllers / HomeController.cs (see file)

Text
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using MvcMusicStore.Models;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
 
namespace MvcMusicStore.Controllers {
        [SuppressMessage("Gendarme.Rules.Exceptions", "UseObjectDisposedExceptionRule", Justification = "If the controller is disposed mid-call we have bigger issues")]
        public class HomeController : ControllerBase {
 
                public HomeController() { }
                public HomeController(IMusicStoreEntities storeDb) : base(storeDb) { }
 
                //
                // GET: /Home/
                public async Task<ActionResult> IndexAsync() {
                        // Get most popular albums
                        var albums = await GetTopSellingAlbumsAsync(5);
                        
                        return View(albums);
                }
 
                private async Task<List<Album>> GetTopSellingAlbumsAsync(int count) {
                        // Group the order details by album and return
                        // the albums with the highest count
                        return await Task.Factory.StartNew<List<Album>>(() =>
                        {
                                return StoreDB.Albums
                                                                .OrderByDescending(a => a.OrderDetails.Count())
                                                                .Take(count)
                                                                .ToList();
                        });
                }
        }
}
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using MvcMusicStore.Models;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;

namespace MvcMusicStore.Controllers {
        [SuppressMessage("Gendarme.Rules.Exceptions", "UseObjectDisposedExceptionRule", Justification = "If the controller is disposed mid-call we have bigger issues")]
        public class HomeController : ControllerBase {

                public HomeController() { }
                public HomeController(IMusicStoreEntities storeDb) : base(storeDb) { }

                //
                // GET: /Home/
                public async Task<ActionResult> IndexAsync() {
                        // Get most popular albums
                        var albums = await GetTopSellingAlbumsAsync(5);
                        
                        return View(albums);
                }

                private async Task<List<Album>> GetTopSellingAlbumsAsync(int count) {
                        // Group the order details by album and return
                        // the albums with the highest count
                        return await Task.Factory.StartNew<List<Album>>(() =>
                        {
                                return StoreDB.Albums
                                                                .OrderByDescending(a => a.OrderDetails.Count())
                                                                .Take(count)
                                                                .ToList();
                        });
                }
        }
}

Each action has been converted to an async method returning a Task<ActionResult> and my ControllerBase object now inherits from AsyncController instead of Controller.

Because the query logic for this application is spread all over the place (which is common for entity framework in my experience), in some places I have tidy await calls to Async methods (like in the IndexAsync method above), and in some cases I have an inline await Task.Factory call (GetTopSellingAlbumsAsync action). Entity Framework 6 will have asynchronous Query and Save capabilities, but I personally don’t care much for Entity Framework, so it’s unlikely I’ll go through the effort of upgrading this project from EF4 to EF6. In the meantime, I have added my own SaveAsync call to the IMusicStoreEntities interface and DbContext implementation and have added some dirty await/async calls throughout the codebase.

Final result?

Graphs and Numbers, woohoo!

So what happened when I upgrade to MVC4? faster, slower, … ? And how much faster did Asynchronous actions make it? 1 order of magnitude? 2 orders?

What happened was object lessons in why having load measurements are useful and why identifying your performance bottlenecks is critical, instead of treating asynchronous actions as magic pixie dust.

Load test - Request Rates

On the left side of this graph you can see that the MVC 3 rate has averaged 75 requests/second with a pretty high level of variability. Switching to MVC4 moved us to a much more consistent 95-100 requests/second average, then throwing async in the mix reduced the rate to the 80′s. In each case, the requests are all of the files our browser would request when we browse the index page, select a genre to the genre page, select an album to the album page, and then check out, so some of this is dynamic content and some is static image and script files.

Load test - Response Times

The other interesting number is the lack of difference in Response Times. Somehow MVC4 unlocked more throughput for the system without impacting the Response Time for those requests. There was also no change at all in Response Times at all after adding Async actions, something that underlines and bolds the fact that the database calls were not the performance bottleneck for these test VMs.

I Should Upgrade to MVC4 Now!

Well, yes, you should, but don’t expect a matching 30% performance improvement. This improvement was in my test systems which are all VMs residing on the same Hyper-V server, running simulated loads that don’t match what real traffic would look like on a real site with real networks and real servers. That being said, the point of the load test step is to let me know when something significant occurs and there was definitely a measurable, positive performance improvement when I performed the upgrade. Given the fact that I don’t have a pouch of magic pixie dust, I have to assume that either MVC 4 or .Net 4.5 made some improvements.

The moral of the async upgrade is to be aware of your assumptions. In the real world, had I made async-ified my codebase to get a performance improvement, I would have just added a great deal of extra code complexity for a net loss in performance. There was no science to my guess that async would make things faster, it was pure guesswork.