diff --git a/build_config/glib_hal_test.rb b/build_config/glib_hal_test.rb new file mode 100644 index 000000000..c90dd47fe --- /dev/null +++ b/build_config/glib_hal_test.rb @@ -0,0 +1,88 @@ +MRuby::Build.new do |conf| + # load specific toolchain settings + conf.toolchain + + # Use mrbgems + # conf.gem 'examples/mrbgems/ruby_extension_example' + # conf.gem 'examples/mrbgems/c_extension_example' do |g| + # g.cc.flags << '-g' # append cflags in this gem + # end + # conf.gem 'examples/mrbgems/c_and_ruby_extension_example' + # conf.gem :core => 'mruby-eval' + # conf.gem :mgem => 'mruby-onig-regexp' + # conf.gem :github => 'mattn/mruby-onig-regexp' + # conf.gem :git => 'git@github.com:mattn/mruby-onig-regexp.git', :branch => 'master', :options => '-v' + + # include the GEM box + #conf.gembox 'default' + + # C compiler settings + # conf.cc do |cc| + # cc.command = ENV['CC'] || 'gcc' + # cc.flags = [ENV['CFLAGS'] || %w()] + # cc.include_paths = ["#{root}/include"] + # cc.defines = %w() + # cc.option_include_path = %q[-I"%s"] + # cc.option_define = '-D%s' + # cc.compile_options = %Q[%{flags} -MMD -o "%{outfile}" -c "%{infile}"] + # end + + # mrbc settings + # conf.mrbc do |mrbc| + # mrbc.compile_options = "-g -B%{funcname} -o-" # The -g option is required for line numbers + # end + + # Linker settings + # conf.linker do |linker| + # linker.command = ENV['LD'] || 'gcc' + # linker.flags = [ENV['LDFLAGS'] || []] + # linker.flags_before_libraries = [] + # linker.libraries = %w() + # linker.flags_after_libraries = [] + # linker.library_paths = [] + # linker.option_library = '-l%s' + # linker.option_library_path = '-L%s' + # linker.link_options = %Q[%{flags} -o "%{outfile}" %{objs} %{libs}] + # end + + # Archiver settings + # conf.archiver do |archiver| + # archiver.command = ENV['AR'] || 'ar' + # archiver.archive_options = 'rs "%{outfile}" %{objs}' + # end + + # Parser generator settings + # conf.yacc do |yacc| + # yacc.command = ENV['YACC'] || 'bison' + # yacc.compile_options = %q[-o "%{outfile}" "%{infile}"] + # end + + # gperf settings + # conf.gperf do |gperf| + # gperf.command = 'gperf' + # gperf.compile_options = %q[-L ANSI-C -C -j1 -i 1 -o -t -N mrb_reserved_word -k"1,3,$" "%{infile}" > "%{outfile}"] + # end + + # file extensions + # conf.exts do |exts| + # exts.object = '.o' + # exts.executable = '' # '.exe' if Windows + # exts.library = '.a' + # end + + # file separator + # conf.file_separator = '/' + + # change library directory name from the default "lib" if necessary + # conf.libdir_name = 'lib64' + + # Turn on `enable_debug` for better debugging + conf.enable_sanitizer 'address,undefined' + conf.enable_debug + conf.enable_bintest + conf.enable_test + conf.ports :glib + conf.cc.defines << 'MRB_TASK_BUILD_DEMO' + conf.gem core: 'mruby-task' + conf.gem core: 'mruby-compiler' +end diff --git a/mrbgems/mruby-task/mrbgem.rake b/mrbgems/mruby-task/mrbgem.rake index 681bd47f7..ed768a960 100644 --- a/mrbgems/mruby-task/mrbgem.rake +++ b/mrbgems/mruby-task/mrbgem.rake @@ -13,4 +13,41 @@ MRuby::Gem::Specification.new('mruby-task') do |spec| if spec.for_windows? spec.linker.libraries << "winmm" end + + ports = spec.build.effective_ports + + # ports/glib/ needs glib-2.0 (GSource, GMainContext, GRecMutex) and + # gthread-2.0 (GThread). On modern distros gthread-2.0 is a transparent + # alias for glib-2.0; on older ones it's a separate .pc that pulls in + # -lpthread, so query it separately. + if ports.include?('glib') + unless spec.search_package('glib-2.0') && spec.search_package('gthread-2.0') + abort <<~MSG + [mruby-task] conf.ports :glib selected but pkg-config could not find + glib-2.0 / gthread-2.0. Install the GLib development headers + (Debian/Ubuntu: libglib2.0-dev; Fedora: glib2-devel; Arch: glib2; + macOS Homebrew: glib). For non-default install locations, set + PKG_CONFIG_PATH before invoking rake. + MSG + end + end + + # Optional demo tool that exercises the GLib HAL end-to-end (basic + # scheduling, priority ordering, timeslice preemption, auto-execution + # under a foreign GLib main loop). Default off; opt in from your + # build_config with: + # + # conf.cc.defines << 'MRB_TASK_BUILD_DEMO' + # conf.ports :glib + # conf.gem core: 'mruby-task' + # + # When enabled, `rake` produces bin/mruby-task-demo from + # tools/mruby-task-demo/. The define is only inspected here -- the + # demo's C source does not condition on it. + if spec.build.cc.defines.include?('MRB_TASK_BUILD_DEMO') + unless ports.include?('glib') + abort '[mruby-task] MRB_TASK_BUILD_DEMO requires conf.ports :glib' + end + spec.bins = %w(mruby_task_demo) + end end diff --git a/mrbgems/mruby-task/ports/glib/task_hal.c b/mrbgems/mruby-task/ports/glib/task_hal.c new file mode 100644 index 000000000..a8a1dbe6a --- /dev/null +++ b/mrbgems/mruby-task/ports/glib/task_hal.c @@ -0,0 +1,536 @@ +/* +** task_hal.c - GLib HAL for mruby-task +** +** See Copyright Notice in mruby.h +** +** Drives the mruby-task scheduler from an embedding application's GLib +** main loop. No Task.run is required from Ruby; the HAL fires +** mrb_task_run_once and mrb_tick automatically as the host loop iterates, +** which is the integration pattern a GTK or webview app uses in practice. +** +** Two GSources collaborate: +** +** 1. VM-run source on the thread-default GMainContext. Its callback +** runs mrb_task_run_once on every registered VM. +** +** 2. Tick source on a dedicated GMainContext, iterated by a private +** GThread. Its callback runs mrb_tick on every registered VM under +** a recursive mutex. The separate thread is what gives us +** preemption: it can fire mrb_tick while the VM thread is blocked +** inside mrb_vm_exec. +** +** Both are manual GSources -- NULL prepare/check, dispatch driven purely +** by ready_time updates via g_source_set_ready_time. g_timeout_source's +** auto-reschedule would race with park-when-idle. +** +** State machine, evaluated after every dispatch under the IRQ lock: +** +** has_ready (any q_ready_) : vm_run = 0, tick = +1 interval +** has_sleep (q_waiting_) : vm_run = -1 (parked), tick = soonest sleeper +** neither : both = -1 (parked) +** +** In has_sleep state the ticker is the sole waker: it fires at the +** sleeper deadline, catches the scheduler clock up via mrb_tick, and +** sets vm_run = 0 once a task is promoted to ready. In neither state, +** mrb_task_enable_irq is the wake: any Ruby-side scheduler activity +** (Task.new from a bind callback, etc.) sets vm_run = 0 from outside. +** +** Tickless catch-up: in has_sleep state the ticker can be parked for +** arbitrarily long. On fire we compute (now - last_fire_us) + remainder, +** divide by MRB_TICK_INTERVAL_US, and call mrb_tick that many times in +** one go. The leftover < interval is carried in remainder_us to the +** next fire, keeping the scheduler clock aligned with monotonic time. +** Net effect: a loop of long sleeps costs one wakeup per sleep period. +** +** Threading: +** - Per-thread state lives in heap-allocated mrb_task_thread_state, +** reachable via thread-local `ts`. The ticker thread receives a +** pointer at spawn; it never touches another thread's TLS. +** - On the main thread no GMainContext push is needed; the default +** context is used implicitly. +** - On any other thread that opens an mrb_state, the caller MUST first +** call g_main_context_push_thread_default(). This is the standard +** GLib convention used by libsoup, GIO async, GTask, etc. +** +** Locking: +** - irq_lock (GRecMutex) covers every mutation of mrb->task state. +** mrb_task_disable_irq / mrb_task_enable_irq are lock / unlock, +** with the enable side additionally setting vm_run = 0. +** - The ticker holds the lock across its mrb_tick batch. +** - arm_locked runs with the lock held so a concurrent ticker can't +** promote a sleeper between our inspection and our arm decision. +** - mrb_vm_exec runs WITHOUT the lock; the ticker can preempt it +** mid-execution by setting switching_. +** +** Supported platforms: any system with GLib 2.x and GThread (Linux, +** BSD, macOS, Windows with MinGW or MSVC, ...). +*/ + +#include +#include +#include "task.h" +#include "task_hal.h" +#include +#include +#include + +#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L +# define MRB_TASK_TLS _Thread_local +#elif defined(__GNUC__) || defined(__clang__) || defined(__SUNPRO_C) || defined(__xlC__) || defined(__IBMC__) +# define MRB_TASK_TLS __thread +#elif defined(_MSC_VER) || defined(__BORLANDC__) +# define MRB_TASK_TLS __declspec(thread) +#else +# error "mruby-task GLib HAL: no thread-local storage qualifier known for this compiler" +#endif + +#define MRB_TICK_INTERVAL_US ((gint64)MRB_TICK_UNIT * 1000) + +typedef struct mrb_task_thread_state { + mrb_state *vm_list[MRB_TASK_MAX_VMS]; + int vm_count; + GRecMutex irq_lock; + + GMainContext *vm_ctx; + GSource *vm_run_src; + + GMainContext *tick_ctx; + GMainLoop *tick_loop; + GSource *tick_src; + GThread *ticker; + + /* Tickless catch-up: last_fire_us is the monotonic anchor; remainder_us + * is the sub-interval carry from the previous fire. */ + gint64 last_fire_us; + uint32_t remainder_us; +} mrb_task_thread_state; + +static MRB_TASK_TLS mrb_task_thread_state *ts; + +static gboolean +deadline_source_dispatch(GSource *source, GSourceFunc callback, gpointer user_data) +{ + (void)source; + if (!callback) { + return G_SOURCE_REMOVE; + } + return callback(user_data); +} + +static GSourceFuncs deadline_source_funcs = { + NULL, NULL, deadline_source_dispatch, NULL, NULL, NULL, +}; + +static void +free_thread_state(mrb_task_thread_state *s) +{ + if (!s) { + return; + } + if (s->ticker) { + if (s->tick_loop) { + g_main_loop_quit(s->tick_loop); + } + (void)g_thread_join(s->ticker); + s->ticker = NULL; + } + if (s->tick_src) { + g_source_destroy(s->tick_src); + g_source_unref(s->tick_src); + s->tick_src = NULL; + } + if (s->tick_loop) { + g_main_loop_unref(s->tick_loop); + s->tick_loop = NULL; + } + if (s->tick_ctx) { + g_main_context_unref(s->tick_ctx); + s->tick_ctx = NULL; + } + if (s->vm_run_src) { + g_source_destroy(s->vm_run_src); + g_source_unref(s->vm_run_src); + s->vm_run_src = NULL; + } + if (s->vm_ctx) { + g_main_context_unref(s->vm_ctx); + s->vm_ctx = NULL; + } + g_rec_mutex_clear(&s->irq_lock); + g_free(s); +} + +/* + * Walk every VM's queues, decide the arm state, apply it. Called with + * the IRQ lock held. q_waiting_ is walked directly to find the soonest + * SLEEP-reason wakeup; this is authoritative regardless of what + * mrb->task.wakeup_tick currently reads. + * + * Idempotent. Deadlines are computed relative to s->last_fire_us (the + * last actual ticker fire) rather than g_get_monotonic_time(), so + * repeated calls between fires yield the same ready_time and don't + * drift the cadence. set_ready_time is skipped when the value + * wouldn't change, to avoid the eventfd-write side effect. + */ +static void +arm_locked(mrb_task_thread_state *s) +{ + gint64 monotonic_now = g_get_monotonic_time(); + int32_t min_wake_offset = 0; + gboolean has_ready = FALSE; + gboolean has_sleep = FALSE; + gint64 new_vm_ready; + gint64 new_tick_ready; + gint64 cur_vm_ready; + gint64 cur_tick_ready; + int i; + + for (i = 0; i < s->vm_count; i++) { + mrb_state *vm = s->vm_list[i]; + if (!vm) continue; + + if (vm->task.queues[MRB_TASK_QUEUE_READY] != NULL) { + has_ready = TRUE; + continue; + } + + mrb_task *w = vm->task.queues[MRB_TASK_QUEUE_WAITING]; + while (w) { + if (w->reason == MRB_TASK_REASON_SLEEP) { + uint32_t wake = w->wait.wakeup_tick; + uint32_t tick = vm->task.tick; + int32_t offset = (int32_t)(wake - tick); + if (!has_sleep || offset < min_wake_offset) { + min_wake_offset = offset; + has_sleep = TRUE; + } + } + w = w->next; + } + } + + /* Anchor the catch-up clock when the ticker transitions from parked + * to active, so elapsed time counts from "now" rather than from + * before the park. */ + if ((has_ready || has_sleep) && + g_source_get_ready_time(s->tick_src) == -1) { + s->last_fire_us = monotonic_now; + s->remainder_us = 0; + } + + if (has_ready) { + new_vm_ready = 0; + new_tick_ready = s->last_fire_us + MRB_TICK_INTERVAL_US; + } + else if (has_sleep) { + new_vm_ready = -1; + if (min_wake_offset <= 0) { + new_tick_ready = 0; /* overdue, fire immediately */ + } + else { + new_tick_ready = s->last_fire_us + + (gint64)min_wake_offset * MRB_TICK_INTERVAL_US; + } + } + else { + new_vm_ready = -1; + new_tick_ready = -1; + } + + cur_vm_ready = g_source_get_ready_time(s->vm_run_src); + cur_tick_ready = g_source_get_ready_time(s->tick_src); + + if (new_vm_ready != cur_vm_ready) { + g_source_set_ready_time(s->vm_run_src, new_vm_ready); + } + if (new_tick_ready != cur_tick_ready) { + g_source_set_ready_time(s->tick_src, new_tick_ready); + } +} + +static gpointer +ticker_thread(gpointer data) +{ + mrb_task_thread_state *s = (mrb_task_thread_state *)data; + g_main_context_push_thread_default(s->tick_ctx); + g_main_loop_run(s->tick_loop); + g_main_context_pop_thread_default(s->tick_ctx); + return NULL; +} + +/* + * Tick GSource callback on the ticker thread. Computes catch-up ticks + * from elapsed monotonic time plus carried remainder, calls mrb_tick + * that many times under irq_lock, then lets arm_locked decide the + * next state (steady-state cadence vs tickless deadline vs full park). + */ +static gboolean +tick_source_cb(gpointer user_data) +{ + mrb_task_thread_state *s = (mrb_task_thread_state *)user_data; + int i; + gint64 now = g_get_monotonic_time(); + gint64 total_us = (now - s->last_fire_us) + (gint64)s->remainder_us; + gint64 raw_ticks = total_us / (gint64)MRB_TICK_INTERVAL_US; + uint32_t catch_up_ticks = (raw_ticks > (gint64)UINT32_MAX) + ? UINT32_MAX + : (uint32_t)raw_ticks; + + s->remainder_us = (uint32_t)(total_us - + (gint64)catch_up_ticks * (gint64)MRB_TICK_INTERVAL_US); + s->last_fire_us = now; + + g_rec_mutex_lock(&s->irq_lock); + for (i = 0; i < s->vm_count; i++) { + mrb_state *vm = s->vm_list[i]; + if (!vm) continue; + for (uint32_t k = 0; k < catch_up_ticks; k++) { + mrb_tick(vm); + } + } + arm_locked(s); + g_rec_mutex_unlock(&s->irq_lock); + + return G_SOURCE_CONTINUE; +} + +/* + * VM-run GSource callback on the VM thread. Snapshots vm_list under + * the IRQ lock, runs mrb_task_run_once on each entry outside the + * lock, then re-acquires to decide the next arm state. The snapshot + * protects against list-shape changes while we're iterating (e.g., a + * task body that registers or removes another mrb_state on this + * thread). + */ +static gboolean +vm_run_source_cb(gpointer user_data) +{ + mrb_state *snapshot[MRB_TASK_MAX_VMS]; + int snapshot_count; + int i; + (void)user_data; + + if (!ts) { + return G_SOURCE_CONTINUE; + } + + g_rec_mutex_lock(&ts->irq_lock); + snapshot_count = ts->vm_count; + memcpy(snapshot, ts->vm_list, (size_t)snapshot_count * sizeof(mrb_state *)); + g_rec_mutex_unlock(&ts->irq_lock); + + for (i = 0; i < snapshot_count; i++) { + if (snapshot[i]) { + (void)mrb_task_run_once(snapshot[i]); + } + } + + g_rec_mutex_lock(&ts->irq_lock); + arm_locked(ts); + g_rec_mutex_unlock(&ts->irq_lock); + + return G_SOURCE_CONTINUE; +} + +void +mrb_hal_task_init(mrb_state *mrb) +{ + int i; + int idx = -1; + gboolean first_on_thread = FALSE; + guint attach_id; + GError *err = NULL; + gchar *err_msg; + + for (i = 0; i < MRB_NUM_TASK_QUEUE; i++) { + mrb->task.queues[i] = NULL; + } + mrb->task.tick = 0; + mrb->task.wakeup_tick = UINT32_MAX; + mrb->task.switching = FALSE; + + if (ts == NULL) { + ts = g_new0(mrb_task_thread_state, 1); + g_rec_mutex_init(&ts->irq_lock); + ts->last_fire_us = g_get_monotonic_time(); + first_on_thread = TRUE; + } + + g_rec_mutex_lock(&ts->irq_lock); + + for (i = 0; i < ts->vm_count; i++) { + if (ts->vm_list[i] == mrb) { + idx = i; + break; + } + } + + if (idx < 0) { + if (ts->vm_count >= MRB_TASK_MAX_VMS) { + g_rec_mutex_unlock(&ts->irq_lock); + if (first_on_thread) { + free_thread_state(ts); + ts = NULL; + } + mrb_raisef(mrb, E_RUNTIME_ERROR, + "too many mrb_states with task scheduler on this thread " + "(max: %d)", + MRB_TASK_MAX_VMS); + } + ts->vm_list[ts->vm_count++] = mrb; + } + + g_rec_mutex_unlock(&ts->irq_lock); + + if (first_on_thread) { + ts->vm_ctx = g_main_context_ref_thread_default(); + g_assert_nonnull(ts->vm_ctx); + + ts->vm_run_src = g_source_new(&deadline_source_funcs, sizeof(GSource)); + g_assert_nonnull(ts->vm_run_src); + g_source_set_callback(ts->vm_run_src, vm_run_source_cb, NULL, NULL); + g_source_set_ready_time(ts->vm_run_src, -1); + + attach_id = g_source_attach(ts->vm_run_src, ts->vm_ctx); + if (attach_id == 0) { + free_thread_state(ts); + ts = NULL; + mrb_raise(mrb, E_RUNTIME_ERROR, + "mruby-task GLib HAL: g_source_attach failed for VM-run source"); + } + + ts->tick_ctx = g_main_context_new(); + g_assert_nonnull(ts->tick_ctx); + + ts->tick_loop = g_main_loop_new(ts->tick_ctx, FALSE); + g_assert_nonnull(ts->tick_loop); + + ts->tick_src = g_source_new(&deadline_source_funcs, sizeof(GSource)); + g_assert_nonnull(ts->tick_src); + g_source_set_callback(ts->tick_src, tick_source_cb, ts, NULL); + g_source_set_ready_time(ts->tick_src, -1); + + attach_id = g_source_attach(ts->tick_src, ts->tick_ctx); + if (attach_id == 0) { + free_thread_state(ts); + ts = NULL; + mrb_raise(mrb, E_RUNTIME_ERROR, + "mruby-task GLib HAL: g_source_attach failed for tick source"); + } + + ts->ticker = g_thread_try_new("mruby-task-tick", ticker_thread, ts, &err); + if (ts->ticker == NULL) { + /* Copy GLib's error message into a stack buffer before any mruby + * allocation, so mrb_raise's longjmp can't strand the GLib heap. */ + char buf[256]; + err_msg = g_strdup_printf( + "mruby-task GLib HAL: failed to spawn ticker thread: %s", + err ? err->message : "unknown error"); + g_strlcpy(buf, err_msg, sizeof(buf)); + g_free(err_msg); + if (err) { + g_error_free(err); + } + free_thread_state(ts); + ts = NULL; + mrb_raise(mrb, E_RUNTIME_ERROR, buf); + } + } +} + +void +mrb_hal_task_final(mrb_state *mrb) +{ + int i, j; + gboolean last_on_thread = FALSE; + + if (ts == NULL) { + return; + } + + g_rec_mutex_lock(&ts->irq_lock); + + for (i = 0; i < ts->vm_count; i++) { + if (ts->vm_list[i] == mrb) { + for (j = i; j < ts->vm_count - 1; j++) { + ts->vm_list[j] = ts->vm_list[j + 1]; + } + ts->vm_list[ts->vm_count - 1] = NULL; + ts->vm_count--; + break; + } + } + + if (ts->vm_count == 0) { + last_on_thread = TRUE; + } + + g_rec_mutex_unlock(&ts->irq_lock); + + if (last_on_thread) { + free_thread_state(ts); + ts = NULL; + } +} + +void +mrb_task_disable_irq(void) +{ + if (ts) { + g_rec_mutex_lock(&ts->irq_lock); + } +} + +/* Hooked into the scheduler's IRQ-release path. After any state + * change, re-evaluate the arm state of both sources via arm_locked. + * This is what wires up preemption: arm_locked sets tick_src's + * ready_time so the ticker thread fires mrb_tick on cadence. + * + * Without this, only vm_run_source_cb (the foreign-loop dispatch + * callback) ever calls arm_locked, so a Task.run-driven scheduler + * never arms the ticker -- preemption never happens, sleepers in + * q_waiting_ are never woken, and CPU-bound tasks spin forever. + * Calling arm_locked here covers both Task.run and foreign-loop + * drivers symmetrically. + * + * arm_locked also sets vm_run_src ready_time to 0 only when there's + * a ready task, which prevents the spurious wake during mrb_task_run's + * idle queue check (the disable_irq/check/enable_irq pattern over a + * read-only check produces no state change, so vm_run_src stays + * parked at -1). */ +void +mrb_task_enable_irq(void) +{ + if (!ts) { + return; + } + arm_locked(ts); + g_rec_mutex_unlock(&ts->irq_lock); +} + +/* Called only from mrb_task_run's idle loop. Iterates the VM context + * to dispatch any pending vm_run_src (e.g., the ticker just woke us + * because a sleeper became ready), then returns. Block-wait is OK now + * because mrb_task_enable_irq's q_ready_ guard prevents the spurious- + * wake loop that would otherwise spin this iteration. */ +void +mrb_hal_task_idle_cpu(mrb_state *mrb) +{ + (void)mrb; + if (ts && ts->vm_ctx) { + (void)g_main_context_iteration(ts->vm_ctx, TRUE); + } + else { + g_usleep(MRB_TICK_UNIT * 1000); + } +} + +void +mrb_hal_task_sleep_us(mrb_state *mrb, mrb_int usec) +{ + (void)mrb; + if (usec <= 0) { + return; + } + g_usleep((gulong)usec); +} diff --git a/mrbgems/mruby-task/tools/mruby_task_demo/mruby_task_demo.c b/mrbgems/mruby-task/tools/mruby_task_demo/mruby_task_demo.c new file mode 100644 index 000000000..a8e712639 --- /dev/null +++ b/mrbgems/mruby-task/tools/mruby_task_demo/mruby_task_demo.c @@ -0,0 +1,329 @@ +/* +** mruby-task-demo.c +** +** Three-thread test of the mruby-task GLib HAL. Each thread owns its +** own mrb_state and its own GMainContext (the HAL is thread-local and +** picks up the thread-default context at mrb_open time, then spawns +** its own ticker thread internally). +** +** T1 pure foreign-loop driver. Tasks are registered, then +** g_main_loop_run is what dispatches the scheduler via the +** HAL's vm_run_src. No Task.run anywhere. +** +** T2 mix. Phase 1 registers tasks and calls Task.run to drain +** them synchronously. Phase 2 registers more tasks and lets +** g_main_loop_run drive them. Exercises both drivers on the +** same mrb_state in sequence. +** +** T3 Task.run only. Registers tasks and calls Task.run. The demo +** thread never enters g_main_loop_run -- Task.run is the +** scheduler driver, and the HAL's idle hook iterates vm_ctx +** from inside Task.run so the ticker's cross-thread wakes +** still get dispatched. +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static gint64 start_us; + +static void +log_line(const char *msg) +{ + gint64 ms = (g_get_monotonic_time() - start_us) / 1000; + printf("[t=%5" PRId64 " ms] %s\n", ms, msg); + fflush(stdout); +} + +static mrb_value +rb_log(mrb_state *mrb, mrb_value self) +{ + const char *msg; + (void)self; + mrb_get_args(mrb, "z", &msg); + log_line(msg); + return mrb_nil_value(); +} + +static void +run_ruby(mrb_state *mrb, const char *code) +{ + mrb_load_string(mrb, code); + if (mrb->exc) { + mrb_value exc = mrb_obj_value(mrb->exc); + mrb_value str = mrb_funcall(mrb, exc, "to_s", 0); + fprintf(stderr, "Ruby error: %s\n", RSTRING_PTR(str)); + fflush(stderr); + mrb->exc = NULL; + } +} + +static void +banner(const char *msg) +{ + printf("===== %s =====\n", msg); + fflush(stdout); +} + +/* + * T1 -- pure foreign-loop driver. Runs on the main thread (no + * separate GThread for T1; the main thread is the foreign loop). + * + * Pulse task with mixed sleep styles, three staggered sleepers, and + * spinner + stopper for timeslice preemption. g_main_loop_run is the + * only scheduler driver. + */ +static void +run_glib_only(void) +{ + GMainContext *ctx; + GMainLoop *loop; + GSource *timeout; + mrb_state *mrb; + + ctx = g_main_context_new(); + g_main_context_push_thread_default(ctx); + loop = g_main_loop_new(ctx, FALSE); + + mrb = mrb_open(); + mrb_define_method(mrb, mrb->object_class, "log", rb_log, MRB_ARGS_REQ(1)); + + banner("T1 (glib-only): pulse + 3 staggered sleepers + spinner/stopper"); + + run_ruby(mrb, + "$t1_done = false\n" + "Task.new(name: 'T1.pulse') {\n" + " log 'T1.pulse: usleep 8000 x5'\n" + " 5.times { usleep 8000; log 'T1.pulse: micro' }\n" + " log 'T1.pulse: sleep_ms 120 x2'\n" + " 2.times { sleep_ms 120; log 'T1.pulse: chunk' }\n" + " log 'T1.pulse: sleep 0.3'\n" + " sleep 0.3\n" + " log 'T1.pulse: long done'\n" + "}\n" + "[30, 60, 90].each do |ms|\n" + " Task.new(name: 'T1.s' + ms.to_s) {\n" + " log 'T1.sleeper' + ms.to_s + ': sleeping'\n" + " sleep_ms ms\n" + " log 'T1.sleeper' + ms.to_s + ': woke'\n" + " }\n" + "end\n" + "Task.new(name: 'T1.spinner', priority: 200) {\n" + " log 'T1.spinner: entering tight loop'\n" + " loops = 0\n" + " loop {\n" + " loops += 1\n" + " break if $t1_done\n" + " break if loops > 200_000_000\n" + " }\n" + " log 'T1.spinner: exit loops=' + loops.to_s + ' done=' + $t1_done.to_s\n" + "}\n" + "Task.new(name: 'T1.stopper', priority: 50) {\n" + " log 'T1.stopper: sleeping 100 ms'\n" + " sleep 0.1\n" + " log 'T1.stopper: setting $t1_done'\n" + " $t1_done = true\n" + "}\n" + ); + + timeout = g_timeout_source_new(700); + g_source_set_callback(timeout, (GSourceFunc)g_main_loop_quit, loop, NULL); + g_source_attach(timeout, ctx); + g_source_unref(timeout); + + log_line("T1: entering g_main_loop_run (700 ms cap)"); + g_main_loop_run(loop); + log_line("T1: g_main_loop_run returned"); + + mrb_close(mrb); + g_main_loop_unref(loop); + g_main_context_pop_thread_default(ctx); + g_main_context_unref(ctx); +} + +/* + * T2 -- mixed driver. + * + * Phase 1: register a small task set, call Task.run, which blocks + * until those tasks drain. Phase 2: register more tasks and let + * g_main_loop_run drive them. + */ +static gpointer +thread_glib_and_taskrun(gpointer data) +{ + GMainContext *ctx; + GMainLoop *loop; + GSource *timeout; + mrb_state *mrb; + (void)data; + + ctx = g_main_context_new(); + g_main_context_push_thread_default(ctx); + loop = g_main_loop_new(ctx, FALSE); + + mrb = mrb_open(); + mrb_define_method(mrb, mrb->object_class, "log", rb_log, MRB_ARGS_REQ(1)); + + banner("T2 (mix): phase 1 = yield + suspend/resume, drained by Task.run"); + + run_ruby(mrb, + "victim = Task.new(name: 'T2.victim') {\n" + " log 'T2.victim: sleeping 200 ms'\n" + " sleep 0.2\n" + " log 'T2.victim: woke'\n" + "}\n" + "Task.new(name: 'T2.controller', priority: 50) {\n" + " sleep 0.05\n" + " log 'T2.controller: suspending victim (was ' + victim.status.to_s + ')'\n" + " victim.suspend\n" + " log 'T2.controller: victim now ' + victim.status.to_s\n" + " sleep 0.1\n" + " log 'T2.controller: resuming victim (was ' + victim.status.to_s + ')'\n" + " victim.resume\n" + " log 'T2.controller: victim now ' + victim.status.to_s\n" + "}\n" + "Task.new(name: 'T2.yieldA', priority: 100) {\n" + " 3.times { |i| log 'T2.yieldA: iter ' + i.to_s; Task.pass }\n" + "}\n" + "Task.new(name: 'T2.yieldB', priority: 100) {\n" + " 3.times { |i| log 'T2.yieldB: iter ' + i.to_s; Task.pass }\n" + "}\n" + "log 'T2: calling Task.run (drains phase 1)'\n" + "Task.run\n" + "log 'T2: Task.run returned'\n" + ); + + banner("T2 (mix): phase 2 = 3 staggered sleepers, driven by g_main_loop_run"); + + run_ruby(mrb, + "[40, 80, 120].each do |ms|\n" + " Task.new(name: 'T2.s' + ms.to_s) {\n" + " log 'T2.sleeper' + ms.to_s + ': sleeping'\n" + " sleep_ms ms\n" + " log 'T2.sleeper' + ms.to_s + ': woke'\n" + " }\n" + "end\n" + "log 'T2: phase 2 registered'\n" + ); + + timeout = g_timeout_source_new(400); + g_source_set_callback(timeout, (GSourceFunc)g_main_loop_quit, loop, NULL); + g_source_attach(timeout, ctx); + g_source_unref(timeout); + + log_line("T2: entering g_main_loop_run (400 ms cap)"); + g_main_loop_run(loop); + log_line("T2: g_main_loop_run returned"); + + mrb_close(mrb); + g_main_loop_unref(loop); + g_main_context_pop_thread_default(ctx); + g_main_context_unref(ctx); + return NULL; +} + +/* + * T3 -- Task.run only. + * + * No g_main_loop_run on the demo thread. Task.run drives the + * scheduler; mrb_hal_task_idle_cpu iterates vm_ctx from inside + * Task.run's idle loop so the ticker's cross-thread set_ready_time + * still wakes the demo thread when sleepers come due. Returns when + * all queues drain. + */ +static gpointer +thread_taskrun_only(gpointer data) +{ + GMainContext *ctx; + mrb_state *mrb; + (void)data; + + ctx = g_main_context_new(); + g_main_context_push_thread_default(ctx); + + mrb = mrb_open(); + mrb_define_method(mrb, mrb->object_class, "log", rb_log, MRB_ARGS_REQ(1)); + + banner("T3 (Task.run only): zombie/executioner + spinner/stopper + sleepers"); + + run_ruby(mrb, + "$t3_done = false\n" + "$t3_zombie_ticks = 0\n" + "zombie = Task.new(name: 'T3.zombie') {\n" + " log 'T3.zombie: alive (will tick every 50 ms forever)'\n" + " loop {\n" + " sleep_ms 50\n" + " $t3_zombie_ticks += 1\n" + " log 'T3.zombie: tick ' + $t3_zombie_ticks.to_s\n" + " }\n" + " log 'T3.zombie: NEVER REACHED'\n" + "}\n" + "Task.new(name: 'T3.executioner', priority: 50) {\n" + " sleep_ms 175\n" + " log 'T3.executioner: terminating zombie (was ' + zombie.status.to_s + ')'\n" + " zombie.terminate\n" + " log 'T3.executioner: zombie is now ' + zombie.status.to_s\n" + "}\n" + "Task.new(name: 'T3.spinner', priority: 200) {\n" + " log 'T3.spinner: entering tight loop'\n" + " loops = 0\n" + " loop {\n" + " loops += 1\n" + " break if $t3_done\n" + " break if loops > 200_000_000\n" + " }\n" + " log 'T3.spinner: exit loops=' + loops.to_s + ' done=' + $t3_done.to_s\n" + "}\n" + "Task.new(name: 'T3.stopper', priority: 50) {\n" + " log 'T3.stopper: sleeping 100 ms'\n" + " sleep 0.1\n" + " log 'T3.stopper: setting $t3_done'\n" + " $t3_done = true\n" + "}\n" + "[20, 40, 60].each do |ms|\n" + " Task.new(name: 'T3.s' + ms.to_s) {\n" + " log 'T3.sleeper' + ms.to_s + ': sleeping'\n" + " sleep_ms ms\n" + " log 'T3.sleeper' + ms.to_s + ': woke'\n" + " }\n" + "end\n" + "log 'T3: calling Task.run'\n" + "Task.run\n" + "log 'T3: Task.run returned (all queues empty)'\n" + ); + + mrb_close(mrb); + g_main_context_pop_thread_default(ctx); + g_main_context_unref(ctx); + return NULL; +} + +int main(int argc, char **argv) +{ + GThread *t2, *t3; + (void)argc; + (void)argv; + + start_us = g_get_monotonic_time(); + + log_line("main: spawning T2 + T3; running T1 (glib-only) on main thread"); + + t2 = g_thread_new("T2.mix", thread_glib_and_taskrun, NULL); + t3 = g_thread_new("T3.taskrun", thread_taskrun_only, NULL); + + run_glib_only(); + + g_thread_join(t2); + g_thread_join(t3); + + log_line("main: T2 and T3 joined"); + printf("All scenarios completed.\n"); + return 0; +}