-
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
Reduce IVsFreeThreadedFileChangeEvents2.DirectoryChangedEx2 notifications from VS shell #75815
base: main
Are you sure you want to change the base?
Reduce IVsFreeThreadedFileChangeEvents2.DirectoryChangedEx2 notifications from VS shell #75815
Conversation
…ions from VS shell While looking into another FileWatcher issue, I noticed that we were getting more notifications from shell on directory changes than I would have expected. It turns out this is because we don't currently combine WatchDirectory operations as we do WatchFile/UnwatchFile/UnwatchDirectories. This is generally the desired behavior as vs shell doesn't support multiple directories being supported by a single notification. However, they do support multiple filters on the same directory, and this is exactly the case that we usualy experience when processing a batch of WatcherOperations. This PR simply allows combining of directory watch notifications for the same directory (and sink), keeping track of the combined set of filters that all requests had. Note that this PR also changed the mergability of WatchFiles by only allowing merging if the sinks are the same. I don't have context to be completely confident the prior behavior is a bug, but it sure seems wrong.
return (_kind == other._kind); | ||
if (_kind == Kind.WatchFiles) | ||
{ | ||
return other._sink == _sink; |
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.
@jasonmalinowski -- This is the code that seemed like a bug before. I definitely see WatchFiles requests with different sinks getting merged.
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.
Looks like this is fixing #74716 then.
In this PR, or in a followup, can you examine if TimeSpan.Zero is really a good idea for the ABWQ delay. As this is IO and FS stuff, i really think it would make more sense to have a more reasonable delay (like 500ms or a second). From user perspective, they'll never notice this. But it means we can collect a tremendous amount more, and process and analyze the results in much larger batches for things like deduping and whatnot. TimeSpan.Zero is really for cases of "we are both going to getting an insane firehose of messages AND we must issue them immediately or else badness will occur". For disk notifications, the latter just doesn't apply here. No one will be harmed AFAICT if we wait 500ms. |
From local testing of open/close roslyn sln, I saw the following: 0 ms delay 100 ms 500 ms |
Love it. This is also nice as we really do want to "back off" when notifications come in. They often come in in huge flurries, and we don't want to then be contending with the disk while we're being notified about chnages from other files. 500ms is a nice compromise. |
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 believe this breaks how the cookies have to get back to the original context for the directory watch, so later unsubscribes won't unsubscribe. I could be wrong, but we should really have a test either way that two directory watches combine, and you can successfully remove them later.
// 📝 Empirical testing during high activity (e.g. solution close) showed strong batching performance even | ||
// though the batching delay is 0. | ||
// 📝 Empirical testing during high activity (e.g. solution open/close) showed strong batching performance | ||
// with batching delay of 500 ms. |
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 have no particular opinion what this number should be, but do we have some way to actually differentiate between zero and 500 ms for which is better here? If both numbers were "showing strong batching performance" how did we come up with this change?
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 make sure, you saw the earlier comment I made regarding the different performance characteristics I saw when trying out various values?
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 did not! Maybe we should add that data as a comment to the code.
return (_kind == other._kind); | ||
if (_kind == Kind.WatchFiles) | ||
{ | ||
return other._sink == _sink; |
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.
It might be easier/prudent just to compare the _sink regardless of the underlying type; that way we don't ever forget to update this check and combine a different kind of operation by accident.
for (var i = 0; i < op._filters.Length; i++) | ||
filtersBuilder.Add(op._filters[i]); | ||
|
||
cookiesBuilder.AddRange(op._cookies); |
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 I think is going to break things -- the list of cookies in the WatchDirectory case isn't just the list that can get copied around, but it's actually the list that gets filled back in. Put another way, the List<uint>
object is held by the Context object, and is expected that exact instance gets passed around to the ApplyAsync method so it can be filled in later. That way that same list object gets passed to the UnwatchDirectories later.
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 really should add some tests here. Specifically, have a test that merging two directories and then unsubscribing them successfully results in no watches once we're done. I expect that to be broken.
@svick's test in #74716 is a pretty good start for the general pattern. Rather than firing events and seeing what's fired, it might be easier to just assert the number of watches on the underlying mocked file change service. (Although making sure events actually fire right wouldn't hurt either.)
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.
Backed out the watch directory merging, wasn't going to be easy to solve and found a different way to get some watcher reductions.
case Kind.WatchDirectory: | ||
filtersBuilder.RemoveDuplicates(); | ||
return WatchDirectory(firstOp._paths[0], filtersBuilder.ToImmutable(), firstOp._sink, cookiesBuilder.ToList()); | ||
case Kind.UnwatchDirectories: |
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's probably another problem here too: if two watches against a directory are for different filters, and we combine them, what happens on unsubscribe? Do we still keep the other one around?
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.
Backed out the watch directory merging, wasn't going to be easy to solve and found a different way to get some watcher reductions.
…issues with that approach. However, we can still improve batching by changing the WatchDirectory callers to create fewer WatchedDirectory objects that instead specify multiple filters each.
@@ -367,8 +361,13 @@ public Context(FileChangeWatcher fileChangeWatcher, ImmutableArray<WatchedDirect | |||
|
|||
foreach (var watchedDirectory in watchedDirectories) | |||
{ | |||
_fileChangeWatcher._taskQueue.AddWork(watchedDirectories.Select( | |||
watchedDirectory => WatcherOperation.WatchDirectory(watchedDirectory.Path, watchedDirectory.ExtensionFilter, this, _directoryWatchCookies))); | |||
var item = WatcherOperation.WatchDirectory( |
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.
Backed out the watch directory merging, wasn't going to be easy to solve and found a different way to get some watcher reductions. In reply to: 2424933376 |
@@ -61,7 +62,7 @@ public async Task CreatingDirectoryWatchRequestsDirectoryWatch() | |||
var tempDirectory = tempRoot.CreateDirectory(); | |||
|
|||
// Try creating a context and ensure we created the registration | |||
var context = lspFileChangeWatcher.CreateContext([new ProjectSystem.WatchedDirectory(tempDirectory.Path, extensionFilter: null)]); | |||
var context = lspFileChangeWatcher.CreateContext([new ProjectSystem.WatchedDirectory(tempDirectory.Path, extensionFilters: ImmutableArray<string>.Empty)]); |
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.
var context = lspFileChangeWatcher.CreateContext([new ProjectSystem.WatchedDirectory(tempDirectory.Path, extensionFilters: ImmutableArray<string>.Empty)]); | |
var context = lspFileChangeWatcher.CreateContext([new ProjectSystem.WatchedDirectory(tempDirectory.Path, extensionFilters: [])]); |
@@ -252,21 +252,15 @@ public static WatcherOperation CombineRange(ImmutableSegmentedList<WatcherOperat | |||
{ | |||
case Kind.WatchFiles: | |||
for (var i = 0; i < op._paths.Count; i++) | |||
{ | |||
fileNamesBuilder.Add(op._paths[i]); |
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 this not just .AddRange (and the same for the below lines)?
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.
op._paths is a OneOrMany, I don't think there is an override for ArrayBuilder.AddRange that takes that in.
@@ -297,7 +291,7 @@ public bool CanCombineWith(in WatcherOperation other) | |||
if (_kind == Kind.WatchDirectory) | |||
return false; | |||
|
|||
return (_kind == other._kind); | |||
return _kind == other._kind && _sink == other._sink; |
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.
consider doc'ing. up to you.
|
||
Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.ProjectSystemShim | ||
<UseExportProvider> | ||
Public Class FileChangeWatcherTests |
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.
Public Class FileChangeWatcherTests | |
Public NotInheritable Class FileChangeWatcherTests |
While looking into another FileWatcher issue, I noticed that we were getting more notifications from shell on directory changes than I would have expected. It turns out this is because we don't currently combine WatchDirectory operations as we do WatchFile/UnwatchFile/UnwatchDirectories.
This is generally the desired behavior as vs shell doesn't support multiple directories being supported by a single notification. However, they do support multiple filters on the same directory, and this is exactly the case that we usualy experience when processing a batch of WatcherOperations. This PR simply allows combining of directory watch notifications for the same directory (and sink), keeping track of the combined set of filters that all requests had.
Note that this PR also changed the mergability of WatchFiles by only allowing merging if the sinks are the same. I don't have context to be completely confident the prior behavior is a bug, but it sure seems wrong.