New Performance Counters for HttpWebRequest

Performance counters!  You can never get enough!  And starting with the .NET version 4.0 Beta 2, we in the System.Net team have added six new counters to the old set.  But before we begin, some ground rules:

  1. Performance counters have to be enabled in your application’s .config file.  I’ve put a sample of my config file at the end of this post.
  2. The new counters need the .NET version 4 Beta 2 runtime.
  3. The counters are now called “.NET CLR Networking 4.0.0.0” (the version number is new).  You only get the new performance counters when you run using the .NET version 4 CLR.  If your program runs using an older CLR, it will use the older performance counters.
  4. The counters are now initialized on a separate thread.  This means that for the first small amount of time, activity isn’t recorded by the performance monitor.

Without any further delay, let’s take a look at the new counters. 

The first two performance counters let you know that your application is running smoothly.

1.    HttpWebRequests Created/Sec
2.    HttpWebRequests Average Lifetime

The ‘Created/Sec’ is just what it says: it’s updated whenever an HttpWebRequest is created. The performance counter takes care of adjusting the raw numbers into a “per sample interval” – just beware that the label says “per second” but it’s really “per performance counter sample time interval”.  It turns out that the default sample interval is one second, and the label is much shorter when we just call it “/Sec”.

You can see the effects of this adjustment for yourself – make a program that creates a fixed number of HttpWebRequests per second.  When the performance monitor is set to “Sample every 1 seconds”, it gives that amount.  Set it to “Sample every 2 seconds” and the number doubles.

But watch out!  System.Net will make HttpWebRequest objects for you – in particular, the System.Net automatic proxy detection on Windows XP will create an HttpWebRequest internally to resolve your proxy.  System.Net will also make extra objects for tunneling through proxies.  It’s even possible for FTP requests to silently make HttpWebRequests.  Exactly when we make these extra objects can change from one release to the next.

The average lifetime is a little sneakier.  It’s the lifetime of the entire HTTP transaction, starting when the HttpWebRequest is created all the way until the resulting HttpWebResponse is closed.  If you ever see this counter stuck at zero even though you’re making requests and getting replies, it’s probably because you haven’t closed your HttpWebResponse!  The ‘average’ is the average for the last performance sample interval, so it’s just for the time in the “Sample every ___ seconds” option in the Performance Monitor properties box.

There are two performance counters to look for Queuing issues.   The .NET framework doesn’t let you (by default) send lots of requests to a server all at once.  Instead, only up to ‘ConnectionLimit’ requests can be active at once (by default, this is two); after that we put the request onto a queue.  Thanks to pipelining, a request can be ‘waiting’ without being ‘queued’. This happens when you make a request, and there’s a perfectly good connection to the server ready to be used, but it’s busy with another request.  The new request will be pipelined with an earlier request and not queued (but it still has to wait for the earlier request to end).

The connection limit is trickier than you might realize because HttpWebRequests that are bound for different places but which share the same proxy share the same ServicePoint and therefore share the same set of connections.

The Queue performance counters are:
3.    HttpWebRequests Queued/Sec
4.    HttpWebRequests Average Queue Time

The Queued/Sec counter is updated whenever an item is put onto the queue; the Average Queue Time is updated whenever an item is removed from the queue.  This means that it’s possible to see items being added to the queue like crazy even though the average queue time is zero – it could just mean that the server is slow and nothing is coming off of the queue. 

Lastly, there are two counters to tell if your requests aren’t completing correctly.
5.    HttpWebRequests Aborted/Sec
6.    HttpWebRequests Failed/Sec

A request is Aborted when the ‘Abort()’ method is called.  The Abort() method is automatically called by System.Net in some cases, all having to do with failures – when the request times out, for example, or sometimes when the request handler code triggers an exception.  Your application code can also call Abort() on a request.

The most common reason that a request is Failed is that the final server response is a “could not fulfill your request.”  For example, a server reply code of 404 Not Found is a failure.  Technically speaking, the failed counter is updated when a request triggers an exception (other than exceptions thrown in your application code).  This is a good definition of failed because all of the failed server codes will throw a WebException.  Server codes that are not final – for example, a 401 Unauthorized return code will usually result in the System.Net code automatically retrying with proper credentials.  In this case, the 401 Unauthorized is not final and does not trigger an exception.

That’s a lot: how about a diagram?
Certainly!  Here’s a diagram that our developer created to help understand what all of the timings are.  Each of the seven green circles represents one of the six performance counters (there are two ‘5’ items because 5 is the average lifetime, and there are two code paths that will affect that counter).

Diagram

As promised, here’s a sample of my config file:

The performanceCounters value is documented on MSDN at https://msdn.microsoft.com/en-us/library/ms229151.aspx.

<?xml version="1.0" encoding="utf-8" ?>

<configuration>

  <system.net>

    <settings>

      <performanceCounters enabled="true"/>

    </settings>

  </system.net>

</configuration>

I also had to add in a “requiredRuntime” to my .config file: <startup><requiredRuntime version="v4.0" /></startup>.  Otherwise, my application – which I compiled with an older compiler – wouldn’t use the 4.0 CLR.