From c840d753950bb6265448f9ac98d3f1a6e4bf5c16 Mon Sep 17 00:00:00 2001 From: Christian Hergert Date: Wed, 6 Mar 2024 13:49:50 -0800 Subject: [PATCH 1/2] glib/gmain: plumb timeout as microseconds This gets access to the timeout as microseconds up until we are about to enter the GPollFunc. This is useful so that alternative means may be used to poll with more precision for timeout. --- glib/gmain.c | 157 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 101 insertions(+), 56 deletions(-) diff --git a/glib/gmain.c b/glib/gmain.c index 6d56ab3e8..fecd11aa9 100644 --- a/glib/gmain.c +++ b/glib/gmain.c @@ -189,7 +189,7 @@ struct _GMainContext GHashTable *sources; /* guint -> GSource */ GPtrArray *pending_dispatches; - gint timeout; /* Timeout for current iteration */ + gint64 timeout_usec; /* Timeout for current iteration */ guint next_id; GQueue source_lists; @@ -322,7 +322,7 @@ static gboolean g_main_context_prepare_unlocked (GMainContext *context, gint *priority); static gint g_main_context_query_unlocked (GMainContext *context, gint max_priority, - gint *timeout, + gint64 *timeout_usec, GPollFD *fds, gint n_fds); static gboolean g_main_context_check_unlocked (GMainContext *context, @@ -331,7 +331,7 @@ static gboolean g_main_context_check_unlocked (GMainContext *context, gint n_fds); static void g_main_context_dispatch_unlocked (GMainContext *context); static void g_main_context_poll_unlocked (GMainContext *context, - int timeout, + gint64 timeout_usec, int priority, GPollFD *fds, int n_fds); @@ -3633,6 +3633,45 @@ g_main_context_prepare (GMainContext *context, return ready; } +static inline int +round_timeout_to_msec (gint64 timeout_usec) +{ + /* We need to round to milliseconds from our internal microseconds for + * various external API and GPollFunc which requires milliseconds. + * + * However, we want to ensure a few invariants for this. + * + * Return == -1 if we have no timeout specified + * Return == 0 if we don't want to block at all + * Return > 0 if we have any timeout to avoid spinning the CPU + * + * This does cause jitter if the microsecond timeout is < 1000 usec + * because that is beyond our precision. However, using ppoll() instead + * of poll() (when available) avoids this jitter. + */ + + if (timeout_usec == 0) + return 0; + + if (timeout_usec > 0) + { + guint64 timeout_msec = (timeout_usec + 999) / 1000; + + return (int) MIN (timeout_msec, G_MAXINT); + } + + return -1; +} + +static inline gint64 +extend_timeout_to_usec (int timeout_msec) +{ + if (timeout_msec >= 0) + return (gint64) timeout_msec * 1000; + + return -1; +} + static gboolean g_main_context_prepare_unlocked (GMainContext *context, gint *priority) @@ -3676,12 +3715,12 @@ g_main_context_prepare_unlocked (GMainContext *context, /* Prepare all sources */ - context->timeout = -1; + context->timeout_usec = -1; g_source_iter_init (&iter, context, TRUE); while (g_source_iter_next (&iter, &source)) { - gint source_timeout = -1; + gint64 source_timeout_usec = -1; if (SOURCE_DESTROYED (source) || SOURCE_BLOCKED (source)) continue; @@ -3699,14 +3738,17 @@ g_main_context_prepare_unlocked (GMainContext *context, if (prepare) { gint64 begin_time_nsec G_GNUC_UNUSED; + int source_timeout_msec = -1; context->in_check_or_prepare++; UNLOCK_CONTEXT (context); begin_time_nsec = G_TRACE_CURRENT_TIME; - result = (* prepare) (source, &source_timeout); - TRACE (GLIB_MAIN_AFTER_PREPARE (source, prepare, source_timeout)); + result = (*prepare) (source, &source_timeout_msec); + TRACE (GLIB_MAIN_AFTER_PREPARE (source, prepare, source_timeout_msec)); + + source_timeout_usec = extend_timeout_to_usec (source_timeout_msec); g_trace_mark (begin_time_nsec, G_TRACE_CURRENT_TIME - begin_time_nsec, "GLib", "GSource.prepare", @@ -3730,18 +3772,13 @@ g_main_context_prepare_unlocked (GMainContext *context, if (source->priv->ready_time <= context->time) { - source_timeout = 0; + source_timeout_usec = 0; result = TRUE; } - else + else if (source_timeout_usec < 0 || + (source->priv->ready_time < context->time + source_timeout_usec)) { - gint64 timeout; - - /* rounding down will lead to spinning, so always round up */ - timeout = (source->priv->ready_time - context->time + 999) / 1000; - - if (source_timeout < 0 || timeout < source_timeout) - source_timeout = MIN (timeout, G_MAXINT); + source_timeout_usec = MAX (0, source->priv->ready_time - context->time); } } @@ -3761,16 +3798,16 @@ g_main_context_prepare_unlocked (GMainContext *context, { n_ready++; current_priority = source->priority; - context->timeout = 0; - } - - if (source_timeout >= 0) - { - if (context->timeout < 0) - context->timeout = source_timeout; - else - context->timeout = MIN (context->timeout, source_timeout); + context->timeout_usec = 0; } + + if (source_timeout_usec >= 0) + { + if (context->timeout_usec < 0) + context->timeout_usec = source_timeout_usec; + else + context->timeout_usec = MIN (context->timeout_usec, source_timeout_usec); + } } g_source_iter_clear (&iter); @@ -3807,28 +3844,32 @@ g_main_context_prepare_unlocked (GMainContext *context, gint g_main_context_query (GMainContext *context, gint max_priority, - gint *timeout, + gint *timeout_msec, GPollFD *fds, gint n_fds) { + gint64 timeout_usec; gint n_poll; if (context == NULL) context = g_main_context_default (); - + LOCK_CONTEXT (context); - n_poll = g_main_context_query_unlocked (context, max_priority, timeout, fds, n_fds); + n_poll = g_main_context_query_unlocked (context, max_priority, &timeout_usec, fds, n_fds); UNLOCK_CONTEXT (context); + if (timeout_msec != NULL) + *timeout_msec = round_timeout_to_msec (timeout_usec); + return n_poll; } static gint g_main_context_query_unlocked (GMainContext *context, gint max_priority, - gint *timeout, + gint64 *timeout_usec, GPollFD *fds, gint n_fds) { @@ -3881,15 +3922,15 @@ g_main_context_query_unlocked (GMainContext *context, } context->poll_changed = FALSE; - - if (timeout) + + if (timeout_usec) { - *timeout = context->timeout; - if (*timeout != 0) + *timeout_usec = context->timeout_usec; + if (*timeout_usec != 0) context->time_is_fresh = FALSE; } - TRACE (GLIB_MAIN_CONTEXT_AFTER_QUERY (context, context->timeout, + TRACE (GLIB_MAIN_CONTEXT_AFTER_QUERY (context, context->timeout_usec, fds, n_poll)); return n_poll; @@ -4163,7 +4204,7 @@ g_main_context_iterate_unlocked (GMainContext *context, GThread *self) { gint max_priority = 0; - gint timeout; + gint64 timeout_usec; gboolean some_ready; gint nfds, allocated_nfds; GPollFD *fds = NULL; @@ -4196,10 +4237,10 @@ g_main_context_iterate_unlocked (GMainContext *context, fds = context->cached_poll_array; g_main_context_prepare_unlocked (context, &max_priority); - + while ((nfds = g_main_context_query_unlocked ( - context, max_priority, &timeout, fds, - allocated_nfds)) > allocated_nfds) + context, max_priority, &timeout_usec, fds, + allocated_nfds)) > allocated_nfds) { g_free (fds); context->cached_poll_array_size = allocated_nfds = nfds; @@ -4207,10 +4248,10 @@ g_main_context_iterate_unlocked (GMainContext *context, } if (!block) - timeout = 0; - - g_main_context_poll_unlocked (context, timeout, max_priority, fds, nfds); - + timeout_usec = 0; + + g_main_context_poll_unlocked (context, timeout_usec, max_priority, fds, nfds); + some_ready = g_main_context_check_unlocked (context, max_priority, fds, nfds); if (dispatch) @@ -4489,7 +4530,7 @@ g_main_loop_get_context (GMainLoop *loop) /* HOLDS: context's lock */ static void g_main_context_poll_unlocked (GMainContext *context, - int timeout, + gint64 timeout_usec, int priority, GPollFD *fds, int n_fds) @@ -4502,7 +4543,7 @@ g_main_context_poll_unlocked (GMainContext *context, GPollFunc poll_func; - if (n_fds || timeout != 0) + if (n_fds || timeout_usec != 0) { int ret, errsv; @@ -4510,16 +4551,20 @@ g_main_context_poll_unlocked (GMainContext *context, poll_timer = NULL; if (_g_main_poll_debug) { - g_print ("polling context=%p n=%d timeout=%d\n", - context, n_fds, timeout); - poll_timer = g_timer_new (); + g_print ("polling context=%p n=%d timeout_usec=%"G_GINT64_FORMAT"\n", + context, n_fds, timeout_usec); + poll_timer = g_timer_new (); } #endif poll_func = context->poll_func; - UNLOCK_CONTEXT (context); - ret = (*poll_func) (fds, n_fds, timeout); - LOCK_CONTEXT (context); + { + int timeout_msec = round_timeout_to_msec (timeout_usec); + + UNLOCK_CONTEXT (context); + ret = (*poll_func) (fds, n_fds, timeout_msec); + LOCK_CONTEXT (context); + } errsv = errno; if (ret < 0 && errsv != EINTR) @@ -4535,11 +4580,11 @@ g_main_context_poll_unlocked (GMainContext *context, #ifdef G_MAIN_POLL_DEBUG if (_g_main_poll_debug) { - g_print ("g_main_poll(%d) timeout: %d - elapsed %12.10f seconds", - n_fds, - timeout, - g_timer_elapsed (poll_timer, NULL)); - g_timer_destroy (poll_timer); + g_print ("g_main_poll(%d) timeout_usec: %"G_GINT64_FORMAT" - elapsed %12.10f seconds", + n_fds, + timeout_usec, + g_timer_elapsed (poll_timer, NULL)); + g_timer_destroy (poll_timer); pollrec = context->poll_records; while (pollrec != NULL) @@ -4573,7 +4618,7 @@ g_main_context_poll_unlocked (GMainContext *context, g_print ("\n"); } #endif - } /* if (n_fds || timeout != 0) */ + } /* if (n_fds || timeout_usec != 0) */ } /** From 368cb7eb7b08848968d0cf304cfc07236e54daf6 Mon Sep 17 00:00:00 2001 From: Christian Hergert Date: Wed, 6 Mar 2024 13:52:01 -0800 Subject: [PATCH 2/2] glib/gmain: use ppoll() when possible If our GPollFunc is set to g_poll() then we can optionally use a poll() alternative with higher precision. ppoll() provides poll() equivalence but with timeouts in nanoseconds. With more precise polling timouts, frame clocks and other timing sensitive APIs are not restricted to a minimum of 1 millisecond timeout. --- glib/gmain.c | 23 +++++++++++++++++++++++ meson.build | 13 +++++++++++++ 2 files changed, 36 insertions(+) diff --git a/glib/gmain.c b/glib/gmain.c index fecd11aa9..ded5dccbf 100644 --- a/glib/gmain.c +++ b/glib/gmain.c @@ -69,6 +69,10 @@ #include #include +#ifdef HAVE_POLL_H +#include +#endif + #ifdef HAVE_PIDFD #include #include @@ -4558,6 +4562,25 @@ g_main_context_poll_unlocked (GMainContext *context, #endif poll_func = context->poll_func; +#if defined(HAVE_PPOLL) && defined(HAVE_POLL) + if (poll_func == g_poll) + { + struct timespec spec; + struct timespec *spec_p = NULL; + + if (timeout_usec > -1) + { + spec.tv_sec = timeout_usec / G_USEC_PER_SEC; + spec.tv_nsec = (timeout_usec % G_USEC_PER_SEC) * 1000L; + spec_p = &spec; + } + + UNLOCK_CONTEXT (context); + ret = ppoll ((struct pollfd *) fds, n_fds, spec_p, NULL); + LOCK_CONTEXT (context); + } + else +#endif { int timeout_msec = round_timeout_to_msec (timeout_usec); diff --git a/meson.build b/meson.build index 753454209..0ba7ca131 100644 --- a/meson.build +++ b/meson.build @@ -1008,6 +1008,19 @@ if cc.links('''#include glib_conf.set('HAVE_EVENTFD', 1) endif +# Check for ppoll(2) +if cc.links('''#define _GNU_SOURCE + #include + #include + int main (int argc, char ** argv) { + struct pollfd fds[1] = {{0}}; + struct timespec ts = {0}; + ppoll (fds, 1, NULL, NULL); + return 0; + }''', name : 'ppoll(2) system call') + glib_conf.set('HAVE_PPOLL', 1) +endif + # Check for pidfd_open(2) if cc.links('''#include #include