X11: Listening for an event with a timeout the right way

A common need when using x11 is to wait for an event but with a timeout. For example in sxcs I need to magnify the cursor position when it moves. But even when the cursor isn't moving I still need to redraw periodically so that the magnifier doesn't appear to be "hung".

In this case, what I need is something like XNextEventTimeout which allows me to set a timeout in case no events occur within that time-frame. Unfortunately no such functionality exists, and some of the common ways people achieve this goal aren't really correct.

Thankfully the relatively unknown X Synchronization Extension Protocol provides alarm facility which can be used to simulate a timeout when using XNextEvent. The goal of this article is to shine some spotlight so that more people are aware of this functionality and to share some hiccups that I ran into when trying to switch sxcs to use XSyncAlarm so that you can avoid them.

But before showing the XSyncAlarm method, I'd like to briefly mentions some of the common methods and their downfalls.

Using Signals

This seems like a common approach a lot of people make; which is to set up a signal (typically SIGALRM) and then generate some XEvent inside the signal handler. I've seen at least 3 open-source projects which try this, and all 3 of them have user reports on the application randomly freezing.

Signal handlers are special functions with very strict constrains. A signal handler cannot call any function which isn't async-signal-safe. And guess what, almost none of the Xlib functions are async-signal-safe.

So definitely don't call any Xlib functions (or any function which isn't explicitly declared as async-signal-safe) inside a sig-handler. Doing so is just asking for defect reports.

Using Threads

I haven't yet seen any real world project use this approach. But when searching online, a good amount of examples try using a separate thread to generate a dummy event. This method suffers from similar problem as the signal method. That is, XLib isn't thread safe, so you cannot just call XLib functions from another thread an expect it to work properly and consistently.

Using Poll

This seems to be the most common approach towards solving the problem. First we grab the X server's fd using ConnectionNumber and then we poll on that fd. Seems simple enough.

while (1) { /* main event loop */
    struct pollfd pfd = {
        .fd = ConnectionNumber(x11_display),
        .events = POLLIN,
    };
    Bool pending = XPending(x11_display) > 0 || poll(&pfd, 1, TIMEOUT) > 0;

    if (!pending) {
        continue;
    }
    XNextEvent(...);
    /* process the event */
}

One thing to note here is that we need to call XPending before calling poll. This is because if there's events already in the queue, then poll would just go to sleep.

One more thing to note is the if (!pending) continue part, poll can wake up for a number of reasons, one of them is interrupts. Waking up due to interrupts may very well be desirable but if that's not the case then you'll need to deal with this somehow. In this case I'm just continuing the loop. But you may want to readjust the timeout before polling again.

And while this method works fine in practice, it's actually subtly incorrect. As noted by u/skeeto

My concern is that it's relying on Xlib implementation details, and there's no documented guarantee that XNextEvent won't block on its internal socket read because, say, it's trying to pre-read an extra event.

So while this seems to work, it's actually case of Hyrum's law.

While there still might be a couple cases where using this method might be necessary (more on this later), suffice to say, we should try to avoid depending on internal implementation details as they're the cause of many real-world bugs.

Using XSyncAlarm

Conceptually this is as simple as "set an alarm right before calling XNextEvent which will act as the timeout." Unfortunately, but perhaps not surprisingly since X is involved, you'll have to go through a bunch of mundane rituals before you get to the point of being able to set an alarm.

Initialization

First we need to query for support and initialize the library.

int sync_event, sync_error, tmp;

if (!XSyncQueryExtension(x11.dpy, &sync_event, &sync_error))
    fatal("XSync extension not available");
if (!XSyncInitialize(x11.dpy, &tmp, &tmp))
    fatal("failed to initialize XSync extension");

We'll need sync_event for later use. The major/minor version returned by XSyncInitialize is not that important for me, so I've stuffed them into a tmp var.

For sxcs, the alarm will be critical for functioning, so I've decided to fatally error out in case any of those 2 calls fail. You may wish to deal with the error differently depending on your needs.

One interesting thing to note here is that almost all XSync functions seems to become no-op if XSync support isn't available. So you probably won't need to manually track weather XSync was enabled or not and guard any XSync calls under something like if (xsync_available) { ... }.

Other thing to note here is calling XSyncInitialize is very important, as the library documentation states:

The only XSync function that may be called before this function is XSyncQueryExtension. If a client violates this rule, the effects of all XSync calls that it makes are undefined.

Getting a list of system counters

In order to set an alarm we'll need a counter to set the alarm against. SERVERTIME counter, which counts milliseconds from some arbitrary starting point, is probably what you want . It's also the only counter guaranteed to exist by the XSync extension protocol.

int ncounter;
XSyncSystemCounter *counters;
XSyncCounter servertime = None;

if ((counters = XSyncListSystemCounters(x11.dpy, &ncounter)) != NULL) {
    for (int i = 0; i < ncounter; i++) {
        if (strcmp(counters[i].name, "SERVERTIME") == 0) {
            servertime = counter[i].counter;
            break;
        }
    }
    XSyncFreeSystemCounterList(counters);
}
if (servertime == None)
    fatal("SERVERTIME counter not found");

We'll have to loop over all the available system counters and test their .name against "SERVERTIME" to grab the right one.


There's also an "IDLETIME" counter which seems interesting and might be useful for certain use-cases. But I wasn't able to find any documentation of what exactly it counts.


Creating an alarm

After all that ritual, we're finally at a point where we can set an alarm.

XSyncAlarmAttributes attr;
unsigned long flags = 0;

First we declare a XSyncAlarmAttributes, which we'll need for filling in information about the alarm. The flag variable is a bitmask which tells the function which values we have explicitly set.

attr.trigger.counter = servertime;
flags |= XSyncCACounter;

First we set the counter to SERVERTIME and accordingly set the XSyncCACounter bit in flags.

XSyncIntToValue(&attr.trigger.wait_value, 16);
flags |= XSyncCAValue;
attr.trigger.value_type = XSyncRelative;
flags |= XSyncCAValueType;
attr.trigger.test_type = XSyncPositiveComparison;
flags |= XSyncCATestType;

Since I want the timeout to be 16ms, I'm setting the wait_value to 16. The value_type is set to XSyncRelative since my wait_value is relative to the counter and not absolute. XSyncPositiveComparison should already be the default, so it's likely redundant.

XSyncIntToValue(&attr.delta, 0);
flags |= XSyncCADelta;

For now let's set the delta to 0, this will deactivate the alarm after firing once. We'll come back to this in a bit.

And now with everything in place, we can create the alarm right before entering the main event loop, and then reset the alarm after processing each event. You could also move the call to XSyncChangeAlarm right before calling XNextEvent.

XSyncAlarm alarm = XSyncCreateAlarm(x11.dpy, sync.flags, &sync.attr);
for (XEvent ev; !XNextEvent(x11.dpy, &ev); /* no-op */) {
    switch (ev.type) {
    case MotionNotify:
        redraw();
        break;
    /* process other events... */
    default:
        if (ev.type == (sync_event + XSyncAlarmNotify)) {
            /* got an alarm */
            redraw();
        }
        break;
    }

    XSyncChangeAlarm(x11.dpy, alarm, flags, &attr);
}

And that's more or less it, now you've got yourself a nice way to poll for events with a timeout 🎉.

Some modifications for sxcs

While the above method "works", it wasn't exactly correct for sxcs. The goal here was that we don't go more than 16ms without doing a redraw. So we don't want to set the alarm after processing each event, instead we want to set it only after doing a redraw.

In sxcs, we're redrawing only on MotionNotify and XSyncAlarmNotify, so we can simply remove the XSyncChangeAlarm and call it only if we get these two events.

case MotionNotify:
    redraw();
    XSyncChangeAlarm(...);
    break;

This takes care of MotionNotify, however in case of an alarm, instead of calling XSyncChangeAlarm manually we can just set a delta instead.

When a delta is set to non-zero value, instead of deactivating the alarm after firing, it will set another alarm according to the delta. So instead of setting the delta to 0 above in attr.delta we can just set it to 16 and that'll automatically take care of the XSyncAlarmNotify case for us.

Limitations of XSyncAlarm

Unfortunately there are a couple cases where using XSyncAlarm won't work.

For example if you're trying to immediately deal with interrupts then XSyncAlarm isn't very useful since XNextEvent will just go back to blocking.

Another case when XSyncAlarm isn't useful is when you want to poll other things (e.g an inotify fd) as well.

For these cases, you'll unfortunately need to go back to using the poll method even though it's not entirely correct.

There are a couple other nuances which I've omitted because they're not related to XSyncAlarm specifically, but are a general problem when dealing with time.

Things such as missed deadlines, timer resolution or simply setting the timeout at the wrong time can end up giving unwanted results. There are ways to deal with these of course, such as:

I won't go into details on these as they are out of scope of this article; and for sxcs I didn't think slightly missing a deadline would be too noticeable so I've decided not to complicate things and simply not dealing with these issues.

Conclusion

And that's more or less it. Hopefully this article introduced you to a x11 feature which might be useful to you in the future (maybe even for purposes other than setting timeouts).



RSS Feed