-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
AssemblyLoadContext - WPF - Collectable Assemblies are not garbage collected #13226
Comments
Out of interest, if you add I've seen that attribute used in a several examples, as it helps prevent assemblies being inadvertently 'rooted', e.g. |
It's possible that the jit might extend GC lifetimes of inlinee locals beyond the extent of the inlinee. dotnet/coreclr#9479 tried to put a stop to this, so if you find examples where this happens I'd be happy to take a look. For allocations within the same method where you force GC, even if you null out your locals after using them, the jit may have stashed references in jit temporaries that don't get nulled. Keeping your expected to be dead allocations and references in a noinline method is the most reliable fix. |
cc @janvorli |
Adding |
It's strange ... if i am change Assemblies are now garbage collected. |
@x1c3 how do UserControl and UserControl1 differ? |
UserControl1 inherit from UserControl. UserControl1 has no special code implementation. It is empty.
|
@x1c3 I have just noticed you only call GC.Collect(); GC.WaitForPendingFinalizers(); once. That doesn't guarantee that the unload will complete after that. If you want to make sure that everything is unloaded at some specific point, you need to do the following (as described in the https://docs.microsoft.com/en-us/dotnet/standard/assembly/unloadability-howto#example-source-with-unloadability-issues).
|
I think that the problem is not the implementation, but the wpf control (UserControl1). Without this UserControl1 the assembly load context and the loaded assembly is garbage collected immediately.
|
@x1c3 so do I understand it correctly that the code above fails to unload with UserControl1 even with many iterations in the |
@janvorli |
@x1c3 I've tried to replicate this locally and I get the following output:
So it seems to work. I wonder - is your main app that you are testing it with a WPF app or a console app? I am testing it with a console one, so that may cause the difference. |
@janvorli I am using a console app. I would like to know, what is different in your implementation. |
Ok, my local repro has the UserControl in a separate assembly. That makes the difference. I've just split your Plugin project into two - one with the Plugin.cs and the other with the UserControl1 files. |
@janvorli |
I agree that it is strange. I'll debug it to get full understanding of the different behavior. |
@x1c3 I have found why it works when the control is in a separate assembly. I have missed before that your custom CollectableAssemblyLoadContext doesn't resolve other assemblies than the plugin one into this context (the Load method returns null). So when the control is in a separate assembly, it is loaded into the default context and thus it doesn't prevent unloading of the context. But that's not what you wanted do end up with. The real culprit is in the WPF itself. The MS.Internal.Resources.ResourceManagerWrapper holds on the assembly containing the control. And it is rooted (transitively) by two different roots. I've just digged that from the !gcroots command in WinDbg at the point where the unload fails. So it seems WPF will need to be made aware of unloadability to make your scenario work. |
@janvorli |
Someone will need to fix this in the WPF repo (https://github.com/dotnet/wpf). Since it is open source, it doesn't have to be the wpf team. |
Created issue here: dotnet/wpf#1764 |
I mentioned your issue in my post since, at least to me, seems related. The staff marked my post initially with the "investigate" label and now sent it to future milestone. This make me think that the fix is something that can take months and more. UnloadWpfLibrary.zip |
I just want to update this issue with a new sample project where I applied the workaround suggested from @janvorli . This time one of the two assemblies is unloaded, but all of the others are still loaded included WpfLibrary. |
@netcorefan1 I have tried your latest sample and after the loop with GC collect, I have verified that everything was correctly unloaded. So I wonder if you were testing it when running under visual studio or standalone. We had some issues with Visual Studio itself locking the assemblies. |
@janvorli Thanks for your help! I have tried through VS and standalone. In VS, after a few GC iterations the only assembly unloaded is ProxyClass.dll, but all the other WPF framework assemblies remain loaded: In standalone, in process explorer the same WPF Framework assemblies are still loaded: In standalone memory is not released. It should be around 4-5 mb (because when I instantiate MainWindow.cs it loads all the Framework dependencies and memory usage increases to 25Mb): I opened in StackOverflow a thread related to that matter where I show sample code which basically is made of just a WPF app. In that project I demonstrate that after some time (still to understand why at random time) the memory increase due to opening and closing a WPF MainWindow is freed and return back to its initial value. And I was asking to the users if its really worth to deal with AssemblyLoadContext which force to have 4 different projects when with one project I end up with the same memory reduction. You should find some interesting things in that thread. |
The WPF assemblies are loaded into the default context, that's why they are not unloaded. Your implementation of the WpfAppAssemblyLoadContext returns null from the Load method, which means that all dependencies of the ProxyClass.dll should be loaded into the default context. |
The Load method is called only two times, for System.Runtime and ProxyInterface. I left them to return null because they are already loaded in the default context. None of the wpf assemblies pass through the same Load method although they are references of ProxyClass. All the wpf assemblies goes automatically into the default context, no matter what I do and I have no idea of what to do because Microsoft does not provide any doc on how to include the framework assemblies in the custom unloadable context. I don't understand why has not been implemented a simple boolean switch which allow to auto load in ALC all the references of the target assembly, not just the assembly itself. |
@vitek-karas do you have any idea why the Load method on the assembly load context is not called for the WpfLibrary.dll and PresentationFramework even though the ProxyClass.dll directly references it? This is the list of references in the ProxyClass.dll: I've tried the repro locally and I can confirm that it is really called just for the System.Runtime and ProxyInterface |
If the goal is to load a WPF control as a "plugin" and when unloaded all of WPF will be gone as well, then this is not currently possible.
Currently the recommended solution is to load frameworks into the default load context (as this app does) and only the plugins are loaded into a custom load context - which can be unloaded. The framework remains loaded for the lifetime of the process. Unfortunately dotnet/wpf#1764 makes it rather hard if not impossible to work with unloading when using WPF. As for the project design - a good guide how to build a plugin app is here: https://docs.microsoft.com/en-us/dotnet/core/tutorials/creating-app-with-plugin-support (it has a working sample app here: https://github.com/dotnet/samples/tree/master/core/extensions/AppWithPlugin). Specifically I want to point out how to reference the "shared" assemblies from the plugin: https://docs.microsoft.com/en-us/dotnet/core/tutorials/creating-app-with-plugin-support#simple-plugin-with-no-dependencies. In your case this is about the project reference from Then you should not need a project reference from the MainApp to the plugin (ProxyClass in your case) and using the AssemblyDependencyResolver in your custom load context to resolve its dependencies. |
@vitek-karas I was actually also asking about the WpfLibrary, which is an assembly built by the sample project and that the ProxyClass depends on (verified using ILSpy). It seems strange that we don't call the Load method for it at all. |
I see the |
Ah, that's possible, I haven't realized that the load happens later and just debugged it till it loaded the test assembly. |
@vitek-karas @janvorli @ryalanms added my issue to Future milestone. I am afraid he was not aware that this is not supported and probably will never be supported. Someone can remove my issue from Future milestone? Now... As far as I know, there are two only possible alternatives:
I am not sure what is the best between the two solutions (and I never had to deal with IPC), so if I said something wrong please correct me. With solution 2, although memory will return to the initial state soon or later, the framework will be still loaded for the full lifetime of the application and I am not sure how this could affect system resource in comparison with an IPC solution which is supposed to keep everything isolated. |
The WPF specific issue is that even if you do include the entire framework in the app, using plugins which rely on WPF may mean that the plugin cannot be unloaded (the plugin itself, not WPF). Having WPF loaded should not hurt performance outside of some relatively small memory consumption. WPF assemblies should be R2R images so there should be very little JITing happening and so most of the memory should be files mapped into memory - so called "shared" memory. Meaning that if there's another WPF app using the same framework, the memory will be shared between the processes. Private working set introduced by having the assemblies loaded should be very small. That said WPF itself probably allocates lot of resources (all of the graphics stuff) - I don't know when/how is that freed if at all. That's something WPF owners should be able to answer. As for recommendations:
|
I moved to a different team, so no longer keeping issues assigned to me, that's all 😅... and I continue to help out occasionally and hope to contribute on and off. There are still folks like @ryalanms who continue to work on important stuff. That said, eliminating global state in WPF is going to be tedious because there are certainly many global objects that are kept around intentionally. It can be done, but I can think of a handful of refactorings that that would make the framework more robust, performant and easier to contribute-to etc..., so these things always come down to prioritization 😄 |
@vitek-karas Not sure if I have understood well, but in my case the plugin itself is just a WPF window and the only situation where I need WPF. Once I call CloseWindow() I don't need anymore that Window and all the related staff of the framework. A sample case could be a settings window or a window displaying an update progress, some operations that may occur rarely. When I run the program, before instantiating the MainWindow, in task manager the memory usage is 4.6 Mb, then becomes around 25-26 Mb. After some time, whether I call GC or not, the memory decrease and return to the initial value of 4.6 Mb. Sometimes within minutes, sometimes hours, sometimes after 12 hours the memory is still 25-26. With MaterialDesign framework can easily reach hundreds of MB and for a system with limited RAM I think I should take this in consideration especially after what seems to be my wrong assumptions regarding memory management because from I understood, even if task manager shows the memory returned in its initial state, it is not really fully released. 3- This is another solution that came to me in mind right now as an additional step of solution 2 to be used in case that our app is the only one that needs wpf (and consequently can't rely on shared memory). Restart the entire application which means saving some sort of state (if needed) in a settings file, call CloseWindow() and then something like @vatsan-madhavan Thanks for your participation. Yes, I saw how can be tedious and it's good to know that can be done. At least now we have some alternatives, not great like the ability to fully unload an assembly, but acceptable. |
In theory the MaterialDesign should be unloadable, there's no limitation to that on the runtime side of things. Unfortunately the WPF issue might mean this won't work either. Note that making WPF not hold onto random assemblies is very likely a MUCH easier fix than making entire WPF unloadable. Depending on the operation you need to do with the UI, but if it's for example the mentioned "update", you could run the application itself as a child process with some command line arguments to do the operation, while the parent simply waits for the child to exit. Maybe you won't even need any kind of complex IPC, just some command line stuff. |
Thanks for the precious information. I suppose this means that maybe needed the same workaround from @janvorli until the Team fixes the issue. This could be a nice solution although I have some concerns. The maximum I can do to get data back is redirect standard output and make some weird string extraction to get the required data. Problem is that I can't share objects. For example I may need HttpClient to some preliminary operations in MainApp and then reuse it to the child process for the real operation. I am not sure, but I am afraid this is only possible with IPC. Anyway its worth to try both the methods and see what is best suitable. Thanks for providing another jolly to play with. |
With out-of-proc solution I don't think it's possible to share objects. You can serialize objects via some JSON/XML/gRPC to the other process, but it will not "share" the object as such. I highly doubt this would work for .NET Framework had remoting which sort of made this possible (but I'm not sure it would have worked on |
Thanks vitek, you saved me from a lot of headaches. So, the trick is to leave an object on a side or another and interact with it from both the sides using commands passed through methods. Unless I encounter something unexpected, to me seems pretty straightforward. I will go for this solution and I think all the people reading this should, at least until (and if) the team will make possible to unload the framework assemblies too. Thanks to all guys! |
It is 2022 and I presume this problem still has no solution to it. I have a WPF app, and I too cannot unload the plugin if it creates a WPF window. I don't care about unloading the actual WPF-related dependency assemblies that get loaded, since like I said my base app is WPF anyways. I just need a way to unload the actual plugin with the WPF window in it. Is there currently no way in .NET to create an application that has GUI plugins and unload them? |
I have a problem with a plugin that has a wpf usercontrol. If an instance of the control is created then the plugin assembly will not be garbage collected. Whats wrong here?
The text was updated successfully, but these errors were encountered: