-
Notifications
You must be signed in to change notification settings - Fork 4.9k
Share CFRunLoop for FSEventStreams in FileSystemWatcher on OSX #33865
Conversation
/cc @marek-safar |
@dotnet-bot test Windows x64 Debug Build |
@dotnet-bot test Windows x86 Release Build |
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 for working on this.
{ | ||
lock (StopLock) | ||
// A reference to the RunLoop that we can use to start or stop a Watcher | ||
private static CFRunLoopRef _watcherRunLoop = IntPtr.Zero; |
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.
Nit: s_watcherRunLoop
// A reference to the RunLoop that we can use to start or stop a Watcher | ||
private static CFRunLoopRef _watcherRunLoop = IntPtr.Zero; | ||
|
||
private static int scheduledStreamsCount = 0; |
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.
Nit: s_scheduledStreamsCount
|
||
private static int scheduledStreamsCount = 0; | ||
|
||
private static object lockObject = new object(); |
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.
Nit: readonly
Nit: s_lockObject
|
||
public static void ScheduleEventStream(SafeEventStreamHandle eventStream) | ||
{ | ||
if(_watcherRunLoop == IntPtr.Zero) |
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.
Nit: space before open paren
new Thread(WatchForFileSystemEventsThreadStart) { IsBackground = true }.Start(new object[] { runLoopStarted, eventStream }); | ||
runLoopStarted.Wait(); | ||
|
||
Interlocked.Increment(ref scheduledStreamsCount); |
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.
This increment needs to be done before the thread is started to avoid a race condition.
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 starting/stopping FileSystemWatcher generally a hotly contended path? I wouldn't think so. In which case, I'd prefer we avoid trying to be lock-free and instead just use a lock, where we have a better chance of getting the synchronization correct.
Interop.EventStream.FSEventStreamUnscheduleFromRunLoop(eventStream, _watcherRunLoop, Interop.RunLoop.kCFRunLoopDefaultMode); | ||
Interlocked.Decrement(ref scheduledStreamsCount); | ||
|
||
if (scheduledStreamsCount == 0) |
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.
This isn't thread-safe. It should be using the return value of Decrement.
Also what happens if this thread decrements to 0, goes to stop the loop, and then another thread increments and assumes it's already been started?
|
||
|
||
// Schedule the EventStream to run on the thread's RunLoop | ||
Interop.EventStream.FSEventStreamScheduleWithRunLoop(_eventStream, _watcherRunLoop, Interop.RunLoop.kCFRunLoopDefaultMode); |
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.
Why is the first call / the one spinning up the thread different from all of the others? i.e. why does this call need to be part of WatchForFilesystemEventsThreadStart rather than just being in ScheduleEventStream?
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.
RunLoop won't run without any scheduled event stream.
|
||
private void CancellationCallback() | ||
{ | ||
lock (StopLock) { |
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.
Shouldn't this be the same lock object that's used to coordinate other activities within StaticWatcherRunLoopManager?
bool started = Interop.EventStream.FSEventStreamStart(_eventStream); | ||
if (!started) | ||
{ | ||
// Try to get the Watcher to raise the error event; if we can't do that, just silently exist since the watcher is gone anyway |
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.
Nit: preexisting your change, but exist => exit
cc: @JeremyKuhne |
@dotnet-bot test Outerloop OSX x64 Debug Build please |
1 similar comment
@dotnet-bot test Outerloop OSX x64 Debug Build please |
@dotnet-bot test Outerloop OSX x64 Debug Build please |
@dotnet-bot test Outerloop OSX x64 Debug Build please |
@dotnet-bot test OSX x64 Debug Build please |
@stephentoub good to merge? |
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 for working on this. Looking pretty good. A few more comments.
|
||
public static void UnscheduleFromRunLoop(SafeEventStreamHandle eventStream) | ||
{ | ||
if (s_watcherRunLoop != IntPtr.Zero) |
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.
In what situation would UnscheduleFromRunLoop be called with s_watcherRunLoop == IntPtr.Zero? And do we really need the double-checked locking here, or could this outer check be removed?
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.
Can you remove the outer check and add an assert that s_watcherRunLoop isn't zero?
src/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.OSX.cs
Outdated
Show resolved
Hide resolved
src/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.OSX.cs
Outdated
Show resolved
Hide resolved
lock(s_lockObject) | ||
{ | ||
Interop.CoreFoundation.CFRelease(runLoop); | ||
s_watcherRunLoop = IntPtr.Zero; |
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.
This looks like a bug / race condition. What happens if the last watcher gets removed and then immediately after that a new one is created? We could end up stomping here over the new value set by the latter.
Is this write even necessary? Aren't we already setting this to zero when unscheduling the last watcher?
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.
This should be removed, right?
src/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.OSX.cs
Outdated
Show resolved
Hide resolved
src/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.OSX.cs
Outdated
Show resolved
Hide resolved
src/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.OSX.cs
Outdated
Show resolved
Hide resolved
src/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.OSX.cs
Outdated
Show resolved
Hide resolved
@dotnet-bot test Outerloop OSX x64 Debug Build please |
1 similar comment
@dotnet-bot test Outerloop OSX x64 Debug Build please |
src/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.OSX.cs
Outdated
Show resolved
Hide resolved
@dotnet-bot test Outerloop OSX x64 Debug Build please |
1 similar comment
@dotnet-bot test Outerloop OSX x64 Debug Build please |
@dotnet-bot test OSX x64 Debug Build please |
@dotnet-bot test Outerloop OSX x64 Debug Build please |
@dotnet-bot test Windows x86 Release Build please |
# Conflicts: # src/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.File.Create.cs
@dotnet-bot test Outerloop OSX x64 Debug Build please |
@dotnet-bot test Outerloop OSX x64 Debug Build please |
1 similar comment
@dotnet-bot test Outerloop OSX x64 Debug Build please |
[Fact] | ||
public void FileSystemWatcher_File_Create_MultipleWatchers_ExecutionContextFlowed() | ||
public void FileSystemWatcher_File_Create_EnablingDisablingNotAffectRaisingEvent() |
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.
Could this use the ExpectEvent helper? Or if not, use ExecuteWithRetry around the whole body? We've had too many CI failures due to FSW flakiness, so we have retries in pretty much all of our FSW tests that actually expect events. The ExpectEvent helper has a built-in retry.
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.
wrapped in ExecuteWithRetry helper method
[Fact] | ||
public void FileSystemWatcher_File_Create_MultipleWatchers_ExecutionContextFlowed() | ||
public void FileSystemWatcher_File_Create_EnablingDisablingNotAffectRaisingEvent() |
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 should probably make this Outerloop as well, since it's going to end up creating 100 threads, and that could in theory cause issues for other concurrently running tests if the CI machine was resource starved.
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.
Done
[OuterLoop] | ||
[Fact] | ||
public void FileSystemWatcher_File_Create_WatchOwnPath() | ||
{ |
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.
Same ExecuteWithRetry comment/question.
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.
Done
{ | ||
string fileName = Path.Combine(TestDirectory, "file"); | ||
AutoResetEvent[] autoResetEvents = new AutoResetEvent[64]; | ||
FileSystemWatcher[] watchers = new FileSystemWatcher[64]; |
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 should probably dispose all of these in a finally: that way we'll promptly tear them down when the test completes, even in the case of an exception or assert failure.
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.
Done
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!
…vent test in ExecuteWithRetry helper method and mark it with Outerloop
…try helper method
…leSystemWatcher_File_Create_ForceLoopRestart test
@dotnet-bot test Outerloop OSX x64 Debug Build please |
@dotnet-bot test Outerloop OSX x64 Debug Build please |
@dotnet-bot test Outerloop Linux x64 Debug Build please |
@dotnet-bot test Outerloop Linux x64 Debug Build please |
Thanks! |
Thank you for review and help! |
…t/corefx#33865) * Share CFRunLoop for FSEventStreams in FileSystemWatcher on OSX * Address review comments * Add execution context capturing * Fix nits * Add some tests to check multiple watchers * Address review comments * Update FileSystemWatcher tests * Add timeout for non-expected events * Wrap FileSystemWatcher_File_Create_EnablingDisablingNotAffectRaisingEvent test in ExecuteWithRetry helper method and mark it with Outerloop * Wrap FileSystemWatcher_File_Create_WatchOwnPath test in ExecuteWithRetry helper method * Move disposing FileSystemWatcher instances to a finally section in FileSystemWatcher_File_Create_ForceLoopRestart test * Use null-conditional operator before disposing Commit migrated from dotnet/corefx@8a06434
This is an attempt to solve the problem https://github.com/dotnet/corefx/issues/30600 using the idea of sharing CFRunLoop for FSEventStreams.
I added StaticWatcherRunLoopManager class to share RunLoop among FSEventStreams and updated Start and CancellationCallback methods.
I applied these changes to the latest Mono and used Visual Studio Community 2017 for Mac Version 7.7 (build 1868) to run a sample with 200 FileSystemWatcher instances. mono/mono#11945
It showed reduced number of used threads.