From 6cfba975e56955ed3357452cbe19e7f08debc379 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Mon, 1 Dec 2025 15:38:04 -0600 Subject: [PATCH] remove sdl dependency --- CLAUDE.md | 2 +- Makefile | 4 + docs/tutorial.md | 2 +- meson.build | 66 +--- scripts/cellfs.cm | 4 +- scripts/fd.c | 56 +-- scripts/os.c | 29 +- scripts/qop.c | 4 +- source/cell.c | 539 +--------------------------- source/cell.h | 10 - source/cell_internal.h | 92 +++++ source/qjs_actor.c | 24 ++ source/quickjs.c | 3 +- source/scheduler_sdl.c | 1 - source/scheduler_threaded.c | 691 ++++++++++++++++++++++++++++++++++++ 15 files changed, 876 insertions(+), 651 deletions(-) create mode 100644 source/cell_internal.h delete mode 100644 source/scheduler_sdl.c create mode 100644 source/scheduler_threaded.c diff --git a/CLAUDE.md b/CLAUDE.md index 374ba109..ddc329d8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -129,7 +129,7 @@ meson test -C build_dbg ## File I/O - `io.slurp(path)` - Reads a file as text -- `io.slurpbytes(path)` - Reads a file as an ArrayBuffer +- `io.slurp(path)` - Reads a file as an ArrayBuffer - `io.slurpwrite(path, data)` - Writes data (string or ArrayBuffer) to a file - `io.exists(path)` - Checks if a file exists diff --git a/Makefile b/Makefile index 22b97c04..cc05a720 100755 --- a/Makefile +++ b/Makefile @@ -14,6 +14,10 @@ sanitize: FORCE meson setup -Db_sanitize=address -Db_sanitize=memory -Db_sanitize=leak -Db_sanitize=undefined build_sani meson install -C build_sani +thread: FORCE + meson setup build_thread -Db_sanitize=thread -Dbuildtype=debugoptimized + meson install --only-changed -C build_thread + small: FORCE meson setup -Dbuildtype=minsize -Db_lto=true -Db_ndebug=true build_small meson install -C build_small diff --git a/docs/tutorial.md b/docs/tutorial.md index 9c1b49d2..cc9a2d4b 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -11,7 +11,7 @@ The ```config.js``` module must return a single object that describes your game. | **title** | `Prosperon [\${prosperon.version}-\${prosperon.revision}]` | string | Title of the game window, typically including version information. | | **width** | `1280` | number | Initial width of the game window. | | **height** | `720` | number | Initial height of the game window. | -| **icon** | `graphics.make_texture(io.slurpbytes('icons/moon.gif'))` | object | Icon texture for the game window, loaded from the provided file. | +| **icon** | `graphics.make_texture(io.slurp('icons/moon.gif'))` | object | Icon texture for the game window, loaded from the provided file. | | **high_dpi** | `0` | number | Enables (1) or disables (0) High DPI support for the window. | | **alpha** | `1` | number | Alpha channel setting for the window (0 to disable, 1 to enable transparency). | | **fullscreen** | `0` | number | Sets whether the window should launch in fullscreen (1) or windowed (0). | diff --git a/meson.build b/meson.build index 5673765f..56c97f90 100644 --- a/meson.build +++ b/meson.build @@ -65,17 +65,6 @@ endif cmake = import('cmake') -sdl3_opts = cmake.subproject_options() -sdl3_opts.add_cmake_defines({ - 'SDL_STATIC': 'ON', - 'SDL_SHARED': 'OFF', - 'SDL_TEST': 'OFF', - 'CMAKE_BUILD_TYPE': 'Release', - 'SDL_THREADS': 'ON', - 'SDL_PIPEWIRE': 'ON', - 'SDL_PULSEAUDIO': 'ON', -}) - cc = meson.get_compiler('c') if host_machine.system() == 'linux' @@ -93,63 +82,10 @@ if host_machine.system() == 'windows' deps += cc.find_library('version') deps += cc.find_library('cfgmgr32') deps += cc.find_library('bcrypt') - sdl3_opts.add_cmake_defines({'HAVE_ISINF': '1'}) # Hack for MSYS2 - sdl3_opts.add_cmake_defines({'HAVE_ISNAN': '1'}) link += ['-static', '-static-libgcc', '-static-libstdc++'] add_project_link_arguments('-static-libgcc', '-static-libstdc++', language: ['c', 'cpp']) endif -if host_machine.system() == 'emscripten' - message('⚙ Building SDL3 subproject for Emscripten...') - sdl3_opts.append_compile_args( - 'c', - '-pthread', - '-sUSE_PTHREADS=1', - ) - sdl3_opts.append_compile_args( - 'cpp', - '-pthread', - '-sUSE_PTHREADS=1', - ) - - # 3. And into every link step - sdl3_opts.append_link_args( - '-pthread', - '-sUSE_PTHREADS=1', - '-sPTHREAD_POOL_SIZE=4', - ) - - sdl3_proj = cmake.subproject('sdl3', options: sdl3_opts) - deps += sdl3_proj.dependency('SDL3-static') - - add_project_arguments('-DPATH_MAX=4096', language: 'c') - - add_project_arguments( - '-pthread', - '-sUSE_PTHREADS=1', - '-sPTHREAD_POOL_SIZE=4', - language: ['c', 'cpp']) - - add_project_link_arguments( - '--use-port=emdawnwebgpu', - '-sUSE_PTHREADS=1', - '-pthread', - '-sPTHREAD_POOL_SIZE=4', - '-sMALLOC=mimalloc', - language: ['c','cpp']) -else - # Try to find system-installed SDL3 first - sdl3_dep = dependency('sdl3', static: true, required: false) - - if not sdl3_dep.found() - message('⚙ System SDL3 not found, building subproject...') - sdl3_proj = cmake.subproject('sdl3', options : sdl3_opts) - deps += sdl3_proj.dependency('SDL3-static') - else - deps += sdl3_dep - endif -endif - miniz_dep = dependency('miniz', static: true, required: false) if not miniz_dep.found() message('⚙ System miniz not found, building subproject...') @@ -216,7 +152,7 @@ src += [ # core 'wildmatch.c', 'qjs_actor.c', 'qjs_wota.c', - 'scheduler.c' + 'scheduler_threaded.c', ] src += ['quickjs.c', 'libregexp.c', 'libunicode.c', 'cutils.c', 'dtoa.c'] diff --git a/scripts/cellfs.cm b/scripts/cellfs.cm index bc1bf2a8..d6535381 100644 --- a/scripts/cellfs.cm +++ b/scripts/cellfs.cm @@ -456,7 +456,7 @@ function globfs(globs, dir) { return results } -function slurpbytes(path) { +function slurp(path) { return slurp(path) } @@ -479,7 +479,7 @@ cellfs.writepath = set_writepath cellfs.basedir = basedir cellfs.prefdir = prefdir cellfs.realdir = realdir -cellfs.slurpbytes = slurpbytes +cellfs.slurp = slurp cellfs.mount('.') diff --git a/scripts/fd.c b/scripts/fd.c index d36e67fb..28f3a6a4 100644 --- a/scripts/fd.c +++ b/scripts/fd.c @@ -55,7 +55,6 @@ static ssize_t js_fd_write_helper(JSContext *js, int fd, JSValue val) // POSIX FILE DESCRIPTOR FUNCTIONS - JSC_SCALL(fd_open, int flags = O_RDWR | O_CREAT; mode_t mode = 0644; @@ -338,10 +337,15 @@ JSC_CCALL(fd_fstat, return obj; ) -JSC_SCALL(fd_stat, +JSC_CCALL(fd_stat, + const char *path = JS_ToCString(js, argv[0]); + if (!path) return JS_EXCEPTION; + struct stat st; - if (stat(str, &st) != 0) + if (stat(path, &st) != 0) { + JS_FreeCString(js, path); return JS_NewObject(js); + } JSValue obj = JS_NewObject(js); JS_SetPropertyStr(js, obj, "size", JS_NewInt64(js, st.st_size)); @@ -372,6 +376,7 @@ JSC_SCALL(fd_stat, JS_SetPropertyStr(js, obj, "isCharDevice", JS_NewBool(js, S_ISCHR(st.st_mode))); JS_SetPropertyStr(js, obj, "isBlockDevice", JS_NewBool(js, S_ISBLK(st.st_mode))); + JS_FreeCString(js, path); return obj; ) @@ -410,17 +415,31 @@ JSC_SCALL(fd_readdir, #endif ) -JSC_SCALL(fd_is_file, +JSC_CCALL(fd_is_file, + const char *path = JS_ToCString(js, argv[0]); + if (!path) return JS_EXCEPTION; + struct stat st; - if (stat(str, &st) != 0) + if (stat(path, &st) != 0) { + JS_FreeCString(js, path); return JS_NewBool(js, false); + } + + JS_FreeCString(js, path); return JS_NewBool(js, S_ISREG(st.st_mode)); ) -JSC_SCALL(fd_is_dir, +JSC_CCALL(fd_is_dir, + const char *path = JS_ToCString(js, argv[0]); + if (!path) return JS_EXCEPTION; + struct stat st; - if (stat(str, &st) != 0) + if (stat(path, &st) != 0) { + JS_FreeCString(js, path); return JS_NewBool(js, false); + } + + JS_FreeCString(js, path); return JS_NewBool(js, S_ISDIR(st.st_mode)); ) @@ -499,33 +518,22 @@ static void visit_directory(JSContext *js, JSValue results, int *result_count, c #endif } -JSC_CCALL(fd_enumerate, - const char *path = NULL; +JSC_SCALL(fd_enumerate, + const char *path = str; + if (!path) path = "."; int recurse = 0; - if (argc > 0 && JS_IsString(argv[0])) { - path = JS_ToCString(js, argv[0]); - } else { - path = "."; - } - - if (argc > 1) { + if (argc > 1) recurse = JS_ToBool(js, argv[1]); - } JSValue results = JS_NewArray(js); int result_count = 0; struct stat st; - if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) { + if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) visit_directory(js, results, &result_count, path, "", recurse); - } - if (path != NULL && strcmp(path, ".") != 0) { - JS_FreeCString(js, path); - } - - return results; + ret = results; ) static const JSCFunctionListEntry js_fd_funcs[] = { diff --git a/scripts/os.c b/scripts/os.c index 7681f78b..d2440875 100644 --- a/scripts/os.c +++ b/scripts/os.c @@ -48,36 +48,43 @@ static JSClassDef js_dylib_class = { #include -JSC_CCALL(os_now, +uint64_t cell_ns() +{ #ifdef _WIN32 LARGE_INTEGER frequency, counter; QueryPerformanceFrequency(&frequency); QueryPerformanceCounter(&counter); - return number2js(js, (double)counter.QuadPart / (double)frequency.QuadPart); + return (uint64_t)((double)counter.QuadPart / (double)frequency.QuadPart * 1e9); #elif defined(__APPLE__) static mach_timebase_info_data_t timebase = {0, 0}; if (timebase.denom == 0) { mach_timebase_info(&timebase); } uint64_t time = mach_absolute_time(); - return number2js(js, (double)time * timebase.numer / timebase.denom / 1000000000.0); + return time * timebase.numer / timebase.denom; #else struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); - return number2js(js, (double)ts.tv_sec + (double)ts.tv_nsec / 1000000000.0); + return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec; #endif -) +} -JSC_CCALL(os_sleep, - double secs = js2number(js,argv[0]); +JSC_CCALL(os_now, return number2js(js, cell_ns()); ) + +void cell_sleep(double seconds) +{ #ifdef _WIN32 - Sleep((DWORD)(secs * 1000)); + Sleep((DWORD)(seconds * 1000)); #else struct timespec ts; - ts.tv_sec = (time_t)secs; - ts.tv_nsec = (long)((secs - ts.tv_sec) * 1000000000); + ts.tv_sec = (time_t)seconds; + ts.tv_nsec = (long)((seconds - ts.tv_sec) * 1000000000); nanosleep(&ts, NULL); #endif +} +JSC_CCALL(os_sleep, + double secs = js2number(js,argv[0]); + cell_sleep(secs); ) static JSValue js_os_totalmem(JSContext *js, JSValue self, int argc, JSValue *argv) { @@ -319,7 +326,7 @@ JSC_SCALL(os_system, ret = number2js(js,err); ) -JSC_SCALL(os_exit, +JSC_CCALL(os_exit, exit(0); ) diff --git a/scripts/qop.c b/scripts/qop.c index 9988b2b8..6cd0e503 100644 --- a/scripts/qop.c +++ b/scripts/qop.c @@ -103,7 +103,7 @@ static int js_qop_ensure_index(JSContext *js, qop_desc *qop) { return 1; } -JSC_SCALL(qop_open, +JSC_CCALL(qop_open, size_t len; void *data = js_get_blob_data(js, &len, argv[0]); if (!data) @@ -126,7 +126,7 @@ JSC_SCALL(qop_open, } ) -JSC_SCALL(qop_write, +JSC_CCALL(qop_write, const char *path = JS_ToCString(js, argv[0]); if (!path) return JS_EXCEPTION; diff --git a/source/cell.c b/source/cell.c index a00522c4..5ec456f7 100644 --- a/source/cell.c +++ b/source/cell.c @@ -11,8 +11,6 @@ #include #endif -#include - #define WOTA_IMPLEMENTATION #include "wota.h" @@ -50,148 +48,10 @@ int tracy_profiling_enabled = 0; #define ENGINE "engine.cm" -static cell_rt **ready_queue = NULL; -static SDL_Semaphore *ready_sem; -static SDL_SpinLock queue_lock = 0; - -static cell_rt **main_queue = NULL; -static SDL_Semaphore *main_sem; -static SDL_SpinLock main_queue_lock = 0; - -static SDL_Mutex *actors_mutex = NULL; -static struct { char *key; cell_rt *value; } *actors = NULL; - static qop_desc qop_core; static qop_file *qop_hashmap = NULL; cell_rt *root_cell = NULL; -static SDL_AtomicInt shutting_down; -static SDL_AtomicInt runners_count; - -static void exit_handler(void) -{ - SDL_SetAtomicInt(&shutting_down, 1); - - /* Signal all waiting threads */ - int count = SDL_GetAtomicInt(&runners_count); - for (int i = 0; i < count; i++) - SDL_SignalSemaphore(ready_sem); - - /* Signal main thread in case it's waiting */ - SDL_SignalSemaphore(main_sem); - - /* Wait for all runner threads to exit */ - while (SDL_GetAtomicInt(&runners_count) > 0) { - SDL_Delay(10); - } - - if (ready_sem) - SDL_DestroySemaphore(ready_sem); - if (main_sem) - SDL_DestroySemaphore(main_sem); - if (actors_mutex) - SDL_DestroyMutex(actors_mutex); - - /* Clean up QOP resources */ - if (qop_hashmap) { - free(qop_hashmap); - qop_hashmap = NULL; - } - if (qop_core.data) { - free(qop_core.data); - qop_core.data = NULL; - } - qop_close(&qop_core); - - SDL_Quit(); - exit(0); -} - -void actor_free(cell_rt *actor) -{ - // Delete it out of actors first so it can no longer get messages - SDL_LockMutex(actors_mutex); - shdel(actors, actor->id); - SDL_UnlockMutex(actors_mutex); - - // If in a queue, remove it - SDL_LockSpinlock(&queue_lock); - for (int i = 0; i < arrlen(ready_queue); i++) { - if (ready_queue[i] == actor) { - arrdel(ready_queue, i); - break; - } - } - SDL_UnlockSpinlock(&queue_lock); - - SDL_LockSpinlock(&main_queue_lock); - for (int i = 0; i < arrlen(main_queue); i++) { - if (main_queue[i] == actor) { - arrdel(main_queue, i); - break; - } - } - SDL_UnlockSpinlock(&main_queue_lock); - - // Do not go forward with actor destruction until the actor is completely free - SDL_LockMutex(actor->msg_mutex); - SDL_LockMutex(actor->mutex); - - JSContext *js = actor->context; - - JS_FreeValue(js, actor->idx_buffer); - JS_FreeValue(js, actor->message_handle); - JS_FreeValue(js, actor->on_exception); - JS_FreeValue(js, actor->unneeded); - JS_FreeAtom(js, actor->actor_sym); - - SDL_RemoveTimer(actor->ar); - - /* Free timer callbacks stored in actor */ - for (int i = 0; i < hmlen(actor->timers); i++) { - JS_FreeValue(js, actor->timers[i].value); - } - - hmfree(actor->timers); - - /* Free all letters in the queue */ - for (int i = 0; i < arrlen(actor->letters); i++) { - if (actor->letters[i].type == LETTER_BLOB) { - blob_destroy(actor->letters[i].blob_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); - JS_FreeContext(js); - JS_FreeRuntime(rt); - free(actor->id); - - SDL_UnlockMutex(actor->mutex); - SDL_DestroyMutex(actor->mutex); - SDL_UnlockMutex(actor->msg_mutex); - SDL_DestroyMutex(actor->msg_mutex); - -#ifdef HAVE_MIMALLOC - mi_heap_destroy(actor->heap); -#endif - free(actor); - - SDL_LockMutex(actors_mutex); - if (shlen(actors) == 0) - exit(0); - SDL_UnlockMutex(actors_mutex); -} - -void js_dofree(JSRuntime *rt, void *opaque, void *ptr) -{ - js_free_rt(rt, ptr); -} - static size_t js_mi_malloc_usable_size(const void *ptr) { #if defined(__APPLE__) @@ -419,298 +279,6 @@ int prosperon_mount_core(void) return 1; } -void actor_unneeded(cell_rt *actor, JSValue fn, double seconds) -{ - if (actor->disrupt) return; - JS_FreeValue(actor->context, actor->unneeded); - - if (!JS_IsFunction(actor->context, fn)) { - actor->unneeded = JS_NULL; - goto END; - } - - actor->unneeded = JS_DupValue(actor->context, fn); - actor->ar_secs = seconds; - - END: - if (actor->ar) { - SDL_RemoveTimer(actor->ar); - actor->ar = 0; - } - set_actor_state(actor); -} - -cell_rt *create_actor(void *wota) -{ - cell_rt *actor = calloc(sizeof(*actor), 1); -#ifdef HAVE_MIMALLOC - actor->heap = mi_heap_new(); -#endif - actor->init_wota = wota; - actor->idx_buffer = JS_NULL; - actor->message_handle = JS_NULL; - actor->unneeded = JS_NULL; - actor->on_exception = JS_NULL; - actor->actor_sym = JS_ATOM_NULL; - - arrsetcap(actor->letters, 5); - - actor->mutex = SDL_CreateMutex(); /* Protects JSContext + state */ - actor->msg_mutex = SDL_CreateMutex(); /* Mailbox queue lock */ - - /* Lock actor->mutex while initializing JS runtime. */ - SDL_LockMutex(actor->mutex); - script_startup(actor); - set_actor_state(actor); - SDL_UnlockMutex(actor->mutex); - - return actor; -} - -cell_rt *get_actor(char *id) -{ - SDL_LockMutex(actors_mutex); - int idx = shgeti(actors, id); - if (idx == -1) { - SDL_UnlockMutex(actors_mutex); - return NULL; - } - cell_rt *actor = actors[idx].value; - SDL_UnlockMutex(actors_mutex); - return actor; -} - -const char *register_actor(const char *id, cell_rt *actor, int mainthread, double ar) -{ - SDL_LockMutex(actors_mutex); - if (shgeti(actors, id) != -1) { - SDL_UnlockMutex(actors_mutex); - return "Actor with given ID already exists."; - } - actor->main_thread_only = mainthread; - actor->id = strdup(id); - actor->ar_secs = ar; - shput(actors, id, actor); - SDL_UnlockMutex(actors_mutex); - return NULL; -} - -const char *send_message(const char *id, void *msg) -{ - cell_rt *target = get_actor(id); - if (!target) { - blob_destroy((blob *)msg); - return "Could not get actor from id."; - } - - letter l; - l.type = LETTER_BLOB; - l.blob_data = (blob *)msg; - - SDL_LockMutex(target->msg_mutex); - - arrput(target->letters, l); - if (target->ar) { - SDL_RemoveTimer(target->ar); - target->ar = 0; - } - SDL_UnlockMutex(target->msg_mutex); - - set_actor_state(target); - - return NULL; -} - -static Uint32 actor_remove_cb(cell_rt *actor, Uint32 id, Uint32 interval) -{ - actor->disrupt = 1; - - if (!JS_IsNull(actor->unneeded)) { - SDL_LockMutex(actor->mutex); - JSValue ret = JS_Call(actor->context, actor->unneeded, JS_NULL, 0, NULL); - uncaught_exception(actor->context, ret); - SDL_UnlockMutex(actor->mutex); - } - - actor_free(actor); - return 0; -} - -/* Timer callback adds an event to the queue under evt_mutex. */ -Uint32 actor_delay_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); - actor_clock(actor, cb); - JS_FreeValue(actor->context, cb); - - END: - SDL_UnlockMutex(actor->msg_mutex); - return 0; -} - -void set_actor_state(cell_rt *actor) -{ - if (actor->disrupt) { - actor_free(actor); - return; - } - SDL_LockMutex(actor->msg_mutex); - - switch(actor->state) { - case ACTOR_RUNNING: - case ACTOR_READY: - if (actor->ar) { - SDL_RemoveTimer(actor->ar); - actor->ar = 0; - } - break; - - case ACTOR_IDLE: - if (arrlen(actor->letters)) { - actor->state = ACTOR_READY; - if (actor->main_thread_only) { - SDL_LockSpinlock(&main_queue_lock); - arrput(main_queue, actor); - SDL_UnlockSpinlock(&main_queue_lock); - SDL_SignalSemaphore(main_sem); - } else { - SDL_LockSpinlock(&queue_lock); - arrput(ready_queue, actor); - SDL_UnlockSpinlock(&queue_lock); - SDL_SignalSemaphore(ready_sem); - } - } else if (!arrlen(actor->letters) && !hmlen(actor->timers)) - actor->ar = SDL_AddTimerNS(actor->ar_secs*1e9, actor_remove_cb, actor); - break; - } - - SDL_UnlockMutex(actor->msg_mutex); -} - -void actor_turn(cell_rt *actor) -{ - SDL_LockMutex(actor->mutex); - -#ifdef TRACY_ENABLE - int entered = 0; - if (tracy_profiling_enabled && TracyCIsConnected) { - TracyCFiberEnter(actor->name); - entered = 1; - } -#endif - - actor->state = ACTOR_RUNNING; - - TAKETURN: - - SDL_LockMutex(actor->msg_mutex); - JSValue result; - if (!arrlen(actor->letters)) { - SDL_UnlockMutex(actor->msg_mutex); - goto ENDTURN; - } - letter l = actor->letters[0]; - arrdel(actor->letters, 0); - SDL_UnlockMutex(actor->msg_mutex); - - if (l.type == LETTER_BLOB) { - // Create a JS blob from the C blob - size_t size = l.blob_data->length / 8; // Convert bits to bytes - JSValue arg = js_new_blob_stoned_copy(actor->context, l.blob_data->data, size); - blob_destroy(l.blob_data); - result = JS_Call(actor->context, actor->message_handle, JS_NULL, 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_NULL, 0, NULL); - uncaught_exception(actor->context, result); - JS_FreeValue(actor->context, l.callback); - } - - if (actor->disrupt) goto ENDTURN; - - // If there are no waiting threads, bail. otherwise, try for another turn - SDL_LockSpinlock(&queue_lock); - int someone_else_waiting = (arrlen(ready_queue) > 0); - SDL_UnlockSpinlock(&queue_lock); - - if (!someone_else_waiting) goto TAKETURN; - - ENDTURN: - actor->state = ACTOR_IDLE; - -#ifdef TRACY_ENABLE - if (tracy_profiling_enabled && entered) - TracyCFiberLeave(actor->name); -#endif - - set_actor_state(actor); - - SDL_UnlockMutex(actor->mutex); -} - -void actor_clock(cell_rt *actor, JSValue fn) -{ - SDL_LockMutex(actor->msg_mutex); - letter l; - l.type = LETTER_CALLBACK; - l.callback = JS_DupValue(actor->context, fn); - arrput(actor->letters, l); - SDL_UnlockMutex(actor->msg_mutex); - set_actor_state(actor); -} - -/* JS function that schedules a timer. */ -JSValue js_actor_delay(JSContext *js, JSValue self, int argc, JSValue *argv) -{ - if (!JS_IsFunction(js, argv[0])) - return JS_ThrowReferenceError(js, "Argument must be a function."); - - cell_rt *actor = JS_GetContextOpaque(js); - double seconds; - JS_ToFloat64(js, &seconds, argv[1]); - if (seconds <= 0) { - actor_clock(actor, argv[0]); - return JS_NULL; - } - - SDL_LockMutex(actor->msg_mutex); - uint32_t id = SDL_AddTimerNS(seconds*1e9, actor_delay_cb, actor); - JSValue cb = JS_DupValue(js, argv[0]); - hmput(actor->timers, id, cb); - SDL_UnlockMutex(actor->msg_mutex); - return JS_NewUint32(js, id); -} - -JSValue js_actor_removetimer(JSContext *js, JSValue self, int argc, JSValue *argv) -{ - cell_rt *actor = JS_GetContextOpaque(js); - uint32_t timer_id; - JS_ToUint32(js, &timer_id, argv[0]); - if (timer_id == -1) return JS_NULL; - - SDL_RemoveTimer(timer_id); - - JSValue cb = JS_NULL; - - 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); - - return JS_NULL; -} - // Wrapper struct to keep the array pointer stable typedef struct { #ifdef TRACY_ENABLE @@ -720,10 +288,10 @@ typedef struct { #endif } tracy_stack_t; -// Global TLS ID for the Tracy stack + +#ifdef TRACY_ENABLE static SDL_TLSID tracy_stack_id = {0}; -// Cleanup function for the wrapper struct static void tracy_cleanup_stack(void *value) { tracy_stack_t *stack = value; @@ -733,7 +301,6 @@ static void tracy_cleanup_stack(void *value) } } -// Get or initialize the thread-local Tracy stack static tracy_stack_t *get_tracy_stack(void) { tracy_stack_t *stack = SDL_GetTLS(&tracy_stack_id); @@ -748,7 +315,6 @@ static tracy_stack_t *get_tracy_stack(void) void tracy_call_hook(JSContext *js, JSValue fn) { -#ifdef TRACY_ENABLE if (!tracy_profiling_enabled) return; @@ -760,21 +326,20 @@ void tracy_call_hook(JSContext *js, JSValue fn) arrput(stack->arr, ___tracy_emit_zone_begin_alloc(srcloc, 1)); free_js_debug_info(js, &debug); -#endif } void tracy_end_hook(JSContext *js, JSValue fn) { -#ifdef TRACY_ENABLE if (!tracy_profiling_enabled) return; tracy_stack_t *stack = get_tracy_stack(); if (arrlen(stack->arr) > 0) ___tracy_emit_zone_end(arrpop(stack->arr)); -#endif } +#endif + void actor_disrupt(cell_rt *crt) { crt->disrupt = 1; @@ -782,11 +347,6 @@ void actor_disrupt(cell_rt *crt) actor_free(crt); } -static int actor_interrupt_cb(JSRuntime *rt, cell_rt *crt) -{ - return SDL_GetAtomicInt(&shutting_down) || crt->disrupt; -} - JSValue js_os_use(JSContext *js); void script_startup(cell_rt *prt) @@ -884,49 +444,6 @@ void script_startup(cell_rt *prt) set_actor_state(crt); } -int uncaught_exception(JSContext *js, JSValue v) -{ - cell_rt *rt = JS_GetContextOpaque(js); - SDL_LockMutex(rt->mutex); - - if (!JS_HasException(js)) { - JS_FreeValue(js,v); - SDL_UnlockMutex(rt->mutex); - return 1; - } - - JSValue exp = JS_GetException(js); - JSValue ret = JS_Call(js, rt->on_exception, JS_NULL, 1, &exp); - JS_FreeValue(js,ret); - JS_FreeValue(js, exp); - JS_FreeValue(js,v); - SDL_UnlockMutex(rt->mutex); - return 0; -} - -static int actor_runner(void *data) -{ - SDL_AddAtomicInt(&runners_count, 1); - - while (!SDL_GetAtomicInt(&shutting_down)) { - SDL_WaitSemaphore(ready_sem); - SDL_LockSpinlock(&queue_lock); - - cell_rt *actor = NULL; - if (arrlen(ready_queue) > 0) { - actor = ready_queue[0]; - arrdel(ready_queue, 0); - } - SDL_UnlockSpinlock(&queue_lock); - - if (actor) - actor_turn(actor); - } - - SDL_AddAtomicInt(&runners_count, -1); - return 0; -} - static void signal_handler(int sig) { const char *str = NULL; @@ -943,33 +460,6 @@ static void signal_handler(int sig) exit_handler(); } -static void add_runners(int n) -{ - /* Launch runner threads */ - for (int i = 0; i < n; i++) { - char threadname[128]; - snprintf(threadname, sizeof(threadname), "actor runner %d", i); - SDL_Thread *thread = SDL_CreateThread(actor_runner, threadname, NULL); - SDL_DetachThread(thread); - /* Thread is detached, no need to track */ - } -} - -static void loop() -{ - while (!SDL_GetAtomicInt(&shutting_down)) { - SDL_WaitSemaphore(main_sem); - SDL_LockSpinlock(&main_queue_lock); - cell_rt *actor = NULL; - if (arrlen(main_queue) > 0) { - actor = main_queue[0]; - arrdel(main_queue, 0); - } - SDL_UnlockSpinlock(&main_queue_lock); - actor_turn(actor); - } -} - int main(int argc, char **argv) { int profile_enabled = 0; @@ -1009,13 +499,7 @@ int main(int argc, char **argv) wota_write_text(&startwota, actor_argv[i]); /* Initialize synchronization primitives */ - ready_sem = SDL_CreateSemaphore(0); - main_sem = SDL_CreateSemaphore(0); - actors_mutex = SDL_CreateMutex(); - SDL_SetAtomicInt(&shutting_down, 0); - SDL_SetAtomicInt(&runners_count, 0); - - add_runners(SDL_GetNumLogicalCPUCores()); + actor_initialize(); root_cell = create_actor(startwota.data); @@ -1025,22 +509,11 @@ int main(int argc, char **argv) signal(SIGSEGV, signal_handler); signal(SIGABRT, signal_handler); - loop(); + actor_loop(); return 0; } -int actor_exists(const char *id) -{ - SDL_LockMutex(actors_mutex); - int idx = shgeti(actors,id); - SDL_UnlockMutex(actors_mutex); - if (idx == -1) - return 0; - else - return 1; -} - int JS_ArrayLength(JSContext *js, JSValue a) { JSValue length = JS_GetPropertyStr(js, a, "length"); diff --git a/source/cell.h b/source/cell.h index 5e632ba2..9387c18b 100644 --- a/source/cell.h +++ b/source/cell.h @@ -72,16 +72,6 @@ void *value2wota(JSContext *js, JSValue v, JSValue replacer, size_t *bytes); #define CGETSET_ADD(ID, ENTRY) MIST_CGETSET_DEF(#ENTRY, js_##ID##_get_##ENTRY, js_##ID##_set_##ENTRY) #define CGETSET_ADD_HID(ID, ENTRY) MIST_CGETSET_BASE(#ENTRY, js_##ID##_get_##ENTRY, js_##ID##_set_##ENTRY, JS_PROP_CONFIGURABLE) -#define JSC_DCALL(FN) JSValue js_##FN (JSContext *js, JSValue self, int argc, JSValue *argv) { FN(); return JS_NULL; } - -#define JSC_1CALL(FN) JSValue js_##FN (JSContext *js, JSValue self, int argc, JSValue *argv) { FN(js2number(js,argv[0])); return JS_NULL; } -#define JSC_2CALL(FN) JSValue js_##FN (JSContext *js, JSValue self, int argc, JSValue *argv) { FN(js2number(js,argv[0]), js2number(js,argv[1])); return JS_NULL; } -#define JSC_3CALL(FN) JSValue js_##FN (JSContext *js, JSValue self, int argc, JSValue *argv) { FN(js2number(js,argv[0]), js2number(js,argv[1]), js2number(js,argv[2])); return JS_NULL; } -#define JSC_4CALL(FN) JSValue js_##FN (JSContext *js, JSValue self, int argc, JSValue *argv) { FN(js2number(js,argv[0]), js2number(js,argv[1]), js2number(js,argv[2]), js2number(js,argv[3])); return JS_NULL; } -#define JSC_5CALL(FN) JSValue js_##FN (JSContext *js, JSValue self, int argc, JSValue *argv) { FN(js2number(js,argv[0]), js2number(js,argv[1]), js2number(js,argv[2]), js2number(js,argv[3]), js2number(js,argv[4])); return JS_NULL; } -#define JSC_6CALL(FN) JSValue js_##FN (JSContext *js, JSValue self, int argc, JSValue *argv) { FN(js2number(js,argv[0]), js2number(js,argv[1]), js2number(js,argv[2]), js2number(js,argv[3]), js2number(js,argv[4]), js2number(js,argv[5])); return JS_NULL; } -#define JSC_7CALL(FN) JSValue js_##FN (JSContext *js, JSValue self, int argc, JSValue *argv) { FN(js2number(js,argv[0]), js2number(js,argv[1]), js2number(js,argv[2]), js2number(js,argv[3]), js2number(js,argv[4]), js2number(js,argv[5]), js2number(js,argv[6])); return JS_NULL; } - #define GETSETPAIR(ID, ENTRY, TYPE, FN) \ JSValue js_##ID##_set_##ENTRY (JS_SETSIG) { \ js2##ID (js, self)->ENTRY = js2##TYPE (js,val); \ diff --git a/source/cell_internal.h b/source/cell_internal.h new file mode 100644 index 00000000..6c201b19 --- /dev/null +++ b/source/cell_internal.h @@ -0,0 +1,92 @@ +#ifdef HAVE_MIMALLOC +typedef struct mi_heap_s mi_heap_t; +#endif + +#include + +/* Letter type for unified message queue */ +typedef enum { + LETTER_BLOB, /* Blob message */ + LETTER_CALLBACK /* JSValue callback function */ +} letter_type; + +typedef struct letter { + letter_type type; + union { + blob *blob_data; /* For LETTER_BLOB */ + JSValue callback; /* For LETTER_CALLBACK */ + }; +} letter; + +#define ACTOR_IDLE 0 // Actor not doing anything +#define ACTOR_READY 1 // Actor ready for a turn +#define ACTOR_RUNNING 2 // Actor taking a turn +#define ACTOR_EXHAUSTED 3 // Actor waiting for GC +#define ACTOR_RECLAIMING 4 // Actor running GC +#define ACTOR_SLOW 5 // Actor going slowly; deprioritize + +typedef struct cell_rt { + JSContext *context; +#ifdef HAVE_MIMALLOC +mi_heap_t *heap; +#endif + JSValue idx_buffer; + JSValue on_exception; + JSValue message_handle; + + void *init_wota; + + /* Protects JSContext usage */ + pthread_mutex_t *mutex; /* for everything else */ + pthread_mutex_t *msg_mutex; /* For message queue and timers queue */ + + char *id; + + int idx_count; + + /* The “mailbox” for incoming messages + a dedicated lock for it: */ + letter *letters; + + /* CHANGED FOR EVENTS: a separate lock for the actor->events queue */ + struct { Uint32 key; JSValue value; } *timers; + + int state; + Uint32 ar; // timer for unneeded + double ar_secs; // time for unneeded + JSValue unneeded; // fn to call before unneeded + + int disrupt; + int main_thread_only; + int affinity; + + JSAtom actor_sym; + + const char *name; // human friendly name +} cell_rt; + +cell_rt *create_actor(void *wota); +const char *register_actor(const char *id, cell_rt *actor, int mainthread, double ar); +void actor_disrupt(cell_rt *actor); + +JSAtom actor_sym(cell_rt *actor); + +const char *send_message(const char *id, void *msg); +const char *register_actor(const char *id, cell_rt *actor, int mainthread, double ar); +void actor_unneeded(cell_rt *actor, JSValue fn, double seconds); +void script_startup(cell_rt *rt); +cell_rt *create_actor(void *wota); +int uncaught_exception(JSContext *js, JSValue v); +int actor_exists(const char *id); + +void set_actor_state(cell_rt *actor); +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); + +uint64_t cell_ns(); +void cell_sleep(double seconds); diff --git a/source/qjs_actor.c b/source/qjs_actor.c index 32b31687..eba2e096 100644 --- a/source/qjs_actor.c +++ b/source/qjs_actor.c @@ -113,6 +113,30 @@ JSC_CCALL(actor_clock, actor_clock(actor, argv[0]); ) +JSC_CCALL(actor_delay, + if (!JS_IsFunction(js, argv[0])) + return JS_ThrowReferenceError(js, "Argument must be a function."); + + cell_rt *actor = JS_GetContextOpaque(js); + double seconds; + JS_ToFloat64(js, &seconds, argv[1]); + if (seconds <= 0) { + actor_clock(actor, argv[0]); + return JS_NULL; + } + + uint32_t id = actor_delay(actor, argv[0], seconds); + return JS_NewUint32(js, id); +) + +JSC_CCALL(actor_removetimer, + cell_rt *actor = JS_GetContextOpaque(js); + uint32_t timer_id; + JS_ToUint32(js, &timer_id, argv[0]); + JSValue removed = actor_remove_timer(actor, timer_id); + JS_FreeValue(js, removed); +) + static const JSCFunctionListEntry js_actor_funcs[] = { MIST_FUNC_DEF(os, createactor, 1), MIST_FUNC_DEF(os, mailbox_push, 2), diff --git a/source/quickjs.c b/source/quickjs.c index 1ffc7e5f..e3396246 100644 --- a/source/quickjs.c +++ b/source/quickjs.c @@ -96,7 +96,7 @@ /* dump objects freed by the garbage collector */ //#define DUMP_GC_FREE /* dump objects leaking when freeing the runtime */ -//#define DUMP_LEAKS 1 +#define DUMP_LEAKS 1 /* dump memory usage before running the garbage collector */ //#define DUMP_MEM //#define DUMP_OBJECTS /* dump objects in JS_FreeContext */ @@ -1409,6 +1409,7 @@ void JS_SetRuntimeInfo(JSRuntime *rt, const char *s) void JS_FreeRuntime(JSRuntime *rt) { + struct list_head *el, *el1; int i; JS_FreeValueRT(rt, rt->current_exception); diff --git a/source/scheduler_sdl.c b/source/scheduler_sdl.c deleted file mode 100644 index fd27c752..00000000 --- a/source/scheduler_sdl.c +++ /dev/null @@ -1 +0,0 @@ -#include \ No newline at end of file diff --git a/source/scheduler_threaded.c b/source/scheduler_threaded.c new file mode 100644 index 00000000..ec9dfd04 --- /dev/null +++ b/source/scheduler_threaded.c @@ -0,0 +1,691 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "stb_ds.h" +#include "cell.h" +#include "cell_internal.h" + +typedef struct actor_node { + cell_rt *actor; + struct actor_node *next; +} actor_node; + +typedef enum { + TIMER_JS, + TIMER_NATIVE_REMOVE +} timer_type; + +typedef struct { + uint64_t execute_at_ns; + cell_rt *actor; + uint32_t timer_id; + timer_type type; +} timer_node; + +static timer_node *timer_heap = NULL; + +// --- 3. The Global Engine State --- +static struct { + pthread_mutex_t lock; // Protects queue, shutdown flag, and timers + pthread_cond_t wake_cond; // Wakes up workers + pthread_cond_t timer_cond; // Wakes up the timer thread + pthread_cond_t main_cond; // Wakes up the main thread + + actor_node *head; // Ready Queue Head + actor_node *tail; // Ready Queue Tail + + actor_node *main_head; // Main Thread Queue Head + actor_node *main_tail; // Main Thread Queue Tail + + int shutting_down; + + pthread_t *worker_threads; + int num_workers; + pthread_t timer_thread; +} engine; + +static pthread_mutex_t *actors_mutex; +static struct { char *key; cell_rt *value; } *actors = NULL; + +#define lockless_shdel(NAME, KEY) pthread_mutex_lock(NAME##_mutex); shdel(NAME, KEY); pthread_mutex_unlock(NAME##_mutex); +#define lockless_shlen(NAME) ({ \ + pthread_mutex_lock(NAME##_mutex); \ + size_t _len = shlen(NAME); \ + pthread_mutex_unlock(NAME##_mutex); \ + _len; \ +}) +#define lockless_shgeti(NAME, KEY) ({ \ + pthread_mutex_lock(NAME##_mutex); \ + int _idx = shgeti(NAME, KEY); \ + pthread_mutex_unlock(NAME##_mutex); \ + _idx; \ +}) +#define lockless_shget(NAME, KEY) ({ \ + pthread_mutex_lock(NAME##_mutex); \ + cell_rt *_actor = shget(NAME, KEY); \ + pthread_mutex_unlock(NAME##_mutex); \ + _actor; \ +}) +#define lockless_shput_unique(NAME, KEY, VALUE) ({ \ + pthread_mutex_lock(NAME##_mutex); \ + int _exists = shgeti(NAME, KEY) != -1; \ + if (!_exists) shput(NAME, KEY, VALUE); \ + pthread_mutex_unlock(NAME##_mutex); \ + !_exists; \ +}) + +// Forward declarations +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 }; + arrput(timer_heap, node); + + // Bubble up + int i = arrlen(timer_heap) - 1; + while (i > 0) { + int parent = (i - 1) / 2; + if (timer_heap[i].execute_at_ns >= timer_heap[parent].execute_at_ns) break; + + timer_node tmp = timer_heap[i]; + timer_heap[i] = timer_heap[parent]; + timer_heap[parent] = tmp; + i = parent; + } +} + +// Helper: Heap Pop +int heap_pop(timer_node *out) { + if (arrlen(timer_heap) == 0) return 0; + + *out = timer_heap[0]; + timer_node last = arrpop(timer_heap); + + if (arrlen(timer_heap) > 0) { + timer_heap[0] = last; + // Bubble down + int i = 0; + int n = arrlen(timer_heap); + while (1) { + int left = 2 * i + 1; + int right = 2 * i + 2; + int smallest = i; + + if (left < n && timer_heap[left].execute_at_ns < timer_heap[smallest].execute_at_ns) + smallest = left; + if (right < n && timer_heap[right].execute_at_ns < timer_heap[smallest].execute_at_ns) + smallest = right; + + if (smallest == i) break; + + timer_node tmp = timer_heap[i]; + timer_heap[i] = timer_heap[smallest]; + timer_heap[smallest] = tmp; + i = smallest; + } + } + return 1; +} + +void *timer_thread_func(void *arg) { + while (1) { + pthread_mutex_lock(&engine.lock); + + if (engine.shutting_down) { + pthread_mutex_unlock(&engine.lock); + return NULL; + } + + if (arrlen(timer_heap) == 0) { + pthread_cond_wait(&engine.timer_cond, &engine.lock); + } else { + uint64_t now = cell_ns(); + if (timer_heap[0].execute_at_ns <= now) { + // --- TIMER FIRED --- + timer_node t; + heap_pop(&t); + pthread_mutex_unlock(&engine.lock); + + if (t.type == TIMER_NATIVE_REMOVE) { + // Execute native remove callback + actor_remove_cb(t.actor, t.timer_id, 0); + } else { + // Inject event into Actor + 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; + hmdel(t.actor->timers, t.timer_id); + actor_clock(t.actor, cb); + JS_FreeValue(t.actor->context, cb); + } + pthread_mutex_unlock(t.actor->msg_mutex); + } + + // Loop immediately to check for other expired timers + continue; + } else { + // --- WAIT FOR DEADLINE --- + struct timespec ts; + uint64_t ns = timer_heap[0].execute_at_ns; + ts.tv_sec = ns / 1000000000ULL; + ts.tv_nsec = ns % 1000000000ULL; + + pthread_cond_timedwait(&engine.timer_cond, &engine.lock, &ts); + } + } + pthread_mutex_unlock(&engine.lock); + } + return NULL; +} + +void *actor_runner(void *arg) { + while (1) { + pthread_mutex_lock(&engine.lock); + + // Wait while queue is empty AND not shutting down + while (engine.head == NULL && !engine.shutting_down) { + pthread_cond_wait(&engine.wake_cond, &engine.lock); + } + + if (engine.shutting_down && engine.head == NULL) { + pthread_mutex_unlock(&engine.lock); + break; // Exit thread + } + + // Pop from Linked List + actor_node *node = engine.head; + engine.head = node->next; + if (engine.head == NULL) engine.tail = NULL; + + pthread_mutex_unlock(&engine.lock); + + if (node) { + actor_turn(node->actor); + free(node); + } + } + return NULL; +} + +void actor_initialize(void) { + pthread_mutex_init(&engine.lock, NULL); + pthread_cond_init(&engine.wake_cond, NULL); + pthread_cond_init(&engine.timer_cond, NULL); + pthread_cond_init(&engine.main_cond, NULL); + + engine.shutting_down = 0; + engine.head = NULL; + engine.tail = NULL; + engine.main_head = NULL; + engine.main_tail = NULL; + + actors_mutex = malloc(sizeof(pthread_mutex_t)); + pthread_mutex_init(actors_mutex, NULL); + + // Start Timer Thread + pthread_create(&engine.timer_thread, NULL, timer_thread_func, NULL); + + // Start Workers + long n = sysconf(_SC_NPROCESSORS_ONLN); + engine.num_workers = (int)n; + engine.worker_threads = malloc(sizeof(pthread_t) * n); + for (int i=0; i < n; i++) { + pthread_create(&engine.worker_threads[i], NULL, actor_runner, NULL); + } +} + +void actor_free(cell_rt *actor) +{ + lockless_shdel(actors, actor->id); + + // Note: Removing from ready queue is hard with a singly linked list. + // We assume actor_turn handles disrupted/freed actors gracefully or they run once more. + // The old code did lockless_rm(ready_queue, actor), which was O(N). + // Here we rely on the actor->disrupt flag checked in actor_turn. + + // Do not go forward with actor destruction until the actor is completely free + pthread_mutex_lock(actor->msg_mutex); + pthread_mutex_lock(actor->mutex); + + JSContext *js = actor->context; + + JS_FreeValue(js, actor->idx_buffer); + JS_FreeValue(js, actor->message_handle); + JS_FreeValue(js, actor->on_exception); + JS_FreeValue(js, actor->unneeded); + JS_FreeAtom(js, actor->actor_sym); + + for (int i = 0; i < hmlen(actor->timers); i++) { + JS_FreeValue(js, actor->timers[i].value); + } + + hmfree(actor->timers); + + /* Free all letters in the queue */ + for (int i = 0; i < arrlen(actor->letters); i++) { + if (actor->letters[i].type == LETTER_BLOB) { + blob_destroy(actor->letters[i].blob_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); + JS_FreeContext(js); + JS_FreeRuntime(rt); + free(actor->id); + + pthread_mutex_unlock(actor->mutex); + pthread_mutex_destroy(actor->mutex); + free(actor->mutex); + pthread_mutex_unlock(actor->msg_mutex); + pthread_mutex_destroy(actor->msg_mutex); + free(actor->msg_mutex); + +#ifdef HAVE_MIMALLOC + mi_heap_destroy(actor->heap); +#endif + free(actor); + + int actor_count = lockless_shlen(actors); + if (actor_count == 0) exit(0); +} + +void exit_handler(void) { + pthread_mutex_lock(&engine.lock); + engine.shutting_down = 1; + pthread_cond_broadcast(&engine.wake_cond); + pthread_cond_broadcast(&engine.timer_cond); + pthread_cond_broadcast(&engine.main_cond); + pthread_mutex_unlock(&engine.lock); + + pthread_join(engine.timer_thread, NULL); + + for (int i=0; i < engine.num_workers; i++) { + pthread_join(engine.worker_threads[i], NULL); + } + + free(engine.worker_threads); + pthread_mutex_destroy(&engine.lock); + pthread_cond_destroy(&engine.wake_cond); + pthread_cond_destroy(&engine.timer_cond); + pthread_cond_destroy(&engine.main_cond); + + pthread_mutex_destroy(actors_mutex); + free(actors_mutex); + + arrfree(timer_heap); + + exit(0); +} + +int actor_exists(const char *id) +{ + int idx = lockless_shgeti(actors, id); + return idx != -1; +} + +void set_actor_state(cell_rt *actor) +{ + if (actor->disrupt) { + actor_free(actor); + return; + } + pthread_mutex_lock(actor->msg_mutex); + + switch(actor->state) { + case ACTOR_RUNNING: + case ACTOR_READY: + if (actor->ar) + actor->ar = 0; + break; + + case ACTOR_IDLE: + if (arrlen(actor->letters)) { + actor->state = ACTOR_READY; + actor->ar = 0; + + actor_node *n = malloc(sizeof(actor_node)); + n->actor = actor; + n->next = NULL; + + pthread_mutex_lock(&engine.lock); + if (actor->main_thread_only) { + if (engine.main_tail) { + engine.main_tail->next = n; + } else { + engine.main_head = n; + } + engine.main_tail = n; + pthread_cond_signal(&engine.main_cond); + } else { + if (engine.tail) { + engine.tail->next = n; + } else { + engine.head = n; + } + engine.tail = n; + pthread_cond_signal(&engine.wake_cond); + } + pthread_mutex_unlock(&engine.lock); + + } else if (!arrlen(actor->letters) && !hmlen(actor->timers)) { + // Schedule remove timer + static uint32_t global_timer_id = 1; + uint32_t id = global_timer_id++; + actor->ar = id; + + uint64_t now = cell_ns(); + uint64_t execute_at = now + (uint64_t)(actor->ar_secs * 1e9); + + pthread_mutex_lock(&engine.lock); + heap_push(execute_at, actor, id, TIMER_NATIVE_REMOVE); + if (timer_heap[0].timer_id == id) { + pthread_cond_signal(&engine.timer_cond); + } + pthread_mutex_unlock(&engine.lock); + } + break; + } + + pthread_mutex_unlock(actor->msg_mutex); +} + +uint32_t actor_remove_cb(cell_rt *actor, uint32_t id, uint32_t interval) +{ + pthread_mutex_lock(actor->mutex); + // Check if this timer is still valid (match actor->ar) + if (actor->ar != id && id != 0) { // id 0 means force (optional) + pthread_mutex_unlock(actor->mutex); + return 0; + } + + actor->disrupt = 1; + + if (!JS_IsNull(actor->unneeded)) { + JSValue ret = JS_Call(actor->context, actor->unneeded, JS_NULL, 0, NULL); + uncaught_exception(actor->context, ret); + } + + int should_free = (actor->state == ACTOR_IDLE); + pthread_mutex_unlock(actor->mutex); + + if (should_free) actor_free(actor); + return 0; +} + +void actor_unneeded(cell_rt *actor, JSValue fn, double seconds) +{ + if (actor->disrupt) return; + JS_FreeValue(actor->context, actor->unneeded); + + if (!JS_IsFunction(actor->context, fn)) { + actor->unneeded = JS_NULL; + goto END; + } + + actor->unneeded = JS_DupValue(actor->context, fn); + actor->ar_secs = seconds; + + END: + if (actor->ar) + actor->ar = 0; + set_actor_state(actor); +} + +cell_rt *get_actor(char *id) +{ + int idx = lockless_shgeti(actors, id); + if (idx == -1) { + return NULL; + } + + return lockless_shget(actors, id); +} + +int uncaught_exception(JSContext *js, JSValue v) +{ + cell_rt *rt = JS_GetContextOpaque(js); + + if (!JS_HasException(js)) { + JS_FreeValue(js,v); + return 1; + } + + JSValue exp = JS_GetException(js); + JSValue ret = JS_Call(js, rt->on_exception, JS_NULL, 1, &exp); + JS_FreeValue(js,ret); + JS_FreeValue(js, exp); + JS_FreeValue(js,v); + return 0; +} + +void actor_loop() +{ + while (!engine.shutting_down) { // Direct read safe enough here or use lock + pthread_mutex_lock(&engine.lock); + while (engine.main_head == NULL && !engine.shutting_down) { + pthread_cond_wait(&engine.main_cond, &engine.lock); + } + + if (engine.shutting_down && engine.main_head == NULL) { + pthread_mutex_unlock(&engine.lock); + break; + } + + actor_node *node = engine.main_head; + engine.main_head = node->next; + if (engine.main_head == NULL) engine.main_tail = NULL; + + pthread_mutex_unlock(&engine.lock); + + if (node) { + actor_turn(node->actor); + free(node); + } + } +} + +cell_rt *create_actor(void *wota) +{ + cell_rt *actor = calloc(sizeof(*actor), 1); +#ifdef HAVE_MIMALLOC + actor->heap = mi_heap_new(); +#endif + actor->init_wota = wota; + actor->idx_buffer = JS_NULL; + actor->message_handle = JS_NULL; + actor->unneeded = JS_NULL; + actor->on_exception = JS_NULL; + actor->actor_sym = JS_ATOM_NULL; + + arrsetcap(actor->letters, 5); + + actor->mutex = malloc(sizeof(pthread_mutex_t)); + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); + pthread_mutex_init(actor->mutex, &attr); + actor->msg_mutex = malloc(sizeof(pthread_mutex_t)); + pthread_mutex_init(actor->msg_mutex, &attr); // msg_mutex can be recursive too to be safe + pthread_mutexattr_destroy(&attr); + + /* Lock actor->mutex while initializing JS runtime. */ + pthread_mutex_lock(actor->mutex); + script_startup(actor); + set_actor_state(actor); + pthread_mutex_unlock(actor->mutex); + + return actor; +} + +const char *register_actor(const char *id, cell_rt *actor, int mainthread, double ar) +{ + actor->main_thread_only = mainthread; + actor->id = strdup(id); + actor->ar_secs = ar; + int added = lockless_shput_unique(actors, id, actor); + if (!added) { + free(actor->id); + return "Actor with given ID already exists."; + } + return NULL; +} + +int actor_interrupt_cb(JSRuntime *rt, cell_rt *crt) +{ + // Locking engine.lock for shutting_down might be too expensive for interrupt? + // Check atomic-like access or just access it. + // int s; pthread_mutex_lock(&engine.lock); s = engine.shutting_down; pthread_mutex_unlock(&engine.lock); + // But engine.shutting_down is int, atomic read on x86/arm usually ok. + return engine.shutting_down || crt->disrupt; +} + +const char *send_message(const char *id, void *msg) +{ + cell_rt *target = get_actor(id); + if (!target) { + blob_destroy((blob *)msg); + return "Could not get actor from id."; + } + + letter l; + l.type = LETTER_BLOB; + l.blob_data = (blob *)msg; + + pthread_mutex_lock(target->msg_mutex); + arrput(target->letters, l); + pthread_mutex_unlock(target->msg_mutex); + + if (target->ar) + target->ar = 0; + + set_actor_state(target); + + return NULL; +} + +void actor_turn(cell_rt *actor) +{ + pthread_mutex_lock(actor->mutex); + +#ifdef TRACY_ENABLE + int entered = 0; + if (tracy_profiling_enabled && TracyCIsConnected) { + TracyCFiberEnter(actor->name); + entered = 1; + } +#endif + + actor->state = ACTOR_RUNNING; + + TAKETURN: + + pthread_mutex_lock(actor->msg_mutex); + JSValue result; + if (!arrlen(actor->letters)) { + pthread_mutex_unlock(actor->msg_mutex); + goto ENDTURN; + } + letter l = actor->letters[0]; + arrdel(actor->letters, 0); // O(N) but we kept array as requested + pthread_mutex_unlock(actor->msg_mutex); + + if (l.type == LETTER_BLOB) { + // Create a JS blob from the C blob + size_t size = l.blob_data->length / 8; // Convert bits to bytes + JSValue arg = js_new_blob_stoned_copy(actor->context, l.blob_data->data, size); + blob_destroy(l.blob_data); + result = JS_Call(actor->context, actor->message_handle, JS_NULL, 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_NULL, 0, NULL); + uncaught_exception(actor->context, result); + JS_FreeValue(actor->context, l.callback); + } + + if (actor->disrupt) goto ENDTURN; + + // Check if anyone else is waiting? + // In the new system, checking the global queue is expensive (lock). + // And "someone else waiting" logic was to yield. + // With threads, we don't need to yield as much, let the OS schedule. + // But we might want to prevent one actor hogging the worker? + // For now, remove the yield check or implement it with try_lock or simple check. + // int someone_else_waiting = (lockless_arrlen(ready_queue) > 0); + // We'll just remove the optimization/yield for now to simplify. + // if (!someone_else_waiting) goto TAKETURN; + + ENDTURN: + actor->state = ACTOR_IDLE; + +#ifdef TRACY_ENABLE + if (tracy_profiling_enabled && entered) + TracyCFiberLeave(actor->name); +#endif + + set_actor_state(actor); + + pthread_mutex_unlock(actor->mutex); +} + +void actor_clock(cell_rt *actor, JSValue fn) +{ + letter l; + l.type = LETTER_CALLBACK; + l.callback = JS_DupValue(actor->context, fn); + pthread_mutex_lock(actor->msg_mutex); + arrput(actor->letters, l); + pthread_mutex_unlock(actor->msg_mutex); + set_actor_state(actor); +} + +uint32_t actor_delay(cell_rt *actor, JSValue fn, double seconds) +{ + pthread_mutex_lock(actor->msg_mutex); + + static uint32_t global_timer_id = 1; + uint32_t id = global_timer_id++; + + JSValue cb = JS_DupValue(actor->context, fn); + hmput(actor->timers, id, cb); + + uint64_t now = cell_ns(); + uint64_t execute_at = now + (uint64_t)(seconds * 1e9); + + pthread_mutex_lock(&engine.lock); + heap_push(execute_at, actor, id, TIMER_JS); + if (timer_heap[0].timer_id == id) { + pthread_cond_signal(&engine.timer_cond); + } + pthread_mutex_unlock(&engine.lock); + + pthread_mutex_unlock(actor->msg_mutex); + return id; +} + +JSValue actor_remove_timer(cell_rt *actor, uint32_t timer_id) +{ + JSValue cb = JS_NULL; + pthread_mutex_lock(actor->msg_mutex); + int id = hmgeti(actor->timers, timer_id); + if (id != -1) { + cb = actor->timers[id].value; + hmdel(actor->timers, timer_id); + } + pthread_mutex_unlock(actor->msg_mutex); + // Note: We don't remove from heap, it will misfire safely + return cb; +}