diff --git a/include/gc.h b/include/gc.h index 72cc01b..c7b64ab 100644 --- a/include/gc.h +++ b/include/gc.h @@ -11,6 +11,7 @@ #define GC_UPDATE_ARGS ant_t *js, jsoff_t (*fwd_off)(void *ctx, jsoff_t old), GC_FWD_ARGS #define GC_OP_VAL_ARGS void (*op_val)(void *ctx, jsval_t *val), void *ctx +void js_maybe_gc(ant_t *js); size_t js_gc_compact(ant_t *js); #endif \ No newline at end of file diff --git a/include/reactor.h b/include/reactor.h index ac4f3b4..0aa482a 100644 --- a/include/reactor.h +++ b/include/reactor.h @@ -21,6 +21,8 @@ typedef enum { #define WORK_BLOCKING (WORK_MICROTASKS | WORK_IMMEDIATES | WORK_COROUTINES_READY) #define WORK_ASYNC (WORK_READLINE | WORK_STDIN | WORK_TIMERS | WORK_FETCHES | WORK_FS_OPS | WORK_CHILD_PROCS) +#define UV_CHECK_ALIVE uv_loop_alive(uv_default_loop()) + void js_poll_events(ant_t *js) ; void js_run_event_loop(ant_t *js); diff --git a/src/ant.c b/src/ant.c index f71e8db..c694488 100644 --- a/src/ant.c +++ b/src/ant.c @@ -38,6 +38,7 @@ #include #include #include +#include #endif #include "modules/fs.h" @@ -9951,7 +9952,7 @@ typedef struct { bool needs_scope; bool has_func_decl; bool has_func_decl_checked; - jsval_t loop_scope; + jshdl_t loop_scope_handle; } loop_block_ctx_t; static void loop_block_init(struct js *js, loop_block_ctx_t *ctx) { @@ -9959,7 +9960,7 @@ static void loop_block_init(struct js *js, loop_block_ctx_t *ctx) { ctx->needs_scope = false; ctx->has_func_decl = false; ctx->has_func_decl_checked = false; - ctx->loop_scope = js_mkundef(); + ctx->loop_scope_handle = -1; if (ctx->is_block && !(js->flags & F_NOEXEC)) { jsoff_t saved_pos = js->pos; @@ -9974,7 +9975,7 @@ static void loop_block_init(struct js *js, loop_block_ctx_t *ctx) { js->tok = saved_tok; js->consumed = saved_consumed; - if (ctx->needs_scope) ctx->loop_scope = js_mkscope(js); + if (ctx->needs_scope) ctx->loop_scope_handle = js_root(js, js_mkscope(js)); } } @@ -10004,11 +10005,14 @@ static inline jsval_t loop_block_exec(struct js *js, loop_block_ctx_t *ctx) { static inline void loop_block_sync_scope(struct js *js, loop_block_ctx_t *ctx) { struct for_let_ctx *flc = for_let_current(js); - if (flc && vtype(flc->body_scope) == T_OBJ) ctx->loop_scope = flc->body_scope; + if (flc && vtype(flc->body_scope) == T_OBJ) js_root_update(js, ctx->loop_scope_handle, flc->body_scope); } -#define loop_block_clear(js, ctx) if ((ctx)->needs_scope) scope_clear_props(js, (ctx)->loop_scope) -#define loop_block_cleanup(js, ctx) if ((ctx)->needs_scope) delscope(js) +#define loop_block_clear(js, ctx) \ + if ((ctx)->needs_scope) scope_clear_props(js, js_deref(js, (ctx)->loop_scope_handle)) + +#define loop_block_cleanup(js, ctx) \ + do { if ((ctx)->needs_scope) { js_unroot(js, (ctx)->loop_scope_handle); delscope(js); } } while(0) static jsval_t js_if(struct js *js) { js->consumed = 1; @@ -10129,22 +10133,35 @@ static jsval_t for_in_iter_object(struct js *js, for_iter_ctx_t *ctx, jsval_t ob uint8_t obj_type = vtype(obj); if (obj_type == T_NULL || obj_type == T_UNDEF) return js_mkundef(); if (obj_type == T_STR) return for_iter_string_indices(js, ctx, obj); + if (obj_type != T_OBJ && obj_type != T_ARR && obj_type != T_FUNC) return js_mkerr(js, "for-in requires object"); jsval_t iter_obj = (obj_type == T_FUNC) ? mkval(T_OBJ, vdata(obj)) : obj; - jsoff_t iter_obj_off = (jsoff_t)vdata(iter_obj); - jsoff_t prop_off = loadoff(js, iter_obj_off) & ~(3U | FLAGMASK); - - jsval_t prim = get_slot(js, obj, SLOT_PRIMITIVE); + jsval_t prim = get_slot(js, iter_obj, SLOT_PRIMITIVE); if (vtype(prim) == T_STR) return for_iter_string_indices(js, ctx, prim); + jshdl_t h_obj = js_root(js, iter_obj); const char *tag_sym_key = get_toStringTag_sym_key(); size_t tag_sym_len = tag_sym_key ? strlen(tag_sym_key) : 0; - while (prop_off < js->brk && prop_off != 0) { + jsoff_t prop_idx = 0; + char key_buf[256]; + + for (;;) { + jsval_t cur_obj = js_deref(js, h_obj); + jsoff_t cur_obj_off = (jsoff_t)vdata(cur_obj); + jsoff_t prop_off = loadoff(js, cur_obj_off) & ~(3U | FLAGMASK); + + jsoff_t cur_idx = 0; + while (prop_off < js->brk && prop_off != 0 && cur_idx < prop_idx) { + jsoff_t header = loadoff(js, prop_off); + prop_off = next_prop(header); cur_idx++; + } + + if (prop_off >= js->brk || prop_off == 0) break; jsoff_t header = loadoff(js, prop_off); - if (is_slot_prop(header)) { prop_off = next_prop(header); continue; } + if (is_slot_prop(header)) { prop_idx++; continue; } jsoff_t koff = loadoff(js, prop_off + (jsoff_t)sizeof(prop_off)); jsoff_t klen = offtolen(loadoff(js, koff)); @@ -10154,19 +10171,24 @@ static jsval_t for_in_iter_object(struct js *js, for_iter_ctx_t *ctx, jsval_t ob if (!skip && tag_sym_key) skip = streq(key, klen, tag_sym_key, tag_sym_len); if (!skip) { - descriptor_entry_t *desc = lookup_descriptor(iter_obj_off, key, klen); + descriptor_entry_t *desc = lookup_descriptor(cur_obj_off, key, klen); if (desc && !desc->enumerable) skip = true; } if (!skip) { + size_t copy_len = klen < sizeof(key_buf) - 1 ? klen : sizeof(key_buf) - 1; + memcpy(key_buf, key, copy_len); + key_buf[copy_len] = '\0'; + jsval_t out; - int rc = for_iter_step(js, ctx, js_mkstr(js, key, klen), &out); - if (rc) return (rc == 2) ? out : js_mkundef(); + int rc = for_iter_step(js, ctx, js_mkstr(js, key_buf, (jsoff_t)copy_len), &out); + if (rc) { js_unroot(js, h_obj); return (rc == 2) ? out : js_mkundef(); } } - prop_off = next_prop(header); + prop_idx++; } + js_unroot(js, h_obj); return js_mkundef(); } @@ -10593,7 +10615,7 @@ static jsval_t js_for(struct js *js) { loop_block_ctx_t loop_ctx = {0}; if (exe) { loop_block_init(js, &loop_ctx); - if (is_let_loop && let_var_len > 0 && loop_ctx.needs_scope) for_let_set_body_scope(js, loop_ctx.loop_scope); + if (is_let_loop && let_var_len > 0 && loop_ctx.needs_scope) for_let_set_body_scope(js, js_deref(js, loop_ctx.loop_scope_handle)); } js->flags |= F_NOEXEC; @@ -10603,6 +10625,7 @@ static jsval_t js_for(struct js *js) { pos4 = js->pos; while (!(flags & F_NOEXEC)) { + js_maybe_gc(js); js->flags = flags, js->pos = pos1, js->consumed = 1; if (next(js) != TOK_SEMICOLON) { v = resolveprop(js, js_expr(js)); @@ -10711,6 +10734,7 @@ static jsval_t js_while(struct js *js) { if (exe) { while (true) { + js_maybe_gc(js); js->flags = flags; js->pos = cond_start; js->consumed = 1; @@ -10806,6 +10830,7 @@ static jsval_t js_do_while(struct js *js) { if (exe) { do { + js_maybe_gc(js); js->pos = body_start; js->consumed = 1; js->flags = (flags & ~F_NOEXEC) | F_LOOP; @@ -21952,7 +21977,6 @@ static void gc_roots_common(gc_off_op_t op_off, gc_val_op_t op_val, gc_cb_ctx_t child_process_gc_update_roots(op_val, c); readline_gc_update_roots(op_val, c); process_gc_update_roots(op_val, c); - collections_gc_reserve_roots(op_val, c); for (int i = 0; i < c->js->for_let_stack_len; i++) { op_val(c, &c->js->for_let_stack[i].body_scope); @@ -21979,6 +22003,7 @@ void js_gc_reserve_roots(GC_UPDATE_ARGS) { gc_cb_ctx_t cb_ctx = { fwd_off, fwd_val, ctx, js }; gc_roots_common(gc_reserve_off_cb, gc_reserve_val_cb, &cb_ctx); + collections_gc_reserve_roots(gc_reserve_val_cb, &cb_ctx); promise_data_entry_t *pd, *pd_tmp; HASH_ITER(hh, promise_registry, pd, pd_tmp) { @@ -22049,7 +22074,7 @@ void js_gc_update_roots(GC_UPDATE_ARGS) { HASH_DEL(proxy_registry, proxy); jsoff_t old_off = proxy->obj_offset; jsoff_t new_off = fwd_off(ctx, old_off); - if (new_off == old_off && old_off != 0) { free(proxy); continue; } + if (new_off == 0) { free(proxy); continue; } proxy->obj_offset = new_off; FWD_VAL(proxy->target); FWD_VAL(proxy->handler); HASH_ADD(hh, new_proxy_registry, obj_offset, sizeof(jsoff_t), proxy); @@ -22061,7 +22086,7 @@ void js_gc_update_roots(GC_UPDATE_ARGS) { HASH_DEL(accessor_registry, acc); jsoff_t old_off = acc->obj_offset; jsoff_t new_off = fwd_off(ctx, old_off); - if (new_off == old_off && old_off != 0) { free(acc); continue; } + if (new_off == 0) { free(acc); continue; } acc->obj_offset = new_off; HASH_ADD(hh, new_acc_registry, obj_offset, sizeof(jsoff_t), acc); } @@ -22072,7 +22097,7 @@ void js_gc_update_roots(GC_UPDATE_ARGS) { HASH_DEL(desc_registry, desc); jsoff_t old_off = (jsoff_t)(desc->key >> 32); jsoff_t new_off = fwd_off(ctx, old_off); - if (new_off == old_off && old_off != 0) { free(desc); continue; } + if (new_off == 0) { free(desc->prop_name); free(desc); continue; } if (desc->has_getter) FWD_VAL(desc->getter); if (desc->has_setter) FWD_VAL(desc->setter); desc->key = ((uint64_t)new_off << 32) | (uint32_t)(desc->key & 0xFFFFFFFF); diff --git a/src/gc.c b/src/gc.c index ddc6f8c..fb31923 100644 --- a/src/gc.c +++ b/src/gc.c @@ -623,3 +623,15 @@ size_t js_gc_compact(ant_t *js) { return (old_brk > ctx.new_brk ? old_brk - ctx.new_brk : 0); } + +void js_maybe_gc(ant_t *js) { + jsoff_t thresh = js->brk / 2; + if (thresh < 4 * 1024 * 1024) thresh = 4 * 1024 * 1024; + if (thresh > 64 * 1024 * 1024) thresh = 64 * 1024 * 1024; + + if (js->gc_alloc_since > thresh || js->needs_gc) { + js->needs_gc = false; + js_gc_compact(js); + js->gc_alloc_since = 0; + } +} diff --git a/src/modules/collections.c b/src/modules/collections.c index 77ab0cc..dc9e47c 100644 --- a/src/modules/collections.c +++ b/src/modules/collections.c @@ -993,7 +993,8 @@ void init_collections_module(void) { void collections_gc_reserve_roots(void (*op_val)(void *, jsval_t *), void *ctx) { for (size_t i = 0; i < map_registry_count; i++) { - map_entry_t **head = map_registry[i].head; + jsval_t map_obj = mkval(T_OBJ, map_registry[i].obj_offset); + op_val(ctx, &map_obj); map_entry_t **head = map_registry[i].head; if (head && *head) { map_entry_t *entry, *tmp; HASH_ITER(hh, *head, entry, tmp) op_val(ctx, &entry->value); @@ -1001,7 +1002,8 @@ void collections_gc_reserve_roots(void (*op_val)(void *, jsval_t *), void *ctx) } for (size_t i = 0; i < set_registry_count; i++) { - set_entry_t **head = set_registry[i].head; + jsval_t set_obj = mkval(T_OBJ, set_registry[i].obj_offset); + op_val(ctx, &set_obj); set_entry_t **head = set_registry[i].head; if (head && *head) { set_entry_t *entry, *tmp; HASH_ITER(hh, *head, entry, tmp) op_val(ctx, &entry->value); @@ -1036,7 +1038,7 @@ void collections_gc_update_roots(jsoff_t (*fwd_off)(void *ctx, jsoff_t old), GC_ jsoff_t old_off = map_registry[i].obj_offset; jsoff_t new_off = fwd_off(ctx, old_off); - if (new_off == old_off && old_off != 0) { + if (new_off == 0) { free_map_entries(map_registry[i].head); free(map_registry[i].head); continue; @@ -1060,7 +1062,7 @@ void collections_gc_update_roots(jsoff_t (*fwd_off)(void *ctx, jsoff_t old), GC_ jsoff_t old_off = set_registry[i].obj_offset; jsoff_t new_off = fwd_off(ctx, old_off); - if (new_off == old_off && old_off != 0) { + if (new_off == 0) { free_set_entries(set_registry[i].head); free(set_registry[i].head); continue; diff --git a/src/pkg/fetcher.zig b/src/pkg/fetcher.zig index 6c7a409..d14fe0c 100644 --- a/src/pkg/fetcher.zig +++ b/src/pkg/fetcher.zig @@ -83,6 +83,7 @@ const Http2Client = struct { use_tls: bool, connected: i32, connect_pending: bool, + closing: bool, write_buf: std.ArrayListUnmanaged(u8), requests: [MAX_PENDING_REQUESTS]RequestState, request_count: usize, @@ -128,6 +129,7 @@ const Http2Client = struct { .use_tls = use_tls, .connected = 0, .connect_pending = false, + .closing = false, .write_buf = .{}, .requests = undefined, .request_count = 0, @@ -166,6 +168,9 @@ const Http2Client = struct { } pub fn deinit(self: *Http2Client) void { + self.closing = true; + self.connect_pending = false; + for (&self.requests) |*req| { req.on_data = null; req.on_complete = null; @@ -257,6 +262,7 @@ const Http2Client = struct { const tls: *tlsuv.stream_t = @ptrCast(@alignCast(handle)); const client: *Http2Client = @ptrCast(@alignCast(tls.data)); client.connected = -2; + client.connect_pending = false; } fn findRequest(self: *Http2Client, stream_id: i32) ?*RequestState { @@ -351,6 +357,7 @@ const Http2Client = struct { } fn flush(self: *Http2Client) !void { + if (self.closing) return error.ConnectionFailed; if (self.h2_session) |session| while (nghttp2.nghttp2_session_want_write(session) != 0) if (nghttp2.nghttp2_session_send(session) != 0) break; if (self.write_buf.items.len > 0) { const data = try self.allocator.dupe(u8, self.write_buf.items); @@ -381,6 +388,7 @@ const Http2Client = struct { const tls: *tlsuv.stream_t = @ptrCast(@alignCast(stream)); const client: *Http2Client = @ptrCast(@alignCast(tls.data)); defer if (buf.base) |b| std.c.free(b); + if (client.closing) return; if (nread < 0) { for (client.requests[0..client.request_count]) |*req| if (!req.done) { req.done = true; @@ -399,6 +407,12 @@ const Http2Client = struct { fn onConnect(req: *uv.connect_t, status: c_int) callconv(.c) void { const ctx: *ConnectCtx = @ptrCast(@alignCast(req.data)); defer ctx.client.allocator.destroy(ctx); + ctx.client.connect_pending = false; + if (ctx.client.closing) { + ctx.client.connected = -1; + _ = tlsuv.tlsuv_stream_close(&ctx.client.tls, onStreamClose); + return; + } if (status < 0) { ctx.client.connected = -1; return; @@ -416,6 +430,7 @@ const Http2Client = struct { const ConnectCtx = struct { client: *Http2Client, req: uv.connect_t }; fn ensureConnected(self: *Http2Client) !void { + if (self.closing) return error.ConnectionFailed; if (self.connected > 0) return; if (self.connected < 0) return error.ConnectionFailed; @@ -443,6 +458,7 @@ const Http2Client = struct { } pub fn initiateConnectAsync(self: *Http2Client) !void { + if (self.closing) return error.ConnectionFailed; if (self.connected > 0) return; if (self.connected < 0) return error.ConnectionFailed; if (self.connect_pending) return; diff --git a/src/reactor.c b/src/reactor.c index edc02b5..e27ccc3 100644 --- a/src/reactor.c +++ b/src/reactor.c @@ -59,28 +59,22 @@ static inline work_flags_t get_pending_work(void) { return flags; } -static void maybe_gc(ant_t *js) { - jsoff_t thresh = js->brk / 2; - if (thresh < 4 * 1024 * 1024) thresh = 4 * 1024 * 1024; - - if (js->gc_alloc_since > thresh || js->needs_gc) { - js->needs_gc = false; - js_gc_compact(js); - js->gc_alloc_since = 0; - } -} - void js_run_event_loop(ant_t *js) { work_flags_t work; + int uv_alive = UV_CHECK_ALIVE; - while ((work = get_pending_work()) & WORK_PENDING) { + while (((work = get_pending_work()) & WORK_PENDING) || uv_alive) { js_poll_events(js); + work = get_pending_work(); + uv_alive = UV_CHECK_ALIVE; if (work & WORK_BLOCKING) uv_run(uv_default_loop(), UV_RUN_NOWAIT); - else if (work & WORK_ASYNC) { maybe_gc(js); uv_run(uv_default_loop(), UV_RUN_ONCE); } - else if (work & WORK_COROUTINES) break; + else if ((work & WORK_ASYNC) || uv_alive) { + js_maybe_gc(js); + uv_run(uv_default_loop(), UV_RUN_ONCE); + } else if (work & WORK_COROUTINES) break; } js_poll_events(js); -} \ No newline at end of file +} diff --git a/tests/test_event_loop_timers.js b/tests/test_event_loop_timers.js new file mode 100644 index 0000000..564996b --- /dev/null +++ b/tests/test_event_loop_timers.js @@ -0,0 +1,14 @@ +let fired = false; + +setTimeout(() => { + fired = true; + console.log('timer fired'); +}, 5); + +// If the event loop exits early, this will print false. +setTimeout(() => { + if (!fired) { + throw new Error('timer status: missing'); + } + console.log('timer status: ok'); +}, 10); diff --git a/tests/test_timer_chain_iter.js b/tests/test_timer_chain_iter.js new file mode 100644 index 0000000..fbcbb9c --- /dev/null +++ b/tests/test_timer_chain_iter.js @@ -0,0 +1,93 @@ +const WIDTH = 150; +const HEIGHT = 40; + +const cells = new Map(); +const directions = [ + [-1, 1], + [0, 1], + [1, 1], + [-1, 0], + [1, 0], + [-1, -1], + [0, -1], + [1, -1] +]; + +const key_for = (x, y) => `${x}-${y}`; + +for (let y = 0; y < HEIGHT; y++) { + for (let x = 0; x < WIDTH; x++) { + cells.set(key_for(x, y), { + x, + y, + alive: Math.random() <= 0.2, + next_state: false, + neighbours: [] + }); + } +} + +for (const cell of cells.values()) { + for (const [dx, dy] of directions) { + const nx = cell.x + dx; + const ny = cell.y + dy; + if (nx < 0 || ny < 0 || nx >= WIDTH || ny >= HEIGHT) continue; + const neighbour = cells.get(key_for(nx, ny)); + if (neighbour) cell.neighbours.push(neighbour); + } +} + +const dotick = () => { + for (const cell of cells.values()) { + let alive_neighbours = 0; + for (const neighbour of cell.neighbours) { + if (neighbour.alive) alive_neighbours++; + } + + if (!cell.alive && alive_neighbours === 3) { + cell.next_state = true; + } else if (alive_neighbours < 2 || alive_neighbours > 3) { + cell.next_state = false; + } else { + cell.next_state = cell.alive; + } + } + + for (const cell of cells.values()) { + cell.alive = cell.next_state; + } +}; + +const render = () => { + let rendering = ''; + for (let y = 0; y < HEIGHT; y++) { + for (let x = 0; x < WIDTH; x++) { + const cell = cells.get(key_for(x, y)); + rendering += cell && cell.alive ? 'o' : ' '; + } + rendering += '\n'; + } + return rendering; +}; + +let ticks = 0; +const start = performance.now(); +const duration = 30000; +const batch_size = 100; + +const run = () => { + for (let i = 0; i < batch_size && performance.now() - start < duration; i++) { + dotick(); + render(); + } + + ticks += batch_size; + if (performance.now() - start >= duration) { + console.log('done', ticks); + return; + } + + setTimeout(run, 0); +}; + +setTimeout(run, 0);