Skip to content
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

Update plugin assembly loading for .NET 3/5 #5037

Closed
1 of 3 tasks
rainersigwald opened this issue Jan 8, 2020 · 12 comments
Closed
1 of 3 tasks

Update plugin assembly loading for .NET 3/5 #5037

rainersigwald opened this issue Jan 8, 2020 · 12 comments
Labels
Area: Engine Issues impacting the core execution of targets and tasks. Epic Groups multiple user stories. Can be grouped under a theme. triaged
Milestone

Comments

@rainersigwald
Copy link
Member

rainersigwald commented Jan 8, 2020

Several improvements to #4916 are possible once we're moved to target .NET 3+:

@rainersigwald rainersigwald added the Area: Engine Issues impacting the core execution of targets and tasks. label Jan 8, 2020
@rainersigwald rainersigwald added this to the .NET 5 milestone Jan 8, 2020
@rainersigwald
Copy link
Member Author

Note: this was already mostly done in 90ac0c3. We could mostly pull back to that, but we'll need to keep the escape hatches and other behaviors of the current implementation.

@AArnott
Copy link
Contributor

AArnott commented Jun 24, 2020

Please to take a look at the unique requirements proposed by #1755 which was recently duped against this one. The goal being that now that msbuild isn't a Windows, .NET Framework only process, we need a more scalable, simpler model for plugins to ship without shipping the full transitive closure of their own runtime dependencies for every single OS+runtime, while still being able to run on each of these.

@rainersigwald
Copy link
Member Author

rainersigwald commented Jun 24, 2020

@AArnott are you saying there are MSBuild-specific requirements that are not covered by AssemblyDependencyResolver? None jumped out at me.

@AArnott
Copy link
Contributor

AArnott commented Jun 24, 2020

@rainersigwald I'm not sure. I don't see anything in the AssemblyDependencyResolver doc to suggest that it can load a plugin's dependencies from the nuget package cache.

@mwpowellhtx
Copy link

@rainersigwald Are there examples how to plugin with this in view? Thank you...

@rainersigwald
Copy link
Member Author

You might be interested in https://docs.microsoft.com/dotnet/core/tutorials/creating-app-with-plugin-support#plugin-with-library-dependencies, @mwpowellhtx.

@marcin-krystianc
Copy link
Contributor

Hi @rainersigwald, I've recently come across a memory leak caused by the fact that assemblies are loaded into AssemblyLoadContext but they are never unloaded.
The memory footprint of loaded assemblies is small enough that this problem can be ignored most of the time. But for us it is a problem as we want to build a long running service which uses msbuild libraries to evaluate project files. Fortunately there is a workaround (#5098) so we are not blocked on this problem anymore, but it would be great if it was fixed.

According to my tests, it is actually quite an easy fix. It is enough to construct the AssemblyLoadContext with isCollectible = true (available in .NET 3+). When the ALC is collectible, it will automatically unload assemblies when they are not needed (https://github.com/dotnet/runtime/blob/84bad7e24589169975ec8a599743110d703c761b/src/libraries/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyLoadContext.cs#L113-L125).

Documentation says that it is pretty safe, as the assembly will be unloaded only if it is no longer referenced/used (https://docs.microsoft.com/en-us/dotnet/standard/assembly/unloadability). I wonder whether you see any problems with making ALC collectible? Would you accept a PR for it?

@rainersigwald
Copy link
Member Author

rainersigwald commented Sep 10, 2021

@marcin-krystianc We considered making the ALCs collectible, but that can harm performance in some cases--specifically, when we're reusing MSBuild nodes for a subsequent build, having the tasks loaded (and JITted) from a previous run can speed up the dev inner loop.

I'm a bit confused by the leak you're describing, though. Some questions:

  1. Are you really only evaluating projects? That shouldn't require any tasks and shouldn't use any ALCs.
  2. When you observe the leak, what is causing creation of new ALCs? There should be one per task assembly, so assuming you're not constantly changing task assemblies, I would expect extremely slow growth.
  3. How does the workaround from Add an escape hatch for ALC isolation #5098 help? Without that, all the tasks should be loaded into the default ALC and not be collectible, so the growth characteristics should be roughly the same.

@marcin-krystianc
Copy link
Contributor

Ad 1
Yes, to reproduce the problem it is enough to evaluate projects. You are right that project evaluation doesn't require creating new MsBuild tasks, but the leak is not related to tasks at all. There are SdkResolvers which need to be loaded for the project file evaluation (E.g.: Microsoft.Build.NuGetSdkResolver.dll, Microsoft.NET.Sdk.WorkloadMSBuildSdkResolver.dll).
Example call stack is:

AssemblyLoadContext.LoadFromAssemblyPath() 
CoreClrAssemblyLoader.LoadUsingPluginContext()
CoreClrAssemblyLoader.LoadFromPath()
SdkResolverLoader.LoadResolverAssembly()
SdkResolverLoader.LoadResolvers()
SdkResolverLoader.LoadResolvers()
SdkResolverService.Initialize()
SdkResolverService.ResolveSdk()
CachingSdkResolverService.<>n__0()
CachingSdkResolverService.<>c__DisplayClass3_0.<ResolveSdk>b__1()
Lazy<SdkResult>.ViaFactory()
Lazy<SdkResult>.ExecutionAndPublication()
Lazy<SdkResult>.CreateValue()
Lazy<__Canon>.get_Value()
CachingSdkResolverService.ResolveSdk()
Evaluator<ProjectProperty, ProjectItem, ProjectMetadata, ProjectItemDefinition>.ExpandAndLoadImportsFromUnescapedImportExpressionConditioned()
Evaluator<ProjectProperty, ProjectItem, ProjectMetadata, ProjectItemDefinition>.ExpandAndLoadImports()
Evaluator<ProjectProperty, ProjectItem, ProjectMetadata, ProjectItemDefinition>.EvaluateImportElement()
Evaluator<ProjectProperty, ProjectItem, ProjectMetadata, ProjectItemDefinition>.PerformDepthFirstPass()
Evaluator<ProjectProperty, ProjectItem, ProjectMetadata, ProjectItemDefinition>.Evaluate()
Evaluator<ProjectProperty, ProjectItem, ProjectMetadata, ProjectItemDefinition>.Evaluate()
Project.ProjectImpl.Reevaluate()
Project.ProjectImpl.ReevaluateIfNecessary()
Project.ProjectImpl.ReevaluateIfNecessary()
Project.ProjectImpl.ReevaluateIfNecessary()
Project.ProjectImpl.Initialize()
new Project()
Project.FromFile()
...

Ad 2
New ALC are created because SdkResolvers need to be loaded for each evaluated project.
There is a cache for them called CachingSdkResolverService (in the EvaluationContext) so they are loaded only once per each instance of that cache. If it was possible to use single EvaluationContext for the whole lifetime of our long running service that would fix the leak from ALCs, unfortunately it is not possible as it creates other memory leaks (e.g. from the EngineFileUtilities).

Here is simplified version of our code in which we create an EvaluationContext for each solution we process:

voiid ProcessSolution(string path)
{
	using (var projectCollection = new ProjectCollection())
	{
		var projectOptions = new ProjectOptions
		{
			ProjectCollection = projectCollection,
			LoadSettings = ProjectLoadSettings.IgnoreEmptyImports | ProjectLoadSettings.IgnoreInvalidImports |
						   ProjectLoadSettings.RecordDuplicateButNotCircularImports | ProjectLoadSettings.IgnoreMissingImports,
			EvaluationContext = EvaluationContext.Create(EvaluationContext.SharingPolicy.Shared),
		};
		
		var solutionFile = SolutionFile.Parse(path);
		var projects = solutionFile.ProjectsInOrder
			.Where(x => x.ProjectType == SolutionProjectType.KnownToBeMSBuildFormat)
			.Select(x => Project.FromFile(x.AbsolutePath, projectOptions))
			.ToList();
		...
	}
}

Ad 3
My understanding is that when the plugin ALCs are disabled then only the default ALC is used. With the single default ALC all subsequent load requests for already loaded assemblies basically do nothing. In case of SkdResolvers, their path is always the same (e.g. c:\Program Files\dotnet\sdk\5.0.302\SdkResolvers\...) so such assembly is loaded only once into memory and the memory leak is not observed anymore.

@rainersigwald
Copy link
Member Author

@marcin-krystianc Ah, thank you for the details! That should be resolvable; I filed #6842.

AR-May pushed a commit that referenced this issue Sep 20, 2021
We want to use static for CoreClrAssemblyLoader so each unique SDK resolver assembly is loaded into memory and JITted only once.

Fixes #6842 (comment)

### Context

We use static for the `CoreClrAssemblyLoader` field so each unique SDK resolver assembly is loaded into memory and JITted only once. All subsequent load requests will return assembly from the assembly loader's cache instead of loading it again from disk. This change increases the performance of SDK resolution and at the same time avoids leaking memory due to loading the same SDK resolver assembly multiple times and never unloading it.

### Changes Made

The `CoreClrAssemblyLoader` field in the `SdkResolverLoader` class was changed from non-static to static member. The same instance of `CoreClrAssemblyLoader` will be used by all instances of `SdkResolverLoader`. It is consistent now with other uses of `CoreClrAssemblyLoader` in msbuild.

### Testing

Tested manually using repro from #5037 (comment)

### Notes

Alternative approach would be to use collectible `CoreClrAssemblyLoader` / `AssemblyLoadContext` - that would fix the leak as well but it would be less performant as it wouldn't benefit from re-using already loaded and JITed assemblies.
Forgind added a commit that referenced this issue Apr 19, 2022
Fixes #4081 and Fixes #1887; progress towards #5037

Context
MSBuild doesn't currently respect .deps.json files for plugins (tasks). This can lead to incorrect versions of assemblies being found as finding the AnyCPU version of an assembly instead of the windows-specific version.

Changes Made
Use AssemblyDependencyResolver as a second pass (after looking for "well-known assemblies") to automatically use the deps.json file to find the right assembly.

Testing
Verified that for a task assembly with a rid-specific dependency, it finds the rid-specific dependency as specified by the deps.json file. Also verified that it can find native assemblies and that the issue that inspired this (dotnet/sdk#23498) no longer reproduces after giving nuget a .deps.json file specifying the correct version.
wasabii added a commit to ikvmnet/ikvm that referenced this issue Jul 6, 2022
…ler, since MSBuild isn't going to let us do it correctly, currently.

Held up on dotnet/msbuild#5037
@MichalPavlik
Copy link
Member

MichalPavlik commented Aug 20, 2023

@rainersigwald, is this issue still relevant? All linked PRs in original post were merged.

@rainersigwald
Copy link
Member Author

I don't think we rigorously did

But yes, I think we can close.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area: Engine Issues impacting the core execution of targets and tasks. Epic Groups multiple user stories. Can be grouped under a theme. triaged
Projects
None yet
Development

No branches or pull requests

7 participants