Skip to content
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

refactor and modernize the native interface #35

Merged
merged 50 commits into from
Sep 23, 2017

Conversation

xqp
Copy link
Contributor

@xqp xqp commented Sep 12, 2017

as discussed yesterday with @maxkorp in #34 here are a bunch of changes.

  • remove openpa dependencies
  • new build enviroment for libnsfw (CMakeLists)
  • use modern c++ structures and classes (unique_ptr and concurrency wrapper)
  • modernize the native platform implementation with RAII wrapper
  • replace Lock.cpp with std::unique_lock<mutex_type>

to be done in future work:
- implement a c++ callback interface to the native filewatchers (in other PR)

@xqp xqp changed the title refactor and modernize the native interafe refactor and modernize the native interface Sep 12, 2017
@reneme
Copy link
Contributor

reneme commented Sep 12, 2017

Open build and threading issues:

  • race condition on windows (seemed to have been a problem in VC++ 2013 -.-)
  • macOS build fails with node.js v4 (not sure why, input would be welcome)
    (needed to set 'MACOSX_DEPLOYMENT_TARGET': '10.7' in xcode_settings)

@implausible
Copy link
Contributor

Ahhh. This would be greatto get rid of opa! Will look into your issues with macos. I need to start acquiring more time at work for this project. :)

binding.gyp Outdated
"xcode_settings": {
"OTHER_CFLAGS": [
"-std=c++0x",
"-stdlib=libc++"
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we can update this to:

"-std=c++11"

Shouldn't need to define stdlib=c++

@reneme
Copy link
Contributor

reneme commented Sep 13, 2017

We are seeing several races and dead locks both on Travis and AppVeyor. Frankly we believe that they've been in the code base before (see build results of latest master merge). It might be that implementing EventQueue based on C++11 constructs rather than OpenPA is encouraging these to show up. Did you see flaky tests or behaviour of watcher startup in the past?

@implausible
Copy link
Contributor

The polling system actually needs to be removed all together with a throttled callback mechanism.

I'm thinking of using something similar in pattern to what I'm doing in https://github.com/implausible/find-git-repositories/blob/master/src/FindGitRepos.cpp#L134.

Honestly, the the synchronization behavior of the polling thread is not my proudest set up.

@reneme
Copy link
Contributor

reneme commented Sep 15, 2017

The polling system actually needs to be removed all together with a throttled callback mechanism.

Agreed. That sounds a lot better than another spinning thread polling the EventQueue. A similar approach could be useful for our envisioned native API to the native watchers as well.

@reneme
Copy link
Contributor

reneme commented Sep 15, 2017

From our side, this PR is complete and we'd be happy for feedback. Frankly this became a much bigger patch than we initially thought. There are a few more things to be done from our perspective, but I suggest to open another issue for that.

Summary of changes:

  • add a basic build system based on CMake (CMakeLists.txt and src/CMakeLists.txt). CMake is widely used in the C++ world and hence is very convenient for integrating with C++ projects. Down the road we should see how to consolidate this with binding.gyp I guess.
  • remove the dependency on openPA that was used to implement EventQueue and replace it with std::deque and a std::mutex. This is much simpler to use and should serve the purpose just fine. Furthermore it appeared to me that OpenPA is actually not developed further anymore. (no commits since 2014 on their repo)
  • switch to a C++11 compatible compiler across all platforms (that proved to be a bit tricky on macOS and node.js v4: (see here).
  • remove the scoped lock implementation in class Lock and replace it with std::lock_guard and std::unique_lock respectively.
  • add the class SingletonSemaphore (see docstrings for details) as a convenience wrapper around std::condition_variable. We use this to signal the successful worker thread startup on all platforms (replacing uv_sem_t on the macOS implementation).
  • replace pthread_mutex_t and pthread_t with std::mutex and std::thread where possible. On the linux watcher we left pthread_t, because std::thread doesn't allow to cancel a running thread. (which IMHO isn't a good thing to do anyway -> future work)
  • Modernize and refactor the implementation of the windows watcher implementation. We introduced a Controller class that is responsible to provide the API used in NativeInterface and handles the directory to be watched. Furthermore there is a Watcher that is responsible for the worker thread handling, the ReadDirectoryChangesW call and the event interpretation.
  • Fixed some races, potential dead locks and memory leaks along the way

@implausible
Copy link
Contributor

implausible commented Sep 18, 2017

I haven't had a chance to sit down and review this seriously; however, the only immediate concern I have is the use of std::deque with std::mutex. The reason for OpenPA was the lock free queue that came with it (that also worked cross platform and prior to c++11), was so that insertions from the file watchers and the callback consumer could run simultaneously without strong synchronization. With the use of std::deque and the std::mutex, has there been any work to address that difference? IE removing items from the queue before sending them to the callback on the polling thread? For instance, we wouldn't want to add any additional locking on the main thread in javascript where the callback reads from the queue.

@reneme
Copy link
Contributor

reneme commented Sep 18, 2017

Thanks for your quick reply! Regarding your concerns:

With the use of std::deque and the std::mutex, has there been any work to address that difference?

No. In fact the EventQueue now locks a mutex on EventQueue::dequeueAll() (see here).

The reason for OpenPA was the lock free queue that came with it

Did you measure any lock contention on a mutex-based approach before? While I agree that lock free data structures are fancy, I wouldn't go down that road without evident benefits.

For instance, we wouldn't want to add any additional locking on the main thread in javascript where the callback reads from the queue.

I guess you are afraid to starve the javascript execution because of a quickly spinning file event watcher. This seems reasonable but I would argue that it is very unlikely. The EventQueue mutex is acquired by the watchers while posting new events only. They even unlock the mutex after each individual enqueue() (see here).

Both Windows and Linux use blocking and/or asynchronous calls (ReadDirectoryChangesW() and read()) in their watcher implementations. Frankly I'm not sure about the macOS implementation but I would be surprised if its any different. Hence, those threads will yield the CPU at latest after each file event cycle.

Anyhow, I agree that it would be interesting to benchmark this.

@reneme
Copy link
Contributor

reneme commented Sep 18, 2017

Following up on that with some quick'n'dirty measuring attempt using std::mutex::try_lock() in EventQueue::dequeueAll() to approximate (documentation says, that try_lock() is allowed to falsely return false) the contention on my MacBook Pro.

I ran a minimal C++ program that would frequently retrieve all file events from the EventQueue in a separate thread just like the NSFW adapter would. The polling thread was put to sleep after each poll for 1, 5, 10, 25, 100, 250 and 500ms. Each of the frequencies were used for 20 seconds, checking how often try_lock() would report the lock in EventQueue::dequeueAll() to be already held. Concurrently, I ran this script to rapidly create 1,000,000 tiny files in the watched directory:

for i in $(seq 1 1000); do
  mkdir -p $i
  for j in $(seq 1 1000); do
    echo "foobar" > $i/$j
  done
done

The graph below shows how often EventQueue::dequeueAll() wasn't able to immediately acquire the lock per second. In which case I would fall back to a standard std::mutex::lock() (effectively blocking the polling thread). Furthermore I measured the time until the lock() call returned using std::chrono::high_resolution_clock which averaged at less than six microseconds.

screen shot 2017-09-18 at 21 46 47

That means for a reasonable query frequency of say 100ms, the polling thread would only experience around one blocking std::mutex::lock() invocation every 10s holding it back for around 6µs. Notably with a quite unusual file operation workload of more than 6,400 file/directory creations per second.

Edit: The same test on windows and linux show comparable results.

@implausible
Copy link
Contributor

That should be sufficient. Thank you for providing some numbers.

@@ -15,7 +15,7 @@ install:
# Get the latest stable version of Node.js or io.js
- ps: Install-Product node $env:nodejs_version
# install modules
- npm install --msvs_version=2013
- npm install --msvs_version=2015
Copy link
Contributor

Choose a reason for hiding this comment

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

Will this still build with msvs_version set to 2013?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

let us use the 2013 compiler :)

Copy link
Contributor

@implausible implausible left a comment

Choose a reason for hiding this comment

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

This is a first pass. I just stopped at the windows controller/watcher refactor. Will pick up again tomorrow. Most of what I've noticed today is just trying to keep the voice of the project consistent. It's looking great. Will have more on the refactor soon.

* Blocks the calling thread until the semaphore is signaled asynchronously.
* If `signal()` has been called on the semaphore already, this won't block.
*/
void wait()
Copy link
Contributor

Choose a reason for hiding this comment

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

The rest of the project is using 2 spaces for indent and opening braces on the function definition (generally). We should perhaps invest some time in declaring a public style guide now that we are receiving additional contributions, but in general we should try to adhere to the style already used in the base.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

maybe we should use the llvm styleguide
https://llvm.org/docs/CodingStandards.html


std::string getError();
std::vector<Event *> *getEvents();
std::vector<Event *>* getEvents();
Copy link
Contributor

Choose a reason for hiding this comment

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

We're going to need to pick a style, especially in this PR you're swapping between voices. Pointer/reference operator should live on the variable name since comma that is what the operator affects. IE:

int* a, b;

in the above, only a is an integer pointer, where as it seems to be implied that both a and b are integer pointers. I will add a style comment anywhere that I've noticed that.

includes/Queue.h Outdated
std::string directory,
std::string fileA,
std::string fileB = ""
const std::string& directory,
Copy link
Contributor

Choose a reason for hiding this comment

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

style please :)

}

private:
std::mutex mMutex;
Copy link
Contributor

Choose a reason for hiding this comment

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

We aren't enforcing this variable alignment in the code base. Single space will suffice. Thanks!

if (mNSFW->mInterface == NULL) {
uv_mutex_unlock(&mNSFW->mInterfaceLock);
return;
}
uv_mutex_unlock(&mNSFW->mInterfaceLock);
Copy link
Contributor

Choose a reason for hiding this comment

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

the lock manage here is really difficult. I think we can do better, but for another PR. Just to talk about what I'm currently mulling over...

We should consider building a mapping interface between the native watcher and the javascript head. When an NSFW is created on the js side, it should assign itself the next id and create a native file watcher. The native file watcher should know the id or have a callback which knows of the id and be able to reference the js side through that id (when performing callbacks). When the nsfw module itself halts, we should let the native side clean up on its own time, but sever the map between the native and js layer... That way we don't need to sync at all in the stop worker!

What do you think? Just thoughts for another PR I think.

src/Queue.cpp Outdated

Event *event = node->event;
delete node;
auto& front = queue.front();
Copy link
Contributor

Choose a reason for hiding this comment

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

Style please.

}

return events;
std::vector<Event *>* NativeInterface::getEvents() {
Copy link
Contributor

Choose a reason for hiding this comment

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

style please.

src/Queue.cpp Outdated
const auto queueSize = queue.size();
std::unique_ptr<std::vector<Event*>> events(new std::vector<Event*>(queueSize, nullptr));
for (size_t i = 0; i < queueSize; ++i) {
auto& front = queue.front();
Copy link
Contributor

Choose a reason for hiding this comment

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

style please.

src/Queue.cpp Outdated

OPA_Queue_enqueue(&mQueue, node, EventNode, header);
OPA_incr_int(&mNumEvents);
void EventQueue::enqueue(const EventType type, const std::string& directory, const std::string& fileA, const std::string& fileB) {
Copy link
Contributor

Choose a reason for hiding this comment

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

style :D..

scheduleRunLoopWork,
(void *)this
);
mRunLoopThread = std::thread([] (RunLoop *rl) { rl->work(); }, this);
Copy link
Contributor

Choose a reason for hiding this comment

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

Ahhh, lambdas.

@xqp
Copy link
Contributor Author

xqp commented Sep 20, 2017

About style issues:
we should use the google or llvm style guide and a formater like "clang-format" to format our code automatically.

@xqp
Copy link
Contributor Author

xqp commented Sep 20, 2017

we should use the new msvc 15 because the older version can not compile the actual code version.
with msvc 2015 this compile unit works fine

Copy link
Contributor

@implausible implausible left a comment

Choose a reason for hiding this comment

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

Mostly style and some areas we could improve clarity, but this is looking very solid. Thanks!

@@ -0,0 +1,73 @@
#include "../includes/win32/Controller.h"

static std::wstring convertMultiByteToWideChar(const std::string &multiByte)
Copy link
Contributor

Choose a reason for hiding this comment

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

Code base is currently using curly brace on the opening line.

}

HANDLE Controller::openDirectory(const std::wstring &path)
{
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here.

}

Controller::~Controller()
{
Copy link
Contributor

Choose a reason for hiding this comment

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

Open on same line, please

}

std::string Controller::getError()
{
Copy link
Contributor

Choose a reason for hiding this comment

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

Same line here

return mWatcher->getError();
}

bool Controller::hasErrored()
Copy link
Contributor

Choose a reason for hiding this comment

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

and here

void Watcher::stop()
{
mRunning = false;
QueueUserAPC([](__in ULONG_PTR) {}, mRunner.native_handle(), (ULONG_PTR)this);
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we add a comment here, it would be a lot faster for future contributors to recognize that this is to wake up the SleepEx above so that the watcher's loop exits cleanly.

}
}

void Watcher::stop()
Copy link
Contributor

Choose a reason for hiding this comment

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

{ on the same line please

return;
}

QueueUserAPC([](__in ULONG_PTR self) {
Copy link
Contributor

Choose a reason for hiding this comment

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

life is so much better with lambdas.

mRunner.join();
}

void Watcher::setError(const std::string &error)
Copy link
Contributor

Choose a reason for hiding this comment

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

{ on the same line please

}


std::string Watcher::getError() const
Copy link
Contributor

Choose a reason for hiding this comment

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

{ on the same line please

@implausible
Copy link
Contributor

We had a test failure on linux in the test for listening for the creation of a deeply nested filex; however, I have seen that failure in master. I don't think it's related to anything you've done, since the refactor there is minimal and should have no change in semantics.

I've gone ahead and restarted that, test. Though we should open an issue related to discovering why that test fails.

@implausible implausible merged commit 67771cf into Axosoft:master Sep 23, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants