Open depler opened 6 months ago
Can you still repro with this change? Process
implements IDisposable
and you aren't disposing of it so there's a possibility you're just observing native resources associated with that process handle leaking.
- var memory = System.Diagnostics.Process.GetCurrentProcess().PrivateMemorySize64;
+ long memory;
+ using (var process = System.Diagnostics.Process.GetCurrentProcess())
+ {
+ memory = process.PrivateMemorySize64;
+ }
I've running it in docker on mcr.microsoft.com/dotnet/aspnet:8.0
. Looks like ok, I can't reproduce it.
Processor count: 2
Available memory: 6217 MB
68 MB
2102 MB
3136 MB
3138 MB
3123 MB
3104 MB
3127 MB
Code:
using System.Diagnostics;
using System.Runtime.CompilerServices;
var data = new byte[1_000_000_000];
var app = WebApplication
.CreateSlimBuilder(args)
.Build();
Console.WriteLine($"Processor count: {Environment.ProcessorCount}");
Console.WriteLine($"Available memory: {GetAvailableMemory()}");
var downloadApi = app.MapGroup("/download");
downloadApi.MapGet("/", () =>
{
GC.Collect();
Console.WriteLine(GetCurrentMemory());
return Results.File(data, "application/pdf", "test.pdf");
});
await app.RunAsync();
return;
static string GetAvailableMemory()
{
var memoryInfo = GC.GetGCMemoryInfo();
return $"{memoryInfo.TotalAvailableMemoryBytes / 1_000_000} MB";
}
[MethodImpl(MethodImplOptions.NoInlining)]
static string GetCurrentMemory()
{
using var process = Process.GetCurrentProcess();
return $"{process.WorkingSet64 / 1_000_000} MB";
}
So, I think:
ConcurrencyLimiter
in your application's configuration to make this rule clear.Can you still repro with this change?
Process
implementsIDisposable
and you aren't disposing of it so there's a possibility you're just observing native resources associated with that process handle leaking.- var memory = System.Diagnostics.Process.GetCurrentProcess().PrivateMemorySize64; + long memory; + using (var process = System.Diagnostics.Process.GetCurrentProcess()) + { + memory = process.PrivateMemorySize64; + }
Yes, I still can reproduce it. There is probably some leak because of missing Dispose, but not gigabytes.
I've running it in docker on
mcr.microsoft.com/dotnet/aspnet:8.0
. Looks like ok, I can't reproduce it.Processor count: 2 Available memory: 6217 MB 68 MB 2102 MB 3136 MB 3138 MB 3123 MB 3104 MB 3127 MB
Code:
using System.Diagnostics; using System.Runtime.CompilerServices; var data = new byte[1_000_000_000]; var app = WebApplication .CreateSlimBuilder(args) .Build(); Console.WriteLine($"Processor count: {Environment.ProcessorCount}"); Console.WriteLine($"Available memory: {GetAvailableMemory()}"); var downloadApi = app.MapGroup("/download"); downloadApi.MapGet("/", () => { GC.Collect(); Console.WriteLine(GetCurrentMemory()); return Results.File(data, "application/pdf", "test.pdf"); }); await app.RunAsync(); return; static string GetAvailableMemory() { var memoryInfo = GC.GetGCMemoryInfo(); return $"{memoryInfo.TotalAvailableMemoryBytes / 1_000_000} MB"; } [MethodImpl(MethodImplOptions.NoInlining)] static string GetCurrentMemory() { using var process = Process.GetCurrentProcess(); return $"{process.WorkingSet64 / 1_000_000} MB"; }
So, I think:
- You can run your example in a container to eliminate side effects.
- You can read about server garbage collection to understand why memory traffic depends on CPU and available memory.
- Make sure that you download only one file in your test at a time. If you will download in parallel the situation will change. You can use
ConcurrencyLimiter
in your application's configuration to make this rule clear.
return Results.File(data, "application/pdf", "test.pdf");
grows up to half of availiable memory.
If you replace it with return data
- it will grow up to full memory. Different GC settings does not change the behaviour.
There's a number of issues open in dotnet/runtime related to increased memory consumption with containers in .NET 8:
Do any of those correlate with what you're seeing in terms of the runtime environment you use and the work your application typically does?
So, memory is growing up dramatically because of this: https://github.com/dotnet/aspnetcore/blob/8486d31e24f30e3fa1809a95699a0adc16f448d7/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs#L35
And this: https://github.com/dotnet/aspnetcore/blob/v8.0.4/src/Shared/Buffers.MemoryPool/MemoryPoolBlock.cs
You see? Starting from some response length (megabytes and more) Kestrel uses memory pool to store this response. The problem is that PinnedBlockMemoryPool never release memory - it can only grows up. As far as I know there is no way to disable this behavior, and Microsoft has been promising for 3 years to fix this 🙁
I've made code sample to demonstrate the problem and way to reduce memory consumption: https://github.com/depler/DotnetMemoryTest
It uses Harmony (https://github.com/pardeike/Harmony) to override original logic of PinnedBlockMemoryPool
. See method MemoryPatch.Install();
Test steps are following:
Tracking memory
text in consoledownload_loop.bat
file for about 1 minute, then kill itMemory consumption without patch:
Memory consumption with patch:
So this problem is definitely related with incorrect implementation of PinnedBlockMemoryPool
. It is not memory leak by nature, but it leads to accumulation huge amount of almost-not-used byte arrays forever, while process alive. At the very bad scenario - it will lead to OutOfMemoryException
during heavy load.
While this is a problem, the easy way to avoid this is to write to the response in chunks instead of allocating large buffers per request. Though the pinned block memory pool has issues, allocating large buffers on the large object heap per request will result in similar issues.
Is there an existing issue for this?
Describe the bug
Here is code sample:
I am calling this method via
curl
as following 10 times:curl --insecure https://localhost:5000/api/Tests/Download > NUL
And here is output of the sample:
So memory grows over and over, despite the fact that
Data
is static variable andGC.Collect()
called each time. Please someone explain what is going on? And how to reduce memory consumption?Expected Behavior
Much less memory consumption
Steps To Reproduce
No response
Exceptions (if any)
No response
.NET Version
8.0.204
Anything else?
No response