Skip to content

Implement setInterval() #338

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 74 additions & 61 deletions quickjs-libc.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand All @@ -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);
Expand All @@ -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;
}

Expand All @@ -2111,47 +2126,61 @@ 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)
{
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) {
Expand Down Expand Up @@ -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;
Expand All @@ -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 */

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;
}
}
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 */

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);
Expand Down Expand Up @@ -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 ),
Expand Down
27 changes: 25 additions & 2 deletions tests/test_std.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -266,11 +275,25 @@ 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();
test_getline();
test_popen();
test_os();
!isWin && test_os_exec();
test_timer();
test_interval();
test_timeout();
test_timeout_order();