-
Notifications
You must be signed in to change notification settings - Fork 4k
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
Fix more lock contention #59638
Fix more lock contention #59638
Conversation
@@ -0,0 +1,41 @@ | |||
# Locking and Synchronization |
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 document was long overdue, so I used this as an opportunity to write this down. The content here though is not "new" nor is it changed by this PR in any way.
src/VisualStudio/Core/Def/Implementation/ProjectSystem/Readme.md
Outdated
Show resolved
Hide resolved
src/VisualStudio/Core/Def/Implementation/ProjectSystem/Readme.md
Outdated
Show resolved
Hide resolved
else holds onto a VisualStudioProjectOptionsTracker that has a lock, so we avoid any deadlocks there. | ||
|
||
VisualStudioWorkspaceImpl has a nested type OpenFileTracker that has it's own lock to guard it's own fields. It should call nothing | ||
outside of itself while holding that 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.
i both stronglya ppreciate this doc, while recognizing that the complexity here makes me angry.
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.
in general, do we also have some of these comments on those types? e.g. does OpenFileTracker have comments saying "absolutely never do X"?
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.
question still applies @jasonmalinowski
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'll add comments appropriately.
would acquire a project lock. To this end, a few bits of information that may seem to be "project specific" are actually stored | ||
in maps in the VisualStudioWorkspaceImpl; specifically we maintain a list of the output paths of a project which we use to convert | ||
metadata references to project references. This list is maintained in the workspace itself to avoid having to reach back to a | ||
project and ask it for information which might violate this lock hierarchy. |
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 si very good to know. i wonder if it's something we could trap somehow to detect when we're doing this wrong.
foreach (var (documentId, textLoader) in documentsToChange) | ||
{ | ||
if (!s.Workspace.IsDocumentOpen(documentId)) | ||
if (!_project._workspace.IsDocumentOpen(documentId)) |
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.
here's my terror. we're in the workspace lock, calling back into the workspace. How do we know IsDocumentOpen takes no lock itself?
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 if it takes the main workspace lock a second time, we'll know because it'll deadlock. 😄 I agree though yeah I don't have a good answer for how to enforce the hierarchy, generally though.
if (object.Equals(field, newValue)) | ||
{ | ||
return; | ||
} |
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 was very confusing to read since i didn't see why this needed to be in a lock. having there be a ref-read here was not expected :)
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, actual use of ref is...surprising. I can't think when I last used it before this. 😄
src/VisualStudio/Core/Def/Implementation/ProjectSystem/VisualStudioProject.cs
Outdated
Show resolved
Hide resolved
src/VisualStudio/Core/Def/Implementation/ProjectSystem/VisualStudioProject.cs
Outdated
Show resolved
Hide resolved
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.
Comments related to the threading switch do not align with the new implementation
@@ -160,6 +161,8 @@ public VisualStudioWorkspaceImpl(ExportProvider exportProvider, IAsyncServicePro | |||
exportProvider.GetExportedValue<IDiagnosticUpdateSourceRegistrationService>(), | |||
exportProvider.GetExportedValue<IAsynchronousOperationListenerProvider>(), | |||
_threadingContext), isThreadSafe: true); | |||
|
|||
_workspaceListener = Services.GetRequiredService<IWorkspaceAsynchronousOperationListenerProvider>().GetListener(); |
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 could be exposed by a private protected
property in Workspace
:
private protected IAsynchronousOperationListener Listener => _taskQueue.Listener;
src/VisualStudio/Core/Def/Implementation/ProjectSystem/VisualStudioWorkspaceImpl.cs
Outdated
Show resolved
Hide resolved
src/VisualStudio/Core/Def/Implementation/ProjectSystem/VisualStudioWorkspaceImpl.cs
Outdated
Show resolved
Hide resolved
src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/ValueTaskExtensions.cs
Outdated
Show resolved
Hide resolved
src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/ValueTaskExtensions.cs
Outdated
Show resolved
Hide resolved
src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/ValueTaskExtensions.cs
Outdated
Show resolved
Hide resolved
src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/ValueTaskExtensions.cs
Outdated
Show resolved
Hide resolved
var solutionChanges = new SolutionChangeAccumulator(s); | ||
updateSolution(solutionChanges, oldValue); | ||
return solutionChanges; | ||
}).AsTask().GetAwaiter().GetResult(); |
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.
is it desirable to be sync here? as opposed to async, whcih you push up all the way to callers.
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.
Same comment as here: https://github.com/dotnet/roslyn/pull/59638/files/1268f8f6cc2a854311b427ac6a9701fd9742286f..bcc398b3ede4373f53e2584e1ac8fe421b6ec5ac#r810348018 Sorry should have left that unresolved for you to see the answer.
Basically, the sync path here is taken by the legacy project system, that that point we're better off synchronously acquiring the locks since that'll get priority. So other than switching to be a JTF.Run() higher level, we can't really make this properly sync. And I suspect that might actually be worse for perf given SemaphoreSlim's prioritization for synchronous waters.
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.
Somehow you broke GitHub, the current code here anyways is:
_workspace.ApplyBatchChangeToWorkspace(solutionChanges =>
{
updateSolution(solutionChanges, oldValue);
});
So the wart for the .GetResult() is gone.
@@ -1983,38 +1983,49 @@ public void RemoveProjectOutputPath_NoLock(SolutionChangeAccumulator solutionCha | |||
|
|||
private void RefreshMetadataReferencesForFile(object sender, string fullFilePath) | |||
{ | |||
using (_gate.DisposableWait()) | |||
var asyncToken = _workspaceListener.BeginAsyncOperation(nameof(RefreshMetadataReferencesForFile)); | |||
_threadingContext.RunWithShutdownBlockAsync(async cancellationToken => |
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.
we have a .CompletesAsyncToken extension so you don't need the try/finally
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 wait.. is this one of those weird tasks?
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 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.
(GitHub should be showing this comment as outdated since this was already fixed, not sure but @CyrusNajmabadi broke it somehow.)
{ | ||
_projectToRuleSetFilePath.Add(project.Id, ruleSetFilePathFunc); | ||
} | ||
Contract.ThrowIfFalse(ImmutableInterlocked.TryAdd(ref _projectToRuleSetFilePath, project.Id, ruleSetFilePathFunc)); |
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 use of Contract to preserve semantics.
@@ -338,16 +340,14 @@ internal VisualStudioProjectTracker ProjectTracker | |||
// Solution Explorer in the SolutionExplorerShim, where if we just could more directly get to the rule set file it'd simplify this. | |||
internal override string? TryGetRuleSetPathForProject(ProjectId projectId) | |||
{ | |||
using (_gate.DisposableWait()) | |||
// _projectToRuleSetFilePath is immutable, so can be used outside of locks |
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 always worries me as i always think there's .net subtleties about all writes being observed. though i think you'r correct about safety.
@@ -18,9 +18,7 @@ internal static class FSharpProjectExternalErrorReporterFactory | |||
{ | |||
public static IVsLanguageServiceBuildErrorReporter2 Create(ProjectId projectId, string errorCodePrefix, IServiceProvider serviceProvider) | |||
{ | |||
ThreadHelper.ThrowIfNotOnUIThread(); |
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 to check, we're intentionally making this free threaded now?
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.
Correct: the only reason this had a UI thread affinity was to call SubscribeExternalErrorDiagnosticUpdateSourceToSolutionBuildEvents, but that's been moved earlier when the project itself is being created.
If this is for17.2 p2, then I think you need to target release/dev17.2 |
@genlu The plan was since we were stabilizing integration tests, to get a green run here and then retarget once we knew this didn't have regressions. That plan...did not work. 😄 |
These were added for completeness but then not actually used anywhere; rather than try to maintain them just delete them.
We convert metadata references to project references if the output paths match the referenced paths. Thus if we have a solution with a bunch of metadata references, and then we're told a project has a new output path, we have to go looking to see if we can convert those metadata references to project references as well. To do that conversion, we have to acquire the global workspace lock, since any other projects that are also adding or removing metadata references at the same time need to be all coordinated. We also support the concept of "batching" when updating a project, which means a project system can make a bunch of changes at once but we'll raise only a single WorkspaceChanged event once those changes are all done; we also try to ensure that we aren't acquiring the global workspace lock until the batch is actually being applied; that ensures we can do as much in parallel at once. We also recently added the ability for the batch to be applied with an async API, so if there's another project applying changes at once we aren't going to starve the thread pool. There was one bug, however: when you started a batch, and then changed the output path of the project, that updating of the output path should have waited until the batch was applied. We'd update the property in the workspace correctly, but we immediately acquired the global workspace lock trying to look for other projects to update with project references. Functionally this was mostly fine (ignoring there's a small window in time where we converted to project references even if a project didn't have the updated output path), but it created a huge performance issue: we would be acquiring the global workspace lock synchronously, even if the caller was trying to operate in a batch that they were going to apply asynchronously. If the user got particularly unlucky, they might have a bunch of projects all being loaded or reloaded at once, and they might starve off their thread pool. The fix, conceptually, is simple: defer the conversion to project references until the global lock is actually held once we are applying the batch change. This resulted in a bit of churn; when we apply a batch we have a little helper type called a SolutionChangeAccumulator that we pass around that tracks the new Solution object, but also tracks what the resulting "kind" should be for the solution change. Thus this commit required this set of changes: 1. Convert the methods that did the conversion to/from project references to operate on a SolutionChangeAccumulator instead of directly reading CurrentSolution and directly setting the output. 2. Update the ChangeProjectOutputPath and ChangeProjectProperty functions to now operate with SolutionChangeAccumulators. 3. Update the project removal path to also now create a SolutionChangeAccumulator. 4. General refactorings (extracting/merging) of some of our core helpers where we apply changes in the end. Fixes https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1473249
There's still a switch happening in CreateAndAddToWorkspaceAsync, which is where it should be and can be better limited.
One helper requires you to return a SolutionChangeAccumulator. No reason we can't just create one and pass in in rather than each caller having to create it.
…lutionBuildEvents By calling this once when we already are on the UI thread when a project is created, we're able to avoid calling it in a bunch of places which was requiring UI threads in less-intuitive places. It's also not clear to me why this is even marked as needing the UI thread at all, but we'll address that independently.
Now there's a consistent set of sync/async/maybe async.
0ea296d
to
36a7b1e
Compare
Fixes AB#1473249.