Yatendra Khandelwal

May 02

Easy tricks for improving performance of an ASP.Net MVC site

When creating an app in ASP.Net MVC and using IIS 7 to host it, there are some easy tricks that can be used to improve its performance. I have used these in Live Post http://livepost.in which is a news aggregator for India related news. A few of these suggestions might not be helpful for enterprise apps that are highly transactional and displaying latest data is more important than displaying 10 min old data but displaying it very fast.

1.  Enable caching static files

Doing this tells browsers that they can cache specified files and don’t need to download them from the server again when needed. To do this add following in web.config file

  <system.webServer>

    <caching>

      <profiles>

        <add extension=”.gif” policy=”CacheUntilChange” kernelCachePolicy=”DontCache” location=”Client” />

        <add extension=”.jpg” policy=”CacheUntilChange” kernelCachePolicy=”DontCache” location=”Client” />

        <add extension=”.png” policy=”CacheUntilChange” kernelCachePolicy=”DontCache” location=”Client” />

        <add extension=”.css” policy=”CacheUntilChange” kernelCachePolicy=”DontCache” location=”Client” />

        <add extension=”.js” policy=”CacheUntilChange” kernelCachePolicy=”DontCache” location=”Client” />

      </profiles>

    </caching>

    <staticContent>

      <clientCache cacheControlCustom=”public” cacheControlMaxAge=”30.00:00:00” cacheControlMode=”UseMaxAge” />

    </staticContent>

  </system.webServer>

2. Enable compression

GZip compression is supported by all modern browser. Enabling GZip compression reduces transfer size and thus reducing the latency. This can be done simply by adding following in web.config file - 

  <system.webServer>

    <urlCompression doDynamicCompression=”true” doStaticCompression=”true” />

    <httpCompression dynamicCompressionEnableCpuUsage=”80” />

    <staticContent>

      <remove fileExtension=”.js” />

      <mimeMap fileExtension=”.js” mimeType=”text/javascript” />

    </staticContent>

  </system.webServer>

3. Enable keep alive

This keeps the http connection open for future requests from same session and thus saving round trip time (RTT) and time spent in opening and closing connections. It can be done by adding following code in web.config file - 

  <system.webServer>

    <httpProtocol allowKeepAlive=”true” />

  </system.webServer>

4. Output caching

This might not be suitable to enterprise apps where displaying latest transactional  data is more important then displaying few min old data very fast. To do this define a profile by adding following xml snippet in web.config file and associate that profile with any action as follows - 

 <system.web>

    <caching>

      <outputCacheSettings>

        <outputCacheProfiles>

          <clear />

          <add name=”MyCacheProfile” duration=”300” />

        </outputCacheProfiles>

      </outputCacheSettings>

    </caching>

<system.web>

        [OutputCache(CacheProfile = “MyCacheProfile”)]

        public ActionResult Index()

        {

            ….

            return View();

        }

5. Refreshing cache in background thread

This can only be applied where there is finite and know set of items that need to be cached. For example in live post there is a caching layer between database and app server and there is fixed number of news categories and subcategories. In the app I run a thread in the background that refreshes the in memory cache with data for these news categories. To avoid using threading code I have used CacheItemRemovedCallback provided by HttpRuntime.Cache to get a call back after  specified interval and refresh the cache on receiving callback. It all starts on Applicaiton_Start() method in Global.asax file that is called when the application starts in the web server.

protected void Application_Start()

{

       …..

       RegisterCallback();

}

private void RegisterCallback()

{

            HttpRuntime.Cache.Add(“expiryKey”, “any value”, null, DateTime.MaxValue, TimeSpan.FromMilliseconds(60000), CacheItemPriority.Normal, new CacheItemRemovedCallback(MyCallback));

}

private void MyCallback(string key, object value, CacheItemRemovedReason reason)

{

            RefreshCache();

            RegisterCallback();

}

Dec 05

Handling sub-domains in ASP.Net MVC

In SaasApp which is framework for mulituser-multitenant SAAS framework for ASP.Net MVC, what I wanted was that when user types the url without a subdomain my app should display the website where user can get details about the product and plans, select a plan and register an account (in this post account refers to say a company that can have multiple users) with selected plan. Once an account is created their users should be able to go to their selected subdomain like myaccount.[SAAS app domain].com, login and access the product. I wanted the subbdomain “myaccount” to be forwarded as a parameter to required actions in the controller to identify the account I am dealing with. 

Demo: http://saasapp.yatendra.com

ASP.Net MVC has a sophisticated and flexible routing mechanism. It allows developers to create their own customized routing system by extending RouteBase class and providing your own implementation of GetRouteData(HttpContextBase) function.

Following is what I have used - 

using System;

using System.Collections.Generic;

using System.Linq;

using System.Web;

using System.Web.Mvc;

using SaasApp.Utility;

namespace System.Web.Routing

{

    public class DomainRoute : RouteBase

    {

        public override RouteData GetRouteData(HttpContextBase httpContext)

        {

    //get subdomain, the string before first dot in the url

            string subdomain = UtilityHelper.GetSubdomain(httpContext.Request.Headers[“HOST”]);

            RouteData routeData = new RouteData(this, new MvcRouteHandler());

            if (!string.IsNullOrEmpty(subdomain))

            {

//url has subdomain

                routeData.Values.Add(“account”, subdomain);

                string filepath = httpContext.Request.FilePath;

                string[] parts = filepath.Split(‘/’);

                switch (parts[1].ToLower())

                {

                    case “app”:

                        routeData.Values.Add(“controller”, “App”);

                        if (parts.Length > 2)

                        {

                            switch (parts[2].ToLower())

                            {

                                case “index”:

                                    routeData.Values.Add(“action”, “Index”);

                                    break;

                                case “about”:

                                    routeData.Values.Add(“action”, “About”);

                                    break;

                                default:

                                    routeData.Values.Add(“action”, “Index”);

                                    break;

                            }

                        }

                        else

                        {

                            routeData.Values.Add(“action”, “Index”);

                        }

                        break;

                    case “account”:

                        routeData.Values.Add(“controller”, “Account”);

                        if (parts.Length > 2)

                        {

                            switch (parts[2].ToLower())

                            {

                                case “login”:

                                    routeData.Values.Add(“action”, “Login”);

                                    break;

                                case “logout”:

                                    routeData.Values.Add(“action”, “Logout”);

                                    break;

                            }

                        }

                        break;

                    default:

                        if (httpContext.Request.IsAuthenticated)

                        {

                            httpContext.Response.Redirect(“/App/Index”);

                        }

                        httpContext.Response.Redirect(“/Account/Login”);

                        break;

                }

            }

            else

            {

//url does not have subdomain

                string filepath = httpContext.Request.FilePath;

                string[] parts = filepath.Split(‘/’);

                switch (parts[1].ToLower())

                {

                    case “home”:

                        routeData.Values.Add(“controller”, “Home”);

                        if (parts.Length > 2)

                        {

                            switch (parts[2].ToLower())

                            {

                                case “index”:

                                    routeData.Values.Add(“action”, “Index”);

                                    break;

                                case “plans”:

                                    routeData.Values.Add(“action”, “Plans”);

                                    break;

                                case “selectplan”:

                                    routeData.Values.Add(“action”, “SelectPlan”);

                                    if (parts.Length > 3)

                                    {

                                        routeData.Values.Add(“planType”, parts[3]);

                                    }

                                    else

                                    {

                                        routeData.Values.Add(“planType”, “1”);

                                    }

                                    break;

                                case “selectaplan”:

                                    routeData.Values.Add(“action”, “SelectAPlan”);

                                    break;

                                case “accountcreated”:

                                    routeData.Values.Add(“action”, “AccountCreated”);

                                    if (parts.Length > 3)

                                    {

                                        routeData.Values.Add(“accountName”, parts[3]);

                                    }

                                    else

                                    {

                                        routeData.Values.Add(“accountName”, “”);

                                    }

                                    break;

                                case “about”:

                                    routeData.Values.Add(“action”, “About”);

                                    break;

                                default:

                                    routeData.Values.Add(“action”, “Index”);

                                    break;

                            }

                        }

                        else

                        {

                            routeData.Values.Add(“action”, “Index”);

                        }

                        break;

                    default:

                        httpContext.Response.Redirect(“/Home/Index”);

                        break;

                }

            }

            return routeData;

        }

        public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)

        {

            //Implement your formating Url formating here

            return null;

        } 

    }

}

Oct 13

Compressing HTML output in ASP.Net / ASP.Net MVC

Most modern browsers have capability to get output from webservers in compressed form. This reduces number of bytes to be transferred and thus making transmission faster. Browsers that support this functionality specify supported compression type as value of “Accept-Encoding” attribute for example “Accept-Encoding: gzip, deflate”. gzip and deflate being two most commonly supported compression types. This can be configured at web server level but if you want to do have a lot of control over we server configuration like in shared hosting environment you can implement IHttpModule and set it up for your application as follows - 

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Web;

using System.IO.Compression;

namespace MyPackage

{

    public class HttpCompressionModule : IHttpModule

    {

        #region IHttpModule Members

        void IHttpModule.Dispose()

        {            

        }

        void IHttpModule.Init(HttpApplication context)

        {

            context.PostAcquireRequestState += new EventHandler(context_PostAcquireRequestState);

            context.EndRequest += new EventHandler(context_EndRequest);

        }

        void context_EndRequest(object sender, EventArgs e)

        {

            HttpApplication context = sender as HttpApplication;

            context.PostAcquireRequestState -= new EventHandler(context_PostAcquireRequestState);

            context.EndRequest -= new EventHandler(context_EndRequest);

        }

        void context_PostAcquireRequestState(object sender, EventArgs e)

        {

            this.RegisterCompressFilter();    

        }

        private void RegisterCompressFilter()

        {

            HttpContext context = HttpContext.Current;

            if (context.Handler is StaticFileHandler 

                || context.Handler is DefaultHttpHandler) return;

            HttpRequest request = context.Request;            

            string acceptEncoding = request.Headers[“Accept-Encoding”];

            if (string.IsNullOrEmpty(acceptEncoding)) return;

            //if (request.FilePath.EndsWith(“.ashx”)) return;

            if (request.FilePath.Contains(“.”))

            {

                return;

            }

            acceptEncoding = acceptEncoding.ToUpperInvariant();

            HttpResponse response = HttpContext.Current.Response;

            if (acceptEncoding.Contains(“GZIP”))

            {

                response.AppendHeader(“Content-encoding”, “gzip”);

                response.Filter = new GZipStream(response.Filter, CompressionMode.Compress);

            }

            else if (acceptEncoding.Contains(“DEFLATE”))

            {

                response.AppendHeader(“Content-encoding”, “deflate”);

                response.Filter = new DeflateStream(response.Filter, CompressionMode.Compress);

            }

        }

        #endregion

    }

}

Oct 04

MongoDB @ KhojKhabar

(Migrated from old wordpress blog post dated 7/12/2011)

These days NoSQL databases are getting very popular. These databases don’t have a SQL interface for querying data and a lot of times don’t store data as collections of rows and tables. MongoDB is one such database that stores data in form of BSON(a form of JSON) documents and collections. The data in MongoDB can be queried using a REST API. It has a .Net driver available and there are a couple of hosted MongoDB cloud hosted providers with a free plan so I thought I will try to convert KhojKhabar data store from SQLite to MongoDB.

Out of MongoHQ and MongoLab I decided to use MongoLab as it allows 240 MB (vs 16 MB by MongoHQ). MongoLab also provides an option to host your MongoDB instance on Amazon or Rackspace cloud. So if your app is hosted on one of these providers you can reduce latency by hosting MongoDB on the same provider. Moving data storage of khojkhabar to MongoDB was not that difficult as the driver usage is like a combination of dataset style programming and fluid function calls (like used in subsonic 2), so someone with experience these can pick it up easily. There is also a good tutorial available at MongoDb.org, which was very helpful. Now the live khojkhabar site is using MongoLab as the data store.In khojkhabar right now I store the news data of last 10 days only so I think 240 MB storage quota should be fine for now.

AppHarbor - Heroku like service for ASP.Net

(Migrated from old wordpress blog post dated 2/17/2011)

I recently came across AppHarbor. It looks like another interesting startup to come out of  YCombinator. Its a Heroku like service that builds, runs unit tests and deploys your ASP.Net code for you. It works with Git so to upload the code all you need to do is git push. They provide shared and dedicated databases with option of MS SQL server and MySQL.  Whats cool is that they provide a free application instance per applicaiton. So you pay only if your app grows to need another instance. Now if you need to test or host your next ASP.Net MVC app you know where to go.

Tonight I plan to play with it and see how it works for an ASP.Net MVC app I have been working on using Subsonic and SQLite. Details later.

jQuery ListCollapse

(Migrated from old wordpress blog post dated 1/9/2011)

This plugin is meant to collapse/expand a list of items that grows beyond certain predefined number of items. It filters and collapses a list when data is bound, added or removed. If number of items are more than specified number n it displays first n items and displays an expand(customizable) link. When a user clicks on this link it displays the entire list and changes this link to collapse(customizable).

Download listcollapse

(Note: This plugin is based on Collapsorz 1.1 created by Aaron Kuzemchak. I have customized it to be able to be called whenever an item is added or removed and some minor modifications is think were required for dynamic list where items are added and removed on the fly)

Usage

$(listelement).listcollapse();

$(listelement).listcollapse(options);

Options

(All parameters are optional)

toggle

To select elements that you wish to collapse. By default it will collapse all direct children of the list selected.

maximum

Maximum number of elements to show. Collapses all items beyond this number. It defaults to 5.

showText

Text to be shown for the expand link. It defaults to Show.

hideText

Text to be shown for the collapse link. It defaults to Hide.

linkLocation

Whether to display expand/collapse link before or after the selected list. You can choose one of the following settings:

defaultState

Whether the list should be displayed expanded or collapsed by default.

wrapLink

HTML to wrap around the link.

Load balancing

(Migrated from old wordpress blog post dated 12/22/2010)

I read an interesting article today about load balancing. Its a must read for anyone looking to scale a website or anyone just simple interested in knowing how these high traffic websites scale on the web server side. This documents explains different ways of doing it like hardware based approach and software based approach. It also explains various related factors like cookie persistence and using SSL certs in load balancing.

http://www.exceliance.fr/en/ART-2006-making%20applications%20scalable%20with%20LB.pdf

Adding a new volume to EC2 linux instance

(Migrated from old wordpress blog post dated 12/22/2010)

Our postgres EC2 linux instance ran out of disk space so I had to add another volume to the server. Following is what I did -

  1. Login to http://aws.amazon.com and go to EC2 tab.
  2. Go to volumes section and create a new volume.
  3. Enter the size of volume and select the availability zone. I generally skip creating a snapshot unless there is explicit need.
  4. Once the volume is created and its status is available, select it and click attach button. Select instance id of the instance to which it needs to be associated and map it to /dev/sda2
  5. Login to the server as root and execute following commands -
       mkfs -t ext3 /dev/sda2   #dont do it if creating from an existing snapshot
       echo "/dev/sda2  /disk2  ext3     noatime  0 0" >> /etc/fstab
       mkdir /disk2
       mount /disk2
       df -h #you should now see /disk2 as a new volume

Moving postgres data folder

(Migrated from old wordpress blog post dated 12/22/2010)

Our postgres server ran out of disk space on our EC2 server so we figured we should move the postgres data folder to a new bigger volume. We created an additional volume mounted to /disk2 and then moved the data folder as follows -

1. Login to shell as root. Stop the Postgres if running

$service postgresql-9.0 stop

2. Copy the data folder to new location

$cp -R /var/lib/pgsql/9.0/data  /disk2/pgdata/

3. Modify postgres startup script to point to new data directory

In  /etc/init.d/postgresql  file,  change the value of  PGDATA  variable to new location  which is /disk2/pgdata/data

4. Start the db server

$ service postgresql start

Setting up replication in Postgres 9.0

 (Migrated from old wordpress blog post dated 12/22/2010)

For picksie I had to set up streaming replication in Postgres 9.0. We have a master server which is used for read/write and a hot standby where we want the data to updated using streaming replication. Following is a summary of what I did

1. Install postgres on both master and standby

2. Allow standby server to connect to the master by editing pg_hba.conf

host replication postgres 192.168.0.20/22 trust

3. Setup streaming on master by updating postgresql.conf

wal_level = hot_standby

max_wal_senders = 5

wal_keep_segments = 32

archive_mode = on

archive_command = ‘cp %p /path_to/archive/%f’

4. Start postgres on master

5. Make base backup on master

$ psql -c “SELECT pg_start_backup(‘label’, true)”

$ rsync -a ${PGDATA}/ standby:/srv/pgsql/standby/ —exclude postmaster.pid

$ psql -c “SELECT pg_stop_backup()”

6. Do authentication and streaming related configuration in postgresql.conf and pg_hba.conf in standby server so that when needed it can be promoted as master with less effort.

7. Set standby server as a hot standby by updating postgresql.conf

hot_standby = on

8. Create recovery.conf with following configuration on standby server in same folder as postgresql.conf

standby_mode = ‘on’

primary_conninfo = ‘host=192.168.0.10 port=5432 user=postgres’

trigger_file = ‘/path_to/trigger’

restore_command = ‘cp /path_to/archive/%f “%p”’

Note: trigger file is the file, presence of which will make the standby server to stop replication and failover

9. Start postgres service on standby and it will start replication

Reference: http://wiki.postgresql.org/wiki/Streaming_Replication

$ $EDITOR postgresql.conf

listen_addresses = '192.168.0.10'

$ $EDITOR pg_hba.conf

# The standby server must have superuser access privileges.
host  replication  postgres  192.168.0.20/22  trust