I'd like to solicit some design feedback for a new feature that I'd like to contribute. I apologize for the length of this message, but hopefully it contains all of the details needed to get the conversation started.
First, some quick background:
Over the past few years I've written a few modules[1] that provide access to a number of technologies that are either part of Darwin or one of Apple's Darwin-based OSes (OSX, iOS, etc). A common feature that keeps occurring is their reliance on an active run loop. Normally, this is either NSRunloop or CFRunLoop (they are distinct APIs, but they share a common set of event sources.) In the past, I've resorted to adding a persistent call-out to the default back end, which is similar to the approach taken by the GTK2 module. This is normally good enough for sitatuions where events are relatively infrequent, such as GUI-input work.
However, it's not always suitable for higher interrupt volumes and it also has the side effect of presenting a continual, though low, load on the CPU. This isn't a huge problem, but it's unclean and if one were to increase the frequency of the poll-out to the CF/NS run loop, the load becomes higher, and thus harder to ignore.
A Solution:
After doing some research on better ways to approach the problem, I discovered that CF/NS run loops use kqueue at their core. As a result, a kqueue can be added to the run loop and polled for events. That's convenient, because pike uses kqueue on Darwin in its default backend, Pike.PollDeviceBackend. Swapping out the direct call to kevent() with one that calls the run loop results in "native" Pike I/O working the way it should, plus Darwin layered functionality working properly as well (and low CPU utilization, too).
I think that this is a useful new feature to add to Pike, but I've got some concerns about how best to include it. Right now, I've created a branch called hww3/cfrunloop_backend[2] that includes the functionality and swaps out the existing implementation under Darwin. That is, of course, the simplest approach, but I think is probably not the best idea:
Pros:
- Uses kqueue() at its core, so the existing backend will fire events in the same way.
- Allows a number of technologies to function normally when used from with Pike processes, without the use of hacks.
Cons:
- There is exactly one Run Loop per thread, which, if used universally, could present issues because this means behavior would differ from other Backends.
- There is slightly more overhead per trip through the backend due to events being single-shot. This means that the kqueue has to be re-added to the runloop each loop. I don't think it's a major overhead, it still exists.
Alternate Implementations:
A second approach would be to provide an additional backend implementation, say Pike.CFBackend, that's available on Darwin based systems. I see one or two problems with this, though:
- There would need to be a way to activiate this selectively at runtime, something like set_default_backend(Pike.CFBackend()), which probably doesn't exist right now, right?
- Because 90% of the implementation would be shared with PollDeviceBackend, I think it would involve a fair amount of code duplication inside of backend.cmod, which I think would make maintenance more difficult.
A third (and right now, I believe the best) approach would be to add one or two methods to Pike.Backend that could be implemented by PollDeviceBackend on Darwin and be used to activate this alternate mode.
For example:
int(0..1) has_client_mode()
which would return true if there were a client optimization (I suggest client because CFRunLoop would be more useful for client apps than traditional server based apps, though that's certainly not an either/or situaiton) and:
void enable_client_mode(int(0..1))
that would trigger calls to CFRunLoopRunInMode() instead of kevent() directly.
Input Sought:
I think either of the two alternative approaches would be just fine, assuming that the concerns could be addressed. I'm just not sure which would be best from a design and implementation standpoint (I sort of think the last option would be simplest for _me_ to get working, but if someone has desire to help tackle it, I'd be game to play along).
The upshot of allowing the proposed backend changes for other folks using Pike are that:
a) SDL and GL becomes usable out of the box on OSX b) I am prepared to contribute a few pieces of code, namely: - FSEvents module - Updated Filesystem.Monitor that uses FSEvents on Darwin and inotify on Linux to eliminate polling - DNS_SD querying support on OSX - ObjectiveC bridge (possibly)
Hopefully we can have a bit of discussion about the options and approaches and come up with a solution that works without being terribly inconvenient. Please do drop me a note if anything about this proposal is unclear.
Best,
Bill
[1] Public.ObjectiveC, Public.System.FSEvents, Public.IO.IOWarror, additions to DNS-SD; SDL and GL on OSX also share this requirement. [2] http://hg.welliver.org/pike/changesets, not yet pushed to the pike master repo.
I like anything that will allow us to use SDL and GL on OSX.. :)
After reading your proposal, my initial gut feeling is that the second or third option (ie new backend for OSX or extending the existing one) would be the best ones to explore.
I'm sure Grubba will have some thoughts on the subject as well...
I'm not sufficiently versed in the Pike internals to comment on your alternatives, but I too would welcome any CF/NS runloop integration improvements.
- There is exactly one Run Loop per thread, which, if used universally,
could present issues because this means behavior would differ from other Backends.
What do you mean? It can't be necessary to listen to those events in every thread, can it?
A third (and right now, I believe the best) approach would be to add one or two methods to Pike.Backend that could be implemented by PollDeviceBackend on Darwin and be used to activate this alternate mode.
I agree. That way it's possible to choose which backend instance to use for those events, as well. I suggest you use function names that make it really obvious that this for platform specific events. E.g:
void enable_darwin_events (int(0..1) enable); int(0..1) query_darwin_events();
Then one could add a constant like Pike.BACKEND_GOT_DARWIN_EVENTS, to be able to test compile time whether to compile in calls to those functions (which of course wouldn't exist at all on other platforms).
I've got no idea if "darwin_events" is the technically most accurate name, but you get the idea. By using such a clearly platform specific name, it's easier to invent a more generic scheme later if more things like this get added.
Then it's another matter if we somehow can make it easier to do the reverse, i.e. to hook in the pike backend in a foreign one. That'd mean a way to query for all the pike fd's, including the wakeup pipe, I guess. Or if it's possible to solve a bit more elegantly when something like kqueue is used.
I've checked in support for allowing CoreFoundation to manage IO in PollDeviceBackend, for systems so equipped (OSX, etc). I didn't add a query function, which was an oversight on my part.
void enable_core_foundation (int(0..1) enable); constant GOT_CORE_FOUNDATION = 1;
This functionality probably won't bring any major new benefits yet, though it similarly shouldn't break things when enabled. Once I get it polished up a little more, I'll turn my attention to contributing some code that takes advantage of the new functionality.
As always, comments and suggestions are welcome. Bill
I've been thinking about this as well. My scope hasn't been universal, as for the moment my focus is on the built-on-darwin OSen.
In my case, I think it's pretty easy to allow the native run loop handle fd events; it's just a matter of re-registering the kqueue with the runloop after it triggers an event.
I'm less sure about the other aspects of the backend, such as timed events. The run loop has a timer facility, which I think can be adapted to handle call_outs in a pretty straightforward manner. What else do I need to think about if a call to Pike.Backend->`()() is no longer being made?
Bill
Then it's another matter if we somehow can make it easier to do the reverse, i.e. to hook in the pike backend in a foreign one. That'd mean a way to query for all the pike fd's, including the wakeup pipe, I guess. Or if it's possible to solve a bit more elegantly when something like kqueue is used.
I'm less sure about the other aspects of the backend, such as timed events. The run loop has a timer facility, which I think can be adapted to handle call_outs in a pretty straightforward manner. What else do I need to think about if a call to Pike.Backend->`()() is no longer being made?
Grubba is the one who knows this bit best, but afaik you're ok if you make sure the wakeup pipe is included, and set a suitable timeout event from the call out list (see low_backend_once_setup).
pike-devel@lists.lysator.liu.se