Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.
/ corefx Public archive

Share CFRunLoop for FSEventStreams in FileSystemWatcher on OSX #33865

Merged
merged 15 commits into from
Dec 20, 2018

Conversation

MaximLipnin
Copy link
Contributor

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.

@MaximLipnin
Copy link
Contributor Author

/cc @marek-safar

@MaximLipnin
Copy link
Contributor Author

@dotnet-bot test Windows x64 Debug Build

@MaximLipnin
Copy link
Contributor Author

@dotnet-bot test Windows x86 Release Build

Copy link
Member

@stephentoub stephentoub left a 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;
Copy link
Member

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;
Copy link
Member

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();
Copy link
Member

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)
Copy link
Member

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);
Copy link
Member

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.

Copy link
Member

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)
Copy link
Member

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);
Copy link
Member

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?

Copy link
Contributor Author

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) {
Copy link
Member

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
Copy link
Member

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

@stephentoub
Copy link
Member

cc: @JeremyKuhne

@stephentoub
Copy link
Member

@dotnet-bot test Outerloop OSX x64 Debug Build please

1 similar comment
@stephentoub
Copy link
Member

@dotnet-bot test Outerloop OSX x64 Debug Build please

@MaximLipnin MaximLipnin changed the title Share CFRunLoop for FSEventStreams in FileSystemWatcher on OSX [WIP] Share CFRunLoop for FSEventStreams in FileSystemWatcher on OSX Dec 10, 2018
@stephentoub
Copy link
Member

@dotnet-bot test Outerloop OSX x64 Debug Build please

@MaximLipnin
Copy link
Contributor Author

@dotnet-bot test Outerloop OSX x64 Debug Build please

@MaximLipnin
Copy link
Contributor Author

@dotnet-bot test OSX x64 Debug Build please

@MaximLipnin MaximLipnin changed the title [WIP] Share CFRunLoop for FSEventStreams in FileSystemWatcher on OSX Share CFRunLoop for FSEventStreams in FileSystemWatcher on OSX Dec 11, 2018
@danmoseley
Copy link
Member

@stephentoub good to merge?

Copy link
Member

@stephentoub stephentoub left a 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)
Copy link
Member

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?

Copy link
Member

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?

lock(s_lockObject)
{
Interop.CoreFoundation.CFRelease(runLoop);
s_watcherRunLoop = IntPtr.Zero;
Copy link
Member

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?

Copy link
Member

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?

@MaximLipnin
Copy link
Contributor Author

@dotnet-bot test Outerloop OSX x64 Debug Build please

1 similar comment
@MaximLipnin
Copy link
Contributor Author

@dotnet-bot test Outerloop OSX x64 Debug Build please

@MaximLipnin
Copy link
Contributor Author

@dotnet-bot test Outerloop OSX x64 Debug Build please

1 similar comment
@MaximLipnin
Copy link
Contributor Author

@dotnet-bot test Outerloop OSX x64 Debug Build please

@MaximLipnin
Copy link
Contributor Author

@dotnet-bot test OSX x64 Debug Build please

@MaximLipnin
Copy link
Contributor Author

@dotnet-bot test Outerloop OSX x64 Debug Build please

@MaximLipnin
Copy link
Contributor Author

@dotnet-bot test Windows x86 Release Build please

# Conflicts:
#	src/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.File.Create.cs
@MaximLipnin
Copy link
Contributor Author

@dotnet-bot test Outerloop OSX x64 Debug Build please

@MaximLipnin
Copy link
Contributor Author

@dotnet-bot test Outerloop OSX x64 Debug Build please

1 similar comment
@MaximLipnin
Copy link
Contributor Author

@dotnet-bot test Outerloop OSX x64 Debug Build please

[Fact]
public void FileSystemWatcher_File_Create_MultipleWatchers_ExecutionContextFlowed()
public void FileSystemWatcher_File_Create_EnablingDisablingNotAffectRaisingEvent()
Copy link
Member

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.

Copy link
Contributor Author

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()
Copy link
Member

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.

Copy link
Contributor Author

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()
{
Copy link
Member

Choose a reason for hiding this comment

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

Same ExecuteWithRetry comment/question.

Copy link
Contributor Author

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];
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

Copy link
Member

@stephentoub stephentoub left a comment

Choose a reason for hiding this comment

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

Thanks!

@MaximLipnin
Copy link
Contributor Author

@dotnet-bot test Outerloop OSX x64 Debug Build please

@MaximLipnin
Copy link
Contributor Author

@dotnet-bot test Outerloop OSX x64 Debug Build please

@stephentoub
Copy link
Member

@dotnet-bot test Outerloop Linux x64 Debug Build please
@dotnet-bot test Outerloop Windows x64 Debug Build please

@MaximLipnin
Copy link
Contributor Author

@dotnet-bot test Outerloop Linux x64 Debug Build please

@stephentoub
Copy link
Member

Thanks!

@stephentoub stephentoub merged commit 8a06434 into dotnet:master Dec 20, 2018
@MaximLipnin
Copy link
Contributor Author

Thank you for review and help!

@MaximLipnin MaximLipnin deleted the fsw branch December 20, 2018 06:25
@karelz karelz added this to the 3.0 milestone Dec 21, 2018
picenka21 pushed a commit to picenka21/runtime that referenced this pull request Feb 18, 2022
…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
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants