Kirk Evans Blog

.NET From a Markup Perspective

Making The Case for Async Web Parts

@kaevans

Making The Case for Async Web Parts

Rate This
  • Comments 1

This post will show how to make a call to HttpWebRequest asynchronously from a web part and wait on the result or time out.

Years ago, I was fortunate to meet Jeff Richter and attend a session he gave on .NET performance.  He discussed I/O completion ports and showed why any calls from an ASP.NET page to a web service or another page absolutely must be done asynchronously.  In short, when you make a call such as HttpWebRequest.GetResponse from an ASP.NET page, you are blocking the thread.  Instead, replacing it with an asynchronous pattern such as HttpWebRequest.BeginGetResponse will use an I/O completion port to wait for the response, freeing the processing thread to do more work.

I recently worked with a customer that wrote a web part that uses HttpWebRequest to make calls to a backend service to obtain some data.  Remember that an ASP.NET page invokes its child controls synchronously, calling methods on your controls like CreateChildControls and Render, and this is true for web parts as well.  If your web part performs work that can take a few seconds (such as calling another web page), then consider what happens if you place 3 of those web parts on a page.  If you use HttpWebRequest.GetResponse (the synchronous call), then each web part will be invoked serially.  As a more concrete example, if your web part takes 5 seconds to make an HttpWebRequest call, placing 3 of those controls on your page makes the total page render time 15 seconds.

image

Confirming That Web Parts Are Synchronous

While I was working on a solution, a colleague indicated he thought that web parts would be invoked asynchronously.  That’s not true, but let’s test that theory by writing some code and measuring.  I created a class called Helper with a method GetWebPageSource.

        public string GetWebPageSource(string url)
        {
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
            request.Method = "GET";
            WebResponse response = request.GetResponse();

            using (Stream receiveStream = response.GetResponseStream())
            {
                Encoding encode = System.Text.Encoding.GetEncoding("utf-8");
                StreamReader readStream = new StreamReader(receiveStream, encode);
                Result = readStream.ReadToEnd();
            }
            response.Close();
            return Result;
        }

My web part will call this method in its OnPreRender method.

using System;
using System.ComponentModel;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;
using System.Threading;
using Microsoft.SharePoint.Utilities;

namespace Microsoft.PFE.DSE.Samples.Async.WebPageSourceWebPart
{
    
    [ToolboxItemAttribute(false)]
    public class WebPageSourceWebPart : WebPart
    {
        [WebBrowsable]
        [WebDisplayName("Url")]
        [WebDescription("The URL to obtain the source from")]
        [Category("Web Page Source Settings")]
        [Personalizable(PersonalizationScope.Shared)]
        public string Url { get; set; }


        Helper _helper;
        string _pageSource = string.Empty;


        protected override void OnPreRender(EventArgs e)
        {
            using (SPMonitoredScope monitoredScope = new SPMonitoredScope("OnPreRender to " + Url)) 
            {

                if ((!string.IsNullOrEmpty(Url)) && 
                  (WebPartManager.DisplayMode == WebPartManager.BrowseDisplayMode))
                {
                    
                    _helper = new Helper();
                    _pageSource = _helper.GetWebPageSource(Url);
                    
                }
            }
        }


        protected override void Render(HtmlTextWriter writer)
        {
            
            writer.AddAttribute(HtmlTextWriterAttribute.Class, "WePageSourcePart");
            writer.RenderBeginTag(HtmlTextWriterTag.Div);
            writer.WriteEncodedText(_pageSource);
            writer.RenderEndTag();
        }
    }
}

What would be a good way to measure the performance?  The easiest way is to use the Developer Dashboard in SharePoint 2010.

DevDashboardNOAsync

Just to exaggerate the problem, I added 8 web parts to the page that obtain data from various web sites (Microsoft.com, MSN.com, MSDN.Microsoft.com, etc).  The part to pay special attention to here is that 3 of the calls are to a page that I created that takes 5 seconds to render.  Total page rendering time is 17917.48 ms, or nearly 18 seconds to render my page.  Clearly the web parts are processed serially and calls to CreateChildControls, Render, etc are done synchronously.

We know that users are impatient and won’t put up with a page that takes 18 seconds to render.  Think about the performance impact… while you tie up the thread in ASP.NET, it is not available to service additional calls.  If 100 users access this page, you have 100 ASP.NET threads tied up for 18 seconds.  What do you think will eventually happen as these users get impatient and hit F5?  More requests, more threads tied up, and soon your server will become unresponsive because there are no threads available to service requests.  Instead of slow pages, your users will see error pages, and now you have a huge perception problem on your hands because the users are unhappy that you spent all this time and money building a solution and the site can’t handle even a few hundred users.

Making It Async

While it sounds like a dire situation (it is in its current state), the good news is that it can easily be fixed.  For instance, ASP.NET provides an incredibly handy mechanism for asynchronous pages, which is about as simple as adding an Async=True page directive.  The problem in my case was that I am using a SharePoint publishing page, and the SPPageParserFilter class does not allow the Async=True page directive.  We can still achieve the same effect, though, by directly using the same APIs in the same manner.  We already know that many of the APIs that make remote calls, such as HttpWebRequest, provide an asynchronous call mechanism.  It turns out to be very straightforward to change our web part code to call the asynchronous API and wait on the result.  First, let’s change the Helper class to expose the asynchronous methods.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.IO;

namespace Microsoft.PFE.DSE.Samples.Async
{
    public class Helper
    {
        public string Result { get; set; }

        public IAsyncResult BeginGetWebPageSource(string url)
        {
                        
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
            request.Method = "GET";
            return request.BeginGetResponse(null, request);            
        }

        public void EndGetWebPageSource(IAsyncResult iar)
        {
            HttpWebRequest request = iar.AsyncState as HttpWebRequest;
            WebResponse response = request.EndGetResponse(iar);
            
            using (Stream receiveStream = response.GetResponseStream())
            {
                Encoding encode = System.Text.Encoding.GetEncoding("utf-8");                
                StreamReader readStream = new StreamReader(receiveStream, encode);
                Result = readStream.ReadToEnd();                
            }
            response.Close();
        }

        public string GetWebPageSource(string url)
        {
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
            request.Method = "GET";
            WebResponse response = request.GetResponse();

            using (Stream receiveStream = response.GetResponseStream())
            {
                Encoding encode = System.Text.Encoding.GetEncoding("utf-8");
                StreamReader readStream = new StreamReader(receiveStream, encode);
                Result = readStream.ReadToEnd();
            }
            response.Close();
            return Result;
        }
    }
}

This should be familiar, we are just using the asynchronous API.  The interesting part to point out is that we return an IAsyncResult from BeginGetWebPageSource.  Additionally, we provide a null as the first argument to BeginGetResponse, indicating we do not have a callback method.  We’ll point this out again in just a moment.

The next part that we need to change is that we need to invoke our new asynchronous calls.  We will do this on a background thread, so we don’t want ASP.NET to continue processing… we need to wait until the response is received, otherwise our control would complete rendering before the HttpWebRequest ever returns, so we would not see any data.  There are a few ways to achieve this, but the easiest for our purposes is to use a WaitHandle.

using System;
using System.ComponentModel;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;
using System.Threading;
using Microsoft.SharePoint.Utilities;

namespace Microsoft.PFE.DSE.Samples.Async.WebPageSourceWebPart
{
    

    [ToolboxItemAttribute(false)]
    public class WebPageSourceWebPart : WebPart
    {
        [WebBrowsable]
        [WebDisplayName("Url")]
        [WebDescription("The URL to obtain the source from")]
        [Category("Web Page Source Settings")]
        [Personalizable(PersonalizationScope.Shared)]
        public string Url { get; set; }


        [WebBrowsable]
        [WebDisplayName("IsAsync")]
        [WebDescription("Specifies if the control should be process asynchronously")]
        [Category("Web Page Source Settings")]
        [Personalizable(PersonalizationScope.Shared)]
        public bool IsAsync { get; set; }

        IAsyncResult _result;
        Helper _helper;
        string _pageSource = string.Empty;

        protected override void OnLoad(EventArgs e)
        {            
            if ((!string.IsNullOrEmpty(Url)) && 
                (WebPartManager.DisplayMode == WebPartManager.BrowseDisplayMode))
            {
                _helper = new Helper();
                if (IsAsync)
                {
                    _result = _helper.BeginGetWebPageSource(Url);
                }
                
            }
        }

        protected override void OnPreRender(EventArgs e)
        {
            using (SPMonitoredScope monitoredScope = new SPMonitoredScope("OnPreRender to " + Url)) 
            {

                if ((!string.IsNullOrEmpty(Url)) && 
                    (WebPartManager.DisplayMode == WebPartManager.BrowseDisplayMode))
                {
                    if (IsAsync)
                    {
                        if (null != _result)
                        {
                            WaitHandle[] handles = new WaitHandle[1];
                            handles[0] = _result.AsyncWaitHandle;

                            //Wait for 5 seconds or until the result is returned
                            int index = WaitHandle.WaitAny(handles, 10000);
                            if (index == WaitHandle.WaitTimeout)
                            {
                                //The page timed out before we got a result
                                _pageSource = "Timeout before the result was returned";
                            }
                            else
                            {
                                _helper.EndGetWebPageSource(_result);
                                _pageSource = _helper.Result;
                            }
                        }
                    }
                    else
                    {
                        _helper = new Helper();
                        _pageSource = _helper.GetWebPageSource(Url);
                    }
                }
            }
        }


        protected override void Render(HtmlTextWriter writer)
        {
            
            writer.AddAttribute(HtmlTextWriterAttribute.Class, "WePageSourcePart");
            writer.RenderBeginTag(HtmlTextWriterTag.Div);
            writer.WriteEncodedText(_pageSource);
            writer.RenderEndTag();
        }
    }
}

In the OnLoad method, we check to see if the user indicated the web part should be processed asynchronously or not (so that we can easily compare the performance of synchronous versus asynchronous just by checking a checkbox).  If it is asynchronous, then the page starts accessing the backend data asynchronously.  In the OnPreRender method, we check again to see if this should be processed asynchronously or not.  If it is async, then we use the IAsyncResult returned from the HttpWebRequest.BeginGetResponse method to access its AsyncWaitHandle property.  This is how we will know when the external call is completed.  Further, we can provide a 10 second timeout to stop processing in case the page we are getting data from takes too long to respond.

To test this, I added 8 web parts to the page and checked the IsAsync property in the web part settings.

WebPartSettings

The nice thing is that the web requests are not issued unless the page is in BrowseDisplayMode, allowing you to make changes to multiple web parts while in EditDisplayMode.  If you just press the apply button to make changes, the page stays in edit mode, allowing you to change other web parts.  Once you hit OK, the page goes back to BrowseDisplayMode, and you have to wait for all of your HttpWebRequests to complete.

Now that we set each web part’s IsAsync property to true, let’s see if that changed things.  We again turn to the developer dashboard.

DevDashboardWithAsync

Wow!  What a difference, our page makes the same number of calls using HttpWebRequest, but they are done in parallel instead of serially.  Our page that used to take nearly 18 seconds to render now takes just over 5 seconds (5447.34 ms) to render!  Our users are happy, our boss gives us a much needed day off, we can eat a home cooked meal for once instead of cheap pizza during yet another late night at the office.  This happens because the requests are processed in parallel.

The Alternatives

Now that I’ve got you interested to learn more, read the material in the For More Information section, particularly the two posts in Tess’ blog.  Make sure that you catch exceptions and tune your environment’s maxConnections property if you intend on making a lot of calls from the server.

However, you very much need to think about why you are making calls from the server in the first place.  Even though we significantly increased our page performance, it still takes a minimum of 5 seconds to render.  Frankly, that’s horrible and should be avoided at all costs.  Remember that your primary job as a developer is to make web pages as fast as possible, and taking a dependency on a system out of your control is just asking for problems.  A good alternative here would be to add some script to the page that makes the call from the client side, completely relieving the server of that job.  Think carefully if that work can be pushed to the client and off the server, because you would much rather 100 client machines take that load than your web server.

For More Information

Creating ASP.NET Applications Using Wait Handles

ASP.NET Performance Case Study: Web Service Calls Taking Forever

ASP.NET Case Study: Hang on WaitOne, WaitAny or WaitMultiple

Attachment: Microsoft.PFE.DSE.Samples.Async.zip
  • Really nice and informative post mate! :)

Page 1 of 1 (1 items)
Leave a Comment
  • Please add 7 and 2 and type the answer here:
  • Post
Translate This Page
Search
Archive
Archives