-
Notifications
You must be signed in to change notification settings - Fork 199
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
base: main
Are you sure you want to change the base?
Conversation
- 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.
@dotnet/razor-compiler and @dotnet/razor-tooling for reviews please :) |
src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorAssemblyLoadContext.cs
Show resolved
Hide resolved
else | ||
{ | ||
// Roslyn won the race, we need to find the compiler assembly it loaded. | ||
while (true) |
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 mentioned in the doc that we'd need to error eventually in case Roslyn errored. When does that happen?
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.
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.
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.
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 :)
Co-authored-by: Fred Silberberg <[email protected]>
|
||
private object _loaderLock = new(); | ||
|
||
public static readonly RazorAssemblyLoadContext Instance = new(); |
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.
Should the static
one be collectible? If it is collected that basically invalidates the static
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.
Yeah, it probably shouldn't be
public RazorAssemblyLoadContext() | ||
: base(isCollectible: true) | ||
{ | ||
var thisAssembly = GetType().Assembly; |
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.
var thisAssembly = GetType().Assembly; | |
var thisAssembly = typeof(RazorAssemblyLoadContext).Assembly; |
{ | ||
var thisAssembly = GetType().Assembly; | ||
_parent = GetLoadContext(thisAssembly); | ||
_baseDirectory = Path.GetDirectoryName(thisAssembly.Location) ?? ""; |
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.
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) |
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.
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);
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.
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...
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.
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?
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.
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.
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.
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(); |
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.
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);
Moving this to draft while we try a different approach. |
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.