Production code changes to aid with test stabilization#25558
Production code changes to aid with test stabilization#25558sharwell merged 13 commits intodotnet:dev15.7.xfrom
Conversation
| [assembly: TypeForwardedTo(typeof(Microsoft.CodeAnalysis.Shared.TestHooks.IAsynchronousOperationWaiter))] | ||
| [assembly: TypeForwardedTo(typeof(Microsoft.CodeAnalysis.Shared.TestHooks.IAsynchronousOperationListener))] | ||
| [assembly: TypeForwardedTo(typeof(Microsoft.CodeAnalysis.Shared.TestHooks.IAsyncToken))] | ||
| [assembly: TypeForwardedTo(typeof(Microsoft.CodeAnalysis.Shared.TestHooks.TaskExtensions))] |
There was a problem hiding this comment.
Out of curiosity, why are these needed? for F#/TS/JS?
There was a problem hiding this comment.
➡️ Considering the impact of other changes coming in this PR, I'm guessing these are not going to be necessary in the final revision. However, I decided to make the relocation separate from the removal of the original types.
There was a problem hiding this comment.
Do we have some tracking bug to track (eventually) getting rid of them?
There was a problem hiding this comment.
This change was intended to avoid the need to have WorkspacesTestUtilities reference Features. This is no longer avoidable, and WorkspacesTestUtilities ended up taking a reference all the way to RemoteWorkspaces. However, this change may still be beneficial as part of work to ensure all asynchronous operations started by Roslyn are tracked.
| }, | ||
| CancellationToken.None, | ||
| TaskContinuationOptions.ExecuteSynchronously, | ||
| TaskScheduler.Default); |
There was a problem hiding this comment.
Would benefit from comments explaining what's going on here. A comment also explaining that this should run unilaterally, regardless of how AsyncProcessorTask completes might be good.
| }, | ||
| CancellationToken.None, | ||
| TaskContinuationOptions.ExecuteSynchronously, | ||
| TaskScheduler.Default); |
There was a problem hiding this comment.
duplicate code. Can we we share with above?
There was a problem hiding this comment.
Should we be using LazyCancellation here? in other words, is it ok if this code runs before some antecedent tasks completes, but after the cancellation token controlling another intermediary tasks completes? In other words, should this be using the semantics of SafeContinueWith?
There was a problem hiding this comment.
Doesn't LazyCancellation only have an effect on continuations that specify a CancellationToken?
There was a problem hiding this comment.
I don't think so (but i'm not totally sure). I thought it was more about the antecedent tasks and if they used a cancellation token. in other words, if i have tasks:
A <- B <- C (i.e. B depends on A and C depends on B)
And 'A' is running, but B gets canceled. The question is: should 'C' run? LazyCancellation (to me) says: "No. All antecedent tasks are not completed. Even though B transitioned ot Canceled, C should not run yet because 'A' is still running".
This is regardless of the cancellation token passed to C.
There was a problem hiding this comment.
💭 I looked at https://referencesource.microsoft.com and it seems to only impact cases where a cancellation token is involved.
There was a problem hiding this comment.
@CyrusNajmabadi is correct re "I thought it was more about the antecedent tasks and if they used a cancellation token." If you have:
Task a = ...;
Task b = a.ContinueWith(..., token);
Task c = b.ContinueWith(...);and while a is running the token has cancellation requested, should c be allowed to run immediately or must it wait for a to finish.
There was a problem hiding this comment.
@stephentoub I believe you may have misunderstood the context. In my code, I pass CancellationToken.None to ContinueWith. I am claiming that TaskContinuationOptions.LazyCancellation will never have an impact on a call to ContinueWith called in this manner.
There was a problem hiding this comment.
I didn't look at your code; I was commenting on the behavior of LazyCancellation and what it actually does.
Given the code:
Task a = ...;
Task b = a.ContinueWith(..., token);
Task c = b.ContinueWith(...);c can end up running concurrently with a. To prevent that, you can make it:
Task a = ...;
Task b = a.ContinueWith(..., token, TaskContinuationOptions.LazyCancellation, TaskScheduler.Default);
Task c = b.ContinueWith(...);and then c will not run concurrently with a. If that token is None, then it doesn't matter because it won't be canceled prior to a completing (it could still be canceled by other means, e.g. if you passed TaskContinuationOptions.OnlyOnFaulted and the antecedent completed in another state, but in such cases the antecedent needs to be completed for that cancellation to happen).
There was a problem hiding this comment.
If this is the case, can we comment that it's ok for us to not pass LazyCancellation here as it seems to only be relevant for the ContinueWith's that take cancellation tokens?
Thanks!
|
|
||
| public Serializer(HostWorkspaceServices workspaceServices) | ||
| [Obsolete("This exported object must be obtained through the MEF export provider.", error: true)] | ||
| public SerializerService(HostWorkspaceServices workspaceServices) |
There was a problem hiding this comment.
cute. i like that. we should do that in a lot of places :)
| [ExportWorkspaceServiceFactory(typeof(ISerializerService), layer: ServiceLayer.Default), Shared] | ||
| internal class SerializerServiceFactory : IWorkspaceServiceFactory | ||
| { | ||
| [Obsolete("This is the factory method for " + nameof(SerializerService) + ".", error: true)] |
There was a problem hiding this comment.
Could we have our own subclass for this attribute? it reads as if: "this is obsolete and hsould not be used. people should transition to something else". But, in reality, it just means: "it's only for use by mef. we want to keep it, but you should not reference it directly in your code."
Something like "MefConstructorAttribute : ObsoleteAttribute"?
There was a problem hiding this comment.
➡️ ObsoleteAttribute is sealed, but I'm considering creating a class that holds a const string for the message with a comment on that declaration.
| { | ||
| private static readonly ReaderWriterLockSlim s_registryGate = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); | ||
| private static Workspace s_primaryWorkspace; | ||
| private readonly ReaderWriterLockSlim s_registryGate = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); |
There was a problem hiding this comment.
s_registryGate [](start = 46, length = 14)
s_ prefix no longer needed as this is an instance variable.
| private static readonly ReaderWriterLockSlim s_registryGate = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); | ||
| private static Workspace s_primaryWorkspace; | ||
| private readonly ReaderWriterLockSlim s_registryGate = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); | ||
| private Workspace s_primaryWorkspace; |
There was a problem hiding this comment.
s_primaryWorkspace [](start = 26, length = 18)
s_ prefix no longer needed as this is an instance variable.
| private static bool s_infoBarReported = false; | ||
|
|
||
| private static void ShowInfoBar() | ||
| private static void ShowInfoBar(Workspace workspace) |
There was a problem hiding this comment.
Shouldn't we assert when this is null? The API is a no-op if this is null hence seems like a required paramter.
There was a problem hiding this comment.
I have no idea. I'm not familiar with this code and was simply trying to preserve behavior in the absence of static state class PrimaryWorkspace.
| return new MefV1HostServices(container); | ||
| } | ||
|
|
||
| internal static void HookServiceCreation(CreationHook hook) |
There was a problem hiding this comment.
internal static void HookServiceCreation(CreationHook hook) [](start = 7, length = 60)
Should we assert that s_CreationHook is null? Lacking that seems like we will risk introducing bugs by silently overwriting the previous vaule.
There was a problem hiding this comment.
➡️ No. In the cleanup step for the test hook, the creation hook is replaced by a non-null hook that produces an informative exception/test failure in the event code attempts to create an ExportProvider after the test exits the isolation boundary.
There was a problem hiding this comment.
For cases where the overwrite is inteended a force parameter could be employed.
In reply to: 175523407 [](ancestors = 175523407)
|
📝 @CyrusNajmabadi @jaredpar I'm going to apply code changes resulting from code review after I work with HeeJae to complete the final behavior step. |
| AsyncProcessorTask.ContinueWith( | ||
| _ => | ||
| { | ||
| foreach (var (documentId, data) in _pendingWork) |
There was a problem hiding this comment.
documentId, [](start = 42, length = 11)
Consider using _ instead of documentId to make it clear it's not used.
| AsyncProcessorTask.ContinueWith( | ||
| _ => | ||
| { | ||
| foreach (var (projectId, data) in _pendingWork) |
There was a problem hiding this comment.
projectId [](start = 46, length = 9)
consider using _ instead of projecTId to make it clear it's not used.
| data.AsyncToken.Dispose(); | ||
| } | ||
|
|
||
| _pendingWork.Clear(); |
There was a problem hiding this comment.
_pendingWork.Clear(); [](start = 28, length = 21)
Why aren't the operations on _pendingWork guarded by the appropriate gate here?
| protected AbstractGraphProvider( | ||
| IGlyphService glyphService, | ||
| SVsServiceProvider serviceProvider, | ||
| CodeAnalysis.Workspace workspace, |
There was a problem hiding this comment.
CodeAnalysis.Workspace workspace, [](start = 11, length = 34)
A bit disturbing this parameter was provided before and simply ignored.
|
|
||
| namespace Microsoft.CodeAnalysis | ||
| { | ||
| internal static class PrimaryWorkspace |
There was a problem hiding this comment.
internal static class PrimaryWorkspace [](start = 4, length = 38)
I support all removal of global mutable statics.
jasonmalinowski
left a comment
There was a problem hiding this comment.
Hitting request changes because I'm worried about the PrimaryWorkspace change, and specifically that SourceTextContainers are now holding onto workspaces. It's often the case that the extension methods that pass "null" might be the first use of an ITextBuffer (or will be after any refactoring) and thus we'll end up disabling cloning for derived SourceTexts. If this causes a perf regression that blocks insertion that would be a problem. I suggested an alternative use which is still a mutable static but a more constrained mutable static that if only set in VisualStudioWorkspace or the like would mean we can mitigate that risk, until we get @olegtk to give us a proper API and remove the need entirely.
If we can confirm there isn't an RPS regression I'll happily pinch my nose and hit signoff.
I could also imagine the PrimaryWorkspace thing could break Visual Studio for Mac. Any breakage is good in that they should stop using just as we should stop using it, but heads up to @KirillOsenkov either way.
Massive thank you for breaking this up commit-by-commit. Was easy to follow and was pretty easy to guess where you were going.
| [assembly: TypeForwardedTo(typeof(Microsoft.CodeAnalysis.Shared.TestHooks.IAsynchronousOperationWaiter))] | ||
| [assembly: TypeForwardedTo(typeof(Microsoft.CodeAnalysis.Shared.TestHooks.IAsynchronousOperationListener))] | ||
| [assembly: TypeForwardedTo(typeof(Microsoft.CodeAnalysis.Shared.TestHooks.IAsyncToken))] | ||
| [assembly: TypeForwardedTo(typeof(Microsoft.CodeAnalysis.Shared.TestHooks.TaskExtensions))] |
There was a problem hiding this comment.
Do we have some tracking bug to track (eventually) getting rid of them?
| { | ||
| internal delegate MefV1HostServices CreationHook(IEnumerable<Assembly> assemblies); | ||
|
|
||
| private static CreationHook s_CreationHook; |
There was a problem hiding this comment.
Would like a comment that this is test-only. I can imagine people doing really terrible things with this thinking this is a "feature".
| internal static void HookServiceCreation(CreationHook hook) | ||
| { | ||
| s_CreationHook = hook; | ||
| s_defaultHost = null; |
There was a problem hiding this comment.
Comment why. I can guess, but I'm not necessarily the next person to touch this. 😄
| { | ||
| internal delegate MefHostServices CreationHook(IEnumerable<Assembly> assemblies, bool requestingDefaultHost); | ||
|
|
||
| private static CreationHook s_CreationHook; |
There was a problem hiding this comment.
Ditto: comment this is test only. Even better: name the field appropriately.
| { | ||
| public static SourceTextContainer AsTextContainer(this ITextBuffer buffer) | ||
| => TextBufferContainer.From(buffer); | ||
| => TextBufferContainer.From(workspace: null, buffer); |
There was a problem hiding this comment.
This one worries me, since it's entirely likely the very first use of an ITextBuffer goes through this API (at least it is in my project system refactoring), which then means it's not going to get a workspace and never will. This may very well be causing a perf regression if the cloning stuff is necessary. Will probably have to ask for an RPS run then prior to this being merged unless we have an alternate solution, since taking an RPS regression now would cause codeflow to miss a few deadlines.
To be honest, it looks like this is all so we can call ITextBufferCloneService.Clone(ITextImage), which we could just have a static function that's Function<ITextImage, ITextBuffer> and we set that in VisualStudioWorkspace so it's OK for VS, and not set it in tests. Yes, statics are evil, but hey, at least instead of the static for PrimaryWorkspace now it's a single-use static that doesn't open pandora's box. Wishing I did that years ago now...
And that lives long enough until we pester @olegtk or somebody to give us a proper API here, because ITextImage.WithChanges() that returns an ITextImage would be both straightforward and also really useful.
There was a problem hiding this comment.
Can you review commit 1653801 and let me know if the approach in that change is preferable? If so I will add it to this pull request.
| } | ||
|
|
||
| return null; | ||
| return _workspace?.Services.GetService<ITextBufferCloneService>(); |
There was a problem hiding this comment.
i woudl feel better if this service was simply passed in in the start, rather than having to hold onto the _workspace itself. is that doable?
There was a problem hiding this comment.
➡️ I replied to Jason's post regarding this with a proposed alternative. I'm waiting to hear back from reviewers.
|
Sorry my review status is flipping. Was experimenting with CodeFlow features. |
|
|
||
| // check whether we can use text buffer factory | ||
| var factory = TextBufferFactory; | ||
| var factory = _textBufferCloneServiceOpt; |
There was a problem hiding this comment.
Nit: now that that's readonly, we don't have to worry about capturing to a local which was previously required. (But hey, it's shorter than inlining, so...)
There was a problem hiding this comment.
I would likely change it (at least rename) if the code was more complex, but the method seems straightforward enough to keep.
| @@ -82,8 +80,8 @@ public static SourceText From(Workspace workspace, ITextSnapshot editorSnapshot) | |||
| if (!s_textSnapshotMap.TryGetValue(editorSnapshot, out var snapshot)) | |||
| { | |||
| // Avoid capturing `workspace` on the fast path | |||
There was a problem hiding this comment.
Nit: comment name needs updating.
There was a problem hiding this comment.
I'll update this in a commit in my follow-up pull request to avoid needing to rebuild everything again.
|
|
||
| public static SourceText AsText(this ITextSnapshot textSnapshot) | ||
| => SnapshotSourceText.From(workspace: null, textSnapshot); | ||
| => SnapshotSourceText.From(textBufferCloneServiceOpt: null, textSnapshot); |
There was a problem hiding this comment.
Should this try fetching the service like TextBufferContainer does?
There was a problem hiding this comment.
Unfortunately ITextSnapshot doesn't expose any properties to try and fetch from.
|
|
||
| public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) | ||
| { | ||
| return _singleton; |
There was a problem hiding this comment.
_singleton seems a misnomer here: there's one you'll get through the factory, and one directly from the export below. This could just have the [ImportingConstructor] import the type below and return it, just making this an even thinner shim.
(If the only thing still using the workspace service is that inline rename thing, then just disregard this comment and we'll delete this then.)
|
@Pilchie and/or @MattGertz for approval |
|
Have we verified that TypeScript/F#/Xaml work with these changes? Do they have an impact on VS4Mac that we should worry about? |
|
@Pilchie Not specifically, because I'm still not sure how to do that exactly. The listener-related items valuable in testing scenarios have type forwarders and shouldn't be a problem unless Workspaces needs some other IVT that Features already has. The other internal API changes were primarily used in remote workspace / OOP scenarios so it's unlikely to cause problems. |
|
Tagging @minestarks @brettfo @mgoertz-msft - can you three take a look and chime in on whether you think this will impact your scenarios at insertion time? |
|
@Pilchie Shouldn't affect the XAML language service as long as you don't change any of the interfaces in XamlVisualStudio.csproj. The only other concern would be if this were to affect CustomWorkspaces, which we do use in the VS codebase. |
|
Looking through the changes I don't see anything that'll likely break F#, but if I could be emailed when we have a VAL build I'll run through some scenarios. |
|
Approved - thanks for the diligence. |
This addresses a code review feedback point from dotnet#25558.
This pull request contains the changes to production code resulting from a larger effort to stabilize flaky tests.
IdleProcessorstopsISerializerfromSerializerISerializerto workspace serviceISerializerServiceHostServicesimplementationsWorkspace.DisposeAssetService.s_serializera per-instance fieldPrimaryWorkspacea MEF component instead of staticSolutionService.PrimaryWorkspacea per-instance propertyRoslynServices.HostServicesduring testingITextBufferCloneServiceinITextBuffer.Propertiesinstead of separate tracking inSnapshotSourceTextCustomer scenario
This is a change to improve the testability of our code. It is not intended to produce any user-visible behavior or performance changes.
Bugs this fixes
n/a (but supports fixing many flaky test issues)
Workarounds, if any
None
Risk
Relatively low. The risk here primarily lies in a few changes to internal APIs. Downstream consumers accessing
PrimaryWorkspace,Serializer, or a couple Roslyn remoting types through IVT will need to update their code to avoid this path.Performance impact
No expected performance impact in production.
Is this a regression from a previous update?
This is a developer/contributor regression. Our tests have become more sensitive to mutable data sharing across test boundaries as we adopt updated dependencies that impose stricter requirements for asynchronous code, increasing the chances that tests will fail in CI builds.
Root cause analysis
See previous.
How was the bug found?
CI test process dump analysis.
Test documentation updated?
No change to testing approaches results directly from this change.