Mowedline: Non-polling X Event Loop
In a recent entry I outlined the three big problems in Mowedline that have come to my attention in the year since my last big round of work on the program. With one problem solved already, the next that I looked into was high CPU usage due to event polling.
This problem arose out of a wrong solution to a common problem: how to write an event loop that takes events from multiple sources. In mowedline, there are three event sources: X, DBus, and an internal queue used to update clock widgets. The common problem arises from the fact that the simplest method to deal with any one event source generally uses blocking I/O, meaning that the program sits and waits for an event from the given source while all other activity is suspended; in this situation you can't get an event from DBus while waiting for one from X, or vice versa. However, the libraries under consideration all have ways to peek into their respective event queues to see if there are new events, so you can look ahead before invoking a blocking read on the event queue. The wrong solution to the problem is the simplistic one: you make a single loop that constantly checks for waiting events with these peek procedures; you have one event loop that checks all event sources over and over and over. This leads to high CPU usage because the program never goes into a sleep state that allows the kernel to switch to other programs.
A hack workaround to this problem is to insert a sleep call into the event loop. This will reduce CPU usage, but at a cost of lost UI responsiveness.
The correct way to deal with multiple event sources in a Chicken Scheme
program is for each event source to have its own corresponding event loop,
and to run each of these event loops in its own srfi-18 thread, using
thread-wait-for-i/o!
or its equivalent for the library in question to
suspend the thread while there are no events in the queue. I implemented
this solution for the X event loop in mowedline, today.
For XLib programming in Chicken, one does indeed use
thread-wait-for-i/o!
, and you get the file descriptor to pass to it by
the xlib egg procedure xconnectionnumber
, which corresponds to XLib's
XConnectionNumber
procedure, or ConnectionNumber
macro. Pass the
#:input
mode flag to thread-wait-for-i/o!
to ensure only to wait for
incoming data on the event FD.
One thing you will have to be mindful of is that now that you are calling
xnextevent
only when there is an incoming event, you will need to
xflush
the display yourself if you draw to your windows at other times.
This situation is alluded to in the
XFlush documentation;
now that your program is not constantly calling xpending
, you need to
make sure that it explicitly calls xflush
at the appropriate times.
Well, that takes care of one third of Mowedline's high CPU usage problem. I still have to look into non-polling event loops for DBus and the internal event queue. The dbus egg itself seems only to provide for polling in its API, and I am not sure yet whether that limitation is in DBus or just in the egg. It won't be so bad if mowedline is forced to poll dbus though, because the sleep call in such a loop would not visibly impact UI responsiveness as it did in the X event loop. A non-polling event loop for the internal event queue, on the other hand, should be a simple thing with srfi-18 condition variables, or so they tell me.
Postscript: file-select
In interest of completeness, I wanted to mention the other way to write an
event loop that draws from multiple sources in Chicken Scheme. If all of
the event sources are file descriptors, you can use file-select
from
unit posix, to watch multiple file
descriptors in a single event loop. In mowedline's case, I won't be going
this route because mowedline has one, possibly two event sources which are
not file descriptors, but there you have it.