Make VisualStudioRuleSetManager thread safe#26599
Make VisualStudioRuleSetManager thread safe#26599jasonmalinowski merged 3 commits intodotnet:masterfrom
Conversation
This is an interface form of ReferenceCountedDisposable that has the advantage of being covariant. It also means that you can cast a ReferenceCountedDisposable<T> to something that doesn't expose that T is disposable.
There was a problem hiding this comment.
I believe fileChangeService is now truely free-thread?
There was a problem hiding this comment.
It is, but we could have multiple callers of this at the same time if we have multiple projects trying to use the same file. We call this outside of the lock that's guarding the dictionary of all of these.
There was a problem hiding this comment.
reason it being internal?
There was a problem hiding this comment.
Ah, right, that was from before I did the IReferenceCountedDisposable trick.
There was a problem hiding this comment.
is this guranteed to be called after InitializeFileTracking?
There was a problem hiding this comment.
InitializeFileTracking is called before we ever hand out a rule set file, so yes.
There was a problem hiding this comment.
if this file is tracked by IReferenceCountDispose, is it okay to call Dispose directly like this without going through IReferenceCountDipose contract?
There was a problem hiding this comment.
As long as Dispose is idempotent, it's fine, which it's normally supposed to be anyways. I guess there's a small bug here because we might end up doing StopTrackingRuleSetFile more than once in that case. Will fix.
ebb92a8 to
9ffbf8f
Compare
There was a problem hiding this comment.
Can this throw Argument Exception “Item with Same Key has already been added” for case 2. above?
There was a problem hiding this comment.
💭 _disposed is an anti-pattern for types wrapped in ReferenceCountedDisposable<T>, since it provides both a single-dispose guarantee and a way for callers to ensure the type is never used after dispose in a concurrent environment.
There was a problem hiding this comment.
Renamed this and extracted Dispose to a helper for clarity.
There was a problem hiding this comment.
❗️ This breaks an invariant of ReferenceCountedDisposable<T>: If you obtain a non-null reference using TryGetReference and have not called Dispose(), then the target object is not disposed. This is an essential invariant for reasoning about the type at scale.
9ffbf8f to
2a2b67d
Compare
There was a problem hiding this comment.
💡 internal so it's clear on review that it can't be part of IRuleSetFile
There was a problem hiding this comment.
Won't fix: HeeJae asked why it was internal. At this point not going to play flip flop.
There was a problem hiding this comment.
💭 I'm concerned about this being called inside the lock, since it's generally not good to assume the call will execute asynchronously.
There was a problem hiding this comment.
The API is actually void returning -- we call it Async to indicate it doesn't do synchronously. I expect some larger file change watching rewrite to happen in a month or two (or part of the bigger change) so this might get a lot better.
Incidently this wasn't a new problem for this code: if you see the handler for the file change, you'll see it was doing a workarould already. The lock was just invisible: the UI thead was the implicit lock.
There was a problem hiding this comment.
💭 This is another call inside a lock that makes me uncomfortable
There was a problem hiding this comment.
Thinking about it again, I think we're good. In some sense this problem already existed; it was being called in the context of the lock known as the UI thread.
There was a problem hiding this comment.
💡 Consider using ImmutableArray<FileChangeTracker> or ImmutableList<FileChangeTracker> with atomic exchange. The former offers better efficiency if the list is typically constructed from empty, while the latter offers efficiency if the list is typically incrementally populated.
There was a problem hiding this comment.
We directly create the type with the known capacity, so that'll be fastest of all, right?
There was a problem hiding this comment.
Ah, this wasn't a 💡 for efficiency. The immutable collections allow some flexibility with respect to hoisting cleanup operations outside the locks (you can exchange the list with an empty list, then use the local copy to clean things up).
There was a problem hiding this comment.
Aiming for KISS here.
There was a problem hiding this comment.
💡 Consider one of the following:
-
Use the following code to check instances
if (_ruleSetFileMap.TryGetValue(ruleSetFile, out var weakReference)) { using (var reference = weakReference.TryGetReference()) { if (reference == null || reference.Target == ruleSetFile) { _ruleSetFileMap.Remove(ruleSetFile.FilePath); } } }
-
Modify the comment to instead explain that clearing the cache for a different RuleSetFile is not problematic (correctness, plus the performance cost of recreating the item amortized against the likelihood that a mismatch occurs)
There was a problem hiding this comment.
I wanted to avoid 1 to keep it simple. Comment was intended to do 2 but I'll clarify further.
There was a problem hiding this comment.
❕ If possible without breaking the tests, replace Path.GetTempPath() with TempPath.Root. This ensures the temporary directory doesn't get littered even if cleanup fails.
There was a problem hiding this comment.
@sharwell You mean something else? my IDE can't find TempPath.Root.
2a2b67d to
17496e5
Compare
VisualStudioRuleSetManager acts a single global cache for the contents of rule set files, and also offers eventing if the effective contents change. This was previously only being called on the UI thread everywhere, but it won't be much sooner. Now that we have ReferenceCountedDisposable, I also take advantage of that type to change how we do caching: previously, we would keep filling this cache until the solution close, and then we'd clear out everything at once. There are two things that aren't great about this pattern: 1. It's a small memory leak, at least up until solution close if you stopped using a rule set -- we'll keep holding onto it until we close everything. 2. It assumes that once solution close happens, all projects are indeed gone. This isn't a good assumption if we ever open up our project APIs and let anybody add things to VisualStudioWorkspace, since projects may then live past the solution close event.
|
Tagging @Pilchie for ask mode approval. |
|
Approved to merge for 15.8.preview2 |
17496e5 to
024da90
Compare
VisualStudioRuleSetManager acts a single global cache for the contents of rule set files, and also offers eventing if the effective contents change. This was previously only being called on the UI thread everywhere, but it won't be much sooner.
Now that we have ReferenceCountedDisposable, I also take advantage of that type to change how we do caching: previously, we would keep filling this cache until the solution close, and then we'd clear out everything at once. There are two things that aren't great about this pattern:
Ask Mode template
Customer scenario
New feature work: this makes one part of our Visual Studio project system layer thread safe in advance of moving it to the background thread.
Workarounds, if any
None, we need this. 😄
Risk
Moderate: any threading code is never low risk. But the API shape is otherwise more or less untouched, so existing consumers will see the same things they've always seen.
Performance impact
None, at least when things are still on the UI thread.