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
This commit is contained in:
Ben Noordhuis 2024-03-30 09:36:38 +01:00 committed by GitHub
parent 93d1742fc4
commit f80a5b08cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 98 additions and 62 deletions

View file

@ -112,8 +112,10 @@ typedef struct {
typedef struct { typedef struct {
struct list_head link; struct list_head link;
BOOL has_object; uint8_t has_object:1;
uint8_t repeats:1;
int64_t timeout; int64_t timeout;
int64_t delay;
JSValue func; JSValue func;
} JSOSTimer; } JSOSTimer;
@ -2016,6 +2018,11 @@ static JSValue js_os_now(JSContext *ctx, JSValue this_val,
return JS_NewInt64(ctx, js__hrtime_ns() / 1000); 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) static void unlink_timer(JSRuntime *rt, JSOSTimer *th)
{ {
if (th->link.prev) { 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, 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); JSRuntime *rt = JS_GetRuntime(ctx);
JSThreadState *ts = JS_GetRuntimeOpaque(rt); 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"); return JS_ThrowTypeError(ctx, "not a function");
if (JS_ToInt64(ctx, &delay, argv[1])) if (JS_ToInt64(ctx, &delay, argv[1]))
return JS_EXCEPTION; return JS_EXCEPTION;
if (delay < 1)
delay = 1;
obj = JS_NewObjectClass(ctx, js_os_timer_class_id); obj = JS_NewObjectClass(ctx, js_os_timer_class_id);
if (JS_IsException(obj)) if (JS_IsException(obj))
return obj; return obj;
@ -2075,7 +2086,9 @@ static JSValue js_os_setTimeout(JSContext *ctx, JSValue this_val,
return JS_EXCEPTION; return JS_EXCEPTION;
} }
th->has_object = TRUE; 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); th->func = JS_DupValue(ctx, func);
list_add_tail(&th->link, &ts->os_timers); list_add_tail(&th->link, &ts->os_timers);
JS_SetOpaque(obj, th); JS_SetOpaque(obj, th);
@ -2089,6 +2102,8 @@ static JSValue js_os_clearTimeout(JSContext *ctx, JSValue this_val,
if (!th) if (!th)
return JS_EXCEPTION; return JS_EXCEPTION;
unlink_timer(JS_GetRuntime(ctx), th); unlink_timer(JS_GetRuntime(ctx), th);
JS_FreeValue(ctx, th->func);
th->func = JS_UNDEFINED;
return JS_UNDEFINED; return JS_UNDEFINED;
} }
@ -2111,6 +2126,43 @@ static void call_handler(JSContext *ctx, JSValue func)
JS_FreeValue(ctx, ret); 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) #if defined(_WIN32)
static int js_os_poll(JSContext *ctx) static int js_os_poll(JSContext *ctx)
@ -2118,41 +2170,18 @@ static int js_os_poll(JSContext *ctx)
JSRuntime *rt = JS_GetRuntime(ctx); JSRuntime *rt = JS_GetRuntime(ctx);
JSThreadState *ts = JS_GetRuntimeOpaque(rt); JSThreadState *ts = JS_GetRuntimeOpaque(rt);
int min_delay, console_fd; int min_delay, console_fd;
int64_t cur_time, delay;
JSOSRWHandler *rh; JSOSRWHandler *rh;
struct list_head *el; struct list_head *el;
/* XXX: handle signals if useful */ /* XXX: handle signals if useful */
if (list_empty(&ts->os_rw_handlers) && list_empty(&ts->os_timers)) 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 */ 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;
}
console_fd = -1; console_fd = -1;
list_for_each(el, &ts->os_rw_handlers) { list_for_each(el, &ts->os_rw_handlers) {
rh = list_entry(el, JSOSRWHandler, link); rh = list_entry(el, JSOSRWHandler, link);
@ -2270,7 +2299,6 @@ static int js_os_poll(JSContext *ctx)
JSRuntime *rt = JS_GetRuntime(ctx); JSRuntime *rt = JS_GetRuntime(ctx);
JSThreadState *ts = JS_GetRuntimeOpaque(rt); JSThreadState *ts = JS_GetRuntimeOpaque(rt);
int ret, fd_max, min_delay; int ret, fd_max, min_delay;
int64_t cur_time, delay;
fd_set rfds, wfds; fd_set rfds, wfds;
JSOSRWHandler *rh; JSOSRWHandler *rh;
struct list_head *el; 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) && min_delay = js_os_run_timers(rt, ctx, ts);
list_empty(&ts->port_list)) 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 */ return -1; /* no more events */
if (!list_empty(&ts->os_timers)) { tvp = NULL;
cur_time = js__hrtime_ns() / 1e6; if (min_delay >= 0) {
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;
}
}
tv.tv_sec = min_delay / 1000; tv.tv_sec = min_delay / 1000;
tv.tv_usec = (min_delay % 1000) * 1000; tv.tv_usec = (min_delay % 1000) * 1000;
tvp = &tv; tvp = &tv;
} else {
tvp = NULL;
} }
FD_ZERO(&rfds); FD_ZERO(&rfds);
@ -3686,8 +3696,11 @@ static const JSCFunctionListEntry js_os_funcs[] = {
JS_CFUNC_DEF("cputime", 0, js_os_cputime ), JS_CFUNC_DEF("cputime", 0, js_os_cputime ),
#endif #endif
JS_CFUNC_DEF("now", 0, js_os_now ), 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("clearTimeout", 1, js_os_clearTimeout ),
JS_CFUNC_DEF("clearInterval", 1, js_os_clearTimeout ),
JS_PROP_STRING_DEF("platform", OS_PLATFORM, 0 ), JS_PROP_STRING_DEF("platform", OS_PLATFORM, 0 ),
JS_CFUNC_DEF("getcwd", 0, js_os_getcwd ), JS_CFUNC_DEF("getcwd", 0, js_os_getcwd ),
JS_CFUNC_DEF("chdir", 0, js_os_chdir ), JS_CFUNC_DEF("chdir", 0, js_os_chdir ),

View file

@ -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; var th, i;
@ -266,6 +275,18 @@ function test_timer()
os.clearTimeout(th[i]); 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_printf();
test_file1(); test_file1();
test_file2(); test_file2();
@ -273,4 +294,6 @@ test_getline();
test_popen(); test_popen();
test_os(); test_os();
!isWin && test_os_exec(); !isWin && test_os_exec();
test_timer(); test_interval();
test_timeout();
test_timeout_order();