Debouncing with a custom GSource

Last updated: Apr 13, 2021
4 min read

While developing sessiond, a session manager written in C with GLib, I was presented with an ideal use case for debouncing: postponing the execution of code that would otherwise run too frequently based on the influx of external events. sessiond is responsible for monitoring X11 input events, which are processed in the GLib event loop using a custom GSource. Debouncing avoids unnecessarily handling every event, especially mouse input events which are generated constantly while the mouse pointer moves.

Abstract debouncing

In the case of sessiond, only the time an input event occurred is relevant, so an abstract implementation of debouncing might look like this:

debounce_interval = 1

# Called when time given to `schedule_event_processing` is reached.
def process_event():
if current_time() - last_event_time >= debounce_interval:
process()

while true:
if event:
last_event_time = current_time()
schedule_event_processing(process_event,
last_event_time + debounce_interval)

When an event occurs, the current time is recorded as last_event_time. There should be no response to the event until at least debounce_interval has elapsed, so event processing is scheduled for a time in the future equal to the current time plus the debounce interval. If another event occurs before this time has passed, an additional response is scheduled, and last_event_time is updated.

In the processing function, it is important to compare the time that has elapsed since the last event to debounce_interval to ensure additional events were not generated after this response was scheduled. This way, event processing happens only when debounce_interval time has passed since the last event was received—the essence of debouncing.

Custom GSource implementation

This method of debouncing can be implemented with a custom GSource's check and dispatch functions using g_source_set_ready_time.

First, define a custom GSource by declaring a struct containing a GSource:

typedef struct {
GSource source;
gpointer fd;
gint64 last_event_time;
} InputSource;

Next, implement the check function, which determines if the source is ready to be dispatched:

#define DEBOUNCE_US (1 * 1000000)

gboolean
inputsource_check(GSource *source)
{
InputSource *self = (InputSource *)source;
GIOCondition revents = g_source_query_unix_fd(source, self->fd);

if (!(revents & G_IO_IN))
return FALSE;

// Process events...

self->last_event_time = g_get_monotonic_time();
g_source_set_ready_time(source, self->last_event_time + DEBOUNCE_US);
}

In this function, a file descriptor is queried to determine if any events are pending. If there are no events to be handled, the function returns FALSE to signify there is no need to call the dispatch function. Otherwise, pending events should be processed accordingly. Finally, the current monotonic time—being that of the last event(s)—is recorded and the source is instructed to dispatch in DEBOUNCE_US microseconds with g_source_set_ready_time.

Now, implement the dispatch function, which is responsible for calling the callback function provided to this GSource at creation time:

gboolean
inputsource_dispatch(GSource *source, GSourceFunc func, gpointer user_data)
{
XSource *self = (XSource *)source;

if (g_get_monotonic_time() - self->last_event_time >= DEBOUNCE_US) {
g_source_set_ready_time(source, -1);
return func(user_data);
}

return G_SOURCE_CONTINUE;
}

The logic here is as follows: this function is called at the monotonic time given to g_source_set_ready_time in the check function above, so we know at least DEBOUNCE_US time has passed since the handling of those events, but additional events may have been received in the meantime, reflected by an updated self->last_event_time. If at least DEBOUNCE_US time has elapsed since the last event, we call the user-provided callback function, and g_source_set_ready_time(source, -1) is used to stop future dispatching of this source until the check function detects pending events. This is necessary because g_source_set_ready_time will cause the source to be continuously dispatched if the time it was last given is in the past, which will inevitably be the case.

Finally, create the GSourceFuncs struct and initialize the custom GSource:

GSourceFuncs inputsource_funcs = {
NULL, // prepare function
inputsource_check,
inputsource_dispatch,
NULL, // finalize function
NULL,
NULL,
};

GSource *source = g_source_new(&inputsource_funcs, sizeof(InputSource));
InputSource *self = (InputSource *)source;

self->last_event_time = 0;

// Add the file descriptor.
// self->fd = g_source_add_unix_fd(source, ..., G_IO_IN);

// Set callback.
// g_source_set_callback(source, ...);

// Attach source to context.
// g_source_attach(source, ...);

More information

This tutorial describes in greater detail the check, dispatch, and other functions that control the behavior of a custom GSource.

For a complete working example, refer to sessiond's debouncing implementation in xsource.c.