File Coverage

c_eventloop.c
Criterion Covered Total %
statement 90 125 72.0
branch 20 36 55.5
condition n/a
subroutine n/a
pod n/a
total 110 161 68.3


line stmt bran cond sub pod time code
1             /*
2             * Timer insertion is an O(n) operation; in a real world eventloop based on a
3             * heap insertion would be O(log N).
4             */
5             #include
6             #include
7              
8             #include "duktape.h"
9             #include "c_eventloop.h"
10             #include "pl_util.h"
11              
12             #if !defined(DUKTAPE_EVENTLOOP_DEBUG)
13             #define DUKTAPE_EVENTLOOP_DEBUG 0 /* set to 1 to debug with printf */
14             #endif
15              
16             #define TIMERS_SLOT_NAME "eventTimers"
17             #define MIN_DELAY 1.0
18             #define MIN_WAIT 1.0
19             #define MAX_WAIT 60000.0
20             #define MAX_EXPIRIES 10
21             #define MAX_TIMERS 4096 /* this is quite excessive for embedded use, but good for testing */
22              
23             typedef struct {
24             int64_t id; /* numeric ID (returned from e.g. setTimeout); zero if unused */
25             double target; /* next target time */
26             double delay; /* delay/interval */
27             int oneshot; /* oneshot=1 (setTimeout), repeated=0 (setInterval) */
28             int removed; /* timer has been requested for removal */
29              
30             /* The callback associated with the timer is held in the "global stash",
31             * in .eventTimers[String(id)]. The references must be deleted
32             * when a timer struct is deleted.
33             */
34             } ev_timer;
35              
36             /* Active timers. Dense list, terminates to end of list or first unused timer.
37             * The list is sorted by 'target', with lowest 'target' (earliest expiry) last
38             * in the list. When a timer's callback is being called, the timer is moved
39             * to 'timer_expiring' as it needs special handling should the user callback
40             * delete that particular timer.
41             */
42             static ev_timer timer_list[MAX_TIMERS];
43             static ev_timer timer_expiring;
44             static int timer_count; /* last timer at timer_count - 1 */
45             static int64_t timer_next_id = 1;
46              
47 2000372           static ev_timer *find_nearest_timer(void) {
48             /* Last timer expires first (list is always kept sorted). */
49 2000372 100         if (timer_count <= 0) {
50 2000359           return NULL;
51             }
52 13           return timer_list + timer_count - 1;
53             }
54              
55             /* Bubble last timer on timer list backwards until it has been moved to
56             * its proper sorted position (based on 'target' time).
57             */
58 11           static void bubble_last_timer(void) {
59             int i;
60 11           int n = timer_count;
61             ev_timer *t;
62             ev_timer tmp;
63              
64 11 100         for (i = n - 1; i > 0; i--) {
65             /* Timer to bubble is at index i, timer to compare to is
66             * at i-1 (both guaranteed to exist).
67             */
68 4           t = timer_list + i;
69 4 50         if (t->target <= (t-1)->target) {
70             /* 't' expires earlier than (or same time as) 't-1', so we're done. */
71 4           break;
72             } else {
73             /* 't' expires later than 't-1', so swap them and repeat. */
74 0           memcpy((void *) &tmp, (void *) (t - 1), sizeof(ev_timer));
75 0           memcpy((void *) (t - 1), (void *) t, sizeof(ev_timer));
76 0           memcpy((void *) t, (void *) &tmp, sizeof(ev_timer));
77             }
78             }
79 11           }
80              
81 2000372           static void expire_timers(duk_context *ctx) {
82             ev_timer *t;
83 2000372           int sanity = MAX_EXPIRIES;
84             double now;
85             int rc;
86              
87             /* Because a user callback can mutate the timer list (by adding or deleting
88             * a timer), we expire one timer and then rescan from the end again. There
89             * is a sanity limit on how many times we do this per expiry round.
90             */
91              
92 2000372           duk_push_global_stash(ctx);
93 2000372           duk_get_prop_string(ctx, -1, TIMERS_SLOT_NAME);
94             /* [ ... stash eventTimers ] */
95              
96 2000372           now = now_us() / 1000.0;
97 2000383 50         while (sanity-- > 0) {
98             /*
99             * Expired timer(s) still exist?
100             */
101 2000383 100         if (timer_count <= 0) {
102 2000359           break;
103             }
104 24           t = timer_list + timer_count - 1;
105 24 100         if (t->target > now) {
106 13           break;
107             }
108              
109             /*
110             * Move the timer to 'expiring' for the duration of the callback.
111             * Mark a one-shot timer deleted, compute a new target for an interval.
112             */
113 11           memcpy((void *) &timer_expiring, (void *) t, sizeof(ev_timer));
114 11           memset((void *) t, 0, sizeof(ev_timer));
115 11           timer_count--;
116 11           t = &timer_expiring;
117              
118 11 50         if (t->oneshot) {
119 11           t->removed = 1;
120             } else {
121 0           t->target = now + t->delay; /* XXX: or t->target + t->delay? */
122             }
123              
124             /*
125             * Call timer callback. The callback can operate on the timer list:
126             * add new timers, remove timers. The callback can even remove the
127             * expired timer whose callback we're calling. However, because the
128             * timer being expired has been moved to 'timer_expiring', we don't
129             * need to worry about the timer's offset changing on the timer list.
130             */
131             #if DUKTAPE_EVENTLOOP_DEBUG > 0
132             fprintf(stderr, "calling user callback for timer id %d\n", (int) t->id);
133             fflush(stderr);
134             #endif
135              
136 11           duk_push_number(ctx, (double) t->id);
137 11           duk_get_prop(ctx, -2); /* -> [ ... stash eventTimers func ] */
138 11           rc = duk_pcall(ctx, 0 /*nargs*/); /* -> [ ... stash eventTimers retval ] */
139 11           check_duktape_call_for_errors(rc, ctx);
140 11           duk_pop(ctx); /* [ ... stash eventTimers ] */
141              
142 11 50         if (t->removed) {
143             /* One-shot timer (always removed) or removed by user callback. */
144             #if DUKTAPE_EVENTLOOP_DEBUG > 0
145             fprintf(stderr, "deleting callback state for timer %d\n", (int) t->id);
146             fflush(stderr);
147             #endif
148 11           duk_push_number(ctx, (double) t->id);
149 11           duk_del_prop(ctx, -2);
150             } else {
151             /* Interval timer, not removed by user callback. Queue back to
152             * timer list and bubble to its final sorted position.
153             */
154             #if DUKTAPE_EVENTLOOP_DEBUG > 0
155             fprintf(stderr, "queueing timer %d back into active list\n", (int) t->id);
156             fflush(stderr);
157             #endif
158 0 0         if (timer_count >= MAX_TIMERS) {
159 0           (void) duk_error(ctx, DUK_ERR_RANGE_ERROR, "out of timer slots");
160             }
161 0           memcpy((void *) (timer_list + timer_count), (void *) t, sizeof(ev_timer));
162 0           timer_count++;
163 0           bubble_last_timer();
164             }
165             }
166              
167 2000372           memset((void *) &timer_expiring, 0, sizeof(ev_timer));
168              
169 2000372           duk_pop_2(ctx); /* -> [ ... ] */
170 2000372           }
171              
172 2000359           duk_ret_t eventloop_run(duk_context *ctx, void *udata) {
173             ev_timer *t;
174             double now;
175             double diff;
176             int timeout;
177             int rc;
178              
179             (void) udata;
180             for (;;) {
181             /*
182             * Expire timers.
183             */
184 2000372           expire_timers(ctx);
185              
186             /*
187             * Determine poll() timeout (as close to poll() as possible as
188             * the wait is relative).
189             */
190 2000372           now = now_us() / 1000.0;
191 2000372           t = find_nearest_timer();
192 2000372 100         if (t) {
193 13           diff = t->target - now;
194 13 100         if (diff < MIN_WAIT) {
195 9           diff = MIN_WAIT;
196 4 50         } else if (diff > MAX_WAIT) {
197 0           diff = MAX_WAIT;
198             }
199 13           timeout = (int) diff; /* clamping ensures that fits */
200             } else {
201             #if DUKTAPE_EVENTLOOP_DEBUG > 0
202             fprintf(stderr, "no timers to poll, exiting\n");
203             fflush(stderr);
204             #endif
205 2000359           break;
206             }
207              
208             /*
209             * Poll for timeout.
210             */
211             #if DUKTAPE_EVENTLOOP_DEBUG > 0
212             fprintf(stderr, "going to poll, timeout %d ms\n", timeout);
213             fflush(stderr);
214             #endif
215 13           rc = poll(0, 0, timeout);
216             #if DUKTAPE_EVENTLOOP_DEBUG > 0
217             fprintf(stderr, "poll rc: %d\n", rc);
218             fflush(stderr);
219             #endif
220             if (rc < 0) {
221             /* error */
222             } else if (rc == 0) {
223             /* timeout */
224             } else {
225             /* 'rc' fds active -- huh?*/
226             }
227 13           }
228              
229 2000359           return 0;
230             }
231              
232 11           static int create_timer(duk_context *ctx) {
233             double delay;
234             int oneshot;
235             int idx;
236             int64_t timer_id;
237             double now;
238             ev_timer *t;
239              
240 11           now = now_us() / 1000.0;
241              
242             /* indexes:
243             * 0 = function (callback)
244             * 1 = delay
245             * 2 = boolean: oneshot
246             */
247 11           delay = duk_require_number(ctx, 1);
248 11 100         if (delay < MIN_DELAY) {
249 7           delay = MIN_DELAY;
250             }
251 11           oneshot = duk_require_boolean(ctx, 2);
252              
253 11 50         if (timer_count >= MAX_TIMERS) {
254 0           (void) duk_error(ctx, DUK_ERR_RANGE_ERROR, "out of timer slots");
255             }
256 11           idx = timer_count++;
257 11           timer_id = timer_next_id++;
258 11           t = timer_list + idx;
259              
260 11           memset((void *) t, 0, sizeof(ev_timer));
261 11           t->id = timer_id;
262 11           t->target = now + delay;
263 11           t->delay = delay;
264 11           t->oneshot = oneshot;
265 11           t->removed = 0;
266              
267             /* Timer is now at the last position; use swaps to "bubble" it to its
268             * correct sorted position.
269             */
270 11           bubble_last_timer();
271              
272             /* Finally, register the callback to the global stash 'eventTimers' object. */
273 11           duk_push_global_stash(ctx);
274 11           duk_get_prop_string(ctx, -1, TIMERS_SLOT_NAME); /* -> [ func delay oneshot stash eventTimers ] */
275 11           duk_push_number(ctx, (double) timer_id);
276 11           duk_dup(ctx, 0);
277 11           duk_put_prop(ctx, -3); /* eventTimers[timer_id] = callback */
278              
279             /* Return timer id. */
280 11           duk_push_number(ctx, (double) timer_id);
281             #if DUKTAPE_EVENTLOOP_DEBUG > 0
282             fprintf(stderr, "created timer id: %d\n", (int) timer_id);
283             fflush(stderr);
284             #endif
285 11           return 1;
286             }
287              
288 0           static int delete_timer(duk_context *ctx) {
289             int i, n;
290             int64_t timer_id;
291             ev_timer *t;
292 0           int found = 0;
293              
294             /* indexes:
295             * 0 = timer id
296             */
297 0           timer_id = (int64_t) duk_require_number(ctx, 0);
298              
299             /*
300             * Unlike insertion, deletion needs a full scan of the timer list
301             * and an expensive remove. If no match is found, nothing is deleted.
302             * Caller gets a boolean return code indicating match.
303             *
304             * When a timer is being expired and its user callback is running,
305             * the timer has been moved to 'timer_expiring' and its deletion
306             * needs special handling: just mark it to-be-deleted and let the
307             * expiry code remove it.
308             */
309              
310 0           t = &timer_expiring;
311 0 0         if (t->id == timer_id) {
312 0           t->removed = 1;
313 0           duk_push_true(ctx);
314             #if DUKTAPE_EVENTLOOP_DEBUG > 0
315             fprintf(stderr, "deleted expiring timer id: %d\n", (int) timer_id);
316             fflush(stderr);
317             #endif
318 0           return 1;
319             }
320              
321 0           n = timer_count;
322 0 0         for (i = 0; i < n; i++) {
323 0           t = timer_list + i;
324 0 0         if (t->id == timer_id) {
325 0           found = 1;
326              
327             /* Shift elements downwards to keep the timer list dense
328             * (no need if last element).
329             */
330 0 0         if (i < timer_count - 1) {
331 0           memmove((void *) t, (void *) (t + 1), (timer_count - i - 1) * sizeof(ev_timer));
332             }
333              
334             /* Zero last element for clarity. */
335 0           memset((void *) (timer_list + n - 1), 0, sizeof(ev_timer));
336              
337             /* Update timer_count. */
338 0           timer_count--;
339              
340             /* The C state is now up-to-date, but we still need to delete
341             * the timer callback state from the global 'stash'.
342             */
343 0           duk_push_global_stash(ctx);
344 0           duk_get_prop_string(ctx, -1, TIMERS_SLOT_NAME); /* -> [ timer_id stash eventTimers ] */
345 0           duk_push_number(ctx, (double) timer_id);
346 0           duk_del_prop(ctx, -2); /* delete eventTimers[timer_id] */
347              
348             #if DUKTAPE_EVENTLOOP_DEBUG > 0
349             fprintf(stderr, "deleted timer id: %d\n", (int) timer_id);
350             fflush(stderr);
351             #endif
352 0           break;
353             }
354             }
355              
356             #if DUKTAPE_EVENTLOOP_DEBUG > 0
357             if (!found) {
358             fprintf(stderr, "trying to delete timer id %d, but not found; ignoring\n", (int) timer_id);
359             fflush(stderr);
360             }
361             #endif
362              
363 0           duk_push_boolean(ctx, found);
364 0           return 1;
365             }
366              
367             static duk_function_list_entry eventloop_funcs[] = {
368             { "createTimer", create_timer, 3 },
369             { "deleteTimer", delete_timer, 1 },
370             { NULL, NULL, 0 }
371             };
372              
373 269           void eventloop_register(duk_context *ctx) {
374 269           memset((void *) timer_list, 0, MAX_TIMERS * sizeof(ev_timer));
375 269           memset((void *) &timer_expiring, 0, sizeof(ev_timer));
376              
377             /* Set global 'EventLoop'. */
378 269           duk_push_global_object(ctx);
379 269           duk_push_object(ctx);
380 269           duk_put_function_list(ctx, -1, eventloop_funcs);
381 269           duk_put_prop_string(ctx, -2, "EventLoop");
382 269           duk_pop(ctx);
383              
384             /* Initialize global stash 'eventTimers'. */
385 269           duk_push_global_stash(ctx);
386 269           duk_push_object(ctx);
387 269           duk_put_prop_string(ctx, -2, TIMERS_SLOT_NAME);
388 269           duk_pop(ctx);
389 269           }