Show Load Time Of A Page With ASP.Net Web Forms
A little heads up, this solution will not work if you have an ASP.Net AJAX UpdatePanel on your page. The javascript which is loaded by ASP.Net AJAX will throw an error. Of course you're better off throwing out ASP.Net AJAX and just use plain AJAX calls. But if that's not possible, be warned.
A common feature that gets requested on a lot of big web applications is to show the time it took to load the page. Sometimes it's just to show to the user how long a certain action took, other times it's to have a ballpark number when performance issues arise.
First things first, how can the load time of a page be measured? There are different ways of measuring. You can use DateTimes, you can use StopWatch and they all have advantages and disadvantages. For this post DateTimes will suffice, we're looking for a ballpark number.
I was surprised how difficult it was to implement this in ASP.Net Web Forms. For a simple one page project the solution is quite straight forward. You take the page where you want to show the load time and when the Page_Load has finished you subtract the DateTime that was initialized at the beginning of the method from the DateTime that was initialized at the end. The code hopefully speaks for itself:
protected void Page_Load(object sender, EventArgs e)
{
var start = DateTime.UtcNow;
Thread.Sleep(5000);
var loadTime = DateTime.UtcNow - start;
loadTimeLiteral.Text = (loadTime.TotalMilliseconds/1000).ToString(CultureInfo.InvariantCulture);
}
And this is the resulting page:
But in a more complex project with multiple master pages and user controls this solution will not show the correct load time. Take a look at the following structure:
It is a Web Forms page, Complex.aspx, which has a master page to determine the layout. The page also uses 3 user controls. The approach in the previous example will not work in this case, the load time from the Page_Load method of the page is not correct. The Page_Load events in the master page and the user controls have to be taken into account.
First important fact to determine is where to start the measurement of the load time. The ASP.Net Page Lifecycle is an ugly beast which I'd rather not touch.
Image from http://spazzarama.wordpress.com
Instead of looking for the first Page_Load event that gets called: the event from the master, the page or the user controls, the global.asax file is a much simpler place to start the measurement. The Application_BeginRequest is the event we are looking for. This is the first entry point on the server for the client's request.
protected void Application_BeginRequest(object sender, EventArgs e)
{
Context.Items.Add("StartTime", DateTime.UtcNow);
}
The current time is added to the HttpContext, this way it can be acccessed later. The last possible exit point needs to be determined, there the load time of the entire request is calculated and will be correct.
The most logical place would be the Application_EndRequest event in the Global.asax. Unfortunately the current HttpContext is no longer available in the Application_EndRequest, so that event can't be used. The Application_PostReleaseRequestState event is the last possible place where the HttpContext is still available before the response is sent to the client. MSDN learns us when the event gets fired:
Occurs when ASP.NET has completed executing all request event handlers and the request state data has been stored.
It's determined where the request starts and ends, now the load time can be calculated based on these two times. However, there is no way of inserting the calculated load time anywhere in any page or user control. There is no way to reference a certain page and call it from the global.asax.
The solution isn't a pretty one but it does work. By setting the Response.Filter property to a class which inherits from System.IO.Stream we can manipulate the html that is sent to the client right before it is actually sent. This is the newly created class which will handle the manipulating, a bunch of methods have to be implemented but these are simply redirected to calls to the Response.Filter stream from the HttpContext. This is the basic class:
public class AddLoadTimeStream : Stream
{
private readonly Stream _baseStream;
public AddLoadTimeStream(Stream responseStream)
{
_baseStream = responseStream;
}
public override bool CanRead
{
get { return _baseStream.CanRead; }
}
public override bool CanSeek
{
get { return _baseStream.CanSeek; }
}
public override bool CanWrite
{
get { return _baseStream.CanWrite; }
}
public override long Length
{
get { return _baseStream.Length; }
}
public override long Position { get; set; }
public override void Flush()
{
_baseStream.Flush();
}
public override long Seek(
long offset,
SeekOrigin origin)
{
return _baseStream.Seek(offset, origin);
}
public override void SetLength(long value)
{
_baseStream.SetLength(value);
}
public override int Read(
byte[] buffer,
int offset,
int count)
{
return _baseStream.Read(buffer, offset, count);
}
public override void Write(
byte[] buffer,
int offset,
int count)
{
_baseStream.Write(buffer, offset, count);
}
}
To change the html that will be sent back to the client, the Write method of the Stream can be changed. First the original text is read, after which the load time is calculated based on the start time in the Items collection of the HttpContext which was set in the Application_BeginRequest event. To be able to put the load time on any page or any user control, a unique text is replaced with the actual load time. The token I've used in this example is ##load_time##. Make sure the token is unique, otherwise you could have some unwanted results.
public override void Write(
byte[] buffer,
int offset,
int count)
{
string originalText = Encoding.UTF8.GetString(buffer,
offset, count);
var begin = (DateTime)HttpContext.Current.Items["StartTime"];
var end = DateTime.UtcNow;
var formattedTimeElapsed = (((end - begin).TotalMilliseconds) / 1000).ToString(CultureInfo.InvariantCulture);
originalText = originalText.Replace("##load_time##", formattedTimeElapsed);
buffer = Encoding.UTF8.GetBytes(originalText);
_baseStream.Write(buffer, 0, buffer.Length);
}
After replacing the token with the load time, the buffer is set to the altered text and written to the stream. The html that the client receives is now altered with the load time. The AddLoadTimeStream class is used in the Response.Filter property, it is set in the Global.asax when the request ends.
protected void Application_PostReleaseRequestState(
object sender,
EventArgs e)
{
Response.Filter = new AddLoadTimeStream(Response.Filter);
}
The only thing that remains is putting the token on the correct page or usercontrol. It is put in the LoadTimeUserControl.ascx usercontrol.
<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="LoadTimeUserControl.ascx.cs" Inherits="AspNetWebFormsLoadTime.UserControls.LoadTimeUserControl" %>
<div>
Load Time: ##load_time## seconds
</div>
In the Page_Load of each page, master and user control a delay of 5 seconds is put. This means there is a delay of 25 seconds before the page loads because there is one master, one page and three user controls on the Complex.aspx page. This is the final result:
As is often the case with Web Forms, the solution is way too complex for a problem that's quite straight forward. But it does work. All code can be found on Github.