-
Notifications
You must be signed in to change notification settings - Fork 391
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
Language service tidyup #8550
base: main
Are you sure you want to change the base?
Language service tidyup #8550
Changes from all commits
651e59c
1b226ad
67565b5
88a853b
3c94e95
709f5e2
35f3965
3e8ae2f
b63e443
7273840
a4f2588
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,7 +32,7 @@ namespace Microsoft.VisualStudio.ProjectSystem.LanguageServices; | |
[Export(typeof(IWorkspaceWriter))] | ||
[Export(ExportContractNames.Scopes.UnconfiguredProject, typeof(IProjectDynamicLoadComponent))] | ||
[AppliesTo(ProjectCapability.DotNetLanguageService)] | ||
internal sealed class LanguageServiceHost : OnceInitializedOnceDisposedUnderLockAsync, IProjectDynamicLoadComponent, IWorkspaceWriter | ||
internal sealed class LanguageServiceHost : OnceInitializedOnceDisposedAsync, IProjectDynamicLoadComponent, IWorkspaceWriter | ||
{ | ||
private readonly TaskCompletionSource _firstPrimaryWorkspaceSet = new(); | ||
|
||
|
@@ -106,6 +106,9 @@ protected override async Task InitializeCoreAsync(CancellationToken cancellation | |
return; | ||
} | ||
|
||
// Ensure we also cancel on project unload | ||
cancellationToken = _tasksService.LinkUnload(cancellationToken); | ||
|
||
// We have one "workspace" per "slice". | ||
// | ||
// - A "workspace" models the project state that Roslyn needs for a specific configuration. | ||
|
@@ -130,12 +133,15 @@ protected override async Task InitializeCoreAsync(CancellationToken cancellation | |
// We track per-slice data via this source. | ||
_activeConfigurationGroupSubscriptionService.SourceBlock.SyncLinkOptions(), | ||
target: DataflowBlockFactory.CreateActionBlock<IProjectVersionedValue<(ConfiguredProject ActiveConfiguredProject, ConfigurationSubscriptionSources Sources)>>( | ||
async update => await ExecuteUnderLockAsync(cancellationToken => OnSlicesChanged(update, cancellationToken)), | ||
update => OnSlicesChanged(update, cancellationToken), | ||
_unconfiguredProject, | ||
ProjectFaultSeverity.LimitedFunctionality), | ||
ProjectFaultSeverity.LimitedFunctionality, | ||
"LanguageServiceHostSlices {0}"), | ||
linkOptions: DataflowOption.PropagateCompletion, | ||
cancellationToken: cancellationToken), | ||
|
||
ProjectDataSources.JoinUpstreamDataSources(_joinableTaskFactory, _projectFaultHandler, _activeConfiguredProjectProvider, _activeConfigurationGroupSubscriptionService), | ||
|
||
new DisposableDelegate(() => | ||
{ | ||
// Dispose all workspaces. Note that this happens within a lock, so we will not race with project updates. | ||
|
@@ -265,10 +271,10 @@ public Task<bool> IsEnabledAsync(CancellationToken cancellationToken) | |
|
||
public async Task WhenInitialized(CancellationToken token) | ||
{ | ||
await ValidateEnabledAsync(token); | ||
|
||
using (_joinableTaskCollection.Join()) | ||
{ | ||
await ValidateEnabledAsync(token); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This commit description for this change does not look correct. Join is about connecting the JoinableTask created here with the caller of this method. Anyone synchronously blocking on the UI thread on this method will happily let ValidateEnabledAsync switch to the UI thread if they are following JTF rules (ie calling JTF.Run). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ie Join solely exists because there's not a traditional async connection between _firstPrimaryWorkspaceSet and that code I pointed to. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, this ends up calling LanguageServiceHostEnvironment.IsEnabledAsync that no longer needs the UI thread. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. moving ValidateEnabledAsync inside the collection join is unnecessary, because the AsyncLazy has its own JTF chain managed inside. it would not break things either. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This JTC has all upstream data sources joined to it. The
If the join here was removed and the caller was on the main thread, and they trigger the lazy initialisation of the "is command line mode" state, which also require the main thread, but dataflow is running and is blocking the main thread, wouldn't it be able to deadlock? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Oh, I didn't realize that ICriticalPackageService is not public. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Join says "let any tasks added to the JoinableTaskCollection" access the main thread if the caller is blocking it. ValidateEnabledAsync does not add any tasks to the collection, and therefore putting it in the join has no affect. It actually lets dataflow steal the UI thread before the IsCommandLineMode check. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I thought tasks in the same collection are implicitly joined. The goal of having There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Awaiting an AsyncLazy implicitly joins the caller's context with the Func<Task> its executing, so if it needs UI thread, it will get it. All in all, there's probably no real side affect of doing what you are doing (other than allowing dataflow to access UI thread earlier), but its unneeded and the lack of doing this will not cause a hang. |
||
|
||
await _firstPrimaryWorkspaceSet.Task.WithCancellation(token); | ||
} | ||
} | ||
|
@@ -277,8 +283,6 @@ public async Task WriteAsync(Func<IWorkspace, Task> action, CancellationToken to | |
{ | ||
token = _tasksService.LinkUnload(token); | ||
|
||
await ValidateEnabledAsync(token); | ||
|
||
Workspace workspace = await GetPrimaryWorkspaceAsync(token); | ||
|
||
await workspace.WriteAsync(action, token); | ||
|
@@ -288,32 +292,33 @@ public async Task<T> WriteAsync<T>(Func<IWorkspace, Task<T>> action, Cancellatio | |
{ | ||
token = _tasksService.LinkUnload(token); | ||
|
||
await ValidateEnabledAsync(token); | ||
|
||
Workspace workspace = await GetPrimaryWorkspaceAsync(token); | ||
|
||
return await workspace.WriteAsync(action, token); | ||
} | ||
|
||
private async Task<Workspace> GetPrimaryWorkspaceAsync(CancellationToken cancellationToken) | ||
{ | ||
await ValidateEnabledAsync(cancellationToken); | ||
using (_joinableTaskCollection.Join()) | ||
{ | ||
await ValidateEnabledAsync(cancellationToken); | ||
|
||
await WhenProjectLoaded(cancellationToken); | ||
await WhenProjectLoaded(); | ||
} | ||
|
||
return _primaryWorkspace ?? throw Assumes.Fail("Primary workspace unknown."); | ||
} | ||
|
||
private async Task WhenProjectLoaded(CancellationToken cancellationToken) | ||
{ | ||
// The active configuration can change multiple times during initialization in cases where we've incorrectly | ||
// guessed the configuration via our IProjectConfigurationDimensionsProvider3 implementation. | ||
// Wait until that has been determined before we publish the wrong configuration. | ||
await _tasksService.PrioritizedProjectLoadedInHost.WithCancellation(cancellationToken); | ||
|
||
// We block project load on initialization of the primary workspace. | ||
// Therefore by this point we must have set the primary workspace. | ||
System.Diagnostics.Debug.Assert(_firstPrimaryWorkspaceSet.Task is { IsCompleted: true, IsFaulted: false }); | ||
async Task WhenProjectLoaded() | ||
{ | ||
// The active configuration can change multiple times during initialization in cases where we've incorrectly | ||
// guessed the configuration via our IProjectConfigurationDimensionsProvider3 implementation. | ||
// Wait until that has been determined before we publish the wrong configuration. | ||
await _tasksService.PrioritizedProjectLoadedInHost.WithCancellation(cancellationToken); | ||
|
||
// We block project load on initialization of the primary workspace. | ||
// Therefore by this point we must have set the primary workspace. | ||
System.Diagnostics.Debug.Assert(_firstPrimaryWorkspaceSet.Task is { IsCompleted: true, IsFaulted: false }); | ||
drewnoakes marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
#endregion | ||
|
@@ -333,18 +338,15 @@ public async Task AfterLoadInitialConfigurationAsync() | |
// Ensure the project is not considered loaded until our first publication. | ||
Task result = _tasksService.PrioritizedProjectLoadedInHostAsync(async () => | ||
{ | ||
using (_joinableTaskCollection.Join()) | ||
{ | ||
await WhenInitialized(_tasksService.UnloadCancellationToken); | ||
} | ||
await WhenInitialized(_tasksService.UnloadCancellationToken); | ||
drewnoakes marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}); | ||
|
||
// While we want make sure it's loaded before PrioritizedProjectLoadedInHost, | ||
// we don't want to block project factory completion on its load, so fire and forget. | ||
_projectFaultHandler.Forget(result, _unconfiguredProject, ProjectFaultSeverity.LimitedFunctionality); | ||
} | ||
|
||
protected override Task DisposeCoreUnderLockAsync(bool initialized) | ||
protected override Task DisposeCoreAsync(bool initialized) | ||
{ | ||
_firstPrimaryWorkspaceSet.TrySetCanceled(); | ||
|
||
|
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 still have questions around JTF chains in this code. One thing interesting is that we create joinableTaskCollection/Factory here instead of reusing ones created in its base class (OnceInitializeOnceDisposeAsync exposes them through protect members, so joins the collection will join initialization tasks as well (of course, it doesn't matter much, because we ensure the initialization in the LoadAsync method.
this code doesn't seem to JoinUpstream for two blocks linked here including _activeConfiguredProjectProvider and _activeConfigurationGroupSubscriptionService. They should be covered.
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.
Thanks. Fixed in a4f2588. Note that I'm joining to my custom JTC/JTF rather than
OnceInitializedOnceDisposedAsync.JoinableFactory
from the base. We use the custom JTC to join all dataflow work here with callers toIWorkspaceWriter.WriteAsync
.When you and I looked at a hang here a while back, I thought we concluded that we'd need our own JTC to sync all this work. I assume the one in
OnceInitializedOnceDisposedAsync.JoinableFactory
uses a global collection.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.
OnceInitializedOnceDisposedAsync.JoinableFactory
uses its own collection - which is exposed as a property. I think we should use the existing collection and JTF from the base class instead of creating another set and leave those almost unused.