Debouncing with a custom GSource
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.
Subscribe to my newsletter
Read articles from James Reed directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
James Reed
James Reed
Jack of some trades, master of others.