Best ASP.NET Performance Winner For Data Binding - Hands Up To Response.Write()

Alik Levin     To achieve best performance you need to make decisions based on trade-off between coolness, coding productivity, and personal engineering values. I never thought I would be recommending my customer considering using old fashion Response.Write() in his Internet facing ASP.NET web application in order to significantly improve the application’s performance.

Customer Case Study

During load/stress testing customer’s ASP.NET web application we identified high CPU utilization (up to 90%). After quick investigation we noticed that %Time in GC performance counter is less than optimal. Our assumption was that the application uses memory allocation techniques that are less than optimal. From GC Performance Counters:

"First thing you may want to look at is “% Time in GC” ... What is a health value for this counter? It’s hard to say. It depends on what your app does. But if you are seeing a really high value (like 50% or more) then it’s a reasonable time to look at what’s going on inside of the managed heap."

Another resource we used is timeless patterns & practices’ Chapter 15 — Measuring .NET Application Performance:

.NET CLR Memory\% Time in GC

"…The most common cause of a high value is making too many allocations, which may be the case if you are allocating on a per-request basis for ASP.NET applications. You need to study the allocation profile for your application if this counter shows a higher value."

So we headed to looking into the code and this is what we found out.

Analysis

During performance code inspection we identified massive usage of collections. The collections were used to transfer the data between the logical layers and then the collections were transferred into datatables to be bindable for DataGrid (yes, it is .Net 1.1 app).

Eureka! We just spotted 3 performance anti-patterns. Massive memory allocation, massive loops, massive type conversions. I’ve shown it to 4 very respected professionals and everyone was saying the same – current situation is pure performance anti-pattern. Here are few suggestions that came up:

  • Bind collections directly to DataGrid eliminating additional memory allocations and loops.
  • Create Datatable directly from XML skipping collection creation step eliminating additional memory allocations and loops.
  • Use Xslt transformation transforming original Xml into Html table using Xslt elminating memory allocations and loops for both collections and datatables.
  • Use Response.Write() as it’s suggested by patterns & practices:

"Use the Response.Write method. It is one of the fastest ways to return output back to the browser."

Case close? Not really...

Secretly I’ve built Visual Studio 2003 project with these implementations and ran simple stress test using TinyGet utility. The results left us all a bit surprised.

Converting Collection To Datatable (Current Situation)

The code:

    1: //create custom collection
    2: MyCollection myCollection = (MyCollection)SampleServices.GenerateCollection(200);
    3:  
    4: //convert collection to datatable
    5: DataTable datatable = SampleServices.ConvertCollectionTableIntoDataTalbe(myCollection);
    6:  
    7: //bind datatalbe to dynamically created datagrid
    8: datagrid.DataSource = datatable;
    9: datagrid.DataBind();

 

The stress test:

 

tinyget.exe  -srv:192.168.50.68 -uri:/dynamiccontrolsloadingrelease/UseDataTable.aspx -loop:100 -threads:15

 

The result:

 

image

Bind Collection Directly To Grid

The code:

    1: MyCollection myCollection = (MyCollection)SampleServices.GenerateCollection(200);
    2:  
    3: //bind datatalbe to dynamically created datagrid
    4: datagrid.DataSource = myCollection;
    5: datagrid.DataBind();

The stress test:

tinyget.exe  -srv:192.168.50.68 -uri:/dynamiccontrolsloadingrelease/UseCustomCollection.aspx -loop:100 -threads:15

The result:

image

 

Create Datatable From Xml

The code:

    1: string xml = SampleServices.GenerateXml(200);
    2:  
    3: StringReader theReader = new StringReader(xml);
    4: DataSet theDataSet = new DataSet();
    5: theDataSet.ReadXml(theReader);
    6:  
    7: datagrid.DataSource = theDataSet.Tables[0].DefaultView;;
    8: datagrid.DataBind();

The stress test:

tinyget.exe  -srv:192.168.50.68 -uri:/dynamiccontrolsloadingrelease/LoadXmlIntoDataTable.aspx -loop:100 -threads:15

The result:

image 

Use Xslt Transformation To Create Html Table

The code:

    1: Xml1.DocumentContent = SampleServices.GenerateXml(200);
    2: Xml1.TransformSource=@"xsl.xml";

The stress test:

tinyget.exe  -srv:192.168.50.68 -uri:/dynamiccontrolsloadingrelease/XmlXslTransformation.aspx -loop:100 -threads:15

The result:

 image

Use Response.Write()

The code:

    1: MyCollection myCollection = (MyCollection)SampleServices.GenerateCollection(200);
    2:  
    3: // Put user code to initialize the page here
    4: Response.Write("<table>");
    5:  
    6: foreach(MyModelItem item in  myCollection)
    7: {
    8:  
    9:     Response.Write("<tr>");
   10:     Response.Write("<td>" +  item.Address  + "<td>");
   11:     Response.Write("<td>" +  item.City  + "<td>");
   12:     Response.Write("<td>" +  item.Education+ "<td>");
   13:     Response.Write("<td>" +  item.Family  + "<td>");
   14:     Response.Write("<td>" +  item.Name  + "<td>");
   15:     Response.Write("</tr>");
   16: }
   17: Response.Write("</table>");

The stress test:

tinyget.exe  -srv:192.168.50.68 -uri:/dynamiccontrolsloadingrelease/ResponseWrite.aspx -loop:100 -threads:15

The result:

image 

Sample Visual Studio 2003 Project

Interested in testing it yourself? Grab the source code from my SkyDrive here:

Conclusion

After conducting this simple test these are the conclusions I’ve made:

  • “Don't be afraid to challenge the pros, even in their own backyard." - How to Get Things Done - Colin Powell Version
  • Testing IS DA thing. Assumptions are good but nothing speaks louder than facts.
  • Test early - avoid massive rework afterwards. Create POC's (Proof of concept) early in architecture/design stages.
  • Best performance comes on expense of productivity and coolness.

Comments

  • Anonymous
    July 31, 2008
    You've been kicked (a good thing) - Trackback from DotNetKicks.com
  • Anonymous
    July 31, 2008
    Really nice.  I love how thorough you were in the investigation and in reporting the results.Thanks for taking the time to put this together.
  • Anonymous
    July 31, 2008
    James, great to hear you found this useful
  • Anonymous
    July 31, 2008
    &#160;&#160;&#160; In my previous post -&#160; Best ASP.NET Performance Winner For Data Binding - Hands
  • Anonymous
    August 01, 2008
    Well, I see a major flaw in this test since datagrid is not rendered into the pure html table actually.You'd better test this vs. repeater and even repeater supports a bunch of events like itemdatabound and itemcreated.
  • Anonymous
    August 01, 2008
    Another week of great resources for the .NET developer to take in. Amazing how many are out there. I
  • Anonymous
    August 01, 2008
    Anatoly,Thanks for the resourceful remark!My intention was to take customer's code and test it as is. There is simple meta pattern - developers code, end users use, IT guys support. All iwanted is to model what would it feel like for end users and IT guys when developers code the way they code (and as I just witnessed it)Repeater is another test I should have consider though - thanks for calling it out!
  • Anonymous
    August 04, 2008
    Thanks for an interesting post!One question; did you exclude using StringBuilder on purpose?If not, it may be worth testing the "Response.Write" against using a StringBuffer to see how tha compares...Cheers,Joakim
  • Anonymous
    August 04, 2008
    Joakim,Thanks for the insight!Few folks called this out too. I did not use StringBuilder originally as I was lazy to do so. In any way it would only improve it, right. But then I've built the demo that uses StringBuidler and there were no significant improvements.Next post is about using Repeater and ItemDataBoubd event as another reader called out Nice results BTW :)thanks for checking in.
  • Anonymous
    August 04, 2008
    Hi, I think you could make the Response.Write even faster if you avoided the string concat inside of them.Response.Write("<td>" +  item.Address  + "<td>");might be better written asResponse.Write("<td>");Response.Write(item.Address);Response.Write("</td>");I think that might reduce string concat.
  • Anonymous
    August 04, 2008
    Also I think Response.Write just uses a stream which will perform as well or better than StringBuilder. Or at least it should :)
  • Anonymous
    August 04, 2008
    Sam,Thanks for the insightfew pointed this one out too, see my prev response[.Write ] :)Just looked at your web site - oh, lala! Bookmarked
  • Anonymous
    August 04, 2008
    Note that Databinding is doing some additional stuff like cross-site-scripting checks, etc. that ideally you'll need to do if you use this method.
  • Anonymous
    August 04, 2008
    Daniel!!That is perfect remark. Not perf wise but security wise.Brilliant!I do not think that applying htmlencoding will add to much burden vs reflection Databinding does, but i must appreciate your comment.See, security and perf, has never been good friendsVery good catch!
  • Anonymous
    August 04, 2008
    This is not a comparison between datagrid with reponse.write.Have you profiled it to find out what the real performance bottle neck is? Did you try it with serveral different datatypes?What is the influence of looping through the datagrid and then casting to myCollection?You can make a better benchmark to isolate the code before the databind() call and before the foreach loop in the other version.I have tried this before and I'm sure you will get totally different results!
  • Anonymous
    August 04, 2008
    Paco, can you please guide me what should I do to test your scenario? I am not sure i get "looping through the datagrid". When I have datagrid I just call DataBind(). When I do not have datagrid i loop through every record w/response.write(). What scenario you refere to?According to PAG, whne binding there is reflection going on which hits perf.http://msdn.microsoft.com/en-us/library/ms998549.aspxlook at "Minimize Calls to DataBinder.Eval" in it
  • Anonymous
    August 04, 2008
    I'm sorry, I meant looping through the dataset (not the grid as i wrote) when you call the databind method. I'm targeting the loop that will be inside of the DataGrid.DataBind() method.The performace difference between(MyCollection)SampleServices.GenerateCollection(200) and (MyCollection)SampleServices.GenerateXML(200); might be significant here.
  • Anonymous
    August 04, 2008
    Oh, so you say the perf hit has nothing to do with Data Binding but due to data generation itself - either collection or XML. Ok I factored out this one during my tests. I need to post it next as it seems to be interesting too.Thanks for the insight, Paco!Anyway i suggest you reading PAG's stuff at http://msdn.microsoft.com/en-us/library/ms998549.aspx
  • Anonymous
    August 04, 2008
    I'm currently reading the article you suggested.But I mean this:datagrid.DataSource = myDatatable;Stopwatch watch = new Stopwatch();watch.Start();datagrid.DataBind();watch.Stop();Response.Write(watch.ElapsedTicks);watch.Start();foreach (MyClass item in myDatatable.Rows){   Response.Write(item.MyProperty);}watch.Stop();Response.Write(watch.ElapsedTicks);The second ElapsedTicks is always smaller.datagrid.DataSource = myDatatable;Stopwatch watch = new Stopwatch();watch.Start();datagrid.DataBind();watch.Stop();Response.Write(watch.ElapsedTicks);watch.Start();foreach (MyClass item in myDatatable.Rows){   Response.Write(item.MyProperty);}watch.Stop();Response.Write(watch.ElapsedTicks);The second time is always faster at my computer.I don't have 2003/1.1 installed anymore. I tested this with 2008/3.5It's a long time ago I used DataTable/Set/Grid/List, etc.I like castle and Asp.Net MVC and when I have to use classic Asp.Net, I always use repeaters.
  • Anonymous
    August 04, 2008
    We have noticed the same perf hits with databinding. We ended up using repeaters the first time then calling Repeater.RenderControl() and sticking the output into cache. The next time the page is hit we check for the output in cache and just return the string to a literal instead of databinding the repeater again. It has proven to be very fast.
  • Anonymous
    August 04, 2008
    GrillerGeek!Thanks fir the insight. I've done quick and dirty test w/Repeater and it performs close to simple Response.Write which makes Repeater a winner i guess.