-
Notifications
You must be signed in to change notification settings - Fork 2.7k
[WIP] Add support for collectible assemblies via AssemblyLoadContext #1684
Conversation
@jkotas Could you take a look at this to see if the way I'm approaching this is okay? |
@jkotas I got rid of the call to When trying to load a previously collected assembly, it crashes somewhere in Those two caches allocate memory for entries in one of the AppDomain's LoaderHeaps. Should I be trying to remove the entries from there or should they not be in there at all? |
I think they should be allocated on regular heap instead, so that they can be freed once the assembly load context is collected. |
@jkotas I'm removing those entries from the AppDomain now. Now I can load, run and collect an assembly in a loop! I tried adding a dependency to the collectible assembly but it doesn't get loaded through the AssemblyLoadContext, so it wont be collectible. Is this the intended behavior? |
I am not exactly sure what you mean. Each assembly is loaded into some load context. The intended behavior is that all assemblies loaded into unloadable load context are unloaded as a unit when the unloadable load context is collected by the GC. |
Yes, but when I load an assembly into an unloadable context, and that same assembly depends on another assembly, should that also use the unloadable context? This comment seems to suggest it should, but it doesn't check the TPA list before loading. |
@jkotas What I tested was loading Test.dll into a collectible AssemblyLoadContext. However, Test.dll depends on TestDep.dll. Should both Test.dll and TestDep.dll be loaded by the same AssemblyLoadContext? When I try it TestDep.dll gets loaded by the TPA binder, even when it's not on the TPA list! If I check if the assembly is in the TPA list before trying to bind using the TPA binder, TestDep.dll will be loaded into the same AssemblyLoadContext and they will be collected together. |
It depends on implementation of your override for AssemblyLoadContext,Load(AssemblyName assemblyName). If the assembly is not on TPA list and the override for your unloadable context handles both Test and TestDep, e.g.:
then both Test.dll and TestDep.dll should be loaded into the same AssemblyLoadContext. |
I am using something similar to that but without checking the assembly name. I load Test.dll with LoadFromAssemblyName on an instance of my CustomLoadContext, but TestDep.dll will load with the TPA binder. My directory tree looks like this:
And I run Program.exe with: Everything in the runtime directory should be on the TPA list, so loads for those should go through the TPA binder. But when it comes to loading TestDep.dll, it runs this bit of code and ends up using the TPA binder for TestDep.dll. If I replace that line with the following, it will load into my custom AssemblyLoadContext: hr = HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND);
if (m_pTPABinder->IsInTpaList(pAssemblyName->GetSimpleName()))
hr = m_pTPABinder->BindAssemblyByNameWorker(pAssemblyName, &pCoreCLRFoundAssembly); The comments around there suggest it should be checking the TPA list for the assembly name in question, but it never does so. Aside from that, I need to start writing tests for this. Where should they go? It looks like most should go in the corefx repo but what about stress tests? |
Right, corefx repo is the preferred place for contract tests. There are some tests for the System.Runtime.Loader contract already.
Several options:
|
I'm not really sure what I should be doing in the CoreFX repo. Should I be opening another issue there for the API addition? I tried to write some tests but the project references System.Runtime.Loader from MyGet so I couldn't use the new constructor. Getting CoreFX to use my build of CoreCLR also has its own problems: https://github.com/dotnet/coreclr/issues/1197 Thanks @davidfowl! |
@kouvel Could you please update instructions on how to write and run CoreFX tests against private CoreCLR with new APIs, since you went through it for your recent AssemblyLoadContext addition? |
The changes to CoreCLR and CoreFX have to be staged. Once the CoreCLR changes are merged and mscorlib is updated on myget.org, you can consume that and update any contracts in the CoreFX repo. Once the contract changes are merged and myget.org is updated with the new contracts, you can consume them for tests. I'll add some instructions on this. |
{ | ||
} | ||
|
||
protected AssemblyLoadContext(bool isCollectible) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You'll need to update src/mscorlib/model.xml with any new public/protected APIs, otherwise I believe the new APIs will be stripped out prior to publishing. I'm not sure how you would do that though, since the new APIs are enabled conditionally. @weshaggard, do you have a suggestion for this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
model.xml accepts conditions the same way as we set up #ifdef's. However I don't really like public APIs be conditions so we should either take them or not. We should avoid having them sometimes and not other times.
@jkotas I'm still getting a memory leak with my load-run-collect loop. Is there something that can help me find it, or could someone else take a look? It looks like it's leaking about 8 bytes per assembly on each iteration. I'm compiling for x64 so it's probably a pointer being added to some array somewhere. @kouvel This page is about running the CoreFX tests on a custom build of CoreCLR/mscorlib. It's out of date but would be really useful to make sure I didn't break anything. |
I would use perfview because of I am familiar with it - any good memory analysis / leak hunting tool should work though. |
@Rohansi, you can use the these instructions for running CoreFX tests outside Windows. To use a private build of CoreCLR, see the arguments to run-test.sh, you just need to pass in --coreclr-bins and --mscorlib-bins. I'll update the instructions you linked. On Windows, CoreFX tests are run during the build and I believe they use a published build of CoreCLR, not sure if there's currently a way to run against a private build of CoreCLR. |
I'm in the process of adding some relevant testing / new API instructions here: #1761. |
I'm not having any luck tracking down this memory leak. Would someone else be able to take a look? I can upload the programs I'm using to test it. I'm 99% sure it's not allocated in my code and is just something that needs to be deleted when unloading the assembly, but I have no idea what is saved when loading assemblies. I only found the ones I do remove because it crashes when I don't. Also, looking over my current changes for issues would be nice so I can take a break from the memory leak! |
I found it! A value is being added to Now when running memory usage seems to be stable. I saw it increase from 6.7 MB to 7.0 MB over 5 million reloads of 2 assemblies. It was stuck around 7 MB for a while so that might have just been the heap expanding or something. @jkotas Could you look over this to see if there's any problems with my additions? |
if (!m_pLoaderAllocator->IsCollectible()) | ||
{ | ||
if (m_hGrantedPermissionSet != NULL) | ||
m_pAppDomain->ReleaseObjRefPtrsInLargeTable((OBJECTREF*)((UINT_PTR)m_hGrantedPermissionSet - 1), 1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should not be needed. The handle in StoreObjectInLazyHandle
should be allocated from collectible LoaderAllocator
and automatically reclaimed once the collectible LoaderAllocator
is destroyed.
Otherwise, you will have this memory leak problem in every place where LoaderAllocator::AllocateHandle
is called.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I should be moving some of the LoaderAllocator
initialization I added to Assembly::Create()
into AppDomain::LoadDomainAssemblyInternal()
? This would allow me to use the correct LoaderAllocator
when creating the DomainAssembly
and should fix other cases.
I just ran all the corefx tests with a debug build of this and nothing failed, good to know I didn't break anything! @jkotas Is the fix used in Rohansi@1544af8 okay? |
I was originally trying to only have one LoaderAllocator for the AssemblyLoadContext but I kept running into issues with it. Things like the LoaderAllocator GC were only written to handle one assembly per LoaderAllocator, so I tried other ways that didn't require changing how everything else works. Is there lots of overhead per LoaderAllocator or is it just strange? |
I think you will run into cases where one assembly owned by the load context gets collected, but other assemblies owned by the same load context are still going to have reference to it (e.g. pointers burnt in JITed code or EE data structures). |
That shouldn't happen, the CLRPrivBinderAssemblyLoadContext instance associated with the AssemblyLoadContext holds a reference to all the LoaderAllocators and will only release them when the managed part of all LoaderAllocators is gone (phase 3). The remaining reference will prevent the LoaderAllocator GC from collecting it. |
@jkotas I should be defining my own Crst type, right? I reused an existing type to get it working but the documentation says I shouldn't do that. So I added one to CrstTypes.def but I can't find CrstTypeTool! |
Adding the tool in #1789 |
CLRPrivBinderAssemblyLoadContext::~CLRPrivBinderAssemblyLoadContext() | ||
{ | ||
#ifdef FEATURE_COLLECTIBLE_ALC | ||
delete m_loadersCrst; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check for null prior to delete.
@jkotas I just found an issue in this and i'm not sure what problems it could cause, or how to fix it. The managed Assembly objects will get collected before the AssemblyLoadContext if there are no references to them. Using the same example I described before, Program.exe loads Test.dll into a collectible AssemblyLoadContext, Test.dll depends on TestDep.dll. Program uses reflection to call a function in Test which calls a function in TestDep. When the GC runs, TestDep's Assembly object will be collected and disappears from the list returned by The function continues to work after TestDep's managed Assembly object is collected. However, this would mean any WeakReferences to TestDep would be cleared. This is definitely not what we want but I'm not sure how to prevent them from being collected unless there are no references to them. |
Right, this is not what we want. AssemblyLoadContext is referenced via weak handle, and each of the Assembly owned by the AssemblyLoadContext is referenced via weak handle as well. It explains why Assembly can be collected independently on AssemblyLoadContext. The fix should be to get it rid of the per assembly weak handle - have weak handle per AssemblyLoadContext only. |
There is no per Assembly weak handle. I was referring to user code having a weak handle for them. Another problem is AssemblyLoadContext cannot have strong references to any of the Assemblies it loaded or it will never be able to be collected. The Assembly will keep the managed LoaderAllocator alive which prevents the AssemblyLoadContext from finalizing. Finding out how to give the managed objects their proper lifetimes is making my head hurt. There's so many things to keep track of, cycles to avoid, and requirements to meet. I need to start making diagrams. |
There is weak handle per LoaderAllocator. And LoaderAllocator is per Assembly right now. It means that there is per Assembly weak handle - it is just not created in the new code that you have written. I believe that you will need to go with LoaderAllocator for the AssemblyLoadContext to fix these sort of problems. I understand that it may not be straightforward to make it happen, but I do not see a way how to avoid it. |
I'm going to try doing this again. I just don't fully understand what keeps the Assembly objects alive. It looks like somewhere in MethodTable there's a handle for the LoaderAllocator, so maybe there's a reference to the Assembly in LoaderAllocator.m_slots? |
@Rohansi |
@Kingnaoufal I've been busy recently but I did start doing that. I'll see if I can get it into a working state this weekend. |
I don't have much time to work on this, and this doesn't implement the proper lifetimes for assemblies in an AssemblyLoadContext. I'm closing this PR but if I get time to do it properly I'll open a new one. |
Yes, that's reasonable. |
cool, will rebase this PR and try to continue in this direction |
CC @rahku |
@jkotas currently the AssemblyLoadContext.LoadFromPath is expecting to load from the disk. Do you know why it was not designed with a |
There is |
Oops, indeed, sorry missed that one 😅 |
So after looking a bit more at the code, there is currently 3 types of LoaderAllocator:
As we want to add a In the end we would have only:
What do you think? |
Either way sounds fine to me. Merging both may make near term progress harder ... if it is the case, I would lean towards keeping them separate, for now at least. |
I have opened a new PR #8677 as a followup of this one. |
This adds support for collectible assemblies that are loaded with AssemblyLoadContext. As of right now the managed side of this is very incomplete but I am able to load and call something from an assembly, let it get collected by the GC, and can see the assembly disappear from the AppDomain assembly list.
My current issues with this are:
This is related to issue #552.