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:
parent
93d1742fc4
commit
f80a5b08cf
2 changed files with 98 additions and 62 deletions
129
quickjs-libc.c
129
quickjs-libc.c
|
@ -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 ),
|
||||||
|
|
|
@ -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();
|
||||||
|
|
Loading…
Reference in a new issue