Thursday, June 28, 2007

Tiny ASP.NET Ajax Framework Contest

 

Contest context

More than two years ago we've built a small web application used to collect and print documents required to participate in secondary school final examinations in Poland. The application is used quite extensively by people from all over the country.

The application, maturzysta.vulcan.pl, is located here.

From the web developer's point of view there is one interesting aspect of the main form of the application: there are about 30 dropdown lists with complicated logic between their selections: when you change the selection in one list, few others should be cleared or repopulated with context-bound data. There is also quite a lot of validation of various dependencies between user's selections.

The first version of the application was built as a server-side application: all SelectedIndexChanged events were auto-postbacked and processed on the application server. This caused a lot of trouble since the heavy form had to be sent to and from the server.

The year later we've completely redesigned the application, moving the interface logic to the javascript. The web traffic problem has been solved but two other appear. First of all - no one likes javascript, it's clumsy and error-prone. Then - we had to disable ASP.NET page validation mechanism because otherwise the page content modified on the client-side would be rejected on the server-side as potentially dangerous.

In the meantime, few promising AJAX frameworks appeared and we thought that it would be possible to go back to the initial version of the application but instead of expensive postbacks we could take the AJAX approach and replace postbacks with callbacks. This could solve all problems: reduce the traffic and still keep the application logic on the server-side.

Contest participants

Three AJAX frameworks were chosen for the contest:

The first and the third one were chosen because it's fairly straightforward to extend existing application with AJAX capabilities. The second one is perhaps one of the most promising AJAX frameworks out there, designed to fill the gap between Windows.Forms and Web.Forms programming.

In order to make the application compatible with participants, following steps have been taken:

  • in case of ASP.NET AJAX, the ScriptManager has been put on the form page and the whole page content has been put into the UpdatePanel. The concept of several small UpdatePanels with triggers between them has been rejected because of the complexity of the application logic and dependencies between controls.
  • in case of the Ajaxium framework, the Ajaxium initialization routine has been put into the OnBeginRequest event of the global application class as well as into the Page_Load method of the form.
  • in case of the VisualWebGUI framework, the application has been completely rewritten using the VWG library and components.

Compare the layout of the WebForms/AJAX application (save images to see them full size)

with the layout of the rewritten VWG version

 The judge

Microsoft Application Center Test has been choosen as the contest judgement tool. Tests were performed on the Windows XP machine serving as the application server. Exactly 200 sessions have been simulated for 5 concurrent users. Each session has been recorded as the complete user session - forms have been filled with data and dropdown selections have been made.

Each session ended with the application showing the print-ready document.

The results

The results are available here.

Observations:

  • both the Microsoft AJAX.NET and the Ajaxium frameworks are not capable of delivering dynamically created content to the web browser from within the AJAX callback [or at least we do not know how to do that]. This means that when the application produces, let say, XML data file and sends it to the application, instead of a typical "Save - Open - Cancel" prompt, you get ... nothing. Fortunately, this was not considered as a test case.
  • the original Web.Forms server-side application, numer (1) in the result chart, has been able to process 213 requests per second and ended with almost 8 MB of data sent to the server and 80 MB of data received from the server. These results has been considered as reference results.
  • Microsoft ASP.NET AJAX application, numer (2) in the result chart, has been able to process 222 requests per second with 5 MB of data sent to the server and 129 MB (!) of data received from the server.
  • The Ajaxium version, number (3) in the result chart, has been able to process only 111 request per second. It sent 5 MB and received 47 MB of data.
  • The VWG version, numer (4), did 422 requests per second with 2 MB sent and 23 MB received (!)
  • The javascript version, numer (5), did 400 requests per second, with 2 MB sent to the server and 37 MB received.

Results:

  • despite its simplicity, Ajaxium did unexpectably bad. We belive that the framework initialization takes just too much time (look at the "time to first byte") and this downgrades the performance.
  • Microsoft ASP.NET AJAX didn't really help regarding server-side resource consumption. It also sends large amount of data to the client - in our case this could be because large form has been put into single UpdatePanel. In overall it did a nice job of ajaxing the application but nothing else.
  • The javascript version was considered as an easy winner and, to our suprise, it sends quite a lot of data to the client. Well then, scripts are over 90 kB. Multiply it by 200 clients and you have the bandwitch consumption.
  • The VWG is an undoubt winner. It did a great job of serving the highest number of requests per second, 422, and is also a clear winner regarding the amount of data sent to and from the server. The only problem with VWG is that it is still in beta version and is not reliable for the time (it still has stability and compatibility problems, let's hope it will get better every other release).

What could be interesting is that the VWG version seems to be the slowest one from the end-user's point of view. It just feels sluggish inside the web browser. We belive that this is caused by the constant callbacks to the server and as you can see it does not slow down the processing pipeline at all - it is just slow inside your web browser.

I would like to thank my friend Krzysiek Owczarek for helping me to perform the tests.

 

Update 2007-07-15

Mark Krapovek, Ajaxium Support Manager, made a comment on the testing environment:

"The evaluation edition available freely on vendor's website DOES NOT SUPPORT CONCURRENT CONNECTIONS. That's just a limitation of the evaluation edition. Before testing the component for "200 sessions simulated for 5 concurrent users" you had to ask vendor for a version with all limitations removed.
So: if Ajaxium Evaluation Edition was able to serve more than 100 requests per second, being limited for only 1 connection at a time, it is the fastest component among all compared."

Mark, thank you for the information.

Tuesday, June 26, 2007

Wrapped InProcSessionStateStore

Although the .NET 2.0 Provider Model offers three built-in possibilities for persisting the session context information, there exists frameworks which limit the choice to the InPronSessionStateStore.

One of such frameworks is the Visual Web GUI Ajax framework designed to fill the gap between Windows.Forms and Web.Forms programming. The VWG framework looks very, very promising, however I feel that its development slows down a bit.

Nonetheless, the main idea of VWG is dumb client, allmighty server where the interface is rendered in the client's web browser and the application logic is executed completely on the server.

There are few limitations and one of them is the session state management - the session context collects much more data than the usual Web.Forms session does. Since VWG objects are not serializable, the SqlServer session state store cannot be used and what I immediately though about was: how much data is exactly stored in the session context?

To be able to inspect the session store it would be perfect to just override the InProcSessionStateStore. However, the class is internal. The idea below is to build a custom session state store which uses the InProcSessionStateStore internally but makes it possible to put custom logic into the session store-retrieve pipeline.

Two critical methods are GetItemExclusive and SetAndReleaseItemExclusive. This is where the custom code can be injected into the pipeline. The code can, for example, log the content of the session store or modify it somehow.

Can I then make my own custom serializer and store the session context in some persistent storage from the wrapped inproc store? you would ask.

Good question. I was not able to find a generic solution to the serialize non-serialized objects problem. The GeneralSerializableAdapter solution proposed here is probably a step in the right direction, however it does not serialize collections correctly (or when it does after you tweak it, it serializes far too much!) neither it is able to serialize non-serialized properties.

If you are able to write a generic serializer which would be able to serialize objects which are not marked as serialized (and do not contain parameterless constructors), please feel free to go a step further then I did and extend the inproc state store wrapper with the generic serializer.

Here is the code:

   1: /* Wiktor Zychla, 2007 */
   2: using System;
   3: using System.Data;
   4: using System.Configuration;
   5: using System.Web;
   6: using System.Web.Security;
   7: using System.Web.UI;
   8: using System.Web.UI.WebControls;
   9: using System.Web.UI.WebControls.WebParts;
  10: using System.Web.UI.HtmlControls;
  11: using System.Web.SessionState;
  12: using System.Web.Hosting;
  13: using System.Reflection;
  14: using System.Data.SqlClient;
  15: using System.Runtime.InteropServices;
  16: using System.Diagnostics;
  17: using System.IO;
  18: using System.Collections;
  19: using System.Runtime.Serialization.Formatters.Soap;
  20: using System.Xml.Serialization;
  21: using System.Runtime.Serialization.Formatters.Binary;
  22: using System.Runtime.Serialization;
  23:  
  24: public class WrappedInProcSessionStateProvider : SessionStateStoreProviderBase
  25: {
  26:     private object store;
  27:     private Type storeType;
  28:  
  29:     public override void Initialize( 
  30:         string name, 
  31:         System.Collections.Specialized.NameValueCollection config )
  32:     {
  33:         storeType = typeof( SessionStateStoreProviderBase )
  34:             .Assembly.GetType( "System.Web.SessionState.InProcSessionStateStore" );
  35:         store = Activator.CreateInstance( storeType );
  36:  
  37:         base.Initialize( name, config );
  38:     }
  39:  
  40:     public override SessionStateStoreData CreateNewStoreData( HttpContext context, int timeout )
  41:     {
  42:         if ( store != null )
  43:         {
  44:             return (SessionStateStoreData)
  45:                 storeType.InvokeMember(
  46:                 "CreateNewStoreData",
  47:                 System.Reflection.BindingFlags.InvokeMethod | 
  48:                 System.Reflection.BindingFlags.Instance | 
  49:                 System.Reflection.BindingFlags.Public,
  50:                 null, store, new object[] { context, timeout }
  51:             );
  52:         }
  53:         else
  54:             throw new Exception( "State not initialized" );
  55:     }
  56:  
  57:     public override void CreateUninitializedItem( 
  58:         HttpContext context, string id, int timeout )
  59:     {
  60:         if ( store != null )
  61:         {
  62:             storeType.InvokeMember(
  63:                 "CreateUninitializedItem",
  64:                 System.Reflection.BindingFlags.InvokeMethod | 
  65:                 System.Reflection.BindingFlags.Instance | 
  66:                 System.Reflection.BindingFlags.Public,
  67:                 null, store, new object[] { context, id, timeout }
  68:             );
  69:         }
  70:     }
  71:  
  72:     public override void Dispose()
  73:     {
  74:         if ( store != null )
  75:         {
  76:             storeType.InvokeMember(
  77:                 "Dispose",
  78:                 System.Reflection.BindingFlags.InvokeMethod | 
  79:                 System.Reflection.BindingFlags.Instance | 
  80:                 System.Reflection.BindingFlags.Public,
  81:                 null, store, null
  82:             );
  83:         }
  84:     }
  85:  
  86:     public override void EndRequest( HttpContext context )
  87:     {
  88:         if ( store != null )
  89:         {
  90:             storeType.InvokeMember(
  91:                 "EndRequest",
  92:                 System.Reflection.BindingFlags.InvokeMethod | 
  93:                 System.Reflection.BindingFlags.Instance | 
  94:                 System.Reflection.BindingFlags.Public,
  95:                 null, store, new object[] { context }
  96:             );
  97:         }
  98:     }
  99:  
 100:     public override SessionStateStoreData GetItem( 
 101:         HttpContext context, string id, out bool locked, 
 102:         out TimeSpan lockAge, out object lockId, out SessionStateActions actions )
 103:     {
 104:         if ( store != null )
 105:         {
 106:             object[] args = new object[] { context, id, null, null, null, null };
 107:  
 108:             SessionStateStoreData ret =
 109:                 (SessionStateStoreData)
 110:                 storeType.InvokeMember(
 111:                 "GetItem",
 112:                 System.Reflection.BindingFlags.InvokeMethod | 
 113:                 System.Reflection.BindingFlags.Instance | 
 114:                 System.Reflection.BindingFlags.Public,
 115:                 null, store, args
 116:             );
 117:  
 118:             locked  = (bool)args[2];
 119:             lockAge = (TimeSpan)args[3];
 120:             lockId  = args[4];
 121:             actions = (SessionStateActions)args[5];
 122:  
 123:             return ret;
 124:         }
 125:         else
 126:             throw new Exception( "State not initialized" );
 127:     }
 128:  
 129:     public override SessionStateStoreData GetItemExclusive( 
 130:         HttpContext context, string id, out bool locked, 
 131:         out TimeSpan lockAge, out object lockId, out SessionStateActions actions )
 132:     {
 133:         if ( store != null )
 134:         {
 135:             object[] args = new object[] { context, id, null, null, null, null };
 136:  
 137:             SessionStateStoreData ret = 
 138:                 (SessionStateStoreData)
 139:                 storeType.InvokeMember(
 140:                 "GetItemExclusive",
 141:                 System.Reflection.BindingFlags.InvokeMethod | 
 142:                 System.Reflection.BindingFlags.Instance | 
 143:                 System.Reflection.BindingFlags.Public,
 144:                 null, store, args
 145:             );
 146:  
 147:             locked  = (bool)args[2];
 148:             lockAge = (TimeSpan)args[3];
 149:             lockId  = args[4];
 150:             actions = (SessionStateActions)args[5];
 151:  
 152:             return ret;
 153:         }
 154:         else
 155:             throw new Exception( "State not initialized" );
 156:     }
 157:  
 158:     public override void InitializeRequest( HttpContext context )
 159:     {
 160:         if ( store != null )
 161:         {
 162:             storeType.InvokeMember(
 163:                 "InitializeRequest",
 164:                 System.Reflection.BindingFlags.InvokeMethod | 
 165:                 System.Reflection.BindingFlags.Instance | 
 166:                 System.Reflection.BindingFlags.Public,
 167:                 null, store, new object[] { context }
 168:             );
 169:         }
 170:     }
 171:  
 172:     public override void ReleaseItemExclusive( 
 173:         HttpContext context, string id, object lockId )
 174:     {
 175:         if ( store != null )
 176:         {
 177:             storeType.InvokeMember(
 178:                 "ReleaseItemExclusive",
 179:                 System.Reflection.BindingFlags.InvokeMethod | 
 180:                 System.Reflection.BindingFlags.Instance | 
 181:                 System.Reflection.BindingFlags.Public,
 182:                 null, store, new object[] { context, id, lockId }
 183:             );
 184:         }
 185:     }
 186:  
 187:     public override void RemoveItem( 
 188:         HttpContext context, string id, 
 189:         object lockId, SessionStateStoreData item )
 190:     {
 191:         if ( store != null )
 192:         {
 193:             storeType.InvokeMember(
 194:                 "RemoveItem",
 195:                 System.Reflection.BindingFlags.InvokeMethod | 
 196:                 System.Reflection.BindingFlags.Instance | 
 197:                 System.Reflection.BindingFlags.Public,
 198:                 null, store, new object[] { context, id, lockId, item }
 199:             );
 200:         }
 201:     }
 202:  
 203:     public override void ResetItemTimeout( HttpContext context, string id )
 204:     {
 205:         if ( store != null )
 206:         {
 207:             storeType.InvokeMember(
 208:                 "ResetItemTimeout",
 209:                 System.Reflection.BindingFlags.InvokeMethod | 
 210:                 System.Reflection.BindingFlags.Instance | 
 211:                 System.Reflection.BindingFlags.Public,
 212:                 null, store, new object[] { context, id }
 213:             );
 214:         }
 215:     }
 216:  
 217:     public override void SetAndReleaseItemExclusive( 
 218:         HttpContext context, string id, SessionStateStoreData item, 
 219:         object lockId, bool newItem )
 220:     {        
 221:         if ( store != null )
 222:         {
 223:             AnalyzeSessionContent( id, item );
 224:  
 225:             storeType.InvokeMember(
 226:                 "SetAndReleaseItemExclusive",
 227:                 System.Reflection.BindingFlags.InvokeMethod | 
 228:                 System.Reflection.BindingFlags.Instance | 
 229:                 System.Reflection.BindingFlags.Public,
 230:                 null, store, new object[] { context, id, item, lockId, newItem }
 231:             );
 232:         }
 233:     }
 234:  
 235:     private void AnalyzeSessionContent( string id, SessionStateStoreData item )
 236:     {
 237:         // item.Items
 238:     }
 239:  
 240:     public override bool SetItemExpireCallback( 
 241:         SessionStateItemExpireCallback expireCallback )
 242:     {
 243:         if ( store != null )
 244:         {
 245:             return (bool)
 246:                 storeType.InvokeMember(
 247:                 "SetItemExpireCallback",
 248:                 System.Reflection.BindingFlags.InvokeMethod | 
 249:                 System.Reflection.BindingFlags.Instance | 
 250:                 System.Reflection.BindingFlags.Public,
 251:                 null, store, new object[] { expireCallback }
 252:             );
 253:         }
 254:         else
 255:             throw new Exception( "State not initialized" );
 256:     }
 257: }
 258: