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

Integrate with node's event loop #2

Closed
TooTallNate opened this issue Aug 17, 2011 · 36 comments
Closed

Integrate with node's event loop #2

TooTallNate opened this issue Aug 17, 2011 · 36 comments

Comments

@TooTallNate
Copy link
Owner

This is a tough one... But basically since node maintains it's own event loop, it makes it rather hard to make Objective-C's NSRunLoop or CFRunLoop integrate with it.

Currently, some code as simple as this will crash your terminal tab, since node repl runs in raw mode and we're subsequently starting a new event loop on the main thread, even Ctrl+C won't get you out:

var $ = require('NodObjC')
$.import('Foundation')
$.NSRunLoop('currentRunLoop')('run')

There is only one solution that I can think of: Write a Cocoa backend for libev (or libuv) so that it's underlying event loop is NSRunLoop. So far I have not looked into what that would entail, but I'm assuming it's not a trivial task...

Any additional insight on the subject would be appreciated in the meantime.

/cc @ry

@aredridel
Copy link
Contributor

Doing this right -- having access to a single tick in libuv -- is probably not that hard to add to libuv

@TooTallNate
Copy link
Owner Author

What about the inverse? Apple doesn't offer a run_once() type of function, but the various games on iOS probably have to integrate with the event loop somehow. Here looks like one example: http://www.71squared.com/2009/04/maingameloop-changes/

I'm currently playing around with some workaround code like:

function tick () {
  while ($.CFRunLoopRunInMode($.kCFRunLoopDefaultMode, 0.02, 1) == $.kCFRunLoopRunHandledSource);
  process.nextTick(tick)
}
// Begin the ghetto event loop
tick();

It actually seems like it's working! I'm going to do some more experimenting. The only downside with a technique like this is that it would require some NodObjC-specific way of starting the event loop, like $.startLoop() or something...

I would also like to experiment with the inverse, like you're talking about @aredridel, but that looks like it will be a little bit tougher, especially if I want to support node <= 0.6.x (node 0.7.x already has uv_run_once()).

@aredridel
Copy link
Contributor

That makes sense.

You could special-case that API and do it behind the scenes... Or just document that NodObjC needs a special main loop.

@JeanSebTr
Copy link

Maybe this could be useful for someone else using Cocoa:

If you don't want to block libuv's loop with app('run'), you can replace it with:

app('finishLaunching');
var userInfo = $.NSDictionary('dictionaryWithObject', $(1),
                              'forKey', $('NSApplicationLaunchIsDefaultLaunchKey'));
var notifCenter = $.NSNotificationCenter('defaultCenter');
notifCenter('postNotificationName', $.NSApplicationDidFinishLaunchingNotification,
            'object', app, 'userInfo', userInfo);
function tick () {
  var ev;
  while(ev = app('nextEventMatchingMask', 4294967295,
                 //$.NSAnyEventMask is losing precision somewhere
                 'untilDate', null, // don't wait if there is no event
                 'inMode', $.NSDefaultRunLoopMode,
                 'dequeue', 1)) {
    app('sendEvent', ev);
  }
  app('updateWindows');
  if(shouldKeepRunning) {
    process.nextTick(tick);
  }
}
tick();

It's based on Demystifying NSApplication by recreating it
Don't forget the NSApplicationDidFinishLaunchingNotification or your process could segfault while handling events (at least applicationWillTerminate).

Thanks for making NodObjC. It's awesome!

@TooTallNate
Copy link
Owner Author

@JeanSebTr Wow nice code there! That definitely looks like a nice solution for the time being. Thanks!

@TooTallNate
Copy link
Owner Author

Doing this should be more "official" using the new uv_backend_fd() API in libuv: https://github.com/joyent/libuv/blob/4ba03ddd569bdd361b1498d5f19ec0075db01500/include/uv.h#L271-L285

Hopefully I'll get a chance to look into that soon!

@sandeepmistry
Copy link

This might be a good example to look at https://github.com/philips/eventloops

(I also noticed uv_backend_fd is not in node 0.8.x)

@TooTallNate
Copy link
Owner Author

This might be a good example to look at https://github.com/philips/eventloops

Nice find @sandeepmistry! I'll take a look into that.

(I also noticed uv_backend_fd is not in node 0.8.x)

Yes that's correct, it's a relatively new API. It should be in some of the later v0.9.x releases and newer.

@dtinth
Copy link

dtinth commented Jun 20, 2014

For those who found this issue by Google, here is one solution:

npm install uvcf
var $ = require('NodObjC')
require('uvcf').ref() // ← register libuv's event loop in CF's event loop

@trevorlinton
Copy link
Collaborator

For those who come by and see this:

https://gist.github.com/trevorlinton/5cc934f9264629d4e85c

@trevorlinton
Copy link
Collaborator

I've been working on a project integrating node/osx, i've successfully merged the two loops that pass all of node's unit tests and have the same benchmark performance as node. You can see the code at the link below, perhaps there's a way of porting it into a .mm and including it in nodobjc with node-gyp.

https://github.com/trueinteractions/tint2/blob/master/modules/Runtime/Main_mac.mm

@TooTallNate
Copy link
Owner Author

Nice Trevor! So attempts to port the code directly to NodObjC JS didn't quite work correctly, IIRC?

@trevorlinton
Copy link
Collaborator

@TooTallNate Yeah, there doesn't seem to be a way to instruct uv to run its event loop from JS. uv wants to kick up v8 callbacks, but the v8 isolate is locked every time UV tries because its being called from a javascript stack with an already running isolate.

Unless i'm mistaken that seems to be a core issue that's unavoidable, unless you can figure out a way to create and release a v8::isolate while in javascript. And if you can, i'd be impressed :).

@trevorlinton
Copy link
Collaborator

BTW, only the code on lines 50-76 and lines 99 & 100 in https://github.com/trueinteractions/tint2/blob/9558d160e9a18316f76183b0d23afa116c9cb486/modules/Runtime/Main_mac.mm is relevant to the conversation, it could be as easy as doing:

static int embed_closed;
static uv_sem_t embed_sem;
static uv_thread_t embed_thread;

static void uv_event(void *info) {
    int r;
    struct kevent errors[1];

    while (!embed_closed) {
        uv_loop_t* loop = uv_default_loop();

        int timeout = uv_backend_timeout(loop);
        int fd = uv_backend_fd(loop);

        do {
            struct timespec ts;
            ts.tv_sec = timeout / 1000;
            ts.tv_nsec = (timeout % 1000) * 1000000;
            r = kevent(fd, NULL, 0, errors, 1, timeout < 0 ? NULL : &ts);
        } while (r == -1 && errno == EINTR);

        // Do not block, but place a function on the main queue, run the
        // node block then re-post the semaphore to unlock this loop.
        dispatch_async(dispatch_get_main_queue(), ^{
            uv_run(uv_default_loop(), UV_RUN_NOWAIT);
            uv_sem_post(&embed_sem);
        });

        // Wait for the main loop to deal with events.
        uv_sem_wait(&embed_sem);
    }
}

void SomeNodeFunctionThatRunsUV(v8::Arguments ... ) {
    embed_closed = 0;
    uv_sem_init(&embed_sem, 0);
    uv_thread_create(&embed_thread, uv_event, NULL);
}

void SomeNodeFunctionThatStopsUV(v8::Arguments ... ) {
    embed_closed = 1;
    uv_thread_join(&embed_thread);
}

into a node callback function, the only other issue I can see is this would need to be kicked off when NSapp's delegate runs applicationDidFinishLaunching:. I haven't tested it in other contexts.

I'm knee deep in some other things right now, but if I have some spare time i'll through this into a node module and see if I can get it working. Unless, does someone else want to ?

@mz2
Copy link

mz2 commented Aug 26, 2014

Presumably this would work also in an XPC service's main run loop (if configured with one) or another thread's with its CF run loop set up accordingly?

On 26 Aug 2014, at 19:26, Trevor Linton notifications@github.com wrote:

BTW, only the code on lines 50-76 and lines 99 & 100 in https://github.com/trueinteractions/tint2/blob/9558d160e9a18316f76183b0d23afa116c9cb486/modules/Runtime/Main_mac.mm is relevant to the conversation, it could be as easy as doing:

static int embed_closed;
static uv_sem_t embed_sem;
static uv_thread_t embed_thread;

static void uv_event(void *info) {
int r;
struct kevent errors[1];

while (!embed_closed) {
    uv_loop_t* loop = uv_default_loop();

    int timeout = uv_backend_timeout(loop);
    int fd = uv_backend_fd(loop);

    do {
        struct timespec ts;
        ts.tv_sec = timeout / 1000;
        ts.tv_nsec = (timeout % 1000) * 1000000;
        r = kevent(fd, NULL, 0, errors, 1, timeout < 0 ? NULL : &ts);
    } while (r == -1 && errno == EINTR);

    // Do not block, but place a function on the main queue, run the
    // node block then re-post the semaphore to unlock this loop.
    dispatch_async(dispatch_get_main_queue(), ^{
        uv_run(uv_default_loop(), UV_RUN_NOWAIT);
        uv_sem_post(&embed_sem);
    });

    // Wait for the main loop to deal with events.
    uv_sem_wait(&embed_sem);
}

}

void SomeNodeFunctionThatRunsUV(v8::Arguments ... ) {
embed_closed = 0;
uv_sem_init(&embed_sem, 0);
uv_thread_create(&embed_thread, uv_event, NULL);
}

void SomeNodeFunctionThatStopsUV(v8::Arguments ... ) {
embed_closed = 1;
uv_thread_join(&embed_thread);
}
into a node callback function, the only other issue I can see is this would need to be kicked off when NSapp's delegate runs applicationDidFinishLaunching:. I haven't tested it in other contexts.

I'm knee deep in some other things right now, but if I have some spare time i'll through this into a node module and see if I can get it working. Unless, does someone else want to ?


Reply to this email directly or view it on GitHub.

@trevorlinton
Copy link
Collaborator

@mz2 Hypothetically yes, just as long as the uv_event gets its own thread (otherwise it'll block the current one). I should mention if your CFEventLoop isn't inside the main thread (which i'd be baffled as to why/how) you'll find yourself with a whole other issue.

Curiously, I thought XPC was mainly used as a replacement for IPC and was mostly related to process communications.

@mz2
Copy link

mz2 commented Aug 27, 2014

XPC is indeed an IPC mechanism, but the reason it exists really is for building "XPC services", which are launchd managed processes created by applications on OSX (and actually on iOS too though the APIs for it are private). XPC services are helpful for a few reasons when architecting software on OSX:

  1. you can separate the privileges of different parts of an application. For instance, suppose I wanted to build some web service client code in Node. That service would not need access to the filesystem.
  2. you can separate the lifetime of different parts of your app, for instance 3rd party code you don't trust for any particular reason (say, due to memory reasons), into a separate child process and have the OS manage the lifetime of the component. For instance if it crashes, it gets restarted, or if its unused it gets killed and only later spun up again when needed.

Node would make a lot of sense on OSX in writing self contained services which a larger app would call onto. An XPC service can be configured either to emply a 'regular' run loop or simply by GCD based threads (default, if I remember correctly).

There's more info on creating XPC services here:
https://developer.apple.com/library/mac/documentation/macosx/conceptual/bpsystemstartup/chapters/CreatingXPCServices.html

@trevorlinton
Copy link
Collaborator

mz2, hm i'm not too familiar with XPC to comment if its useful, however if the spun up XPC "service" is intended to run on anything other than the main thread you'd have an issue. I believe UV/node are fine on a different thread they just need to be consistant. You'd have to make sure the uv_run is placed on the correct thread, for the code i submitted it just plops it on the main thread.

@mz2
Copy link

mz2 commented Aug 28, 2014

It can indeed be configured to have a main thread with a similar run loop to a regular Cocoa app. Sounds promising :)

@trevorlinton
Copy link
Collaborator

One last note to leave here, if the uv event loop exits due to uv_stop but the process doesn't the uv_event thread will spin up to 100% cpu (since the backend fd is dead). How this should be dealt with varies based on the use case you have.

My use case required uv to spin endlessly, or never run uv_stop. To deal with this I attached an asynchronous dummy event that will never fire to prevent uv_stop from ever being executed (and simply exit when i want too). There may be other ways of listening for uv_stop to terminate the threads, timeout seems to get set to 0 if the uv loop should be stopped, and -1 is returned/set to timeout if uv is trying to say "i have no idea what the timeout should be, there's still events to be processed, but no events left on the schedule, and we're still waiting but who knows how long".

@shanewholloway
Copy link
Contributor

Some non-UI NSMetadataSearch example code leveraging the working event loop made possible by pull request 56. The example monkey patches in the absence of the pull request being applied.

@TooTallNate
Copy link
Owner Author

@shanewholloway Have you figured out a way to make this EventLoop class replace app('run'), i.e. in the NodeCocoaHelloWorld.app example app?

@shanewholloway
Copy link
Contributor

Sure. Updated pull request #56 with revised NodeCocoaHelloWorld.app example app powered by EventLoop module. Also had forked the cocoa-hellow-world gist. I didn't realize they were so similar.

@TooTallNate
Copy link
Owner Author

Are you a god?

@shanewholloway
Copy link
Contributor

Thanks for the high-praise! I've just been here before in a cross-platform C/C++ library with bindings for Node and Python. ;) Most of the work is in learning Node's ffi, ref, and NodObjC specifics. Happy to help out – the knowledge isn't doing anyone good stuck in my head! Can't wait to see what the community does with it.

@trevorlinton
Copy link
Collaborator

@shanewholloway VERY impressive. Your'e the type of guy who if he has a few hours of free time runs a 100 mile marathon, saves a bald eagle... from a shark.

Well done.

+1 for merge.

@TooTallNate
Copy link
Owner Author

@shanewholloway I'm curious as to why on your gist example, the "applicationDidFinishLaunching" callback never gets invoked. Any thoughts there?

@shanewholloway
Copy link
Contributor

@trevorlinton @TooTallNate

Apologies for the oversight. Forgot to add the call to app('finishLaunching') toward the end. Revised by iPhone, so gist change not yet verified. See other gist I put together for question in pull request #56 that did work last night.

2015-01-14 21:05 PST: Edit confirmed to be working on my build.

@TooTallNate
Copy link
Owner Author

Closing. Thanks again @shanewholloway :)

@bernhard-42
Copy link
Contributor

I played with Shane's gist of Jan 14, replaced setInterval by setTimeout to just verify that it works, but not add more load. I observed that helloworld2.js sitting there waiting consumes about 42% of my CPU (Core i7 mac mini) using the EventLoop appraoch.
Moving back to app('run') - obviously the timeout log does not appear any more - the CPU is more or less 0%
Is it expected that the integrated event loop is as "busy"?
The event loop integration works really well, but it seem quite expensive ...

@shanewholloway
Copy link
Contributor

Yes, a CPU-intensive event loop is the expected behavior of this implementation example. The specific combination I put together for the sample is written to go "flat-out", running the Cocoa event loop non-blocking. This will return control of the process to NodeJS for additional javascript-land processing.

A real GUI app will want to tweak untilDate argument in eventLoopCore to a date a little into the future so the process blocks politely in the Cocoa event loop, providing a responsive GUI. You can try this by setting the value to $.NSDate('distantFuture') – you should see your CPU utilization drop to low single digits. However, doing so will starve out the NodeJS event loop.

This balance is why I put EventLoop implementation forward as an example, but not yet as a package. The code is good enough, hopefully, to be forked and improved upon. But I wasn't able to put enough care and thought into the API to provide control over that balance yet. (That pesky day job…) And there are other concerns as well – for instance, I'd suggest looking into cluster module style forking approach to separate GUI and server processes.

@TooTallNate
Copy link
Owner Author

Ideally we would be able to have libuv poll the Cocoa event loop fd and be notified by a JavaScript event whenever there's Cocoa events to process. This would give the optimal CPU usage / responsiveness.

@shanewholloway Do you know if there's any possible way to poll the event loop file descriptor, or something equivalent to that?

@TooTallNate
Copy link
Owner Author

Possibly related: http://www.cocoabuilder.com/archive/cocoa/224547-questions-about-nsapplication-run.html#224616

If you can require Leopard, take a look at CFFileDescriptor. Prior to
Leopard, you can use CFSocket with any file descriptor, so long as you don't
use the socket-specific parts of that interface.

I do indeed require Leopard for other reasons (my Cocoa bridge uses
the Objective C 2.0 runtime API) so that's not an issue. This should
be an easy fix, since my non-blocking I/O code has pluggable backends;
I can use the Core Foundation run loop on OS X and select() on other
Unices. Looks I can solve the Exposé problem I mentioned in my
original e-mail, as well as the pesky 1-3% CPU usage when idle, and
get rid of some hackish code, in one fell swoop.

@shanewholloway
Copy link
Contributor

@TooTallNate I do not know. I would suspect a collection of descriptors.

@bernhard-42
Copy link
Contributor

@shanewholloway I played around with untilDate but did not find a great balance between CPU usage (let's say 1 digit percentage) and GUI responsiveness.
I like your idea about cluster module so I started looking into it. One problem is that server process and GUI process need to communicate. The first idea of using a rpc library like dnode does not work, since GUI process does not react on sockets driven by node eventloop.
Looks like one needs some rpc approach that leverages Cocoa for network connectivity in GUI process and can use some standard node stuff in server process. Need to do some further research ...

@cztomsik
Copy link

cztomsik commented Aug 16, 2020

I know this is old but for anyone else wondering how to integrate node/xxx event loop with cocoa:

  • you need something like setImmediate to "tick" forever (in non-blocking way)
  • you get next timeout/interval (libuv_backend_timeout)
  • you wait for cocoa events with that timeout (block) like this
  • you have a thread which will do kevent(libuv_fd, null, empty_dest_kevent, 1, null)
  • and if it returns anything >0 you know there's a pending nodejs/libuv event (I/O is ready, etc.) so you need to wakeup cocoa (with glfw its glfwPostEmptyEvent and here's how they do it)

It took me a while but it actually works perfectly and it has very low CPU overhead.

BTW: you need to call event handling from the main thread (that's why you need that setImmediate loop) - even if you've found a way to get events from other thread it could stop working in future and it wouldn't work on IOS anyway.

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

No branches or pull requests

10 participants