From b16fa757060aef83476dc5140f5345843c0eae28 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Tue, 17 Feb 2026 17:59:12 -0600 Subject: [PATCH] flag used for actor stopping insetad of counter --- source/cell.c | 8 ++-- source/cell_internal.h | 5 +-- source/mach.c | 45 ++++++++++--------- source/quickjs-internal.h | 33 +++----------- source/quickjs.h | 6 +-- source/runtime.c | 8 ++-- source/scheduler.c | 83 ++++++++++++++++++----------------- source/scheduler_playdate.c | 5 --- tests/actor_suspend_resume.ce | 20 +++++++++ tests/slow_compute_actor.ce | 11 +++++ 10 files changed, 115 insertions(+), 109 deletions(-) create mode 100644 tests/actor_suspend_resume.ce create mode 100644 tests/slow_compute_actor.ce diff --git a/source/cell.c b/source/cell.c index 4cc02b65..c8007f08 100644 --- a/source/cell.c +++ b/source/cell.c @@ -252,6 +252,7 @@ const char* cell_get_core_path(void) { void actor_disrupt(cell_rt *crt) { crt->disrupt = 1; + JS_SetPauseFlag(crt->context, 2); if (crt->state != ACTOR_RUNNING) actor_free(crt); } @@ -265,7 +266,6 @@ void script_startup(cell_rt *prt) g_runtime = JS_NewRuntime(); } JSContext *js = JS_NewContext(g_runtime); - JS_SetInterruptHandler(js, (JSInterruptHandler *)actor_interrupt_cb, prt); JS_SetContextOpaque(js, prt); prt->context = js; @@ -558,7 +558,6 @@ int cell_init(int argc, char **argv) cli_rt->context = ctx; JS_SetContextOpaque(ctx, cli_rt); - JS_SetInterruptHandler(ctx, (JSInterruptHandler *)actor_interrupt_cb, cli_rt); JS_AddGCRef(ctx, &cli_rt->idx_buffer_ref); JS_AddGCRef(ctx, &cli_rt->on_exception_ref); @@ -709,7 +708,6 @@ check_actors: JS_DeleteGCRef(ctx, &cli_rt->message_handle_ref); JS_DeleteGCRef(ctx, &cli_rt->unneeded_ref); JS_DeleteGCRef(ctx, &cli_rt->actor_sym_ref); - JS_SetInterruptHandler(ctx, NULL, NULL); pthread_mutex_destroy(cli_rt->mutex); free(cli_rt->mutex); @@ -772,9 +770,9 @@ int uncaught_exception(JSContext *js, JSValue v) JS_GetException(js); cell_rt *crt = JS_GetContextOpaque(js); if (crt && !JS_IsNull(crt->on_exception_ref.val)) { - /* Disable interrupt handler so actor_die can send messages + /* Disable interruption so actor_die can send messages without being re-interrupted. */ - JS_SetInterruptHandler(js, NULL, NULL); + JS_SetPauseFlag(js, 0); JSValue err = JS_NewString(js, "interrupted"); JS_Call(js, crt->on_exception_ref.val, JS_NULL, 1, &err); /* Clear any secondary exception from the callback. */ diff --git a/source/cell_internal.h b/source/cell_internal.h index 29757aa5..c46c1222 100644 --- a/source/cell_internal.h +++ b/source/cell_internal.h @@ -1,5 +1,6 @@ #include +#include /* Letter type for unified message queue */ typedef enum { @@ -67,8 +68,7 @@ typedef struct cell_rt { int affinity; uint64_t turn_start_ns; // cell_ns() when turn began - uint64_t turn_deadline_ns; // turn_start_ns + fast or slow timer - int is_slow_turn; // 1 if running under slow timer + _Atomic uint32_t turn_gen; // incremented each turn start int slow_strikes; // consecutive slow-completed turns int vm_suspended; // 1 if VM is paused mid-turn @@ -93,7 +93,6 @@ void actor_clock(cell_rt *actor, JSValue fn); uint32_t actor_delay(cell_rt *actor, JSValue fn, double seconds); JSValue actor_remove_timer(cell_rt *actor, uint32_t timer_id); void exit_handler(void); -int actor_interrupt_cb(JSRuntime *rt, cell_rt *crt); void actor_loop(); void actor_initialize(void); void actor_free(cell_rt *actor); diff --git a/source/mach.c b/source/mach.c index ae9f1e9a..117fe113 100644 --- a/source/mach.c +++ b/source/mach.c @@ -678,18 +678,6 @@ static JSValue reg_vm_binop(JSContext *ctx, int op, JSValue a, JSValue b) { return JS_ThrowTypeError(ctx, "type mismatch in binary operation"); } -/* Check for interrupt — returns: 0 = continue, -1 = hard kill, 1 = suspend */ -int reg_vm_check_interrupt(JSContext *ctx) { - if (--ctx->interrupt_counter <= 0) { - ctx->interrupt_counter = JS_INTERRUPT_COUNTER_INIT; - if (ctx->interrupt_handler) { - int r = ctx->interrupt_handler(ctx->rt, ctx->interrupt_opaque); - if (r < 0) return -1; /* hard kill */ - if (r > 0) return 1; /* suspend request */ - } - } - return 0; -} #ifdef HAVE_ASAN void __asan_on_error(void) { @@ -1406,12 +1394,17 @@ vm_dispatch: int offset = MACH_GET_sJ(instr); pc = (uint32_t)((int32_t)pc + offset); if (offset < 0) { - int irc = reg_vm_check_interrupt(ctx); - if (irc < 0) { + int pf = atomic_load_explicit(&ctx->pause_flag, memory_order_relaxed); + if (pf == 2) { result = JS_ThrowInternalError(ctx, "interrupted"); goto done; } - if (irc > 0) goto suspend; + if (pf == 1) { + if (ctx->vm_call_depth > 0) + atomic_store_explicit(&ctx->pause_flag, 0, memory_order_relaxed); + else + goto suspend; + } } VM_BREAK(); } @@ -1426,12 +1419,17 @@ vm_dispatch: int offset = MACH_GET_sBx(instr); pc = (uint32_t)((int32_t)pc + offset); if (offset < 0) { - int irc = reg_vm_check_interrupt(ctx); - if (irc < 0) { + int pf = atomic_load_explicit(&ctx->pause_flag, memory_order_relaxed); + if (pf == 2) { result = JS_ThrowInternalError(ctx, "interrupted"); goto done; } - if (irc > 0) goto suspend; + if (pf == 1) { + if (ctx->vm_call_depth > 0) + atomic_store_explicit(&ctx->pause_flag, 0, memory_order_relaxed); + else + goto suspend; + } } } VM_BREAK(); @@ -1447,12 +1445,17 @@ vm_dispatch: int offset = MACH_GET_sBx(instr); pc = (uint32_t)((int32_t)pc + offset); if (offset < 0) { - int irc = reg_vm_check_interrupt(ctx); - if (irc < 0) { + int pf = atomic_load_explicit(&ctx->pause_flag, memory_order_relaxed); + if (pf == 2) { result = JS_ThrowInternalError(ctx, "interrupted"); goto done; } - if (irc > 0) goto suspend; + if (pf == 1) { + if (ctx->vm_call_depth > 0) + atomic_store_explicit(&ctx->pause_flag, 0, memory_order_relaxed); + else + goto suspend; + } } } VM_BREAK(); diff --git a/source/quickjs-internal.h b/source/quickjs-internal.h index b32415de..332583d2 100644 --- a/source/quickjs-internal.h +++ b/source/quickjs-internal.h @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #if defined(__APPLE__) @@ -1063,10 +1064,6 @@ static JS_BOOL JSText_equal_ascii (const JSText *text, JSValue imm) { /* Forward declarations for stone arena functions (defined after JSContext) */ -/* must be large enough to have a negligible runtime cost and small - enough to call the interrupt callback often. */ -#define JS_INTERRUPT_COUNTER_INIT 10000 - /* Auto-rooted C call argv — GC updates values in-place */ typedef struct CCallRoot { JSValue *argv; /* points to C-stack-local array */ @@ -1123,8 +1120,8 @@ struct JSContext { uint64_t random_state; - /* when the counter reaches zero, JSRutime.interrupt_handler is called */ - int interrupt_counter; + /* 0 = normal, 1 = suspend (fast timer), 2 = kill (slow timer) */ + _Atomic int pause_flag; /* if NULL, RegExp compilation is not supported */ JSValue (*compile_regexp) (JSContext *ctx, JSValue pattern, JSValue flags); @@ -1145,9 +1142,6 @@ struct JSContext { int vm_call_depth; /* 0 = pure bytecode, >0 = C frames on stack */ size_t heap_memory_limit; /* 0 = no limit, else max heap bytes */ - JSInterruptHandler *interrupt_handler; - void *interrupt_opaque; - JSValue current_exception; JS_BOOL disruption_reported; @@ -1544,26 +1538,14 @@ static inline void set_value (JSContext *ctx, JSValue *pval, JSValue new_val) { void JS_ThrowInterrupted (JSContext *ctx); -static no_inline __exception int __js_poll_interrupts (JSContext *ctx) { - ctx->interrupt_counter = JS_INTERRUPT_COUNTER_INIT; - if (ctx->interrupt_handler) { - int r = ctx->interrupt_handler (ctx->rt, ctx->interrupt_opaque); - if (r < 0) { - JS_ThrowInterrupted (ctx); - return -1; - } +static inline __exception int js_poll_interrupts (JSContext *ctx) { + if (unlikely (atomic_load_explicit (&ctx->pause_flag, memory_order_relaxed) >= 2)) { + JS_ThrowInterrupted (ctx); + return -1; } return 0; } -static inline __exception int js_poll_interrupts (JSContext *ctx) { - if (unlikely (--ctx->interrupt_counter <= 0)) { - return __js_poll_interrupts (ctx); - } else { - return 0; - } -} - /* === PPretext (parser pretext, system-malloc, used by cell_js.c parser) === */ typedef struct PPretext { uint32_t *data; @@ -1661,7 +1643,6 @@ JSValue js_key_from_string (JSContext *ctx, JSValue val); /* mach.c exports */ JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code, JSValue this_obj, int argc, JSValue *argv, JSValue env, JSValue outer_frame); JSFrameRegister *alloc_frame_register(JSContext *ctx, int slot_count); -int reg_vm_check_interrupt(JSContext *ctx); #endif /* QUICKJS_INTERNAL_H */ diff --git a/source/quickjs.h b/source/quickjs.h index abcdfc76..d1979738 100644 --- a/source/quickjs.h +++ b/source/quickjs.h @@ -345,10 +345,8 @@ int JS_GetVMCallDepth(JSContext *ctx); /* Set per-context heap memory limit (0 = no limit) */ void JS_SetHeapMemoryLimit(JSContext *ctx, size_t limit); -/* return != 0 if the JS code needs to be interrupted */ -typedef int JSInterruptHandler (JSRuntime *rt, void *opaque); -void JS_SetInterruptHandler (JSContext *ctx, JSInterruptHandler *cb, - void *opaque); +/* Set the pause flag on a context (0=normal, 1=suspend, 2=kill) */ +void JS_SetPauseFlag(JSContext *ctx, int value); JS_BOOL JS_IsLiveObject (JSRuntime *rt, JSValue obj); diff --git a/source/runtime.c b/source/runtime.c index 81e52812..1d7f2558 100644 --- a/source/runtime.c +++ b/source/runtime.c @@ -1879,9 +1879,8 @@ void JS_SetPoolSize (JSRuntime *rt, size_t initial, size_t cap) { #define free(p) free_is_forbidden (p) #define realloc(p, s) realloc_is_forbidden (p, s) -void JS_SetInterruptHandler (JSContext *ctx, JSInterruptHandler *cb, void *opaque) { - ctx->interrupt_handler = cb; - ctx->interrupt_opaque = opaque; +void JS_SetPauseFlag (JSContext *ctx, int value) { + atomic_store_explicit (&ctx->pause_flag, value, memory_order_relaxed); } int JS_GetVMCallDepth(JSContext *ctx) { @@ -5358,8 +5357,7 @@ JSValue js_regexp_toString (JSContext *ctx, JSValue this_val, int argc, JSValue int lre_check_timeout (void *opaque) { JSContext *ctx = opaque; - return (ctx->interrupt_handler - && ctx->interrupt_handler (ctx->rt, ctx->interrupt_opaque)); + return atomic_load_explicit (&ctx->pause_flag, memory_order_relaxed) >= 2; } void *lre_realloc (void *opaque, void *ptr, size_t size) { diff --git a/source/scheduler.c b/source/scheduler.c index 73ad613c..acb4273b 100644 --- a/source/scheduler.c +++ b/source/scheduler.c @@ -22,7 +22,9 @@ typedef struct actor_node { typedef enum { TIMER_JS, - TIMER_NATIVE_REMOVE + TIMER_NATIVE_REMOVE, + TIMER_PAUSE, + TIMER_KILL } timer_type; typedef struct { @@ -30,6 +32,7 @@ typedef struct { cell_rt *actor; uint32_t timer_id; timer_type type; + uint32_t turn_gen; /* generation at registration time */ } timer_node; static timer_node *timer_heap = NULL; @@ -122,7 +125,9 @@ uint32_t actor_remove_cb(cell_rt *actor, uint32_t id, uint32_t interval); void actor_turn(cell_rt *actor); void heap_push(uint64_t when, cell_rt *actor, uint32_t timer_id, timer_type type) { - timer_node node = { .execute_at_ns = when, .actor = actor, .timer_id = timer_id, .type = type }; + timer_node node = { .execute_at_ns = when, .actor = actor, .timer_id = timer_id, .type = type, .turn_gen = 0 }; + if (type == TIMER_PAUSE || type == TIMER_KILL) + node.turn_gen = atomic_load_explicit(&actor->turn_gen, memory_order_relaxed); arrput(timer_heap, node); // Bubble up @@ -192,8 +197,19 @@ void *timer_thread_func(void *arg) { if (t.type == TIMER_NATIVE_REMOVE) { actor_remove_cb(t.actor, t.timer_id, 0); + } else if (t.type == TIMER_PAUSE || t.type == TIMER_KILL) { + /* Only fire if turn_gen still matches (stale timers are ignored) */ + uint32_t cur = atomic_load_explicit(&t.actor->turn_gen, memory_order_relaxed); + if (cur == t.turn_gen) { + if (t.type == TIMER_PAUSE) { + JS_SetPauseFlag(t.actor->context, 1); + } else { + t.actor->disrupt = 1; + JS_SetPauseFlag(t.actor->context, 2); + } + } } else { - pthread_mutex_lock(t.actor->msg_mutex); + pthread_mutex_lock(t.actor->msg_mutex); int idx = hmgeti(t.actor->timers, t.timer_id); if (idx != -1) { JSValue cb = t.actor->timers[idx].value; @@ -343,7 +359,6 @@ void actor_free(cell_rt *actor) arrfree(actor->letters); - JS_SetInterruptHandler(js, NULL, NULL); JS_FreeContext(js); free(actor->id); @@ -628,36 +643,6 @@ const char *register_actor(const char *id, cell_rt *actor, int mainthread, doubl return NULL; } -int actor_interrupt_cb(JSRuntime *rt, cell_rt *crt) -{ - if (engine.shutting_down || crt->disrupt) - return -1; - if (crt->turn_deadline_ns == 0) - return 0; /* no timer set (e.g. during startup) */ - uint64_t now = cell_ns(); - if (now < crt->turn_deadline_ns) - return 0; - if (crt->is_slow_turn) { -#ifdef ACTOR_TRACE - fprintf(stderr, "[ACTOR_TRACE] %s: slow timer expired, killing\n", - crt->name ? crt->name : crt->id); -#endif - crt->disrupt = 1; - return -1; - } - /* Fast timer expired — check if we can suspend */ - if (JS_GetVMCallDepth(crt->context) > 0) { - /* Can't suspend with C frames on stack, switch to slow timer */ - crt->is_slow_turn = 1; - crt->turn_deadline_ns = now + ACTOR_SLOW_TIMER_NS; - return 0; - } -#ifdef ACTOR_TRACE - fprintf(stderr, "[ACTOR_TRACE] %s: fast timer expired, requesting suspend\n", - crt->name ? crt->name : crt->id); -#endif - return 1; -} const char *send_message(const char *id, void *msg) { @@ -699,16 +684,23 @@ void actor_turn(cell_rt *actor) JSValue result; if (actor->vm_suspended) { - /* RESUME path: continue suspended turn under slow timer */ + /* RESUME path: continue suspended turn under kill timer only */ + atomic_fetch_add_explicit(&actor->turn_gen, 1, memory_order_relaxed); + JS_SetPauseFlag(actor->context, 0); actor->turn_start_ns = cell_ns(); - actor->turn_deadline_ns = actor->turn_start_ns + ACTOR_SLOW_TIMER_NS; - actor->is_slow_turn = 1; + + /* Register kill timer only for resume */ + pthread_mutex_lock(&engine.lock); + heap_push(actor->turn_start_ns + ACTOR_SLOW_TIMER_NS, + actor, 0, TIMER_KILL); + pthread_cond_signal(&engine.timer_cond); + pthread_mutex_unlock(&engine.lock); result = JS_ResumeRegisterVM(actor->context); actor->vm_suspended = 0; if (JS_IsSuspended(result)) { - /* Still suspended after slow timer — shouldn't happen, handler returns -1 */ + /* Still suspended after kill timer — shouldn't happen, kill it */ actor->disrupt = 1; goto ENDTURN; } @@ -747,9 +739,18 @@ void actor_turn(cell_rt *actor) arrdel(actor->letters, 0); pthread_mutex_unlock(actor->msg_mutex); + atomic_fetch_add_explicit(&actor->turn_gen, 1, memory_order_relaxed); + JS_SetPauseFlag(actor->context, 0); actor->turn_start_ns = cell_ns(); - actor->turn_deadline_ns = actor->turn_start_ns + ACTOR_FAST_TIMER_NS; - actor->is_slow_turn = 0; + + /* Register both pause and kill timers */ + pthread_mutex_lock(&engine.lock); + heap_push(actor->turn_start_ns + ACTOR_FAST_TIMER_NS, + actor, 0, TIMER_PAUSE); + heap_push(actor->turn_start_ns + ACTOR_SLOW_TIMER_NS + ACTOR_FAST_TIMER_NS, + actor, 0, TIMER_KILL); + pthread_cond_signal(&engine.timer_cond); + pthread_mutex_unlock(&engine.lock); if (l.type == LETTER_BLOB) { size_t size = blob_length(l.blob_data) / 8; @@ -784,6 +785,8 @@ void actor_turn(cell_rt *actor) actor->slow_strikes = 0; /* completed within fast timer */ ENDTURN: + /* Invalidate any outstanding pause/kill timers for this turn */ + atomic_fetch_add_explicit(&actor->turn_gen, 1, memory_order_relaxed); actor->state = ACTOR_IDLE; if (actor->trace_hook) diff --git a/source/scheduler_playdate.c b/source/scheduler_playdate.c index 968e61ba..56af288f 100644 --- a/source/scheduler_playdate.c +++ b/source/scheduler_playdate.c @@ -142,7 +142,6 @@ void actor_free(cell_rt *actor) arrfree(actor->letters); - JS_SetInterruptHandler(js, NULL, NULL); JS_FreeContext(js); free(actor->id); @@ -353,10 +352,6 @@ const char *register_actor(const char *id, cell_rt *actor, int mainthread, doubl return NULL; } -int actor_interrupt_cb(JSRuntime *rt, cell_rt *crt) -{ - return shutting_down || crt->disrupt; -} const char *send_message(const char *id, void *msg) { diff --git a/tests/actor_suspend_resume.ce b/tests/actor_suspend_resume.ce new file mode 100644 index 00000000..387ad1e6 --- /dev/null +++ b/tests/actor_suspend_resume.ce @@ -0,0 +1,20 @@ +// Test: actor is suspended by fast timer and resumed under slow timer, +// completing the computation correctly. +$receiver(function(msg) { + if (msg.sum == 12499997500000) { + print("PASS: actor suspended and resumed correctly") + } else { + print(`FAIL: expected 12499997500000, got ${msg.sum}`) + } + $stop() +}) + +$start(function(event) { + if (event.type == 'greet') { + send(event.actor, {count: 5000000, reply: $self}) + } + if (event.type == 'disrupt') { + print("FAIL: actor was killed instead of completing") + $stop() + } +}, 'tests/slow_compute_actor') diff --git a/tests/slow_compute_actor.ce b/tests/slow_compute_actor.ce new file mode 100644 index 00000000..edb36d9a --- /dev/null +++ b/tests/slow_compute_actor.ce @@ -0,0 +1,11 @@ +// Child actor that does a slow computation and replies with the result +$receiver(function(msg) { + var n = msg.count + var sum = 0 + var i = 0 + while (i < n) { + sum = sum + i + i = i + 1 + } + send(msg.reply, {sum: sum}) +})