Consider the following program, with all of HttpRequestMessage, and HttpResponseMessage, and HttpClient disposed properly.
It always ends up with about 50MB memory at the end, after collection. Add a zero to the number of requests, and the un-reclaimed memory doubles.
class Program
{
static void Main(string[] args)
{
var client = new HttpClient {
BaseAddress = new Uri("http://localhost:5000/")};
var t = Task.Run(async () =>
{
var resps = new List<Task<HttpResponseMessage>>();
var postProcessing = new List<Task>();
for (int i = 0; i < 10000; i++)
{
Console.WriteLine("Firing..");
var req = new HttpRequestMessage(HttpMethod.Get,
"test/delay/5");
var tsk = client.SendAsync(req);
resps.Add(tsk);
postProcessing.Add(tsk.ContinueWith(async ts =>
{
req.Dispose();
var resp = ts.Result;
var content = await resp.Content.ReadAsStringAsync();
resp.Dispose();
Console.WriteLine(content);
}));
}
await Task.WhenAll(resps);
resps.Clear();
Console.WriteLine("All requests done.");
await Task.WhenAll(postProcessing);
postProcessing.Clear();
Console.WriteLine("All postprocessing done.");
});
t.Wait();
Console.Clear();
var t2 = Task.Run(async () =>
{
var resps = new List<Task<HttpResponseMessage>>();
var postProcessing = new List<Task>();
for (int i = 0; i < 10000; i++)
{
Console.WriteLine("Firing..");
var req = new HttpRequestMessage(HttpMethod.Get,
"test/delay/5");
var tsk = client.SendAsync(req);
resps.Add(tsk);
postProcessing.Add(tsk.ContinueWith(async ts =>
{
var resp = ts.Result;
var content = await resp.Content.ReadAsStringAsync();
Console.WriteLine(content);
}));
}
await Task.WhenAll(resps);
resps.Clear();
Console.WriteLine("All requests done.");
await Task.WhenAll(postProcessing);
postProcessing.Clear();
Console.WriteLine("All postprocessing done.");
});
t2.Wait();
Console.Clear();
client.Dispose();
GC.Collect();
Console.WriteLine("Done");
Console.ReadLine();
}
}
On a quick investigation with a memory profiler, it seems that the objects that take up the memory are all of the type Node<Object>
inside mscorlib.
My initial though was that, it was some internal dictionary or a stack, since they are the types that uses Node as an internal structure, but I was unable to turn up any results for a generic Node<T>
in the reference source since this is actually Node<object>
type.
Is this a bug, or somekind of expected optimization (I wouldn't consider a proportional consumption of memory always retained to be a optimization in any way)? And purely academic, what is the Node<Object>
.
Any help in understanding this would be much appreciated. Thanks :)
Update: To extrapolate the results for a much larger test set, I optimized it slightly by throttling it.
Here's the changed program. And now, it seems to stay consistent at 60-70MB
, for a 1 million request set. I'm still baffled at what those Node<object>
s really are, and its allowed to maintain such a high number of irreclaimable objects.
And the logical conclusion from the differences in these two results leads me to guess, this may not really be an issue in with HttpClient or WebRequest, rather something rooted directly with async - Since the real variant in these two test are the number of incomplete async tasks that exist at a given point in time. This is merely a speculation from the quick inspection.
static void Main(string[] args)
{
Console.WriteLine("Ready to start.");
Console.ReadLine();
var client = new HttpClient { BaseAddress =
new Uri("http://localhost:5000/") };
var t = Task.Run(async () =>
{
var resps = new List<Task<HttpResponseMessage>>();
var postProcessing = new List<Task>();
for (int i = 0; i < 1000000; i++)
{
//Console.WriteLine("Firing..");
var req = new HttpRequestMessage(HttpMethod.Get, "test/delay/5");
var tsk = client.SendAsync(req);
resps.Add(tsk);
var n = i;
postProcessing.Add(tsk.ContinueWith(async ts =>
{
var resp = ts.Result;
var content = await resp.Content.ReadAsStringAsync();
if (n%1000 == 0)
{
Console.WriteLine("Requests processed: " + n);
}
//Console.WriteLine(content);
}));
if (n%20000 == 0)
{
await Task.WhenAll(resps);
resps.Clear();
}
}
await Task.WhenAll(resps);
resps.Clear();
Console.WriteLine("All requests done.");
await Task.WhenAll(postProcessing);
postProcessing.Clear();
Console.WriteLine("All postprocessing done.");
});
t.Wait();
Console.Clear();
client.Dispose();
GC.Collect();
Console.WriteLine("Done");
Console.ReadLine();
}
We had the same problems, when we use System.Net.WebRequest for doing some http-requests. Size of w3wp process had range 4-8 Gb, because we do not have a constant load. Sometimes we have 10 request per second and 1000 in other time. Of course buffer does not reused in same scenario.
We are change all place when used System.Net.WebRequest on System.Net.Http.HttpClient because it doesn't have any buffer pools.
If you have many request through your httpclient, make it as static variable for avoid Socket leaks.
I think that more simple way analyze this problem - use PerfView. This application can show reference tree so you can show root case of your problem.