Get rid of the timers infrastructure

It is not needed with a pure state machine.
Note that drag scrolling still has to be ported from using
timers.
This commit is contained in:
Kovid Goyal 2017-09-13 14:17:21 +05:30
parent f3cd0a4966
commit c683725434
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
8 changed files with 75 additions and 383 deletions

View File

@ -13,7 +13,7 @@ from .constants import (
)
from .fast_data_types import (
GLFW_MOUSE_BUTTON_1, GLFW_PRESS, GLFW_REPEAT, ChildMonitor,
Timers as _Timers, destroy_global_data, destroy_sprite_map,
destroy_global_data, destroy_sprite_map,
glfw_post_empty_event, layout_sprite_map
)
from .fonts.render import render_cell_wrapper, set_font_family
@ -25,33 +25,6 @@ from .tabs import SpecialWindow, TabManager
from .utils import safe_print
class Timers(_Timers):
def __init__(self):
_Timers.__init__(self)
self.timer_hash = {}
def add(self, delay, timer, *args):
# Needed because bound methods are recreated on every access
timer = self.timer_hash.setdefault(timer, timer)
return _Timers.add(self, delay, timer, args) if args else _Timers.add(self, delay, timer)
def add_if_before(self, delay, timer, *args):
# Needed because bound methods are recreated on every access
timer = self.timer_hash.setdefault(timer, timer)
return _Timers.add_if_before(self, delay, timer, *args)
def add_if_missing(self, delay, timer, *args):
# Needed because bound methods are recreated on every access
timer = self.timer_hash.setdefault(timer, timer)
return _Timers.add_if_missing(self, delay, timer, *args)
def remove(self, timer):
# Needed because bound methods are recreated on every access
timer = self.timer_hash.setdefault(timer, timer)
return _Timers.remove_event(self, timer)
class DumpCommands: # {{{
def __init__(self, args):
@ -90,10 +63,9 @@ class Boss:
self.window_is_focused = True
self.glfw_window_title = None
self.shutting_down = False
self.ui_timers = Timers()
self.child_monitor = ChildMonitor(
opts.repaint_delay / 1000.0, glfw_window.window_id(),
self.on_child_death, self.ui_timers,
self.on_child_death,
DumpCommands(args) if args.dump_commands or args.dump_bytes else None)
set_boss(self)
self.current_font_size = opts.font_size

View File

@ -83,6 +83,15 @@ set_thread_name(const char *name) {
#define INCREF_CHILD(x) XREF_CHILD(x, Py_INCREF)
#define DECREF_CHILD(x) XREF_CHILD(x, Py_DECREF)
// The max time (in secs) to wait for events from the window system
// before ticking over the main loop. Negative values mean wait forever.
static double maximum_wait = -1.0;
static inline void
set_maximum_wait(double val) {
if (val >= 0 && (val < maximum_wait || maximum_wait < 0)) maximum_wait = val;
}
static void
handle_signal(int sig_num) {
int save_err = errno;
@ -112,12 +121,12 @@ self_pipe(int fds[2]) {
static PyObject *
new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
ChildMonitor *self;
PyObject *dump_callback, *death_notify, *timers, *wid;
PyObject *dump_callback, *death_notify, *wid;
int ret;
double repaint_delay;
if (created) { PyErr_SetString(PyExc_RuntimeError, "Can have only a single ChildMonitor instance"); return NULL; }
if (!PyArg_ParseTuple(args, "dOOOO", &repaint_delay, &wid, &death_notify, &timers, &dump_callback)) return NULL;
if (!PyArg_ParseTuple(args, "dOOO", &repaint_delay, &wid, &death_notify, &dump_callback)) return NULL;
glfw_window_id = PyLong_AsVoidPtr(wid);
created = true;
if ((ret = pthread_mutex_init(&children_lock, NULL)) != 0) {
@ -133,7 +142,6 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
self = (ChildMonitor *)type->tp_alloc(type, 0);
if (self == NULL) return PyErr_NoMemory();
self->death_notify = death_notify; Py_INCREF(death_notify);
self->timers = (Timers*)timers; Py_INCREF(timers);
if (dump_callback != Py_None) {
self->dump_callback = dump_callback; Py_INCREF(dump_callback);
parse_func = parse_worker_dump;
@ -151,7 +159,6 @@ dealloc(ChildMonitor* self) {
pthread_mutex_destroy(&children_lock);
Py_CLEAR(self->dump_callback);
Py_CLEAR(self->death_notify);
Py_CLEAR(self->timers);
Py_TYPE(self)->tp_free((PyObject*)self);
while (remove_queue_count) {
remove_queue_count--;
@ -326,7 +333,7 @@ parse_input(ChildMonitor *self) {
DECREF_CHILD(scratch[i]);
}
if (!parse_needed) {
timers_add_if_before(self->timers, self->repaint_delay - time_since_last_parse, Py_None, NULL);
set_maximum_wait(self->repaint_delay - time_since_last_parse);
}
}
@ -438,7 +445,7 @@ cursor_width(double w, bool vert) {
extern void cocoa_update_title(PyObject*);
static inline void
render_cursor(ChildMonitor *self, Window *w, double now) {
render_cursor(Window *w, double now) {
ScreenRenderData *rd = &w->render_data;
if (rd->screen->scrolled_by || ! screen_is_cursor_visible(rd->screen)) return;
double time_since_start_blink = now - global_state.cursor_blink_zero_time;
@ -449,8 +456,9 @@ render_cursor(ChildMonitor *self, Window *w, double now) {
int d = (int)(OPT(cursor_blink_interval) * 1000);
int n = t / d;
do_draw_cursor = n % 2 == 0 ? true : false;
double delay = MAX(0, ((n + 1) * d / 1000) - time_since_start_blink);
timers_add_if_before(self->timers, delay, Py_None, NULL);
double bucket = (n + 1) * d;
double delay = (bucket / 1000.0) - time_since_start_blink;
set_maximum_wait(delay);
}
if (do_draw_cursor) {
Cursor *cursor = rd->screen->cursor;
@ -469,7 +477,7 @@ render_cursor(ChildMonitor *self, Window *w, double now) {
}
static inline bool
render(ChildMonitor *self, double *timeout, double now) {
render(ChildMonitor *self, double now) {
double time_since_last_render = now - last_render_at;
if (time_since_last_render > self->repaint_delay) {
draw_borders();
@ -481,10 +489,16 @@ render(ChildMonitor *self, double *timeout, double now) {
for (size_t i = 0; i < tab->num_windows; i++) {
Window *w = tab->windows + i;
#define WD w->render_data
if (w->visible && WD.screen) draw_cells(WD.vao_idx, WD.xstart, WD.ystart, WD.dx, WD.dy, WD.screen);
if (w->visible && WD.screen) {
draw_cells(WD.vao_idx, WD.xstart, WD.ystart, WD.dx, WD.dy, WD.screen);
if (WD.screen->start_visual_bell_at != 0) {
double bell_left = global_state.opts.visual_bell_duration - (now - WD.screen->start_visual_bell_at);
set_maximum_wait(bell_left);
}
}
}
Window *w = tab->windows + tab->active_window;
if (w->visible && WD.screen) render_cursor(self, w, now);
if (w->visible && WD.screen) render_cursor(w, now);
if (w->title && w->title != global_state.application_title) {
global_state.application_title = w->title;
glfwSetWindowTitle(glfw_window_id, PyUnicode_AsUTF8(w->title));
@ -497,7 +511,7 @@ render(ChildMonitor *self, double *timeout, double now) {
glfwSwapBuffers(glfw_window_id);
last_render_at = now;
} else {
*timeout = self->repaint_delay - time_since_last_render;
set_maximum_wait(self->repaint_delay - time_since_last_render);
}
return true;
}
@ -553,20 +567,16 @@ cm_thread_write(PyObject UNUSED *self, PyObject *args) {
static PyObject*
main_loop(ChildMonitor *self) {
#define main_loop_doc "The main thread loop"
double timeout = 0, t;
while (!glfwWindowShouldClose(glfw_window_id)) {
double now = monotonic();
if (!render(self, &timeout, now)) break;
maximum_wait = -1;
if (!render(self, now)) break;
if (global_state.mouse_visible && OPT(mouse_hide_wait) > 0 && now - global_state.last_mouse_activity_at > OPT(mouse_hide_wait)) {
glfwSetInputMode(glfw_window_id, GLFW_CURSOR, GLFW_CURSOR_HIDDEN);
global_state.mouse_visible = false;
}
t = timers_timeout(self->timers);
timeout = MIN(timeout, t);
if (timeout < 0) glfwWaitEvents();
else if (timeout > 0) glfwWaitEventsTimeout(timeout);
timers_call(self->timers);
if (maximum_wait < 0) glfwWaitEvents();
else if (maximum_wait > 0) glfwWaitEventsTimeout(maximum_wait);
parse_input(self);
}
if (PyErr_Occurred()) return NULL;

View File

@ -13,6 +13,44 @@
#include <gperftools/profiler.h>
#endif
/* To millisecond (10^-3) */
#define SEC_TO_MS 1000
/* To microseconds (10^-6) */
#define MS_TO_US 1000
#define SEC_TO_US (SEC_TO_MS * MS_TO_US)
/* To nanoseconds (10^-9) */
#define US_TO_NS 1000
#define MS_TO_NS (MS_TO_US * US_TO_NS)
#define SEC_TO_NS (SEC_TO_MS * MS_TO_NS)
/* Conversion from nanoseconds */
#define NS_TO_MS (1000 * 1000)
#define NS_TO_US (1000)
#ifdef __APPLE__
#include <mach/mach_time.h>
static mach_timebase_info_data_t timebase = {0};
static inline double monotonic_() {
return ((double)(mach_absolute_time() * timebase.numer) / timebase.denom)/SEC_TO_NS;
}
#else
#include <time.h>
static inline double monotonic_() {
struct timespec ts = {0};
#ifdef CLOCK_HIGHRES
clock_gettime(CLOCK_HIGHRES, &ts);
#elif CLOCK_MONOTONIC_RAW
clock_gettime(CLOCK_MONOTONIC_RAW, &ts);
#else
clock_gettime(CLOCK_MONOTONIC, &ts);
#endif
return (((double)ts.tv_nsec) / SEC_TO_NS) + (double)ts.tv_sec;
}
#endif
double monotonic() { return monotonic_(); }
static PyObject*
wcwidth_wrap(PyObject UNUSED *self, PyObject *chr) {
@ -87,7 +125,6 @@ static struct PyModuleDef module = {
extern int init_LineBuf(PyObject *);
extern int init_HistoryBuf(PyObject *);
extern int init_Cursor(PyObject *);
extern int init_Timers(PyObject *);
extern int init_ChildMonitor(PyObject *);
extern int init_Line(PyObject *);
extern int init_ColorProfile(PyObject *);
@ -112,13 +149,15 @@ PyInit_fast_data_types(void) {
m = PyModule_Create(&module);
if (m == NULL) return NULL;
#ifdef __APPLE__
mach_timebase_info(&timebase);
#endif
if (m != NULL) {
if (!init_LineBuf(m)) return NULL;
if (!init_HistoryBuf(m)) return NULL;
if (!init_Line(m)) return NULL;
if (!init_Cursor(m)) return NULL;
if (!init_Timers(m)) return NULL;
if (!init_ChildMonitor(m)) return NULL;
if (!init_ColorProfile(m)) return NULL;
if (!init_Screen(m)) return NULL;

View File

@ -265,21 +265,10 @@ typedef struct {
PyObject *args;
} TimerEvent;
typedef struct {
PyObject_HEAD
TimerEvent *events, *buf1, *buf2;
size_t capacity;
size_t count;
bool in_call;
} Timers;
PyTypeObject Timers_Type;
typedef struct {
PyObject_HEAD
PyObject *dump_callback, *update_screen, *death_notify;
Timers *timers;
double repaint_delay;
unsigned int count;
bool shutting_down;
@ -317,11 +306,6 @@ void cursor_reset_display_attrs(Cursor*);
void set_sprite_position(Cell *cell, Cell *previous_cell);
double monotonic();
double timers_timeout(Timers*);
void timers_call(Timers*);
bool timers_add(Timers *self, double delay, bool, PyObject *callback, PyObject *args);
bool timers_add_if_missing(Timers *self, double delay, PyObject *callback, PyObject *args);
bool timers_add_if_before(Timers *self, double delay, PyObject *callback, PyObject *args);
PyObject* cm_thread_write(PyObject *self, PyObject *args);
bool set_iutf8(int, bool);

View File

@ -1061,7 +1061,7 @@ screen_set_cursor(Screen *self, unsigned int mode, uint8_t secondary) {
shape = 0; blink = false;
if (mode > 0) {
blink = mode % 2;
shape = (mode < 3) ? CURSOR_BLOCK : (mode < 5) ? CURSOR_UNDERLINE : (mode < 7) ? CURSOR_BEAM : 0;
shape = (mode < 3) ? CURSOR_BLOCK : (mode < 5) ? CURSOR_UNDERLINE : (mode < 7) ? CURSOR_BEAM : NO_CURSOR_SHAPE;
}
if (shape != self->cursor->shape || blink != self->cursor->blink) {
self->cursor->shape = shape; self->cursor->blink = blink;

View File

@ -1,285 +0,0 @@
/*
* timers.c
* Copyright (C) 2017 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#ifdef __APPLE__
#define EXTRA_INIT mach_timebase_info(&timebase);
#endif
#include "data-types.h"
#include <stdlib.h>
/* To millisecond (10^-3) */
#define SEC_TO_MS 1000
/* To microseconds (10^-6) */
#define MS_TO_US 1000
#define SEC_TO_US (SEC_TO_MS * MS_TO_US)
/* To nanoseconds (10^-9) */
#define US_TO_NS 1000
#define MS_TO_NS (MS_TO_US * US_TO_NS)
#define SEC_TO_NS (SEC_TO_MS * MS_TO_NS)
/* Conversion from nanoseconds */
#define NS_TO_MS (1000 * 1000)
#define NS_TO_US (1000)
#ifdef __APPLE__
#include <mach/mach_time.h>
static mach_timebase_info_data_t timebase = {0};
static inline double monotonic_() {
return ((double)(mach_absolute_time() * timebase.numer) / timebase.denom)/SEC_TO_NS;
}
#else
#include <time.h>
static inline double monotonic_() {
struct timespec ts = {0};
#ifdef CLOCK_HIGHRES
clock_gettime(CLOCK_HIGHRES, &ts);
#elif CLOCK_MONOTONIC_RAW
clock_gettime(CLOCK_MONOTONIC_RAW, &ts);
#else
clock_gettime(CLOCK_MONOTONIC, &ts);
#endif
return (((double)ts.tv_nsec) / SEC_TO_NS) + (double)ts.tv_sec;
}
#endif
double monotonic() { return monotonic_(); }
static PyObject *
new(PyTypeObject *type, PyObject UNUSED *args, PyObject UNUSED *kwds) {
Timers *self;
self = (Timers *)type->tp_alloc(type, 0);
self->capacity = 1024;
self->count = 0;
self->in_call = false;
self->buf1 = (TimerEvent*)PyMem_Calloc(2 * self->capacity, sizeof(TimerEvent));
if (self->buf1 == NULL) { Py_CLEAR(self); return PyErr_NoMemory(); }
self->events = self->buf1;
self->buf2 = self->buf1 + self->capacity;
return (PyObject*) self;
}
static void
dealloc(Timers* self) {
if (self->events) {
for (size_t i = 0; i < self->count; i++) {
Py_CLEAR(self->events[i].callback); Py_CLEAR(self->events[i].args);
}
PyMem_Free(self->buf1); self->events = NULL;
}
Py_TYPE(self)->tp_free((PyObject*)self);
}
static int
compare_events(const void *a, const void *b) {
double av = ((TimerEvent*)(a))->at, bv = ((TimerEvent*)(b))->at;
return av > bv ? 1 : (av == bv ? 0 : -1);
}
static inline bool
_add(Timers *self, double at, PyObject *callback, PyObject *args) {
size_t i;
if (self->count >= self->capacity) {
PyErr_SetString(PyExc_ValueError, "Too many timers");
return false;
}
i = self->count++;
self->events[i].at = at; self->events[i].callback = callback; self->events[i].args = args;
Py_INCREF(callback); Py_XINCREF(args);
qsort(self->events, self->count, sizeof(TimerEvent), compare_events);
return true;
}
static inline bool
timers_add_(Timers *self, double at, bool update, PyObject *callback, PyObject *args) {
if (self->in_call) { PyErr_SetString(PyExc_ValueError, "Cannot add timers in a timer handler"); return false; }
for (size_t i = 0; i < self->count; i++) {
if (self->events[i].callback == callback) {
self->events[i].at = update ? at : MIN(at, self->events[i].at);
Py_CLEAR(self->events[i].args);
self->events[i].args = args;
Py_XINCREF(args);
qsort(self->events, self->count, sizeof(TimerEvent), compare_events);
return true;
}
}
return _add(self, at, callback, args);
}
bool
timers_add(Timers *self, double delay, bool update, PyObject *callback, PyObject *args) {
return timers_add_(self, monotonic_() + delay, update, callback, args);
}
static PyObject *
add(Timers *self, PyObject *fargs) {
#define add_doc "add(delay, callback, args) -> Add callback, replacing it if it already exists"
PyObject *callback, *args = NULL;
double delay;
if (!PyArg_ParseTuple(fargs, "dO|O", &delay, &callback, &args)) return NULL;
if (!timers_add(self, delay, true, callback, args)) return NULL;
Py_RETURN_NONE;
}
bool
timers_add_if_before(Timers *self, double delay, PyObject *callback, PyObject *args) {
double at = monotonic_() + delay;
if (self->in_call) { PyErr_SetString(PyExc_ValueError, "Cannot add timers in a timer handler"); return false; }
for (size_t i = 0; i < self->count; i++) {
if (self->events[i].at < at) {
return true;
}
}
return timers_add_(self, at, true, callback, args);
}
static PyObject *
add_if_before(Timers *self, PyObject *fargs) {
#define add_if_before_doc "add_if_before(delay, callback, args) -> Add callback, unless another callback scheduled before this one already exists."
PyObject *callback, *args = NULL;
double delay;
if (!PyArg_ParseTuple(fargs, "dO|O", &delay, &callback, &args)) return NULL;
if (!timers_add_if_before(self, delay, callback, args)) return NULL;
Py_RETURN_NONE;
}
bool
timers_add_if_missing(Timers *self, double delay, PyObject *callback, PyObject *args) {
if (self->in_call) { PyErr_SetString(PyExc_ValueError, "Cannot add timers in a timer handler"); return false; }
for (size_t i = 0; i < self->count; i++) {
if (self->events[i].callback == callback) {
return true;
}
}
return _add(self, monotonic_() + delay, callback, args);
}
static PyObject *
add_if_missing(Timers *self, PyObject *fargs) {
#define add_if_missing_doc "add_if_missing(delay, callback, args) -> Add callback, unless it already exists"
PyObject *callback, *args = NULL;
double delay;
if (!PyArg_ParseTuple(fargs, "dO|O", &delay, &callback, &args)) return NULL;
if (!timers_add_if_missing(self, delay, callback, args)) return NULL;
Py_RETURN_NONE;
}
static PyObject *
remove_event(Timers *self, PyObject *callback) {
#define remove_event_doc "remove(callback) -> Remove the event with the specified callback, if present"
TimerEvent *other = self->events == self->buf1 ? self->buf2 : self->buf1;
size_t i, j;
if (self->in_call) { PyErr_SetString(PyExc_ValueError, "Cannot remove timers in a timer handler"); return false; }
for (i = 0, j = 0; i < self->count; i++) {
if (self->events[i].callback != callback) {
other[j].callback = self->events[i].callback; other[j].at = self->events[i].at; other[j].args = self->events[i].args;
j++;
} else {
Py_CLEAR(self->events[i].callback); Py_CLEAR(self->events[i].args);
}
}
self->events = other;
self->count = j;
Py_RETURN_NONE;
}
double
timers_timeout(Timers *self) {
if (self->count < 1) return -1;
double ans = self->events[0].at - monotonic_();
return MAX(0, ans);
}
static PyObject *
timeout(Timers *self) {
#define timeout_doc "timeout() -> The time in seconds until the next event"
if (self->count < 1) { Py_RETURN_NONE; }
double ans = self->events[0].at - monotonic_();
return PyFloat_FromDouble(MAX(0, ans));
}
void
timers_call(Timers *self) {
if (self->count < 1) return;
TimerEvent *other = self->events == self->buf1 ? self->buf2 : self->buf1;
double now = monotonic_();
size_t i, j;
self->in_call = true;
bool needs_sort = false;
for (i = 0, j = 0; i < self->count; i++) {
bool remove = false;
if (self->events[i].at <= now) { // expired, call it
/* PyObject_Print(self->events[i].callback, stdout, 1); */
/* printf("\n"); */
remove = true;
if (self->events[i].callback != Py_None) {
PyObject *ret = self->events[i].args ? PyObject_CallObject(self->events[i].callback, self->events[i].args) : PyObject_CallFunctionObjArgs(self->events[i].callback, NULL);
if (ret == NULL) PyErr_Print();
else {
if (ret != Py_None && PyFloat_Check(ret)) {
self->events[i].at = monotonic_() + PyFloat_AS_DOUBLE(ret);
remove = false;
needs_sort = true;
}
Py_DECREF(ret);
}
}
}
if (remove) {
Py_CLEAR(self->events[i].callback); Py_CLEAR(self->events[i].args);
} else {
other[j].callback = self->events[i].callback; other[j].at = self->events[i].at; other[j].args = self->events[i].args;
j++;
}
}
self->events = other;
self->count = j;
self->in_call = false;
if (needs_sort) qsort(self->events, self->count, sizeof(TimerEvent), compare_events);
}
static PyObject *
call(Timers *self) {
#define call_doc "call() -> Dispatch all expired events"
timers_call(self);
Py_RETURN_NONE;
}
// Boilerplate {{{
static PyMethodDef methods[] = {
METHOD(add, METH_VARARGS)
METHOD(add_if_missing, METH_VARARGS)
METHOD(add_if_before, METH_VARARGS)
METHOD(remove_event, METH_O)
METHOD(timeout, METH_NOARGS)
METHOD(call, METH_NOARGS)
{NULL} /* Sentinel */
};
PyTypeObject Timers_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "fast_data_types.Timers",
.tp_basicsize = sizeof(Timers),
.tp_dealloc = (destructor)dealloc,
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
.tp_doc = "Timers",
.tp_methods = methods,
.tp_new = new,
};
INIT_TYPE(Timers)
// }}}

View File

@ -39,10 +39,6 @@ window_counter = count()
next(window_counter)
def clear_visual_bell():
pass
class Window:
def __init__(self, tab, child, opts, args):
@ -59,7 +55,6 @@ class Window:
self.title = appname
self.is_visible_in_layout = True
self.child, self.opts = child, opts
self.start_visual_bell_at = None
self.screen = Screen(self, 24, 80, opts.scrollback_lines)
self.char_grid = CharGrid(self.screen, opts)
self.current_pty_size = None
@ -125,9 +120,6 @@ class Window:
boss = get_boss()
boss.request_attention()
glfw_post_empty_event()
if self.opts.visual_bell_duration > 0:
# ensure that the UI thread wakes up and clears the visual bell
boss.ui_timers.add(self.opts.visual_bell_duration + 0.01, clear_visual_bell)
def use_utf8(self, on):
get_boss().child_monitor.set_iutf8(self.window_id, on)

View File

@ -7,7 +7,7 @@ from unittest import skipIf
from kitty.config import build_ansi_color_table, defaults
from kitty.fast_data_types import (
REVERSE, ColorProfile, Cursor as C, HistoryBuf, LineBuf, Timers,
REVERSE, ColorProfile, Cursor as C, HistoryBuf, LineBuf,
sprite_map_set_layout, sprite_map_set_limits, sprite_position_for
)
from kitty.utils import sanitize_title, wcwidth
@ -367,23 +367,3 @@ class TestDataTypes(BaseTest):
a = []
hb.as_ansi(a.append)
self.ae(a, ['\x1b[0m' + str(hb.line(i)) + '\n' for i in range(hb.count - 1, -1, -1)])
def test_timers(self):
t = Timers()
a = []
t.add(10, a.append, (1,))
t.add(0, a.append, (2,))
t.add_if_missing(20, a.append, (3,))
self.ae(t.timeout(), 0)
t.call(), t.call()
self.ae(a, [2])
del a[:]
t.add(200, a.append, (4,))
d = t.timeout()
self.assertGreater(d, 8)
self.assertLess(d, 10.1)
t.call()
self.ae(a, [])
t.add(0, a.append, (5,))
t.call()
self.ae(a, [5])