-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
Razor synchronize loader #76357
Razor synchronize loader #76357
Conversation
/// in debug builds. | ||
/// </summary> | ||
private static bool s_razorRequested = false; | ||
private static readonly List<AssemblyName>? s_assembliesRequested = []; |
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.
All access to this is done under a lock, and there's only a single instance, so didn't seem worth using an immutable variant or pooling it.
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.
Why is this marked as nullable? Are you trying to account for a static
constructor race here?
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.
Just a hangover from an earlier version pre-refactoring that I hadn't cleaned up. Fixed.
{ | ||
var results = await project.GetSourceGeneratedDocumentsAsync(cancellationToken).ConfigureAwait(false); | ||
|
||
var generatorIdentity = new SourceGeneratorIdentity(assemblyName, assemblyPath, assemblyVersion, typeName); |
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.
SourceGeneratorIdentity
isn't public, so have to pass through the requisite parts. We could make an IVT copy but doesn't seem worth it.
/// in debug builds. | ||
/// </summary> | ||
private static bool s_razorRequested = false; | ||
private static readonly List<AssemblyName>? s_assembliesRequested = []; |
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.
Why is this marked as nullable? Are you trying to account for a static
constructor race here?
Debug.Assert(s_assemblyResolver == null, "Assembly resolver should not be set multiple times."); | ||
Debug.Assert(!s_razorRequested, "A razor assembly has already been requested before setting the resolver."); | ||
s_assemblyResolver = value; | ||
if (s_assemblyResolver is not null && s_assembliesRequested is not null && !s_assembliesRequested.Contains(canaryAssembly)) |
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.
Not sure how s_assembliesRequested
could be null
here. Even in a static
ctor race s_resolverLock
would also be null
so we'd throw before this part.
Shouldn't the check be s_assemblyResolver is null
? The field starts off as null
and this is the only place that it is written to so as stated this will never be set (I believe).
lock (s_resolverLock) | ||
{ | ||
s_assembliesRequested?.Add(assemblyName); | ||
return s_assemblyResolver?.Invoke(assemblyName); |
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 is doing a callback inside of a lock
. That usually gives me a bit of pause. Is there a specific reason it needs to be inside the lock
? Did you consider say saving the field to a local and then invoking that outside the lock
?
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, we need to retrieve the resolver under the lock, but once we've done that I can't think of any reason why we need to actually execute it under the lock.
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.
There's no concern about the resolver being invoked on different threads without synchronization, right? Just a need for the s_assembliesRequested
update and s_assemblyResolver
read to be synchronized?
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.
Thinking about it, we don't even need to retrieve it under the lock. Before we enter the lock we either 1) have the resolver set or 2) we don't:
In the case of 1) the locking doesn't actually have any effect.
In the case of 2) if the assembly being requested is the canary assembly, then once we exit the lock the resolver can never be set, so it's also safe to just use it.
In the case of 2) where it's not the canary assembly being requested, we actually have a race condition. We exit the lock having added the load to the list, but the state of the resolver can still change. If the install call comes in in the time between exiting the lock and executing the resolver we'll use the resolver, if it doesn't we wont. Now, for our code this actually doesn't matter, really, we only want to execute the resolver for the canary assembly anyway (but we can't do that because we don't know ahead of time what it is).
We can make the last part deterministic by retrieving the loader under the lock and executing against the local value, but don't need to for it to be correct for our usage. I'm leaning towards making it deterministic just for ease-of-understanding the code though.
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.
Ended up just making this deterministic in all cases so its easier to reason about.
Could you please link to related issues/PRs so that I can get the full context on the scenario this PR is addressing? Thanks! |
@RikkiGibson There isn't really a good issue/PR to address this, but I linked a doc in the description which should give you the context you need. |
Debug.Assert(s_assemblyResolver == null, "Assembly resolver should not be set multiple times."); | ||
Debug.Assert(!s_razorRequested, "A razor assembly has already been requested before setting the resolver."); | ||
s_assemblyResolver = value; | ||
if (s_assemblyResolver is not null && s_assembliesRequested is not null && !s_assembliesRequested.Contains(canaryAssembly)) |
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 am confused as to how the s_assemblyResolver
can ever become non-null, if entering the block that assigns it requires it to already be non-null.
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.
Just a hangover from an earlier version pre-refactoring that I hadn't cleaned up. Fixed.
I took a look and things are mostly making sense. Just trying to get a full sense of the impact here. It sounds like we already had some logic to coordinate this generator load across roslyn and razor, it's just that there were reliability issues due to lack of synchronization. Is that right? |
We were previously assuming the Razor tooling would come up before Roslyn loaded the source generator, but it's actually a non-fixable race between which set of tooling gets loaded first, due to the way the service hub process comes up (and a bunch of other stuff like what files a user has open). It's completely legal for either razor or Roslyn to start first, so we have to have this synchronization to ensure we only load a single copy. |
src/Tools/ExternalAccess/Razor/RazorAnalyzerAssemblyResolver.cs
Outdated
Show resolved
Hide resolved
Co-authored-by: Jared Parsons <[email protected]>
src/Tools/ExternalAccess/Razor/RazorAnalyzerAssemblyResolver.cs
Outdated
Show resolved
Hide resolved
Co-authored-by: Jan Jones <[email protected]>
} | ||
} | ||
|
||
public Assembly? ResolveAssembly(AssemblyName assemblyName) | ||
{ | ||
s_razorRequested |= assemblyName.FullName.Contains("Razor"); |
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.
Are there any potential problems with razor being out of sync with this EA? i.e. razor is older than roslyn, and razor expects the old assembly hook pattern to be used, or roslyn is older than razor, and razor expects the new hook pattern.
Maybe we just have to take care to dual insert and we'll be ok?
|
||
var generatorIdentity = new SourceGeneratorIdentity(assemblyName, assemblyPath, assemblyVersion, typeName); | ||
return results.Where(s => s.Identity.Generator == generatorIdentity); | ||
|
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.
nit: extra blank line
/// in debug builds. | ||
/// </summary> | ||
private static bool s_razorRequested = false; | ||
private static readonly List<AssemblyName> s_assembliesRequested = []; |
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.
Since all of the code here just adds items to this collection and checks to see if items are present, should this be a HashSet<AssemblyName>
rather than a List<AssemblyName>
?
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.
Good idea, thanks.
…i/roslyn into razor_synchronize_loader
This updates the way the razor assembly loader installs its handler to ensure we can synchronize correctly between Razor and Roslyn. The approach is to track which assemblies have been loaded and only install the filter if the requested assembly has not yet been loaded.
Most of the work is on the Razor side, the Roslyn side is deliberately simple with nothing hard coded so we aren't baking in any Razor specific logic or dependencies. A WIP doc explaining the process from both sides is available at https://gist.github.com/chsienki/f3b3b19bf6cf6d1f8e047e59242db648.
Also adds a method to retrieve only the documents from a specific source generator. Again, we don't bake in the razor generator, so that we can change that without needing to change roslyn.