-
-
Notifications
You must be signed in to change notification settings - Fork 835
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
OnActivated leaks memory #1252
Comments
It'd probably be good to throw this in a memory profiler to see what's actually holding references to what. For example, I'm unclear what the So:
On a side note... @alistairjevans this seems familiar, like we saw a similar issue where, again, some sort of assignment or work done inside the event handler was causing issues. Am I imagining that? I searched the issues list and couldn't find it. |
Some quick replies...
|
A lot has changed over the years, especially in the internals. You aren't guaranteed the exact same behavior over three major version releases. However, if you can show us what exactly is leaking the memory, or at least what's actually holding it so we can be sure it's Autofac and not application code, then it's something we can investigate. Unfortunately, given the very small number of unpaid maintainers here, where support isn't our actual 'day job,' we need all the help we can get in pinpointing what's going on. The more help you can provide, the faster your problem will be solved. |
The reason that there may appear to be a memory leak here (and why there isn't one when you remove the assignment from the callback) is that when we execute the OnActivated callback you are creating a closure that captures the local context and storing it outside the callback. As of v6 that callback's closure will capture
That said, when the container instance and the capturing |
That's kinda what I thought, too, that the Oh, I'd also recommend doing all this when building/testing in Release mode. There's a huge difference between building in Debug and building in Release, especially around garbage collection. In Debug mode the app never knows when you're going to break the debugger on a method and want to check the locals, so it doesn't do as aggressive a memory cleanup when garbage collecting - locals won't actually be cleaned up by the time you call that |
We will dig deeper here to see what more we can find out. |
I am not really able to recreate the memory leak. Here are the steps I've taken: First, I checked out your PR branch from #1253. I noticed you hadn't attached I started out by running the test as-is, where
What that shows is that, per 1000 operations, there were 3.7842 Gen 0 garbage collections, 1.3428 Gen 1 garbage collections, and 23.2KB allocated per op. OK, cool. Next I deleted the
Not a significant change. I'm guessing that the additional time spent is in capturing the context and the inability of the compiler to optimize some things out. Finally, I deleted the whole
While capturing the action outside the event seems way slower, I don't see anything significantly different across those runs. Time to do some memory leak debugging and try to recreate it a different way to see if I'm missing something. In this case, I created a quick console app that runs the code from your benchmark in a loop. It does no garbage collection, just lets the memory mount up or whatever. I used .NET 5 because I'm on a Mac and It looks like this: <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Autofac" Version="6.1.0" />
</ItemGroup>
</Project> and using System;
using System.Collections.Generic;
using Autofac;
namespace AutofacDemo
{
public static class Program
{
public static void Main()
{
Console.WriteLine("Running the test - use Ctrl+C to exit.");
var count = 0;
while(true)
{
ResolveWithOnActivatedWithAction();
count++;
if(count % 10000 == 0)
{
Console.WriteLine(count);
}
}
}
public static void ResolveWithOnActivatedWithAction()
{
var builder = new ContainerBuilder();
Action<ContainerBuilder> someAction;
builder
.RegisterType<FakeService>()
.OnActivated(c =>
{
someAction = b =>
{
b.RegisterInstance(c.Instance);
};
});
using var container = builder.Build();
container.Resolve<FakeService>();
}
}
public class FakeService
{
private IEnumerable<int> _data;
public FakeService()
{
_data = new int[1000];
}
}
} Basically it just runs your benchmark thing over and over, writing to console every 1000 iterations so I can see that it's still going. I started that in one console window. Following that documentation I linked I then did a The output, which refreshed once a second, looked like this:
The important bit there is that GC Heap Size. I watched as it started around 8MB, went up to around 14MB, then garbage got collected and it returned to 8. Over and over. I ran the test for about 3 million operations and it didn't grow, it didn't get out of control or anything. Basically, after all that, I still didn't see any leak. Sorry. This article on troubleshooting .NET memory leaks may help you. I did note that the example seems to be this desire to capture an instance of something resolved and then... have a lambda that will register that instance in a different In any case, I'm not sure there's anything we can do. I'm not going to be able to really spend much more time trying to troubleshoot this since I can't replicate it. It seems like what's probably happening is that you're capturing stuff in the OnActivated event and doing something with it, and that's what's causing the problem. I'll leave this issue open and marked |
First, thanks for your reply and effort! Much appreciated! In the PR I sent in, I didn't even got a result, the memory pressure was so extreme, so adding When I run your example (the console app), I have no chance waiting for "3 million operations"... Using same tool as you do, this is the result in iteration ~1.65million... ...not even close to your result. |
Not sure what to say here. Machine specific stuff can affect things, OS can affect it, amount of memory you have can affect it... like, for example, let's say you have NCover installed and have been using it. It runs on the profiler API so it can instrument things and get coverage results for you. If that's turned on for all .NET processes, it can mess with the perf and memory. (I'm not saying you are using NCover, I'm saying there are things you can install or use that may affect things.) I would very, very strongly recommend you get a .NET memory profiler on this thing to see what's going on. It seems like something is obviously happening, but since I can't repro it, it means you're unfortunately on your own. The only way you're going to really track it down is with the right tools. In the meantime, something you may want to consider if you can - set up a super basic Windows Hyper-V VM. No extra tooling, just .NET Core SDK and the |
I'm actually on a newly installed machine so I was thinking the opposite. Maybe on a "clean" machine the problem occurs, you might have a kb, some update or similar fixing the issue? Pretty far-fetched I know. On the other hand, my colleague (and our build agents) have the same problem as I do. We have solved the problem in our app now, avoiding the lambda in the callback. Still, there's something fishy here I guess. Not at all sure it's related to autofac code, not saying that at all. Gave a mem profiler a shot a few days ago but couldn't (easily) find out what ref causing the objects to still be held. Could see lots of objects kept in memory because of (indirect) refs from |
I run "your" console app in dotmemory. See the same thing as when I had a look a few days ago, cannot find anything new, seems like the Why the Here's a SO thread about the subject. |
I am referencing an owin issue which stated similar concerns to get better search results on that issue. |
Oh, good memory, @alsami! I knew there was something tickling the back of my brain, that was it. I set up a Windows VM and tried this same test. It seems my Windows machine has a much larger tolerance for
It climbed until about 12GB of heap before it did a garbage collection. that was around the 660,000 iteration range. But it never did get back down to the original size. However, it absolutely slowed to a crawl, which the Mac version didn't. So, so weird. I got to 3 million in a few minutes on Mac; in Windows it took like two minutes to go from 740,000 to 750,000. It probably ties in a bit with this blog article where thread local storage usage caused slowdown in .NET 4. I'm guessing the extra thread local storage being used here is also probably causing challenges. I wonder if the answer is as simple as "be sure to set the Thanks all for keeping on this; it's really hard to nail down these things so I appreciate the help thus far. |
I would be happy if I'm wrong but I doubt setting |
OK, I've spent a couple of hours trying to be pretty belligerent with I would assume the next thing to try would be to use something other than I will say... I just (20 minutes or so ago?) got word that the "day job" project I'm on has been, uh, "accelerated" to have a required delivery date several months ahead of schedule. That probably limits the amount of time I might be able to spend on this in the immediate future, though I do think it needs to be solved. If other folks can pick up where I left off, great; if not, I'll have to get back to it after the heat is off. |
Ok, thanks for your replies, time and help. Anyhow, fwiw, to reproduce a simple
|
I can take a look at this when I get a bit of time (day job also pretty busy over here). I'm fairly familiar with the synchronisation requirements of the |
OK, I've figured out why this memory leak happens, and in turn, what the solution is. Worth noting it was trivial for me to recreate the huge memory leak on Windows with @tillig's test program. Basically, the problem is that when the For any other collection type, this wouldn't be a problem; normally the GC would collect the collection, which would in turn collect every item in it. However, due to the use of So, the solution then is just to explicitly clear that Will raise a PR shortly. Also, thanks @RogerKratz for persisting with this despite our initial opposition! |
Sounds great but... In isolated tests outside autofac, emptying the collection won't make any difference, I tried that before and I double checked it now again. Eg snippet above, #1252 (comment), behaves the same in my environment no matter clearing the collection or not. |
I can confirm that if I use the simple loop from your comment, clearing the bag after each iteration has no effect on memory usage in my environment (i.e. there still appears to be a leak, as you have seen). But emptying definitely has a big impact in Autofac, so I think I can rule out the environmental. |
Super weird this only happens on Windows. They must have implemented it differently in the Mac stack, or the Mac garbage collector works differently enough to clean up the thread local storage once ConcurrentBag references are gone. |
There's an interesting comment in the runtime about the finalizer getting called when the thread exits. I suspect, as you say, that the GC for thread local storage works differently on the two platforms. |
It seems OnActivated (and possibly other callbacks?) leaks memory. Below a little test that uses more and more memory. If OnActivated is removed, problem is gone
The text was updated successfully, but these errors were encountered: