|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionMicrosoft ASP.NET Ajax is a very powerful Ajax framework. However, when you build a real Ajax site like those out there in the Web 2.0 world, you face many problems that you will hardly find documented anywhere. In this article, I will show some advance-level ideas that I learned while building Pageflakes. We will look at the advantages and disadvantages of Batch calls, Ajax call timeouts, browser call jam problems, ASP.NET 2.0's bug in web service response caching, and so on. Why Use ASP.NET AjaxWhen others see Pageflakes, the first question they ask me is, "Why did you not use Protopage or Dojo library? Why Atlas?" Microsoft Atlas (renamed to ASP.NET Ajax) is a very promising Ajax framework. They are putting a lot of effort into it, making lots of reusable components that can really save you a lot of time and give your web application a complete face lift at reasonably low effort or change. It integrates with ASP.NET very well and is compatible with the ASP.NET Membership and Profile provider. The Ajax Control Toolkit project contains 28 extenders that you can drag & drop onto your page, tweak some properties and add pretty cool effects on the page. Check out the examples to see how powerful the ASP.NET Ajax framework has really become. When we first started developing Pageflakes, Atlas was in infancy. We were only able to use the page method and Web Service method call features of Atlas. We had to make our own drag & drop, component architecture, pop-ups, collapse/expand features, etc. Now, however, you can have all these from Atlas and thus save a lot of development time. The web service proxy feature of Atlas is a marvel. You can point a The JavaScript class contains the exact methods that you have on the web service class. This makes it really easy to add/remove new web services and add/remove methods in web services that do not require any changes on the client side. It also offers a lot of control over the Ajax calls and provides rich exception trapping features on the JavaScript. Server side exceptions are nicely thrown to the client side JavaScript code, and you can trap them and show nicely formatted error messages to the user. Atlas works really well with ASP.NET 2.0, eliminating the integration problem completely. You need not worry about authentication and authorization on page methods and web service methods. You thus save a lot of code on the client side -- of course, the Atlas Runtime is huge for this reason -- and you can concentrate more on your own code than on building up all this framework-related code. The recent version of Atlas works nicely with ASP.NET Membership and Profile services, giving you login/logout features from JavaScript without requiring page post-backs. You can read/write On earlier versions of Atlas, there was no way to make HTTP Batch Calls Are Not Always FasterASP.NET Ajax had a feature in the CTP release (and previous releases) that allowed batching of multiple requests into one request. It worked transparently, so you wouldn't notice anything, nor would you need to write any special code. Once you turned on the Batch feature, all web service calls made within a duration got batched into one call. Thus, it saved round-trip time and total response time. The actual response time might be reduced, but the perceived delay is higher. If three web service calls are batched, the first call does not finish first. If you are doing some UI updates, all three calls finish at the same time, upon completion of each WS call; it does not happen one-by-one. All of the calls complete in one shot and then the UI gets updated in one shot. As a result, you do not see incremental updates on the UI. Instead, you see a long delay before the UI updates. If any of the calls -- say the third call -- downloads a lot of data, the user sees nothing happening until all three calls complete. So, the duration of the first call becomes nearly the duration of the sum of all three calls. Although the actual total duration is reduced, the perceived duration is higher. Batch calls are handy when each call is transmitting a small amount of data. Thus, three small calls get executed in one round trip. Let's work on a scenario where three calls are made one-by-one. Here's how the calls actually get executed.
The second call takes a little bit of time to reach the server because the first call is eating up the bandwidth. For the same reason, it takes longer to download. Browsers open two simultaneous connections to the server so, at a time, only two calls are made. Once the second/first call completes, the third call is made. When these three calls are batched into one:
Here the total download time is reduced (if IIS compression is enabled) and there's only one network latency overhead. All three calls get executed on the server in one shot and the combined response is downloaded in one call. To the user, however, the perceived speed is slower because all the UI updates happen after the entire batch call completes. The total duration the batch call will take to complete will always be higher than that for two calls. Moreover, if you do a lot of UI updates one after another, Internet Explorer freezes for awhile, giving the user a bad impression. Sometimes, expensive updates on the UI make the browser screen go blank and white. Firefox and Opera do not have this problem. Batch calls have some advantages, too. The total download time is less than that for downloading individual call responses because if you use gzip compression in IIS, the total result is compressed instead of individually compressing each result. So, generally, a batch call is better for small calls. However, if a call is going to send a large amount of data or is going to return, say, 20 KB of response, then it's better not to use batch. Another problem with batch calls occurs when, say, two calls are very small but the third call is quite big. If these three calls get batched, the smaller calls are going to suffer from the long delay due to the third larger call. Bad Calls Make Good Calls Time OutIf two HTTP calls somehow get stuck for too long, those two bad calls are going to make some good calls expire too, which in the meantime got queued. Here's a nice example: function TestTimeout()
{
debug.trace("--Start--");
TestService.set_defaultFailedCallback(
function(result, userContext, methodName)
{
var timedOut = result.get_timedOut();
if( timedOut )
debug.trace( "Timedout: " + methodName );
else
debug.trace( "Error: " + methodName );
});
TestService.set_defaultSucceededCallback( function(result)
{
debug.trace( result );
});
TestService.set_timeout(5000);
TestService.HelloWorld("Call 1");
TestService.Timeout("Call 2");
TestService.Timeout("Call 3");
TestService.HelloWorld("Call 4");
TestService.HelloWorld("Call 5");
TestService.HelloWorld(null); // This one will produce Error
}
On the server side, the web service is very simple: [WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[ScriptService]
public class TestService : System.Web.Services.WebService {
public TestService () {
//Uncomment the following line if using designed components
//InitializeComponent();
}
[WebMethod][ScriptMethod(UseHttpGet=true)]
public string HelloWorld(string param) {
Thread.Sleep(1000);
return param;
}
[WebMethod][ScriptMethod(UseHttpGet=true)]
public string Timeout(string param) {
Thread.Sleep(10000);
return param;
}
}
I am calling a method named
Only the first call succeeded. So, if at any moment the browser's two connections get jammed, then you can expect that other waiting calls are going to time out as well. In Pageflakes, we used to get nearly 400 to 600 timeout error reports from users' browsers. We could never figure out how this could happen. First, we suspected slow internet connections, but that cannot happen for so many users. Then we suspected something was wrong with the hosting provider's network. We did a lot of network analysis to find out whether there were any problems on the network or not, but we could not detect any. We used SQL Profiler to see whether there were any long-running queries that timed out the ASP.NET request execution time, but no luck. We finally discovered that it mostly happened due to some bad calls which got stuck and made the good calls expire, too. So, we modified the Atlas Runtime and introduced automatic retry on it, and the problem disappeared completely. However, this auto-retry requires a sophisticated open heart bypass surgery on the ASP.NET Ajax framework JavaScript. The idea is to make each and every call retry once when it times out. In order to do that, we need to intercept all web method calls and implement a hook on the Another interesting discovery we made while we were traveling was that whenever we tried to visit Pageflakes from a hotel or an airport wireless internet connection, the first visit always failed and all the web service calls on first attempt always failed. Until we did a refresh, nothing worked. This was another major reason why we implemented immediate auto-retry of web service calls, which fixed the problem. Here's how to do it. The Sys.Net.WebServiceProxy.retryOnFailure =
function(result, userContext, methodName, retryParams, onFailure)
{
if( result.get_timedOut() )
{
if( typeof retryParams != "undefined" )
{
debug.trace("Retry: " + methodName);
Sys.Net.WebServiceProxy.original_invoke.apply(this, retryParams );
}
else
{
if( onFailure ) onFailure(result, userContext, methodName);
}
}
else
{
if( onFailure ) onFailure(result, userContext, methodName);
}
}
Sys.Net.WebServiceProxy.original_invoke = Sys.Net.WebServiceProxy.invoke;
Sys.Net.WebServiceProxy.invoke =
function Sys$Net$WebServiceProxy$invoke(servicePath, methodName, useGet,
params, onSuccess, onFailure, userContext, timeout)
{
var retryParams = [ servicePath, methodName, useGet, params,
onSuccess, onFailure, userContext, timeout ];
// Call original invoke but with a new onFailure
// handler which does the auto retry
var newOnFailure = Function.createDelegate( this,
function(result, userContext, methodName)
{
Sys.Net.WebServiceProxy.retryOnFailure(result, userContext,
methodName, retryParams, onFailure);
} );
Sys.Net.WebServiceProxy.original_invoke(servicePath, methodName, useGet,
params, onSuccess, newOnFailure, userContext, timeout);
}
When run, it will retry each timed-out call once.
Here you see that the first method succeeded and all the others timed out and retried. However, you will see that after a retry, they all succeeded. This happened because server side methods do not time out on retry. So, this proves that our implementation is correct. Browsers Allow Two Calls at a Time and Don't Expect any OrderBrowsers make two concurrent Ajax calls at a time to a domain. If you make five Ajax calls, the browser is going to make two calls first and then wait for any one of them to complete. Then it makes another call until all four remaining calls are complete. Moreover, you cannot expect calls to execute in the same order as you make the calls. Here's why:
Here you see that call 3's response download is quite big, and thus takes longer than call 5. So, call 5 actually gets executed before call 3. The world of HTTP is unpredictable. Browsers Do Not Respond when More Than Two Calls Are in QueueTry this: go to any start page in the world that will load a lot of RSS on the first visit (e.g. Pageflakes, Netvibes, Protopage) and, while loading, try to click on a link that will take you to another site or try to visit another site. You will see that the browser is stuck. Until all queued Ajax calls in the browser complete, the browser will not accept any other activity. This is worst in Internet Explorer; Firefox and Opera do not have this much of a problem. The problem is that when you make a lot of Ajax calls, the browser keeps all calls in a queue and executes two at a time. So, if you click on something or try to navigate to another site, the browser has to wait for running calls to complete before it can take another call. The solution to this problem is to prevent more than two calls from being queued in the browser at a time. We need to maintain a queue ourselves and send calls to the browser's queue from our queue on-by-one. The solution is quite shocking; brace for impact: var GlobalCallQueue = {
_callQueue : [], // Maintains the list of webmethods to call
_callInProgress : 0, // Number of calls currently in progress by browser
_maxConcurrentCall : 2, // Max number of calls to execute at a time
_delayBetweenCalls : 50, // Delay between execution of calls
call : function(servicePath, methodName, useGet,
params, onSuccess, onFailure, userContext, timeout)
{
var queuedCall = new QueuedCall(servicePath, methodName, useGet,
params, onSuccess, onFailure, userContext, timeout);
Array.add(GlobalCallQueue._callQueue,queuedCall);
GlobalCallQueue.run();
},
run : function()
{
/// Execute a call from the call queue
if( 0 == GlobalCallQueue._callQueue.length ) return;
if( GlobalCallQueue._callInProgress <
GlobalCallQueue._maxConcurrentCall )
{
GlobalCallQueue._callInProgress ++;
// Get the first call queued
var queuedCall = GlobalCallQueue._callQueue[0];
Array.removeAt( GlobalCallQueue._callQueue, 0 );
// Call the web method
queuedCall.execute();
}
else
{
// cannot run another call. Maximum concurrent
// webservice method call in progress
}
},
callComplete : function()
{
GlobalCallQueue._callInProgress --;
GlobalCallQueue.run();
}
};
QueuedCall = function( servicePath, methodName, useGet, params,
onSuccess, onFailure, userContext, timeout )
{
this._servicePath = servicePath;
this._methodName = methodName;
this._useGet = useGet;
this._params = params;
this._onSuccess = onSuccess;
this._onFailure = onFailure;
this._userContext = userContext;
this._timeout = timeout;
}
QueuedCall.prototype =
{
execute : function()
{
Sys.Net.WebServiceProxy.original_invoke(
this._servicePath, this._methodName, this._useGet, this._params,
Function.createDelegate(this, this.onSuccess), // Handle call complete
Function.createDelegate(this, this.onFailure), // Handle call complete
this._userContext, this._timeout );
},
onSuccess : function(result, userContext, methodName)
{
this._onSuccess(result, userContext, methodName);
GlobalCallQueue.callComplete();
},
onFailure : function(result, userContext, methodName)
{
this._onFailure(result, userContext, methodName);
GlobalCallQueue.callComplete();
}
};
Sys.Net.WebServiceProxy.original_invoke = Sys.Net.WebServiceProxy.invoke;
Sys.Net.WebServiceProxy.invoke =
function Sys$Net$WebServiceProxy$invoke(servicePath, methodName,
useGet, params, onSuccess, onFailure, userContext, timeout)
{
GlobalCallQueue.call(servicePath, methodName, useGet, params,
onSuccess, onFailure, userContext, timeout);
}
Caching Web Service Response on the Browser and Saving Bandwidth SignificantlyBrowsers can cache images, JavaScript and CSS files on a user's hard drive, and they can also cache XML HTTP calls if the calls are HTTP At Pageflakes, we cache the user's state so that when the user visits again the following day, the user gets a cached page that loads instantly from the browser cache, not from the server. Thus, the second-time load becomes very fast. We also cache several small parts of the page that appear on users' actions. When the user does the same action again, a cached result is loaded immediately from the local cache, which saves on the network round trip time. The user gets a fast-loading site and a very responsive site. The perceived speed increases dramatically. The idea is to make HTTP HTTP/1.1 200 OK
Expires: Fri, 1 Jan 2030
Cache-Control: public
This will instruct the browser to cache the response until January, 2030. As long as you make the same XML HTTP call with the same parameters, you will get a cached response from the computer and no call will go to the server. There are more advanced ways to get further control over response caching. For example, here is a header that will instruct the browser to cache for 60 seconds, but not contact the server and get a fresh response after 60 seconds. It will also prevent proxies from returning cached responses when the browser local cache expires after 60 seconds. HTTP/1.1 200 OK
Cache-Control: private, must-revalidate, proxy-revalidate, max-age=60
Let's try to produce such response headers from an ASP.NET web service call: [WebMethod][ScriptMethod(UseHttpGet=true)]
public string CachedGet()
{
TimeSpan cacheDuration = TimeSpan.FromMinutes(1);
Context.Response.Cache.SetCacheability(HttpCacheability.Public);
Context.Response.Cache.SetExpires(DateTime.Now.Add(cacheDuration));
Context.Response.Cache.SetMaxAge(cacheDuration);
Context.Response.Cache.AppendCacheExtension(
"must-revalidate, proxy-revalidate");
return DateTime.Now.ToString();
}
This will result in the following response headers:
The
There's a bug in ASP.NET 2.0 where you cannot change the
Somehow, [WebMethod][ScriptMethod(UseHttpGet=true)]
public string CachedGet2()
{
TimeSpan cacheDuration = TimeSpan.FromMinutes(1);
FieldInfo maxAge = Context.Response.Cache.GetType().GetField("_maxAge",
BindingFlags.Instance|BindingFlags.NonPublic);
maxAge.SetValue(Context.Response.Cache, cacheDuration);
Context.Response.Cache.SetCacheability(HttpCacheability.Public);
Context.Response.Cache.SetExpires(DateTime.Now.Add(cacheDuration));
Context.Response.Cache.AppendCacheExtension(
"must-revalidate, proxy-revalidate");
return DateTime.Now.ToString();
}
This will return the following headers:
Now
After 1 minute, the cache expires and the browser makes a call to the server again. The client-side code is like this: function testCache()
{
TestService.CachedGet(function(result)
{
debug.trace(result);
});
}
There's another problem to solve. In web.config, you will see ASP.NET Ajax adding: <system.web>
<trust level="Medium"/>
This prevents us from setting the <system.web>
<trust level="Full"/>
When "this" is Not Really "this"Atlas call-backs are not executed on the same context where they are called. For example, if you are making a Web method call from a JavaScript class like this: function SampleClass()
{
this.id = 1;
this.call = function()
{
TestService.DoSomething( "Hi", function(result)
{
debug.dump( this.id );
} );
}
}
What happens when you call the function SampleClass()
{
this.id = 1;
this.call = function()
{
TestService.DoSomething( "Hi", function(result)
{
debug.dump( this.id );
} );
}
}
<input type="button" id="ButtonID" onclick="o.onclick" />
If you click the button, you see function SampleClass()
{
this.id = 1;
this.call = function()
{
TestService.DoSomething( "Hi",
Function.createDelegate( this, function(result)
{
debug.dump( this.id );
} ) );
}
}
Here, Function.createDelegate = function(instance, method) {
return function() {
return method.apply(instance, arguments);
}
}
HTTP POST Is Slower than HTTP GET, but It Is Default in ASP.NET AjaxASP.NET Ajax, by default, makes HTTP [WebMethod] [ScriptMethod(UseHttpGet=true)]
public string HelloWorld()
{
}
Another problem of From the above picture, you see that ConclusionThe above extreme hacks are already implemented in Pageflakes, not the exact way as mentioned here, but the principles are the same. So, you can happily rely on these techniques. These techniques will save you from many problems that you will probably never realize in your development environment, but people from all over the world will face these problems when you go for large scale deployment. Having these tricks implemented right from the beginning will save you a lot of development and customer support effort. Keep both eyes on my blog for more tricks to come. History
| ||||||||||||||||||||