File Coverage

c_eventloop.c
Criterion Covered Total %
statement 89 124 71.7
branch 20 36 55.5
condition n/a
subroutine n/a
pod n/a
total 109 160 68.1


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