1
0
Fork 0
mutter-performance-source/clutter/clutter-timeout-pool.c
Emmanuele Bassi 68bbe4cd89 2007-08-12 Emmanuele Bassi <ebassi@openedhand.com>
* clutter/clutter-timeout-pool.c: Fix removing and adding timeouts
	to the timeout pool during a dispatch of a timeout source already
	inside the pool. (#456, based on a patch by Neil Roberts)

	(clutter_timeout_dispatch), (clutter_timeout_pool_dispatch): Hold
	the main Clutter lock in the pool dispatch function, instead of
	the per-timeout dispatch; this guarantees that the ref+unref of
	the single timeouts are done under the main lock.
2007-08-12 13:19:31 +00:00

586 lines
16 KiB
C

/*
* Clutter.
*
* An OpenGL based 'interactive canvas' library.
*
* Authored By Matthew Allum <mallum@openedhand.com>
*
* Copyright (C) 2006 OpenedHand
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the
* Free Software Foundation, Inc., 59 Temple Place - Suite 330,
* Boston, MA 02111-1307, USA.
*
* ClutterTimeoutPool: pool of timeout functions using the same slice of
* the GLib main loop
*
* Author: Emmanuele Bassi <ebassi@openedhand.com>
*
* Based on similar code by Tristan van Berkom
*/
#include "config.h"
#include "clutter-debug.h"
#include "clutter-timeout-pool.h"
typedef struct _ClutterTimeout ClutterTimeout;
typedef enum {
CLUTTER_TIMEOUT_NONE = 0,
CLUTTER_TIMEOUT_READY = 1 << 1
} ClutterTimeoutFlags;
struct _ClutterTimeout
{
guint id;
ClutterTimeoutFlags flags;
gint refcount;
guint interval;
GSourceFunc func;
gpointer data;
GDestroyNotify notify;
GTimeVal expiration;
};
struct _ClutterTimeoutPool
{
GSource source;
guint next_id;
GList *timeouts, *dispatched_timeouts;
gint ready;
guint id;
};
#define TIMEOUT_READY(timeout) (timeout->flags & CLUTTER_TIMEOUT_READY)
static gboolean clutter_timeout_pool_prepare (GSource *source,
gint *next_timeout);
static gboolean clutter_timeout_pool_check (GSource *source);
static gboolean clutter_timeout_pool_dispatch (GSource *source,
GSourceFunc callback,
gpointer data);
static void clutter_timeout_pool_finalize (GSource *source);
static GSourceFuncs clutter_timeout_pool_funcs =
{
clutter_timeout_pool_prepare,
clutter_timeout_pool_check,
clutter_timeout_pool_dispatch,
clutter_timeout_pool_finalize
};
static gint
clutter_timeout_sort (gconstpointer a,
gconstpointer b)
{
const ClutterTimeout *t_a = a;
const ClutterTimeout *t_b = b;
gint comparison;
/* Keep 'ready' timeouts at the front */
return (comparison = TIMEOUT_READY (t_a) - TIMEOUT_READY (t_b))
? -comparison
/* Otherwise sort by expiration time */
: ((comparison = t_a->expiration.tv_sec - t_b->expiration.tv_sec)
? comparison
: t_a->expiration.tv_usec - t_b->expiration.tv_usec);
}
static gint
clutter_timeout_find_by_id (gconstpointer a,
gconstpointer b)
{
const ClutterTimeout *t_a = a;
return t_a->id == GPOINTER_TO_UINT (b) ? 0 : 1;
}
static void
clutter_timeout_set_expiration (ClutterTimeout *timeout,
GTimeVal *current_time)
{
guint seconds = timeout->interval / 1000;
guint msecs = timeout->interval - seconds * 1000;
timeout->expiration.tv_sec = current_time->tv_sec + seconds;
timeout->expiration.tv_usec = current_time->tv_usec + msecs * 1000;
if (timeout->expiration.tv_usec >= 1000000)
{
timeout->expiration.tv_usec -= 1000000;
timeout->expiration.tv_sec += 1;
}
}
static gboolean
clutter_timeout_prepare (GSource *source,
ClutterTimeout *timeout,
gint *next_timeout)
{
glong sec;
glong msec;
GTimeVal current_time;
g_source_get_current_time (source, &current_time);
sec = timeout->expiration.tv_sec - current_time.tv_sec;
msec = (timeout->expiration.tv_usec - current_time.tv_usec) / 1000;
if (sec < 0 || (sec == 0 && msec < 0))
msec = 0;
else
{
glong interval_sec = timeout->interval / 1000;
glong interval_msec = timeout->interval % 1000;
if (msec < 0)
{
msec += 1000;
sec -= 1;
}
if (sec > interval_sec ||
(sec == interval_sec && msec > interval_msec))
{
clutter_timeout_set_expiration (timeout, &current_time);
msec = MIN (G_MAXINT, timeout->interval);
}
else
msec = MIN (G_MAXINT, (guint) msec + 1000 * (guint) sec);
}
*next_timeout = (gint) msec;
return (msec == 0);
}
static gboolean
clutter_timeout_check (GSource *source,
ClutterTimeout *timeout)
{
GTimeVal current_time;
g_source_get_current_time (source, &current_time);
return ((timeout->expiration.tv_sec < current_time.tv_sec) ||
((timeout->expiration.tv_sec == current_time.tv_sec) &&
(timeout->expiration.tv_usec <= current_time.tv_usec)));
}
static gboolean
clutter_timeout_dispatch (GSource *source,
ClutterTimeout *timeout)
{
gboolean retval = FALSE;
if (G_UNLIKELY (!timeout->func))
{
g_warning ("Timeout dispatched without a callback.");
return FALSE;
}
if (timeout->func (timeout->data))
{
GTimeVal current_time;
g_source_get_current_time (source, &current_time);
clutter_timeout_set_expiration (timeout, &current_time);
retval = TRUE;
}
return retval;
}
static ClutterTimeout *
clutter_timeout_new (guint interval)
{
ClutterTimeout *timeout;
GTimeVal current_time;
timeout = g_slice_new0 (ClutterTimeout);
timeout->interval = interval;
timeout->flags = CLUTTER_TIMEOUT_NONE;
timeout->refcount = 1;
g_get_current_time (&current_time);
clutter_timeout_set_expiration (timeout, &current_time);
return timeout;
}
/* ref and unref are always called under the main Clutter lock, so there
* is not need for us to use g_atomic_int_* API.
*/
static ClutterTimeout *
clutter_timeout_ref (ClutterTimeout *timeout)
{
g_return_val_if_fail (timeout != NULL, timeout);
g_return_val_if_fail (timeout->refcount > 0, timeout);
timeout->refcount += 1;
return timeout;
}
static void
clutter_timeout_unref (ClutterTimeout *timeout)
{
g_return_if_fail (timeout != NULL);
g_return_if_fail (timeout->refcount > 0);
timeout->refcount -= 1;
if (timeout->refcount == 0)
{
if (timeout->notify)
timeout->notify (timeout->data);
g_slice_free (ClutterTimeout, timeout);
}
}
static void
clutter_timeout_free (ClutterTimeout *timeout)
{
if (G_LIKELY (timeout))
{
if (timeout->notify)
timeout->notify (timeout->data);
g_slice_free (ClutterTimeout, timeout);
}
}
static gboolean
clutter_timeout_pool_prepare (GSource *source,
gint *next_timeout)
{
ClutterTimeoutPool *pool = (ClutterTimeoutPool *) source;
GList *l = pool->timeouts;
/* the pool is ready if the first timeout is ready */
if (l && l->data)
{
ClutterTimeout *timeout = l->data;
return clutter_timeout_prepare (source, timeout, next_timeout);
}
else
{
*next_timeout = -1;
return FALSE;
}
}
static gboolean
clutter_timeout_pool_check (GSource *source)
{
ClutterTimeoutPool *pool = (ClutterTimeoutPool *) source;
GList *l = pool->timeouts;
clutter_threads_enter ();
for (l = pool->timeouts; l; l = l->next)
{
ClutterTimeout *timeout = l->data;
/* since the timeouts are sorted by expiration, as soon
* as we get a check returning FALSE we know that the
* following timeouts are not expiring, so we break as
* soon as possible
*/
if (clutter_timeout_check (source, timeout))
{
timeout->flags |= CLUTTER_TIMEOUT_READY;
pool->ready += 1;
}
else
break;
}
clutter_threads_leave ();
return (pool->ready > 0);
}
static GList *
clutter_timeout_pool_insert_sorted (GList *node, GList *list)
{
GList *l, *last = NULL, *next;
/* Find the first timeout which expires after this one */
for (l = list; l; l = l->next)
{
ClutterTimeout *t_a = node->data;
ClutterTimeout *t_b = l->data;
last = l;
next = l->next;
if (clutter_timeout_sort (t_a, t_b) < 0)
break;
}
/* If we didn't find an entry then add it at the end */
if (!l)
{
node->prev = last;
node->next = NULL;
if (last)
last->next = node;
}
else
{
/* Otherwise insert it before the found entry */
node->next = l;
node->prev = l->prev;
l->prev = node;
if (node->prev)
node->prev->next = node;
}
return node->prev ? list : node;
}
static gboolean
clutter_timeout_pool_dispatch (GSource *source,
GSourceFunc func,
gpointer data)
{
ClutterTimeoutPool *pool = (ClutterTimeoutPool *) source;
GList *l;
/* the main loop might have predicted this, so we repeat the
* check for ready timeouts.
*/
if (!pool->ready)
clutter_timeout_pool_check (source);
clutter_threads_enter ();
/* Iterate by moving the actual start of the list along so that it
* can cope with adds and removes while a timeout is being dispatched
*/
while (pool->timeouts && pool->timeouts->data && pool->ready-- > 0)
{
ClutterTimeout *timeout = pool->timeouts->data;
/* One of the ready timeouts may have been removed during dispatch,
* in which case pool->ready will be wrong, but the ready timeouts
* are always kept at the start of the list so we can stop once
* we've reached the first non-ready timeout
*/
if (!(TIMEOUT_READY (timeout)))
break;
/* Add a reference to the timeout so it can't disappear
* while it's being dispatched
*/
clutter_timeout_ref (timeout);
timeout->flags &= ~CLUTTER_TIMEOUT_READY;
/* Move the list node to a list of dispatched timeouts */
l = pool->timeouts;
if (l->next)
l->next->prev = NULL;
pool->timeouts = l->next;
if (pool->dispatched_timeouts)
pool->dispatched_timeouts->prev = l;
l->prev = NULL;
l->next = pool->dispatched_timeouts;
pool->dispatched_timeouts = l;
if (!clutter_timeout_dispatch (source, timeout))
{
/* The timeout may have already been removed, but nothing
* can be added to the dispatched_timeout list except in this
* function so it will always either be at the head of the
* dispatched list or have been removed
*/
if (pool->dispatched_timeouts &&
pool->dispatched_timeouts->data == timeout)
{
pool->dispatched_timeouts =
g_list_delete_link (pool->dispatched_timeouts,
pool->dispatched_timeouts);
/* Remove the reference that was held by it being in the list */
clutter_timeout_unref (timeout);
}
}
clutter_timeout_unref (timeout);
}
/* Re-insert the dispatched timeouts in sorted order */
while (pool->dispatched_timeouts)
{
GList *next = pool->dispatched_timeouts->next;
pool->timeouts = clutter_timeout_pool_insert_sorted (pool->dispatched_timeouts,
pool->timeouts);
pool->dispatched_timeouts = next;
}
pool->ready = 0;
clutter_threads_leave ();
return TRUE;
}
static void
clutter_timeout_pool_finalize (GSource *source)
{
ClutterTimeoutPool *pool = (ClutterTimeoutPool *) source;
/* force destruction */
g_list_foreach (pool->timeouts, (GFunc) clutter_timeout_free, NULL);
g_list_free (pool->timeouts);
}
/**
* clutter_timeout_pool_new:
* @priority: the priority of the timeout pool. Typically this will
* be #G_PRIORITY_DEFAULT
*
* Creates a new timeout pool source. A timeout pool should be used when
* multiple timeout functions, running at the same priority, are needed and
* the g_timeout_add() API might lead to starvation of the time slice of
* the main loop. A timeout pool allocates a single time slice of the main
* loop and runs every timeout function inside it. The timeout pool is
* always sorted, so that the extraction of the next timeout function is
* a constant time operation.
*
* Inside Clutter, every #ClutterTimeline share the same timeout pool, unless
* the CLUTTER_TIMELINE=no-pool environment variable is set.
*
* Return value: the newly created #ClutterTimeoutPool
*
* Since: 0.4
*/
ClutterTimeoutPool *
clutter_timeout_pool_new (gint priority)
{
ClutterTimeoutPool *pool;
GSource *source;
source = g_source_new (&clutter_timeout_pool_funcs,
sizeof (ClutterTimeoutPool));
if (!source)
return NULL;
if (priority != G_PRIORITY_DEFAULT)
g_source_set_priority (source, priority);
pool = (ClutterTimeoutPool *) source;
pool->next_id = 1;
pool->id = g_source_attach (source, NULL);
g_source_unref (source);
return pool;
}
/**
* clutter_timeout_pool_add:
* @pool: a #ClutterTimeoutPool
* @interval: the time between calls to the function, in milliseconds
* @func: function to call
* @data: data to pass to the function, or %NULL
* @notify: function to call when the timeout is removed, or %NULL
*
* Sets a function to be called at regular intervals, and puts it inside
* the @pool. The function is repeatedly called until it returns %FALSE,
* at which point the timeout is automatically destroyed and the function
* won't be called again. If @notify is not %NULL, the @notify function
* will be called. The first call to @func will be at the end of @interval.
*
* Note that timeout functions may be delayed, due to the processing of other
* event sources. Thus they should not be relied on for precise timing.
* After each call to the timeout function, the time of the next
* timeout is recalculated based on the current time and the given interval
* (it does not try to 'catch up' time lost in delays).
*
* Return value: the ID (greater than 0) of the timeout inside the pool.
* Use clutter_timeout_pool_remove() to stop the timeout.
*
* Since: 0.4
*/
guint
clutter_timeout_pool_add (ClutterTimeoutPool *pool,
guint interval,
GSourceFunc func,
gpointer data,
GDestroyNotify notify)
{
ClutterTimeout *timeout;
guint retval = 0;
timeout = clutter_timeout_new (interval);
retval = timeout->id = pool->next_id++;
timeout->func = func;
timeout->data = data;
timeout->notify = notify;
pool->timeouts = g_list_insert_sorted (pool->timeouts, timeout,
clutter_timeout_sort);
return retval;
}
/**
* clutter_timeout_pool_remove:
* @pool: a #ClutterTimeoutPool
* @id: the id of the timeout to remove
*
* Removes a timeout function with @id from the timeout pool. The id
* is the same returned when adding a function to the timeout pool with
* clutter_timeout_pool_add().
*
* Since: 0.4
*/
void
clutter_timeout_pool_remove (ClutterTimeoutPool *pool,
guint id)
{
GList *l;
if ((l = g_list_find_custom (pool->timeouts, GUINT_TO_POINTER (id),
clutter_timeout_find_by_id)))
{
clutter_timeout_unref (l->data);
pool->timeouts = g_list_delete_link (pool->timeouts, l);
}
else if ((l = g_list_find_custom (pool->dispatched_timeouts, GUINT_TO_POINTER (id),
clutter_timeout_find_by_id)))
{
clutter_timeout_unref (l->data);
pool->dispatched_timeouts = g_list_delete_link (pool->dispatched_timeouts, l);
}
}