I have noticed that the .NET IHttpAsyncHandler (and the IHttpHandler, to a lesser degree) leak memory when subjected to concurrent web requests.
In my tests, the Visual Studio web server (Cassini) jumps from 6MB memory to over 100MB, and once the test is finished, none of it is reclaimed.
The problem can be reproduced easily. Create a new solution (LeakyHandler) with two projects:
- An ASP.NET web application (LeakyHandler.WebApp)
- A Console application (LeakyHandler.ConsoleApp)
In LeakyHandler.WebApp:
- Create a class called TestHandler that implements IHttpAsyncHandler.
- In the request processing, do a brief Sleep and end the response.
- Add the HTTP handler to Web.config as test.ashx.
In LeakyHandler.ConsoleApp:
- Generate a large number of HttpWebRequests to test.ashx and execute them asynchronously.
As the number of HttpWebRequests (sampleSize) is increased, the memory leak is made more and more apparent.
LeakyHandler.WebApp > TestHandler.cs
namespace LeakyHandler.WebApp
{
public class TestHandler : IHttpAsyncHandler
{
#region IHttpAsyncHandler Members
private ProcessRequestDelegate Delegate { get; set; }
public delegate void ProcessRequestDelegate(HttpContext context);
public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)
{
Delegate = ProcessRequest;
return Delegate.BeginInvoke(context, cb, extraData);
}
public void EndProcessRequest(IAsyncResult result)
{
Delegate.EndInvoke(result);
}
#endregion
#region IHttpHandler Members
public bool IsReusable
{
get { return true; }
}
public void ProcessRequest(HttpContext context)
{
Thread.Sleep(10);
context.Response.End();
}
#endregion
}
}
LeakyHandler.WebApp > Web.config
<?xml version="1.0"?>
<configuration>
<system.web>
<compilation debug="false" />
<httpHandlers>
<add verb="POST" path="test.ashx" type="LeakyHandler.WebApp.TestHandler" />
</httpHandlers>
</system.web>
</configuration>
LeakyHandler.ConsoleApp > Program.cs
namespace LeakyHandler.ConsoleApp
{
class Program
{
private static int sampleSize = 10000;
private static int startedCount = 0;
private static int completedCount = 0;
static void Main(string[] args)
{
Console.WriteLine("Press any key to start.");
Console.ReadKey();
string url = "http://localhost:3000/test.ashx";
for (int i = 0; i < sampleSize; i++)
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.Method = "POST";
request.BeginGetResponse(GetResponseCallback, request);
Console.WriteLine("S: " + Interlocked.Increment(ref startedCount));
}
Console.ReadKey();
}
static void GetResponseCallback(IAsyncResult result)
{
HttpWebRequest request = (HttpWebRequest)result.AsyncState;
HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(result);
try
{
using (Stream stream = response.GetResponseStream())
{
using (StreamReader streamReader = new StreamReader(stream))
{
streamReader.ReadToEnd();
System.Console.WriteLine("C: " + Interlocked.Increment(ref completedCount));
}
}
response.Close();
}
catch (Exception ex)
{
System.Console.WriteLine("Error processing response: " + ex.Message);
}
}
}
}
Debugging Update
I used WinDbg to look into the dump files, and a few suspicious types are being held in memory and never released. Each time I run a test with a sample size of 10,000, I end up with 10,000 more of these objects being held in memory.
System.Runtime.Remoting.ServerIdentity
System.Runtime.Remoting.ObjRef
Microsoft.VisualStudio.WebHost.Connection
System.Runtime.Remoting.Messaging.StackBuilderSink
System.Runtime.Remoting.ChannelInfo
System.Runtime.Remoting.Messaging.ServerObjectTerminatorSink
These objects lie in the Generation 2 heap and are not collected, even after a forced full garbage collection.
Important Note
The problem exists even when forcing sequential requests and even without the Thread.Sleep(10)
in ProcessRequest
, it's just a lot more subtle. The example exacerbates the problem by making it more readily apparent, but the fundamentals are the same.