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 {
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,41 +2170,18 @@ 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))
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 */
/* 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;
list_for_each(el, &ts->os_rw_handlers) {
rh = list_entry(el, JSOSRWHandler, link);
@ -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))
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 ),

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;
@ -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();