dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.28k stars 9.96k forks source link

Memory Leak in HostingApplication/WebApplication during integrationtest #54342

Open JanEggers opened 7 months ago

JanEggers commented 7 months ago

Is there an existing issue for this?

Describe the bug

When running integrationtests memory usage accumulates besides the WebApplication is disposed.

image

there are also a bunch of classes kept alive by service provider internals again my pov is that when the webapplication is disposed everything should be cleaned up. I can see the serviceProvider is disposed but it still contains a buch of stuff that keeps objects alive.

image

I tried to manually cleanup stuff which improved the situation but not really cleaned it up all the way there are still classes that are kept by for example som DFAMatcher

image

here is my code I use to improve dispose behavior. Im not sure which are sideffects and should be gone automatically once the webapplication is no longer kept alive but its hard to see if this kind of large objects are kept alive.

var server = _host.Services.GetRequiredService<IServer>();

_host?.StopAsync().GetAwaiter().GetResult();
_host?.Dispose();

var callSiteFactory = serviceProvider.GetType().GetProperty("CallSiteFactory", BindingFlags.Instance | BindingFlags.NonPublic)!;

var callSiteFactoryValue = callSiteFactory.GetValue(serviceProvider)!;
var descriptors = callSiteFactoryValue.GetType().GetField("_descriptors", BindingFlags.Instance | BindingFlags.NonPublic)!;
var descriptorLookup = callSiteFactoryValue.GetType().GetField("_descriptorLookup", BindingFlags.Instance | BindingFlags.NonPublic)!;

var descriptorsValue = (IList)descriptors.GetValue(callSiteFactoryValue)!;
descriptorsValue.Clear();

var dictionary = descriptorLookup.GetValue(callSiteFactoryValue)!;
var clearDictionary = dictionary.GetType().GetMethod("Clear")!;
clearDictionary.Invoke(dictionary, new object[0]);

var callSiteCache = callSiteFactoryValue.GetType().GetField("_callSiteCache", BindingFlags.Instance | BindingFlags.NonPublic)!;
var callSiteCacheDictionary = callSiteCache.GetValue(callSiteFactoryValue)!;
var clearcallSiteCacheDictionary = callSiteCacheDictionary.GetType().GetMethod("Clear")!;
clearcallSiteCacheDictionary.Invoke(callSiteCacheDictionary, new object[0]);

var callSiteLocks = callSiteFactoryValue.GetType().GetField("_callSiteLocks", BindingFlags.Instance | BindingFlags.NonPublic)!;
var callSiteLocksDictionary = callSiteLocks.GetValue(callSiteFactoryValue)!;
var clearcallSiteLocksDictionary = callSiteLocksDictionary.GetType().GetMethod("Clear")!;
clearcallSiteLocksDictionary.Invoke(callSiteLocksDictionary, new object[0]);

var serviceAccessors = serviceProvider.GetType().GetField("_serviceAccessors", BindingFlags.Instance | BindingFlags.NonPublic)!;
var serviceAccessorsDictionary = serviceAccessors.GetValue(serviceProvider)!;
var serviceAccessorsClearDictionary = serviceAccessorsDictionary.GetType().GetMethod("Clear")!;
serviceAccessorsClearDictionary.Invoke(serviceAccessorsDictionary, new object[0]);

var host = _host!.GetType().GetField("_host", BindingFlags.Instance | BindingFlags.NonPublic)!;
var hostValue = host.GetValue(_host)!;

var hostedServices = hostValue!.GetType().GetField("_hostedServices", BindingFlags.Instance | BindingFlags.NonPublic)!;
hostedServices.SetValue(hostValue, null);

var options = server.GetType().GetProperty("Options", BindingFlags.Instance | BindingFlags.Public)!;
var optionsValue = options.GetValue(server)!;
var codeBackedListenOptions = optionsValue.GetType().GetProperty("CodeBackedListenOptions", BindingFlags.Instance | BindingFlags.NonPublic)!;
var codeBackedListenOptionsValue = (IEnumerable)codeBackedListenOptions.GetValue(optionsValue)!;

foreach (var listenOption in codeBackedListenOptionsValue)
{
    var middleware = listenOption.GetType().BaseType!.GetField("_middleware", BindingFlags.Instance | BindingFlags.NonPublic)!;
    var middlewareValue = (IList)middleware.GetValue(listenOption)!;
    middlewareValue.Clear();
}

var addressBindingContext = server.GetType().GetProperty("AddressBindContext", BindingFlags.Instance | BindingFlags.NonPublic)!;
addressBindingContext.SetValue(server, null);

Expected Behavior

Disposing the webapplication should free all related Memory.

Steps To Reproduce

No response

Exceptions (if any)

No response

.NET Version

8.0.2

Anything else?

No response

JanEggers commented 7 months ago

another Root seems to be a Timer in DefaultHttpClientFactory

image

martincostello commented 7 months ago

another Root seems to be a Timer in DefaultHttpClientFactory

That's internal to DefaultHttpClientFactory itself: code.

It's going to keep running until it's satisfied that there's no handlers in active use. If you think that's the case and it's still running for no reason, then that's an issue to file over in the dotnet/runtime repository.

JanEggers commented 7 months ago

for me the mayor issue seems to be that there is some kind of policy missing that ensures that all classes that store delegates for whatever reason need to implement IDisposable and clear the delegates during dispose

amcasey commented 6 months ago

@JanEggers Can you please provide a small repro program demonstrating the issue? Probably, this would look like a single test followed by the cleanup you were expecting to free the memory. Thanks!