From f80a5b08cfcafbed30b21c332dad9012d33f0dca Mon Sep 17 00:00:00 2001 From: Ben Noordhuis Date: Sat, 30 Mar 2024 09:36:38 +0100 Subject: [PATCH] Implement setInterval() (#338) Coincidentally fixes a timer ordering bug for which a regression test has been added. Fixes: https://github.com/quickjs-ng/quickjs/issues/279 --- quickjs-libc.c | 133 +++++++++++++++++++++++++--------------------- tests/test_std.js | 27 +++++++++- 2 files changed, 98 insertions(+), 62 deletions(-) diff --git a/quickjs-libc.c b/quickjs-libc.c index 42fd770..c5b1235 100644 --- a/quickjs-libc.c +++ b/quickjs-libc.c @@ -112,8 +112,10 @@ typedef struct { typedef struct { struct list_head link; - BOOL has_object; + uint8_t has_object:1; + uint8_t repeats:1; int64_t timeout; + int64_t delay; JSValue func; } JSOSTimer; @@ -2016,6 +2018,11 @@ static JSValue js_os_now(JSContext *ctx, JSValue this_val, return JS_NewInt64(ctx, js__hrtime_ns() / 1000); } +static uint64_t js__hrtime_ms(void) +{ + return js__hrtime_ns() / (1000 * 1000); +} + static void unlink_timer(JSRuntime *rt, JSOSTimer *th) { if (th->link.prev) { @@ -2051,8 +2058,10 @@ static void js_os_timer_mark(JSRuntime *rt, JSValue val, } } +// TODO(bnoordhuis) accept string as first arg and eval at timer expiry +// TODO(bnoordhuis) retain argv[2..] as args for callback if argc > 2 static JSValue js_os_setTimeout(JSContext *ctx, JSValue this_val, - int argc, JSValue *argv) + int argc, JSValue *argv, int magic) { JSRuntime *rt = JS_GetRuntime(ctx); JSThreadState *ts = JS_GetRuntimeOpaque(rt); @@ -2066,6 +2075,8 @@ static JSValue js_os_setTimeout(JSContext *ctx, JSValue this_val, return JS_ThrowTypeError(ctx, "not a function"); if (JS_ToInt64(ctx, &delay, argv[1])) return JS_EXCEPTION; + if (delay < 1) + delay = 1; obj = JS_NewObjectClass(ctx, js_os_timer_class_id); if (JS_IsException(obj)) return obj; @@ -2075,7 +2086,9 @@ static JSValue js_os_setTimeout(JSContext *ctx, JSValue this_val, return JS_EXCEPTION; } th->has_object = TRUE; - th->timeout = js__hrtime_ns() / 1e6 + delay; + th->repeats = (magic > 0); + th->timeout = js__hrtime_ms() + delay; + th->delay = delay; th->func = JS_DupValue(ctx, func); list_add_tail(&th->link, &ts->os_timers); JS_SetOpaque(obj, th); @@ -2089,6 +2102,8 @@ static JSValue js_os_clearTimeout(JSContext *ctx, JSValue this_val, if (!th) return JS_EXCEPTION; unlink_timer(JS_GetRuntime(ctx), th); + JS_FreeValue(ctx, th->func); + th->func = JS_UNDEFINED; return JS_UNDEFINED; } @@ -2111,6 +2126,43 @@ static void call_handler(JSContext *ctx, JSValue func) JS_FreeValue(ctx, ret); } +static int js_os_run_timers(JSRuntime *rt, JSContext *ctx, JSThreadState *ts) +{ + JSValue func; + JSOSTimer *th; + int min_delay; + int64_t cur_time, delay; + struct list_head *el; + + if (list_empty(&ts->os_timers)) + return -1; + + cur_time = js__hrtime_ms(); + min_delay = 10000; + + list_for_each(el, &ts->os_timers) { + th = list_entry(el, JSOSTimer, link); + delay = th->timeout - cur_time; + if (delay > 0) { + min_delay = min_int(min_delay, delay); + } else { + func = JS_DupValueRT(rt, th->func); + unlink_timer(rt, th); + if (th->repeats) { + th->timeout = cur_time + th->delay; + list_add_tail(&th->link, &ts->os_timers); + } else if (!th->has_object) { + free_timer(rt, th); + } + call_handler(ctx, func); + JS_FreeValueRT(rt, func); + return 0; + } + } + + return min_delay; +} + #if defined(_WIN32) static int js_os_poll(JSContext *ctx) @@ -2118,40 +2170,17 @@ static int js_os_poll(JSContext *ctx) JSRuntime *rt = JS_GetRuntime(ctx); JSThreadState *ts = JS_GetRuntimeOpaque(rt); int min_delay, console_fd; - int64_t cur_time, delay; JSOSRWHandler *rh; struct list_head *el; /* XXX: handle signals if useful */ - if (list_empty(&ts->os_rw_handlers) && list_empty(&ts->os_timers)) - return -1; /* no more events */ - - /* XXX: only timers and basic console input are supported */ - if (!list_empty(&ts->os_timers)) { - cur_time = js__hrtime_ns() / 1e6; - min_delay = 10000; - list_for_each(el, &ts->os_timers) { - JSOSTimer *th = list_entry(el, JSOSTimer, link); - delay = th->timeout - cur_time; - if (delay <= 0) { - JSValue func; - /* the timer expired */ - func = th->func; - th->func = JS_UNDEFINED; - unlink_timer(rt, th); - if (!th->has_object) - free_timer(rt, th); - call_handler(ctx, func); - JS_FreeValue(ctx, func); - return 0; - } else if (delay < min_delay) { - min_delay = delay; - } - } - } else { - min_delay = -1; - } + min_delay = js_os_run_timers(rt, ctx, ts); + if (min_delay == 0) + return 0; // expired timer + if (min_delay < 0) + if (list_empty(&ts->os_rw_handlers)) + return -1; /* no more events */ console_fd = -1; list_for_each(el, &ts->os_rw_handlers) { @@ -2270,7 +2299,6 @@ static int js_os_poll(JSContext *ctx) JSRuntime *rt = JS_GetRuntime(ctx); JSThreadState *ts = JS_GetRuntimeOpaque(rt); int ret, fd_max, min_delay; - int64_t cur_time, delay; fd_set rfds, wfds; JSOSRWHandler *rh; struct list_head *el; @@ -2293,36 +2321,18 @@ static int js_os_poll(JSContext *ctx) } } - if (list_empty(&ts->os_rw_handlers) && list_empty(&ts->os_timers) && - list_empty(&ts->port_list)) - return -1; /* no more events */ + min_delay = js_os_run_timers(rt, ctx, ts); + if (min_delay == 0) + return 0; // expired timer + if (min_delay < 0) + if (list_empty(&ts->os_rw_handlers) && list_empty(&ts->port_list)) + return -1; /* no more events */ - if (!list_empty(&ts->os_timers)) { - cur_time = js__hrtime_ns() / 1e6; - min_delay = 10000; - list_for_each(el, &ts->os_timers) { - JSOSTimer *th = list_entry(el, JSOSTimer, link); - delay = th->timeout - cur_time; - if (delay <= 0) { - JSValue func; - /* the timer expired */ - func = th->func; - th->func = JS_UNDEFINED; - unlink_timer(rt, th); - if (!th->has_object) - free_timer(rt, th); - call_handler(ctx, func); - JS_FreeValue(ctx, func); - return 0; - } else if (delay < min_delay) { - min_delay = delay; - } - } + tvp = NULL; + if (min_delay >= 0) { tv.tv_sec = min_delay / 1000; tv.tv_usec = (min_delay % 1000) * 1000; tvp = &tv; - } else { - tvp = NULL; } FD_ZERO(&rfds); @@ -3686,8 +3696,11 @@ static const JSCFunctionListEntry js_os_funcs[] = { JS_CFUNC_DEF("cputime", 0, js_os_cputime ), #endif JS_CFUNC_DEF("now", 0, js_os_now ), - JS_CFUNC_DEF("setTimeout", 2, js_os_setTimeout ), + JS_CFUNC_MAGIC_DEF("setTimeout", 2, js_os_setTimeout, 0 ), + JS_CFUNC_MAGIC_DEF("setInterval", 2, js_os_setTimeout, 1 ), + // per spec: both functions can cancel timeouts and intervals JS_CFUNC_DEF("clearTimeout", 1, js_os_clearTimeout ), + JS_CFUNC_DEF("clearInterval", 1, js_os_clearTimeout ), JS_PROP_STRING_DEF("platform", OS_PLATFORM, 0 ), JS_CFUNC_DEF("getcwd", 0, js_os_getcwd ), JS_CFUNC_DEF("chdir", 0, js_os_chdir ), diff --git a/tests/test_std.js b/tests/test_std.js index 1b2378c..f722064 100644 --- a/tests/test_std.js +++ b/tests/test_std.js @@ -254,7 +254,16 @@ function test_os_exec() } } -function test_timer() +function test_interval() +{ + var t = os.setInterval(f, 1); + function f() { + if (++f.count === 3) os.clearInterval(t); + } + f.count = 0; +} + +function test_timeout() { var th, i; @@ -266,6 +275,18 @@ function test_timer() os.clearTimeout(th[i]); } +function test_timeout_order() +{ + var s = ""; + os.setTimeout(a, 1); + os.setTimeout(b, 2); + os.setTimeout(d, 5); + function a() { s += "a"; os.setTimeout(c, 0); } + function b() { s += "b"; } + function c() { s += "c"; } + function d() { assert(s === "abc"); } // not "acb" +} + test_printf(); test_file1(); test_file2(); @@ -273,4 +294,6 @@ test_getline(); test_popen(); test_os(); !isWin && test_os_exec(); -test_timer(); +test_interval(); +test_timeout(); +test_timeout_order();