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

Synchronize razor compiler assembly loading #11394

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

chsienki
Copy link
Contributor

This PR ensures that the razor compiler is only loaded a single time between Roslyn and Razor. Whichever succeeds in first loading the assembly will provide their copy to the other.

There is a doc in the first commit that explains the strategy. You should read this before reviewing the rest of the PR.

Note this is blocked on dotnet/roslyn#76752 but it wont require code changes other than updating the roslyn version.

- Add a new ALC that is 'razor compiler' aware
- Atomically load the razor compiler or use a copy already loaded by roslyn
- When creating a new service instance, first load self into the razor alc, then create the service in the razor alc
- This ensures we have full control over how dependencies are resolved.
@chsienki chsienki requested a review from a team as a code owner January 16, 2025 00:56
@chsienki
Copy link
Contributor Author

@dotnet/razor-compiler and @dotnet/razor-tooling for reviews please :)

docs/AssemblyLoadingStrategy.md Outdated Show resolved Hide resolved
docs/AssemblyLoadingStrategy.md Outdated Show resolved Hide resolved
docs/AssemblyLoadingStrategy.md Outdated Show resolved Hide resolved
docs/AssemblyLoadingStrategy.md Outdated Show resolved Hide resolved
else
{
// Roslyn won the race, we need to find the compiler assembly it loaded.
while (true)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mentioned in the doc that we'd need to error eventually in case Roslyn errored. When does that happen?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thats a good point. When writing the doc, I figured we'd need to do that, but at implementation I realized it's impossible to do so deterministically as-is, as its essentially a halting problem: we don't know if its not-loaded or failed to load and so another Yield might actually succeed. We could stick some arbitrary amount of retries in, but while unlikely, we could still prematurely assume failure if we hit the retry limit because the other thread just didn't get scheduled. We would need to add some other synchronization primitive to roslyn that allows you to query the status of a given assembly load.

The only way we could get into this state where it really has failed to load is if the assembly is missing or corrupted. Roslyn and razor both load the same assembly from disk, so even if we could detect that the load failed in Roslyn, it's just going to fail again in Razor anyway.

Given that its an error case either way, I'm inclined to just update the doc to note this and leave it as-is. If we think that's not acceptable let's file a bug and not block this work on it as its not trivial to do.

Copy link
Contributor

@davidwengier davidwengier left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need Remote.Razor to target netstandard any more, except possibly for tests. But also don't think its anything for you to worry about in this PR :)


private object _loaderLock = new();

public static readonly RazorAssemblyLoadContext Instance = new();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the static one be collectible? If it is collected that basically invalidates the static

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it probably shouldn't be

public RazorAssemblyLoadContext()
: base(isCollectible: true)
{
var thisAssembly = GetType().Assembly;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var thisAssembly = GetType().Assembly;
var thisAssembly = typeof(RazorAssemblyLoadContext).Assembly;

{
var thisAssembly = GetType().Assembly;
_parent = GetLoadContext(thisAssembly);
_baseDirectory = Path.GetDirectoryName(thisAssembly.Location) ?? "";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think we should throw here on null vs. using "". If this is null then something is very wrong.

// Roslyn won the race, we need to find the compiler assembly it loaded.
while (true)
{
foreach (var alc in AssemblyLoadContext.All)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider what happens if another AssemblyLoadContext decides to load razor. This strategy of searching all of the ALC could lead us to picking that instance vs. the one that Roslyn loaded. Why not just search the Roslyn ALC directly and grab the Razor DLL from there

var roslynAlc = AssemblyLoadContext.GetLoadContext(typeof(Microsoft.CodeAnalysis.Compilation).Assembly);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, but it's not loaded into the Roslyn ALC. It's loaded into a shadow copy ALC that's created for the specific directory. Ideally we would check that the path matches our base directory to know its the same assembly but we can't really do that because the shadow copy causes the path to move...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. What if we load the compiler into a metadata only ALC, then we can read the MVID. Then we can check the MVID against the rest of the ALCs to ensure we're picking the correct one?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah that's true ... but this should still wokr. Rather than iterating through the loaded assemblies just do the following:

var roslynAlc = AssemblyLoadContext.GetLoadContext(typeof(Microsoft.CodeAnalysis.Compilation).Assembly);
var razorCompilerAssembly = roslynAlc.LoadFromAssemblyName(assemblyName);

This will go through all of the shadow copy madness in the roslyn ALC and return the assembly if it's available. If not then can proceed to insert the resolver.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, that's a neat trick I hadn't thought about. That's a really nice simplification.

}
}
// we didn't find it, so it's possible that the Roslyn loader is still in the process of loading it. Yield and try again.
Thread.Yield();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other advantage of getting the specific roslyn ALC is I believe we can avoid this yield. Essentially, could we turn this method into effectively the following:

if (IsRazorCompiler(assemblyName))
{
  var roslynAlc = AssemblyLoadContext.GetLoadContext(typeof(Microsoft.CodeAnalysis.Compilation).Assembly);
  var roslynRazorCompiler = alc.Assemblies.SingleOrDefault(a => IsRazorCompiler(a.GetName()));
  if (roslynRazorCompiler is not null)
  {
      return roslynRazorCompiler;
  }
  // Either we set this or other thread did. Either way, we own the resolver after this.
  _ = RazorAnalyzerAssemblyResolver.TrySetAssemblyResolver(ResolveAssembly, assemblyName);
  return roslynAlc.LoadFromAssemblyName(assemblyName);

@chsienki
Copy link
Contributor Author

Moving this to draft while we try a different approach.

@chsienki chsienki marked this pull request as draft January 17, 2025 18:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants