diff options
| author | Albrecht Schlosser <albrechts.fltk@online.de> | 2022-01-31 22:27:17 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-01-31 22:27:17 +0100 |
| commit | 29d9e31c51e6c11d6e33abf9bc4551afd9de3836 (patch) | |
| tree | 82dcb5f84dde9bb6cbfe50983094baa12215e57e /src/Fl_Timeout.cxx | |
| parent | cf4a832e6a801b46c38f6236369c74056e8f89ec (diff) | |
Consolidate timeout handling across platforms (#379)
Add Fl_Timeout class
Move platform independent code of Fl::wait() to main part
- basic timeout handling
- Fl::run_checks()
- Fl::run_idle()
- Fl::idle()
- waiting time calculation (next timeout)
- remove platform dependent "screen driver" stuff
Diffstat (limited to 'src/Fl_Timeout.cxx')
| -rw-r--r-- | src/Fl_Timeout.cxx | 456 |
1 files changed, 456 insertions, 0 deletions
diff --git a/src/Fl_Timeout.cxx b/src/Fl_Timeout.cxx new file mode 100644 index 000000000..fed9fbad6 --- /dev/null +++ b/src/Fl_Timeout.cxx @@ -0,0 +1,456 @@ +// +// Timeout support functions for the Fast Light Tool Kit (FLTK). +// +// Author: Albrecht Schlosser +// Copyright 2021-2022 by Bill Spitzak and others. +// +// This library is free software. Distribution and use rights are outlined in +// the file "COPYING" which should have been included with this file. If this +// file is missing or damaged, see the license at: +// +// https://www.fltk.org/COPYING.php +// +// Please see the following page on how to report bugs and issues: +// +// https://www.fltk.org/bugs.php +// + +#include "Fl_Timeout.h" +#include "Fl_System_Driver.H" + +#include <stdio.h> + +/** + \file Fl_Timeout.cxx +*/ + +// static class variables + +Fl_Timeout *Fl_Timeout::free_timeout = 0; +Fl_Timeout *Fl_Timeout::first_timeout = 0; +Fl_Timeout *Fl_Timeout::current_timeout = 0; + +#if FL_TIMEOUT_DEBUG +static int num_timers = 0; // DEBUG +#endif + +// Internal timestamp, used for delta time calculation. +// Note: FLTK naming convention is not used here to avoid potential conflicts +// in the future. + +struct FlTimeStamp { + long sec; + long usec; +}; + +typedef struct FlTimeStamp FlTimeStamp_t; + +// Get a timestamp of type FlTimeStamp. + +// Depending on the system the resolution may be milliseconds or microseconds. +// Under certain conditions (particularly on Windows) the value in member `sec' +// may wrap around and does not represent a real time (maybe runtime of the system). +// Function elapsed_time() below uses this to subtract two timestamps which is always +// a correct delta time with milliseconds or microseconds resolution. + +// To do: Fl::system_driver()->gettime() was implemented for the Forms library and +// has a limited resolution (on Windows: milliseconds). On POSIX platforms it uses +// gettimeofday() with microsecond resolution. +// A new function could use a better resolution on Windows with its multimedia +// timers which requires a new dependency: winmm.lib (dll). This could be a future +// improvement, maybe set as a build option or generally (requires Win95 or 98?). + +static void get_timestamp(FlTimeStamp_t *ts) { + time_t sec; + int usec; + Fl::system_driver()->gettime(&sec, &usec); + ts->sec = (long)sec; + ts->usec = usec; +} + +// Returns 0 and initializes the "previous" timestamp when called for the first time. + +/* + Return the elapsed time since the last call in seconds. + + The first call initializes the internal "previous" timestamp and returns 0. + This must only be called from Fl_Timeout::elapse_timeouts(). + + Todo: remove static variable in this function: previous time should be + maintained in the caller. + + Return: double Elapsed time since the last call +*/ +static double elapsed_time() { + static int first = 1; // initialization + static FlTimeStamp_t prev; // previous timestamp + FlTimeStamp_t now; // current timestamp + double elapsed = 0.0; + get_timestamp(&now); + if (first) { + first = 0; + } else { + elapsed = double((now.sec - prev.sec) + (now.usec - prev.usec) / 1000000.); + } + prev = now; + return elapsed; +} + +/** + Insert a timer entry into the active timer queue. + + The base class Fl_Timeout inserts the timer as the first entry in + the queue of active timers. The default implementation is sufficient + for macOS and Windows. + + Derived classes (e.g. Fl_Timeout) can override this method. + Currently the Posix timeout handling (Unix, Linux) does this so + the timer queue entries are ordered by due time. + + \param[in] t Timer to be inserted (Fl_Timeout or derived class) +*/ +void Fl_Timeout::insert() { + Fl_Timeout **p = (Fl_Timeout **)&first_timeout; + while (*p && (*p)->time <= time) { + p = (Fl_Timeout **)&((*p)->next); + } + next = *p; + *p = this; +} + +/** + Returns whether the given timeout is active. + + This returns whether a timeout handler already exists in the queue + of active timers. + + If \p data == NULL only the Fl_Timeout_Handler \p cb must match to return + true, otherwise \p data must also match. + + \note It is a restriction that there is no way to look for a timeout whose + \p data is NULL (zero). Therefore using 0 (zero, NULL) as the timeout + \p data value is discouraged, unless you're sure that you will never + need to use <kbd>Fl::has_timeout(callback, (void *)0);</kbd>. + + Implements Fl::has_timeout(Fl_Timeout_Handler cb, void *data) + + \param[in] cb Timer callback (must match) + \param[in] data Wildcard if NULL, must match otherwise + + \returns whether the timer was found in the queue + \retval 0 not found + \retval 1 found +*/ + +int Fl_Timeout::has_timeout(Fl_Timeout_Handler cb, void *data) { + for (Fl_Timeout *t = first_timeout; t; t = t->next) { + if (t->callback == cb && t->data == data) + return 1; + } + return 0; +} + +void Fl_Timeout::add_timeout(double time, Fl_Timeout_Handler cb, void *data) { + elapse_timeouts(); + Fl_Timeout *t = get(time, cb, data); + t->Fl_Timeout::insert(); +} + +void Fl_Timeout::repeat_timeout(double time, Fl_Timeout_Handler cb, void *data) { + elapse_timeouts(); + Fl_Timeout *t = (Fl_Timeout *)get(time, cb, data); + Fl_Timeout *cur = current_timeout; + if (cur) { + t->time += cur->time; // was: missed_timeout_by (always <= 0.0) + } + if (t->time < 0.0) + t->time = 0.001; // at least 1 ms + t->insert(); +} + +/** + Remove a timeout callback. It is harmless to remove a timeout + callback that no longer exists. + + \note This version removes all matching timeouts, not just the first one. + This may change in the future. + + Implements Fl::remove_timeout(Fl_Timeout_Handler cb, void *data) +*/ +void Fl_Timeout::remove_timeout(Fl_Timeout_Handler cb, void *data) { + for (Fl_Timeout** p = &first_timeout; *p;) { + Fl_Timeout* t = *p; + if (t->callback == cb && (t->data == data || !data)) { + *p = t->next; + t->next = free_timeout; + free_timeout = t; + } else { + p = &(t->next); + } + } +} + +/** + Remove the timeout from the active timer queue and push it onto + the stack of currently running callbacks. + + This becomes the current() timeout which can be used in + Fl::repeat_timeout(). + + \see Fl_Timeout::current() +*/ +void Fl_Timeout::make_current() { + // printf("[%4d] Fl_Timeout::make_current(%p)\n", __LINE__, this); + // remove the timer entry from the active timer queue + for (Fl_Timeout** p = &first_timeout; *p;) { + Fl_Timeout* t = *p; + if (t == this) { + *p = t->next; + // push it to the current timer stack + t->next = current_timeout; + current_timeout = t; + break; + } else { + p = &(t->next); + } + } +} + +/** + Remove the top-most timeout from the stack of currently running + timeout callbacks and insert it into the list of free timers. + + This should always return a non-NULL value, otherwise there's a bug + in the library. Typical code in the library would look like: + \code + // The timeout \p Fl_Timeout *t has exired, run its callback + t->make_current(); + (t->callback)(t->data); + t->release(); + \endcode + + \return Fl_Timeout* current timeout or NULL +*/ +void Fl_Timeout::release() { + Fl_Timeout *t = current_timeout; + if (t) { + + // The first timer in the "current" list *should* be 'this' but we + // check it to be sure. Issue an error message which should never appear. + // If it would happen we'd remove the wrong timer from the current timer + // list. This is not good but it doesn't really do harm. + + if (t != this) { + Fl::error("*** Fl_Timeout::release() *** timer t (%p) != this (%p)\n", t, this); + } + + // remove the timer from the list + current_timeout = t->next; + } + // put the timer into the list of free timers + t->next = free_timeout; + free_timeout = t; +} + +/** + Returns the first (top-most) timeout from the current timeout stack. + + This returns a pointer to the timeout but does not remove it from the + list of current timeouts. This should be the timeout that is currently + executing its callback. + + \return Fl_Timeout* The current timeout whose callback is running. + \retval NULL if no callback is currently running. +*/ +Fl_Timeout *Fl_Timeout::current() { + return current_timeout; +} + +/** + Get an Fl_Timeout instance for further handling. + + The timer object will be initialized with the input parameters + as given by Fl::add_timeout() or Fl::repeat_timeout(). + + Fl_Timeout objects are maintained in three queues: + - active timer queue + - list (stack, i.e. LIFO) of currently executing timer callbacks + - free timer entries. + + When the FLTK program is launched all queues are empty. Whenever + a new timer object is required the get() method is called and a timer + object is either found in the queue of free timer entries or a new + timer object is created (operator new). + + Active timer entries are inserted into the "active timer queue" until + they expire and their callback is called. + + Before the callback is called the timer entry is inserted into the list + of current timers, i.e. it becomes the Fl_Timeout::current() timeout. + This can be used in Fl::repeat_timeout() to find out if and how long the + current timeout has been delayed. + + When a timer is no longer used it is popped from the \p current list + and inserted into the "free timer" list so it can be reused later. + + Timer queue entries are never returned to the system, there's no garbage + collection. The total number of timer objects is determined by the + largest number of concurrently active timers. + + \param[in] time requested delta time + \param[in] cb timer callback + \param[in] data userdata for timer callback + + \return Fl_Timeout* Timer entry + + \see Fl::add_timeout(), Fl::repeat_timeout() +*/ + +Fl_Timeout *Fl_Timeout::get(double time, Fl_Timeout_Handler cb, void *data) { + + Fl_Timeout *t = (Fl_Timeout *)free_timeout; + if (t) { + free_timeout = t->next; + t->next = 0; + } else { + t = new Fl_Timeout; +#if FL_TIMEOUT_DEBUG + num_timers++; // DEBUG: count allocated timers +#endif + } + + memset(t, 0, sizeof(*t)); + t->delay(time); + t->callback = cb; + t->data = data; + return t; +} + +/** + Elapse all timers w/o calling their callbacks. + + All timer values are adjusted by the delta time since the last call. + This method does \b NOT call timer callbacks if timers are expired. + + This must be called before new timers are added to the timer queue to make + sure that the next timer decrement does not count down too much time. + + \see Fl_Timeout::do_timeouts() +*/ +void Fl_Timeout::elapse_timeouts() { + double elapsed = elapsed_time(); + // printf("elapse_timeouts: elapsed = %9.6f\n", double(elapsed)/1000000.); + + if (elapsed > 0.0) { + + // active timers + + for (Fl_Timeout* t = first_timeout; t; t = t->next) { + t->time -= elapsed; + } + + // "current" timers, i.e. timers being serviced + + for (Fl_Timeout* t = current_timeout; t; t = t->next) { + t->time -= elapsed; + } + } +} + +/** + Elapse timers and call their callbacks if any timers are expired. +*/ +void Fl_Timeout::do_timeouts() { + if (first_timeout) { + Fl_Timeout::elapse_timeouts(); + Fl_Timeout *t; + while ((t = Fl_Timeout::first_timeout)) { + if (t->time > 0) break; + // make this timeout the "current" timeout + t->make_current(); + // now it is safe for the callback to do add_timeout: + t->callback(t->data); + // release the timer entry + t->release(); + + // Elapse timers (again) because the callback may have used a + // significant amount of time. This is optional though. + + Fl_Timeout::elapse_timeouts(); + } + } +} + +/** + Returns the delay in seconds until the next timer expires, + limited by \p ttw. + + This function calculates the time to wait for the FLTK event queue + processing, depending on the given value \p ttw. + + If at least one timer is active and its timeout value is smaller than + \p ttw then this value is returned. Fl::wait() will wait no longer than + until the next timer expires. + + If no timer is active this returns the input value \p ttw unchanged. + + If at least one timer is expired this returns 0.0 so the event processing + does not wait. + + \param[in] ttw time to wait from Fl::wait() etc. (upper limit) + + \return delay until next timeout or 0.0 (see description) +*/ +double Fl_Timeout::time_to_wait(double ttw) { + Fl_Timeout *t = first_timeout; + if (!t) return ttw; + double tdelay = t->delay(); +if (tdelay < 0.0) + return 0.0; + if (tdelay < ttw) + return tdelay; + return ttw; +} + + +// Write some statistics to stdout for debugging + +#if FL_TIMEOUT_DEBUG + +void Fl_Timeout::debug(int level) { + + printf("\nFl_Timeout::debug: number of allocated timers = %d\n", num_timers); + + int active = 0; + Fl_Timeout *t = first_timeout; + while (t) { + active++; + t = t->next; + } + + int current = 0; + t = current_timeout; + while (t) { + current++; + t = t->next; + } + + int free = 0; + t = free_timeout; + while (t) { + free++; + t = t->next; + } + + printf("Fl_Timeout::debug: active: %d, current: %d, free: %d\n\n", active, current, free); + + t = first_timeout; + int n = 0; + while (t) { + printf("Active timer %3d: time = %10.6f sec\n", n+1, t->delay()); + t = t->next; + n++; + } +} // Fl_Timeout::debug(int) + +#endif // FL_TIMEOUT_DEBUG |
