Skip to content

Make VisualStudioRuleSetManager thread safe#26599

Merged
jasonmalinowski merged 3 commits intodotnet:masterfrom
jasonmalinowski:free-thread-ruleset-manager
May 4, 2018
Merged

Make VisualStudioRuleSetManager thread safe#26599
jasonmalinowski merged 3 commits intodotnet:masterfrom
jasonmalinowski:free-thread-ruleset-manager

Conversation

@jasonmalinowski
Copy link
Copy Markdown
Member

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.
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.

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.
@jasonmalinowski jasonmalinowski requested a review from a team as a code owner May 3, 2018 18:40
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe fileChangeService is now truely free-thread?

Copy link
Copy Markdown
Member Author

@jasonmalinowski jasonmalinowski May 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reason it being internal?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, right, that was from before I did the IReferenceCountedDisposable trick.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this guranteed to be called after InitializeFileTracking?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InitializeFileTracking is called before we ever hand out a rule set file, so yes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this file is tracked by IReferenceCountDispose, is it okay to call Dispose directly like this without going through IReferenceCountDipose contract?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@jasonmalinowski jasonmalinowski force-pushed the free-thread-ruleset-manager branch 2 times, most recently from ebb92a8 to 9ffbf8f Compare May 3, 2018 20:04
@jasonmalinowski jasonmalinowski self-assigned this May 3, 2018
Copy link
Copy Markdown
Contributor

@mavasani mavasani left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this throw Argument Exception “Item with Same Key has already been added” for case 2. above?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Copy link
Copy Markdown
Contributor

@sharwell sharwell May 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💭 _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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed this and extracted Dispose to a helper for clarity.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❗️ 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.

@jasonmalinowski jasonmalinowski force-pushed the free-thread-ruleset-manager branch from 9ffbf8f to 2a2b67d Compare May 3, 2018 22:25
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 internal so it's clear on review that it can't be part of IRuleSetFile

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't fix: HeeJae asked why it was internal. At this point not going to play flip flop.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💭 I'm concerned about this being called inside the lock, since it's generally not good to assume the call will execute asynchronously.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💭 This is another call inside a lock that makes me uncomfortable

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We directly create the type with the known capacity, so that'll be fastest of all, right?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aiming for KISS here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Consider one of the following:

  1. 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);
        }
      }
    }
  2. 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)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to avoid 1 to keep it simple. Comment was intended to do 2 but I'll clarify further.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❕ 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sharwell You mean something else? my IDE can't find TempPath.Root.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, it's TempRoot.Root.

@jasonmalinowski jasonmalinowski force-pushed the free-thread-ruleset-manager branch from 2a2b67d to 17496e5 Compare May 4, 2018 00:09
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.
@jasonmalinowski
Copy link
Copy Markdown
Member Author

Tagging @Pilchie for ask mode approval.

@jinujoseph jinujoseph added this to the 15.8 milestone May 4, 2018
@jinujoseph
Copy link
Copy Markdown
Contributor

Approved to merge for 15.8.preview2

@jasonmalinowski jasonmalinowski force-pushed the free-thread-ruleset-manager branch from 17496e5 to 024da90 Compare May 4, 2018 17:58
@jasonmalinowski jasonmalinowski merged commit 1e6e1ae into dotnet:master May 4, 2018
@jasonmalinowski jasonmalinowski deleted the free-thread-ruleset-manager branch May 4, 2018 19:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants