From 08557011cbf4a3b47e0a7c4d0089f98ce54ef27b Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Wed, 4 Jun 2025 14:39:58 -0500 Subject: [PATCH] single threads; custom timer; letters --- source/cell.c | 317 +++++++++++++++++++++++++++++++------------------- source/cell.h | 23 +++- 2 files changed, 213 insertions(+), 127 deletions(-) diff --git a/source/cell.c b/source/cell.c index ad59f3c7..407d51a2 100644 --- a/source/cell.c +++ b/source/cell.c @@ -9,6 +9,7 @@ #include #include #include +#include #include @@ -61,6 +62,107 @@ static Uint32 queue_event; static SDL_AtomicInt engine_shutdown; static SDL_Thread **runners = NULL; +// TIMERS +struct timer_entry { uint64_t when_ns; cell_rt *actor; uint32_t id; }; +static struct timer_entry *timers = NULL; +static uint32_t timer_counter = 1; +static SDL_Mutex *timer_mutex = NULL; // protects access to timers + +static inline uint64_t now_ns() +{ + return SDL_GetTicksNS(); +} + +static void process_due_timers(void) +{ + uint64_t tnow = now_ns(); + SDL_LockMutex(timer_mutex); + while (arrlen(timers) && timers[0].when_ns <= tnow) { + struct timer_entry te = timers[0]; + arrdel(timers, 0); + SDL_UnlockMutex(timer_mutex); + + /* Marshal callback into actor mailbox. */ + SDL_LockMutex(te.actor->msg_mutex); + int idx = hmgeti(te.actor->timers, te.id); + if (idx != -1) { + JSValue cb = te.actor->timers[idx].value; /* already duped when stored */ + hmdel(te.actor->timers, te.id); + + letter l; + l.type = LETTER_CALLBACK; + l.callback = cb; + arrput(te.actor->letters, l); + } + SDL_UnlockMutex(te.actor->msg_mutex); + set_actor_state(te.actor); + + SDL_LockMutex(timer_mutex); + } + SDL_UnlockMutex(timer_mutex); +} + +static uint64_t next_timeout_ns(void) +{ + SDL_LockMutex(timer_mutex); + uint64_t timeout = arrlen(timers) ? (timers[0].when_ns - now_ns()) : UINT64_MAX; + SDL_UnlockMutex(timer_mutex); + return timeout; +} + +static void timer_insert(struct timer_entry te) +{ + SDL_LockMutex(timer_mutex); + size_t n = arrlen(timers); + arrput(timers, te); + for (size_t i = n; i > 0 && timers[i].when_ns < timers[i-1].when_ns; --i) { + struct timer_entry tmp = timers[i]; + timers[i] = timers[i-1]; + timers[i-1] = tmp; + } + SDL_UnlockMutex(timer_mutex); +} + +/* Public API used by JS glue (replaces SDL_AddTimer). */ +uint32_t scheduler_add_timer(cell_rt *actor, double seconds, JSValue cb) +{ + uint32_t id = SDL_AtomicIncRef((SDL_AtomicInt*)&timer_counter); + uint64_t deadline = now_ns() + (uint64_t)(seconds * 1e9); + + SDL_LockMutex(timer_mutex); + timer_insert((struct timer_entry){ deadline, actor, id }); + SDL_UnlockMutex(timer_mutex); + + /* Stash in actor map so removal/GC works. Duplicated to hold ref. */ + SDL_LockMutex(actor->msg_mutex); + hmput(actor->timers, id, JS_DupValue(actor->context, cb)); + SDL_UnlockMutex(actor->msg_mutex); + + /* Wake a sleeper if this timer is sooner than the current next. */ + SDL_SignalCondition(queue_cond); + return id; +} + +void scheduler_remove_timer(cell_rt *actor, uint32_t id) +{ + SDL_LockMutex(timer_mutex); + for (int i = 0; i < arrlen(timers); ++i) { + if (timers[i].id == id) { + arrdel(timers, i); + break; + } + } + SDL_UnlockMutex(timer_mutex); + + SDL_LockMutex(actor->msg_mutex); + int idx = hmgeti(actor->timers, id); + if (idx != -1) { + JS_FreeValue(actor->context, actor->timers[idx].value); + hmdel(actor->timers, id); + } + SDL_UnlockMutex(actor->msg_mutex); +} + static void exit_handler(void) { SDL_SetAtomicInt(&engine_shutdown, 1); @@ -115,8 +217,17 @@ void actor_free(cell_rt *actor) for (int i = 0; i < arrlen(actor->js_swapchains); i++) JS_FreeValue(js, actor->js_swapchains[i]); + /* Remove all timers for this actor from the global timer queue */ + SDL_LockMutex(timer_mutex); + for (int i = arrlen(timers) - 1; i >= 0; i--) { + if (timers[i].actor == actor) { + arrdel(timers, i); + } + } + SDL_UnlockMutex(timer_mutex); + + /* Free timer callbacks stored in actor */ for (int i = 0; i < hmlen(actor->timers); i++) { - SDL_RemoveTimer(actor->timers[i].key); JS_FreeValue(js, actor->timers[i].value); } @@ -124,16 +235,16 @@ void actor_free(cell_rt *actor) arrfree(actor->js_swapchains); arrfree(actor->module_registry); - for (int i = 0; i < arrlen(actor->messages); i++) - free(actor->messages[i]); - - arrfree(actor->messages); - - /* If still present, free each JSValue. */ - for (int i = 0; i < arrlen(actor->events); i++) - JS_FreeValue(js, actor->events[i]); - - arrfree(actor->events); + /* Free all letters in the queue */ + for (int i = 0; i < arrlen(actor->letters); i++) { + if (actor->letters[i].type == LETTER_WOTA) { + free(actor->letters[i].wota_data); + } else if (actor->letters[i].type == LETTER_CALLBACK) { + JS_FreeValue(js, actor->letters[i].callback); + } + } + + arrfree(actor->letters); JSRuntime *rt = JS_GetRuntime(js); JS_SetInterruptHandler(rt, NULL, NULL); @@ -331,8 +442,7 @@ cell_rt *create_actor(void *wota) actor->unneeded = JS_UNDEFINED; actor->on_exception = JS_UNDEFINED; - arrsetcap(actor->messages, 5); - arrsetcap(actor->events, 5); + arrsetcap(actor->letters, 5); actor->mutex = SDL_CreateMutex(); /* Protects JSContext + state */ actor->msg_mutex = SDL_CreateMutex(); /* Mailbox queue lock */ @@ -384,8 +494,12 @@ const char *send_message(const char *id, void *msg) return "Could not get actor from id."; } + letter l; + l.type = LETTER_WOTA; + l.wota_data = msg; + SDL_LockMutex(target->msg_mutex); - arrput(target->messages, msg); + arrput(target->letters, l); if (target->ar) { SDL_RemoveTimer(target->ar); target->ar = 0; @@ -397,7 +511,7 @@ const char *send_message(const char *id, void *msg) return NULL; } -/* set_actor_state should check if either messages or events are pending. */ +/* set_actor_state should check if any letters are pending. */ void set_actor_state(cell_rt *actor) { SDL_LockMutex(actor->msg_mutex); @@ -408,8 +522,7 @@ void set_actor_state(cell_rt *actor) return; } - int has_messages = arrlen(actor->messages); - int has_events = arrlen(actor->events); + int has_letters = arrlen(actor->letters); int has_upcoming = hmlen(actor->timers); if (actor->state == ACTOR_RUNNING) @@ -417,7 +530,7 @@ void set_actor_state(cell_rt *actor) switch(actor->state) { case ACTOR_IDLE: - if (has_messages || has_events) { + if (has_letters) { actor->state = ACTOR_READY; SDL_LockMutex(queue_mutex); if (actor->main_thread_only) { @@ -435,7 +548,7 @@ void set_actor_state(cell_rt *actor) break; case ACTOR_READY: - if (!has_messages && !has_events) { + if (!has_letters) { actor->state = ACTOR_IDLE; goto END; } @@ -460,45 +573,48 @@ void actor_turn(cell_rt *actor, int greedy) SDL_LockMutex(actor->msg_mutex); actor->state = ACTOR_RUNNING; - int msgs = 0; - int events = 0; + int letters_count = 0; int need_stop = 0; JSValue result; - msgs = arrlen(actor->messages); - events = arrlen(actor->events); + letters_count = arrlen(actor->letters); need_stop = actor->need_stop; SDL_UnlockMutex(actor->msg_mutex); if (need_stop) goto KILL; - if (!msgs && !events) goto END; - if (!msgs) goto EVENT; + if (!letters_count) goto END; - MESSAGE: + PROCESS_LETTER: SDL_LockMutex(actor->msg_mutex); - void *msg = actor->messages[0]; - arrdel(actor->messages,0); + letter l = actor->letters[0]; + arrdel(actor->letters, 0); SDL_UnlockMutex(actor->msg_mutex); SDL_LockMutex(actor->mutex); - JSValue arg = wota2value(actor->context, msg); - free(msg); - result = JS_Call(actor->context, actor->message_handle, JS_UNDEFINED, 1, &arg); - uncaught_exception(actor->context, result); - JS_FreeValue(actor->context,arg); + + if (l.type == LETTER_WOTA) { + JSValue arg = wota2value(actor->context, l.wota_data); + free(l.wota_data); + result = JS_Call(actor->context, actor->message_handle, JS_UNDEFINED, 1, &arg); + uncaught_exception(actor->context, result); + JS_FreeValue(actor->context, arg); + } else if (l.type == LETTER_CALLBACK) { + result = JS_Call(actor->context, l.callback, JS_UNDEFINED, 0, NULL); + uncaught_exception(actor->context, result); + JS_FreeValue(actor->context, l.callback); + } + SDL_UnlockMutex(actor->mutex); if (!greedy) goto END; SDL_LockMutex(actor->msg_mutex); need_stop = actor->need_stop; - msgs = arrlen(actor->messages); - events = arrlen(actor->events); + letters_count = arrlen(actor->letters); SDL_UnlockMutex(actor->msg_mutex); if (need_stop) goto KILL; - - if (!msgs && !events) goto END; + if (!letters_count) goto END; SDL_LockMutex(queue_mutex); int queuen = arrlen(ready_queue); @@ -507,39 +623,7 @@ void actor_turn(cell_rt *actor, int greedy) if (queuen != 0) goto END; - if (msgs) - goto MESSAGE; - - EVENT: - SDL_LockMutex(actor->msg_mutex); - JSValue event = actor->events[0]; - arrdel(actor->events, 0); - SDL_UnlockMutex(actor->msg_mutex); - - SDL_LockMutex(actor->mutex); - result = JS_Call(actor->context, event, JS_UNDEFINED, 0, NULL); - uncaught_exception(actor->context, result); - JS_FreeValue(actor->context, event); - SDL_UnlockMutex(actor->mutex); - - if (!greedy) goto END; - - SDL_LockMutex(actor->msg_mutex); - events = arrlen(actor->events); - need_stop = actor->need_stop; - SDL_UnlockMutex(actor->msg_mutex); - - if (need_stop) goto KILL; - if (!events) goto END; - - SDL_LockMutex(queue_mutex); - int n = arrlen(ready_queue); - SDL_UnlockMutex(queue_mutex); - - if (n != 0) - goto END; - - goto EVENT; + goto PROCESS_LETTER; END: #ifdef TRACY_ENABLE @@ -559,22 +643,6 @@ void actor_turn(cell_rt *actor, int greedy) actor_free(actor); } -/* Timer callback adds an event to the queue under evt_mutex. */ -Uint32 actor_timer_cb(cell_rt *actor, SDL_TimerID id, Uint32 interval) -{ - SDL_LockMutex(actor->msg_mutex); - int idx = hmgeti(actor->timers, id); - if (idx == -1) goto END; - - JSValue cb = actor->timers[idx].value; - hmdel(actor->timers, id); - arrput(actor->events, cb); - set_actor_state(actor); - - END: - SDL_UnlockMutex(actor->msg_mutex); - return 0; -} /* JS function that schedules a timer. */ JSValue js_actor_delay(JSContext *js, JSValue self, int argc, JSValue *argv) @@ -587,18 +655,16 @@ JSValue js_actor_delay(JSContext *js, JSValue self, int argc, JSValue *argv) JS_ToFloat64(js, &seconds, argv[1]); if (seconds <= 0) { SDL_LockMutex(actor->msg_mutex); - JSValue cb = JS_DupValue(js, argv[0]); - arrput(actor->events, cb); + letter l; + l.type = LETTER_CALLBACK; + l.callback = JS_DupValue(js, argv[0]); + arrput(actor->letters, l); SDL_UnlockMutex(actor->msg_mutex); return JS_NewInt32(js, -1); } - Uint64 ns = seconds * SDL_NS_PER_SECOND; - - Uint32 id = SDL_AddTimerNS(ns, actor_timer_cb, actor); + uint32_t id = scheduler_add_timer(actor, seconds, argv[0]); SDL_LockMutex(actor->msg_mutex); - JSValue cb = JS_DupValue(js, argv[0]); - hmput(actor->timers, id, cb); if (actor->ar) { SDL_RemoveTimer(actor->ar); actor->ar = 0; @@ -611,23 +677,11 @@ JSValue js_actor_delay(JSContext *js, JSValue self, int argc, JSValue *argv) JSValue js_actor_removetimer(JSContext *js, JSValue self, int argc, JSValue *argv) { cell_rt *actor = JS_GetContextOpaque(js); - Uint32 timer_id; + uint32_t timer_id; JS_ToUint32(js, &timer_id, argv[0]); - if (timer_id == -1) return JS_UNDEFINED; + if (timer_id == (uint32_t)-1) return JS_UNDEFINED; - SDL_RemoveTimer(timer_id); - - JSValue cb = JS_UNDEFINED; - - SDL_LockMutex(actor->msg_mutex); - int id = hmgeti(actor->timers, timer_id); - if (id != -1) { - cb = actor->timers[id].value; - hmdel(actor->timers, timer_id); - } - SDL_UnlockMutex(actor->msg_mutex); - - JS_FreeValue(js,cb); + scheduler_remove_timer(actor, timer_id); return JS_UNDEFINED; } @@ -1567,6 +1621,7 @@ int main(int argc, char **argv) queue_cond = SDL_CreateCondition(); actors_mutex = SDL_CreateMutex(); event_watchers_mutex = SDL_CreateMutex(); + timer_mutex = SDL_CreateMutex(); /* Create the initial actor from the command line */ int actor_argc = argc - script_start; @@ -1597,25 +1652,43 @@ int main(int argc, char **argv) signal(SIGSEGV, signal_handler); signal(SIGABRT, signal_handler); atexit(exit_handler); - - SDL_AddEventWatch(event_watch, NULL); - + /* Main loop: pump ready actors */ SDL_Event event; - while (SDL_WaitEvent(&event)) { - if (event.type != queue_event) continue; - - QUEUE: - SDL_LockMutex(queue_mutex); - if (arrlen(main_ready_queue) == 0) { - SDL_UnlockMutex(queue_mutex); + const uint64_t fallback_sleep = 1000000ULL; /* 1 ms */ + + while (!SDL_GetAtomicInt(&engine_shutdown)) { + process_due_timers(); // Process any due timers first + + /* Check for SDL events with timeout */ + uint64_t to_ns = next_timeout_ns(); + if (to_ns == UINT64_MAX || to_ns > fallback_sleep) to_ns = fallback_sleep; + + /* Convert nanoseconds to milliseconds for SDL_WaitEventTimeout */ + int timeout_ms = (int)(to_ns / 1000000ULL); + if (timeout_ms < 1) timeout_ms = 1; + + if (SDL_WaitEventTimeout(&event, timeout_ms)) { + if (event.type == queue_event) { + goto QUEUE; + } + /* Process other SDL events as needed */ continue; } - cell_rt *actor = main_ready_queue[0]; - arrdel(main_ready_queue, 0); + + /* Timeout occurred - check queues anyway */ + QUEUE: + SDL_LockMutex(queue_mutex); + cell_rt *actor = NULL; + if (arrlen(main_ready_queue) > 0) { + actor = main_ready_queue[0]; + arrdel(main_ready_queue, 0); + } SDL_UnlockMutex(queue_mutex); - - actor_turn(actor, 0); + + if (actor) { + actor_turn(actor, 0); + } } return 0; diff --git a/source/cell.h b/source/cell.h index e7f1885a..658fb3f2 100644 --- a/source/cell.h +++ b/source/cell.h @@ -7,6 +7,20 @@ #include "qjs_blob.h" #include "blob.h" +/* Letter type for unified message queue */ +typedef enum { + LETTER_WOTA, /* Raw wota message */ + LETTER_CALLBACK /* JSValue callback function */ +} letter_type; + +typedef struct letter { + letter_type type; + union { + void *wota_data; /* For LETTER_WOTA */ + JSValue callback; /* For LETTER_CALLBACK */ + }; +} letter; + #define STATE_VECTOR_LENGTH 624 #define STATE_VECTOR_M 397 @@ -44,18 +58,17 @@ typedef struct cell_rt { /* Protects JSContext usage */ SDL_Mutex *mutex; - SDL_Mutex *turn; - + SDL_Mutex *msg_mutex; /* For messages queue only */ + char *id; MTRand mrand; int idx_count; /* The “mailbox” for incoming messages + a dedicated lock for it: */ - void **messages; - JSValue *events; - SDL_Mutex *msg_mutex; /* For messages queue only */ + struct letter *letters; + /* CHANGED FOR EVENTS: a separate lock for the actor->events queue */ struct { Uint32 key; JSValue value; } *timers;