qr_reader-native 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE-mruby.txt +22 -0
  3. data/LICENSE-spinel.txt +19 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +61 -0
  6. data/ext/qr_reader_native/SPINEL_VERSION +1 -0
  7. data/ext/qr_reader_native/extconf.rb +8 -0
  8. data/ext/qr_reader_native/mruby/error.h +13 -0
  9. data/ext/qr_reader_native/mruby/internal.h +145 -0
  10. data/ext/qr_reader_native/mruby/string.h +22 -0
  11. data/ext/qr_reader_native/mruby.h +93 -0
  12. data/ext/qr_reader_native/mruby_shim.h +118 -0
  13. data/ext/qr_reader_native/qr_reader_native.c +13513 -0
  14. data/ext/qr_reader_native/qr_reader_native.h +29 -0
  15. data/ext/qr_reader_native/qr_reader_native_ext.c +598 -0
  16. data/ext/qr_reader_native/re_cased.h +71 -0
  17. data/ext/qr_reader_native/re_compile.c +5784 -0
  18. data/ext/qr_reader_native/re_ctype.h +3540 -0
  19. data/ext/qr_reader_native/re_exec.c +2190 -0
  20. data/ext/qr_reader_native/re_internal.h +671 -0
  21. data/ext/qr_reader_native/re_prop.h +4629 -0
  22. data/ext/qr_reader_native/re_spinel.c +363 -0
  23. data/ext/qr_reader_native/re_spinel.h +22 -0
  24. data/ext/qr_reader_native/re_spinel_utf8.inc +97 -0
  25. data/ext/qr_reader_native/re_utf8.c +156 -0
  26. data/ext/qr_reader_native/sp_alloc.c +1447 -0
  27. data/ext/qr_reader_native/sp_alloc.h +1196 -0
  28. data/ext/qr_reader_native/sp_argf.h +33 -0
  29. data/ext/qr_reader_native/sp_array.c +1052 -0
  30. data/ext/qr_reader_native/sp_array.h +456 -0
  31. data/ext/qr_reader_native/sp_bigint.c +5826 -0
  32. data/ext/qr_reader_native/sp_bigint.h +117 -0
  33. data/ext/qr_reader_native/sp_cold.c +5259 -0
  34. data/ext/qr_reader_native/sp_compat.h +402 -0
  35. data/ext/qr_reader_native/sp_core.c +818 -0
  36. data/ext/qr_reader_native/sp_core.h +56 -0
  37. data/ext/qr_reader_native/sp_crypto.c +877 -0
  38. data/ext/qr_reader_native/sp_crypto.h +169 -0
  39. data/ext/qr_reader_native/sp_dtoa.c +1533 -0
  40. data/ext/qr_reader_native/sp_dtoa.h +29 -0
  41. data/ext/qr_reader_native/sp_encoding_names.h +172 -0
  42. data/ext/qr_reader_native/sp_enum.h +86 -0
  43. data/ext/qr_reader_native/sp_exc.c +1185 -0
  44. data/ext/qr_reader_native/sp_exc.h +186 -0
  45. data/ext/qr_reader_native/sp_exc_ctx.h +56 -0
  46. data/ext/qr_reader_native/sp_fiber.c +889 -0
  47. data/ext/qr_reader_native/sp_fiber.h +162 -0
  48. data/ext/qr_reader_native/sp_fiber_ctx.h +84 -0
  49. data/ext/qr_reader_native/sp_format.c +695 -0
  50. data/ext/qr_reader_native/sp_format.h +83 -0
  51. data/ext/qr_reader_native/sp_gc.c +1793 -0
  52. data/ext/qr_reader_native/sp_gc.h +762 -0
  53. data/ext/qr_reader_native/sp_hash.c +131 -0
  54. data/ext/qr_reader_native/sp_hash.h +131 -0
  55. data/ext/qr_reader_native/sp_inspect.c +215 -0
  56. data/ext/qr_reader_native/sp_inspect.h +128 -0
  57. data/ext/qr_reader_native/sp_io.c +1676 -0
  58. data/ext/qr_reader_native/sp_io.h +272 -0
  59. data/ext/qr_reader_native/sp_iobuffer.c +1014 -0
  60. data/ext/qr_reader_native/sp_iobuffer.h +167 -0
  61. data/ext/qr_reader_native/sp_marshal.c +487 -0
  62. data/ext/qr_reader_native/sp_marshal.h +60 -0
  63. data/ext/qr_reader_native/sp_net.c +1029 -0
  64. data/ext/qr_reader_native/sp_net.h +205 -0
  65. data/ext/qr_reader_native/sp_pack.c +1254 -0
  66. data/ext/qr_reader_native/sp_poly_cold.c +1220 -0
  67. data/ext/qr_reader_native/sp_proc.c +410 -0
  68. data/ext/qr_reader_native/sp_proc.h +327 -0
  69. data/ext/qr_reader_native/sp_process.c +547 -0
  70. data/ext/qr_reader_native/sp_process_status.c +211 -0
  71. data/ext/qr_reader_native/sp_process_status.h +66 -0
  72. data/ext/qr_reader_native/sp_random.c +287 -0
  73. data/ext/qr_reader_native/sp_random.h +52 -0
  74. data/ext/qr_reader_native/sp_range.h +144 -0
  75. data/ext/qr_reader_native/sp_re.c +1379 -0
  76. data/ext/qr_reader_native/sp_re.h +186 -0
  77. data/ext/qr_reader_native/sp_re_names.h +59 -0
  78. data/ext/qr_reader_native/sp_sched.c +4044 -0
  79. data/ext/qr_reader_native/sp_sched.h +295 -0
  80. data/ext/qr_reader_native/sp_slab.c +1507 -0
  81. data/ext/qr_reader_native/sp_str.c +1462 -0
  82. data/ext/qr_reader_native/sp_str.h +306 -0
  83. data/ext/qr_reader_native/sp_str_crypt.c +23 -0
  84. data/ext/qr_reader_native/sp_string.c +92 -0
  85. data/ext/qr_reader_native/sp_string.h +253 -0
  86. data/ext/qr_reader_native/sp_system.c +78 -0
  87. data/ext/qr_reader_native/sp_system.h +18 -0
  88. data/ext/qr_reader_native/sp_time.c +860 -0
  89. data/ext/qr_reader_native/sp_time.h +100 -0
  90. data/ext/qr_reader_native/sp_types.h +292 -0
  91. data/ext/qr_reader_native/spinel_rt.h +19017 -0
  92. data/ext/qr_reader_native/unicase.c +399 -0
  93. data/ext/qr_reader_native/unicase.h +901 -0
  94. data/lib/qr_reader/native.rb +6 -0
  95. metadata +153 -0
@@ -0,0 +1,4044 @@
1
+ /* sp_sched.c -- cooperative M:N thread scheduler bodies, Phase 0 (N=1).
2
+ * See sp_sched.h. Built on the sp_fiber context switch: the main thread runs on
3
+ * the root fiber and pumps a run queue of green threads whenever it blocks. */
4
+ #include "sp_sched.h"
5
+ #include "sp_alloc.h" /* sp_box_nil / sp_box_obj */
6
+ #include <stdlib.h>
7
+ #include <string.h>
8
+ #include <stdio.h>
9
+ #include <unistd.h> /* sysconf (worker count) */
10
+ #include <time.h> /* clock_gettime (Kernel#sleep) */
11
+ #include <errno.h> /* EINTR (sleep fallback) */
12
+ #ifdef __linux__
13
+ #include <sys/prctl.h> /* PR_SET_NAME (the sweeper threads' name) */
14
+ #endif
15
+ #include <sys/wait.h> /* waitpid (sp_sched_wait_child) */
16
+ #include <signal.h> /* preemption signal (SIGURG by default) */
17
+ #include <strings.h> /* strcasecmp (SPINEL_PREEMPT_SIGNAL by name) */
18
+ #include <stdint.h> /* intptr_t (worker id passed via pthread arg) */
19
+ #include <poll.h> /* poll (scheduler-aware I/O) */
20
+ #include <fcntl.h> /* fcntl O_NONBLOCK (monitor wake pipe) */
21
+
22
+ /* Which readiness backend this build has, decided BEFORE anything that tests
23
+ it. It used to be declared beside the registration table, halfway down --
24
+ below the wake helpers, whose `#ifdef SP_EV_BACKEND` arms were therefore
25
+ compiled out. Every cross-worker kick vanished with them, and only the
26
+ backstop timeout was left to deliver a wake: the ping-pong ran at full speed
27
+ on one worker (which needs no kick) and at a twentieth of it on two. */
28
+ #if defined(SP_THREADS) && defined(__linux__)
29
+ #define SP_EV_BACKEND 1
30
+ #define SP_EV_EPOLL 1
31
+ #include <sys/epoll.h>
32
+ #elif defined(SP_THREADS) && (defined(__APPLE__) || defined(__FreeBSD__) || \
33
+ defined(__OpenBSD__) || defined(__NetBSD__))
34
+ /* kqueue keys by (descriptor, filter), so READ and WRITE are two entries for
35
+ one fd; EV_ONESHOT DELETES the entry after its delivery where epoll only
36
+ disables it, which makes the re-arm a plain EV_ADD. Everything above the
37
+ three backend calls is the same on both. */
38
+ #define SP_EV_BACKEND 1
39
+ #define SP_EV_KQUEUE 1
40
+ #include <sys/types.h> /* <sys/event.h> wants it first on the BSDs */
41
+ #include <sys/event.h>
42
+ #endif
43
+ #ifdef SP_EV_BACKEND
44
+ static void sp_ev_kick(int wid);
45
+ #endif
46
+
47
+ /* Reached by name (defined in lib/sp_alloc.c or the generated TU), exactly as
48
+ lib/sp_fiber.c reaches them. */
49
+ void *sp_gc_alloc(size_t sz, void (*fin)(void *), void (*scn)(void *));
50
+ void sp_re_push_match_roots(void); /* lib/sp_re.c: STW match-register publishing */
51
+ SP_NORETURN void sp_raise_cls(const char *cls, const char *msg);
52
+ void sp_fiber_reraise(const char *cls, const char *msg, void *obj);
53
+ /* Per-context exception handler stack (defined in the generated TU). */
54
+ void *sp_exc_ctx_new(void);
55
+ void sp_exc_ctx_save(void *p);
56
+ void sp_exc_ctx_load(void *p);
57
+ void sp_exc_ctx_free(void *p);
58
+
59
+ /* Safepoint (design 5.1). Set while a collector wants the world stopped; polled
60
+ at loop back-edges (codegen) and at blocking points. Defined unconditionally
61
+ (see sp_sched.h) so a threaded program's poll links against either archive;
62
+ the flag is only ever set in the threaded build. sp_safepoint() parks the
63
+ worker at the GC barrier (defined below, after the lock). At N=1 the flag is
64
+ never set by another worker, so a single worker never parks here. */
65
+ volatile int sp_safepoint_flag = 0;
66
+ void (*sp_safepoint_publish_hook)(void) = NULL; /* set by the generated TU (sp_sched.h) */
67
+
68
+ /* ---- scheduler lock (design 3, Appendix B) ----
69
+ * One mutex guards all scheduler/sync metadata: the run queue, the live-thread
70
+ * registry, every wait list (joiners, Queue/Mutex/ConditionVariable waiters) and
71
+ * the Queue ring buffers. A worker holds it only while touching that metadata
72
+ * and ALWAYS drops it across a fiber transfer (running a green thread or parking
73
+ * itself). The held region therefore never polls a safepoint, allocates from the
74
+ * GC heap, or runs Ruby -- which is what lets a stop-the-world collector make
75
+ * progress: a worker blocked on this lock releases it within a bounded critical
76
+ * section and then reaches its next safepoint. Internal helpers (rq_*, reg_*,
77
+ * sp_sched_wake_one, sp_sched_unpark, sp_thread_wake_joiners) run with the lock
78
+ * already held by their caller; sp_sched_block / sp_sched_pump / sp_sched_pass
79
+ * are entered and left with the lock held, bracketing their transfers. In the
80
+ * single-threaded archive the macros are no-ops, so that build is byte-identical
81
+ * and the N=1 path is unchanged save for the (uncontended) lock calls. */
82
+ #ifndef SP_MAX_WORKERS
83
+ #define SP_MAX_WORKERS 256 /* both builds: sizes the per-worker run-queue array */
84
+ #endif
85
+ /* The monotonic clock, outside the SP_THREADS guard: the monitor uses it for
86
+ deadlines, and so does Thread#join(limit), which is compiled into both
87
+ builds because the codegen emits its symbol unconditionally. */
88
+ static double sp_monotonic_now(void) {
89
+ struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts);
90
+ return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9;
91
+ }
92
+ #ifdef SP_THREADS
93
+ #include <pthread.h>
94
+ static pthread_mutex_t g_sched_lock = PTHREAD_MUTEX_INITIALIZER;
95
+ #define SCHED_LOCK() pthread_mutex_lock(&g_sched_lock)
96
+ #define SCHED_UNLOCK() pthread_mutex_unlock(&g_sched_lock)
97
+
98
+ /* ---- stop-the-world GC barrier (design 6.2) ----
99
+ * All state guarded by g_sched_lock. g_stw_active is the authoritative
100
+ * "collection in progress" predicate; sp_safepoint_flag is a lock-free hint the
101
+ * codegen polls at loop back-edges so a running worker checks in cheaply. The
102
+ * triggering worker (over the alloc threshold) either becomes the sole collector
103
+ * or, if one is already running, parks like everyone else -- there is no
104
+ * separate collector lock to block on, so a worker that crossed the threshold
105
+ * can never stall the collector by being un-parkable. */
106
+ int sp_active_workers = 1; /* exported: worker count */ /* worker count; C-3b raises it past 1 */
107
+ static int g_nparked = 0; /* workers parked at the barrier right now */
108
+ /* The fiber each parked worker was running when it published its roots (a green
109
+ thread, or the worker's root fiber for an idle/main worker). The collector
110
+ marks these: green-thread fibers are also reached via sp_fiber_list_head, but a
111
+ worker's root fiber is not on that list, so without this a helper collector
112
+ would miss the main thread's top-level roots. */
113
+ static sp_Fiber *g_parked_fiber[2 * SP_MAX_WORKERS]; /* up to 2 per worker: current + root */
114
+ static int g_n_parked_fiber = 0;
115
+ static int g_stw_active = 0; /* a collection is in progress */
116
+ /* Parallel sweep phase, inside the stop-the-world window. The workers are
117
+ already parked here doing nothing while the collector sweeps, and the sweep
118
+ is 93% of the stopped time -- so hand each of them its OWN slot instead.
119
+ Its own, not any slot: freeing an object returns it to the arena it was
120
+ allocated from, and a single thread freeing eight workers' objects pays for
121
+ eight arena locks and eight cold metadata sets. */
122
+ static int g_sweep_go = 0; /* collector wants the tasks run */
123
+ static pthread_cond_t g_sweep_cv = PTHREAD_COND_INITIALIZER;
124
+ /* survivors, per slot, spliced onto the shared old heap by the collector */
125
+ static sp_gc_hdr *g_sw_head[SP_MAX_WORKERS];
126
+ static sp_gc_hdr *g_sw_tail[SP_MAX_WORKERS];
127
+ static size_t g_sw_bytes[SP_MAX_WORKERS];
128
+ /* Decided once per collection by sp_sched_par_sweep, read by every worker. */
129
+ static int g_str_sweep = 0;
130
+ static int g_str_major = 0;
131
+ static size_t g_str_promoted[SP_MAX_WORKERS];
132
+ /* The sweep is a list of TASKS, not a slot per worker. One busy worker's
133
+ young lists are the bulk of most cycles on a server (a request handler
134
+ allocates; the others idle), and with a slot per worker that one worker
135
+ swept alone while the rest waited: the longest slot WAS the phase, four
136
+ milliseconds of thirty workers idle. So a slot is several tasks -- its
137
+ object list, its old string list on a major, and each of its SP_STR_YSUB
138
+ young string lists -- and every parked worker claims tasks off a counter
139
+ until none is left. The tasks of one slot touch distinct lists; what they
140
+ share (the slot's old string head and byte counters) is settled by the
141
+ collector afterwards from the per-task results. */
142
+ enum { SW_OBJ, SW_STR_OLD, SW_STR_YOUNG, SW_CHUNKS };
143
+ typedef struct { short kind, wid, sub; } sp_sw_task;
144
+ #define SW_TASK_MAX (SP_MAX_WORKERS * (3 + SP_STR_YSUB))
145
+ static sp_sw_task g_sw_tasks[SW_TASK_MAX];
146
+ static int g_sw_ntasks = 0;
147
+ static int g_sw_next = 0; /* claimed by fetch-add, off the lock */
148
+ static int g_sw_done = 0; /* completed, under the lock */
149
+ /* per (slot, young list): the sweep's local results, spliced in by the collector */
150
+ static sp_str_hdr *g_sy_keep[SP_MAX_WORKERS][SP_STR_YSUB];
151
+ static sp_str_hdr *g_sy_tail[SP_MAX_WORKERS][SP_STR_YSUB];
152
+ static size_t g_sy_moved[SP_MAX_WORKERS][SP_STR_YSUB];
153
+ static size_t g_sy_held[SP_MAX_WORKERS][SP_STR_YSUB];
154
+ /* SPINEL_GC_PHASES: the longest single task of each sweep, summed, beside
155
+ the phase's wall. The gap between the two is the cost of driving the
156
+ parallel phase itself (waking the parked workers and collecting their
157
+ reports), which is what to look at when the phase is long and the tasks
158
+ are not. */
159
+ static unsigned long g_sw_slot_max_us = 0;
160
+ static unsigned long g_sw_task_sum_us = 0; /* every task's time added: the work the phase spread */
161
+ static unsigned long g_sw_task_max_kind[4]; /* the kind of the longest task, for the report */
162
+ void sp_gc_sweep_chunks_slot(int wid); /* lib/sp_gc.c: the slab bitmaps of one slot, this cycle */
163
+ static void sp_sweep_task(const sp_sw_task *t) {
164
+ double t0 = sp_gc_ph_on ? sp_monotonic_now() : 0;
165
+ switch (t->kind) {
166
+ case SW_OBJ: sp_gc_sweep_slot(t->wid, &g_sw_head[t->wid], &g_sw_tail[t->wid], &g_sw_bytes[t->wid]); break;
167
+ case SW_CHUNKS: sp_gc_sweep_chunks_slot(t->wid); break;
168
+ case SW_STR_OLD: sp_str_sweep_old_one(t->wid); break;
169
+ default: sp_str_sweep_young_one(t->wid, t->sub, &g_sy_keep[t->wid][t->sub], &g_sy_tail[t->wid][t->sub],
170
+ &g_sy_moved[t->wid][t->sub], &g_sy_held[t->wid][t->sub]); break;
171
+ }
172
+ if (sp_gc_ph_on) {
173
+ /* microseconds in an integer, so the max is one atomic */
174
+ unsigned long d = (unsigned long)((sp_monotonic_now() - t0) * 1e6), m;
175
+ SP_ATOMIC_FETCH_ADD(&g_sw_task_sum_us, d, __ATOMIC_RELAXED);
176
+ SP_ATOMIC_FETCH_ADD(&g_sw_task_max_kind[t->kind], d, __ATOMIC_RELAXED);
177
+ do { m = SP_ATOMIC_LOAD(&g_sw_slot_max_us, __ATOMIC_RELAXED); if (d <= m) break; }
178
+ while (!SP_ATOMIC_CAS(&g_sw_slot_max_us, &m, d, 0, __ATOMIC_RELAXED, __ATOMIC_RELAXED));
179
+ }
180
+ }
181
+ /* The parallel mark: parked workers lent to the collector's drain. */
182
+ static int g_mk_go = 0; /* a drain wants helpers */
183
+ static unsigned g_mk_gen = 0; /* one per drain; a worker helps a drain once */
184
+ static int g_mk_joined = 0; /* helpers that took a seat this drain */
185
+ static int g_mk_active = 0; /* helpers still inside the drain */
186
+ static int g_mk_max = 0; /* seats: SPINEL_GC_MARKERS, default min(cores, 8) */
187
+ static pthread_cond_t g_mk_cv = PTHREAD_COND_INITIALIZER; /* the collector waits for active == 0 */
188
+ static SP_TLS unsigned g_mk_seen = 0;
189
+ static int g_cs_help = 0; /* parked workers may claim the concurrent sweep's tasks */
190
+ static int g_cs_ntasks = 0, g_cs_finished = 0; /* the concurrent sweep's task list (below) */
191
+ static int g_cs_running = 0; /* sweeper threads and barrier helpers inside the task list */
192
+ extern double sp_gc_ph_park_sweeping; extern unsigned long long sp_gc_ph_park_sweeping_n; /* sp_gc.c */
193
+ static int g_cs_unclaimed = 0; /* tasks nobody has taken yet */
194
+ static void sp_cs_help_run(void);
195
+ static void sp_cs_owner_run(int wid);
196
+ static int g_cs_full; /* the running concurrent sweep is a full cycle's (defined with its siblings below) */
197
+ static int g_cs_owner_env = -1; /* SPINEL_GC_OWNER=0: the sweeper threads take every list */
198
+ /* Claim and run tasks until the list is exhausted. Off the scheduler lock. */
199
+ static int sp_sweep_run_tasks(void) {
200
+ int ran = 0;
201
+ for (;;) {
202
+ int i = SP_ATOMIC_FETCH_ADD(&g_sw_next, 1, __ATOMIC_RELAXED);
203
+ if (i >= g_sw_ntasks) break;
204
+ sp_sweep_task(&g_sw_tasks[i]);
205
+ ran++;
206
+ }
207
+ return ran;
208
+ }
209
+ static unsigned g_stw_epoch = 0; /* bumped each collection; scopes g_nparked to one */
210
+ static SP_TLS int g_collector_active = 0; /* this worker is mid-collection (re-entrancy guard) */
211
+ static int g_shutdown = 0; /* set at drain so helper workers exit their loop */
212
+ static int g_workers_started = 0; /* helper pool + monitor spawned lazily on the first Thread */
213
+ static int g_worker_cap = 0; /* max helper workers = min(cores, SPINEL_WORKERS) */
214
+ static int g_helpers_spawned = 0; /* helpers created so far; ids 1..g_helpers_spawned */
215
+ static pthread_cond_t g_sched_work = PTHREAD_COND_INITIALIZER; /* idle workers wait for runnable work */
216
+ static pthread_cond_t g_stw_request = PTHREAD_COND_INITIALIZER; /* collector waits for parks */
217
+ static pthread_cond_t g_stw_release = PTHREAD_COND_INITIALIZER; /* parked workers wait for clear */
218
+ static pthread_cond_t g_sysmon_cv = PTHREAD_COND_INITIALIZER; /* the monitor thread waits here */
219
+ static pthread_t g_sysmon; /* monitor: wakes sleepers + preempts */
220
+ static int g_sysmon_started = 0;
221
+ static int g_sysmon_idle = 0; /* monitor is parked on g_sysmon_cv (signal it to start ticking) */
222
+ static int g_sysmon_pipe[2] = { -1, -1 }; /* self-pipe: wake the monitor out of poll() */
223
+ static void sp_sched_report_stats(void); /* SPINEL_SCHED_STATS, below */
224
+ /* What the monitor actually did, for SPINEL_SCHED_STATS=1. The cost of a wake
225
+ is O(parked) three times over (rebuild, poll, unlink), so the number that
226
+ sizes a deployment is iterations x set size -- and neither is visible from
227
+ outside the process. Counted unconditionally: they are four increments on
228
+ the monitor's own thread, under the lock it already holds. (#4317) */
229
+ static unsigned long long g_mon_iters = 0; /* monitor loop turns */
230
+ static unsigned long long g_mon_polls = 0; /* poll(2) calls it made */
231
+ static unsigned long long g_mon_pollfds = 0; /* descriptors handed to those polls */
232
+ static unsigned long long g_mon_regs = 0; /* I/O parks registered */
233
+ static unsigned long long g_mon_readied = 0; /* waiters the poll found ready */
234
+ /* The counters are bumped with relaxed atomics: the report can read them
235
+ from an exit hook while the monitor still runs (#6489). */
236
+ #define SP_STAT_ADD(c, n) SP_ATOMIC_FETCH_ADD(&(c), (unsigned long long)(n), __ATOMIC_RELAXED)
237
+ #define SP_STAT_GET(c) SP_ATOMIC_LOAD(&(c), __ATOMIC_RELAXED)
238
+ /* Wake the monitor whether it idles on the condvar or blocks in poll(). PRE: lock held. */
239
+ static void sp_sysmon_wake(void) {
240
+ if (g_sysmon_idle) pthread_cond_signal(&g_sysmon_cv);
241
+ else if (g_sysmon_pipe[1] >= 0) { char c = 1; ssize_t r = write(g_sysmon_pipe[1], &c, 1); (void)r; }
242
+ }
243
+
244
+ /* The monitor's cached next wake time. Timer insertions lower it; canceling its
245
+ current minimum clears it so the monitor recomputes from the heap. With the
246
+ event backend, the monitor can skip rebuilding the I/O poll set until a
247
+ deadline is due. */
248
+ static double g_nearest = 0.0; /* 0 = nothing timed */
249
+ static void sp_deadline_added(double d) { /* PRE: sched lock held */
250
+ if (d <= 0.0) return;
251
+ if (g_nearest == 0.0 || d < g_nearest) {
252
+ g_nearest = d;
253
+ sp_sysmon_wake(); /* it must shorten its wait */
254
+ }
255
+ }
256
+
257
+ /* Timers are appended to a staging buffer while green threads park, then
258
+ inserted into this min-heap together by the monitor. Most waits run to their
259
+ deadline, so this keeps the park path cheap and amortizes heap repair across
260
+ the batch, like IO::Event::Timers. Canceled handles are left in place until
261
+ they reach the root or compaction makes rebuilding cheaper. */
262
+ enum { SP_TIMER_SLEEP, SP_TIMER_IO, SP_TIMER_WAIT };
263
+ struct sp_sched_timer {
264
+ double deadline;
265
+ sp_thread *thread;
266
+ int kind;
267
+ int active;
268
+ int in_heap;
269
+ size_t index;
270
+ };
271
+ #define SP_TIMER_COMPACT_MINIMUM 128
272
+ #define SP_TIMER_HEAPIFY_INSERT_RATIO 2
273
+ static sp_sched_timer **g_timer_heap = NULL;
274
+ static size_t g_timer_heap_len = 0, g_timer_heap_cap = 0;
275
+ static sp_sched_timer **g_timer_scheduled = NULL;
276
+ static size_t g_timer_scheduled_len = 0, g_timer_scheduled_cap = 0;
277
+ static size_t g_timer_cancelled = 0, g_timer_pending = 0;
278
+ static void sp_sched_timer_expire(sp_sched_timer *timer, double now);
279
+
280
+ static int sp_timer_reserve(sp_sched_timer ***items, size_t *capacity, size_t needed) {
281
+ if (*capacity >= needed) return 1;
282
+ size_t nc = *capacity ? *capacity : 16;
283
+ while (nc < needed) {
284
+ if (nc > SIZE_MAX / 2) { nc = needed; break; }
285
+ nc *= 2;
286
+ }
287
+ if (nc > SIZE_MAX / sizeof(sp_sched_timer *)) return 0;
288
+ sp_sched_timer **next = (sp_sched_timer **)realloc(*items, nc * sizeof(sp_sched_timer *));
289
+ if (!next) return 0;
290
+ *items = next;
291
+ *capacity = nc;
292
+ return 1;
293
+ }
294
+
295
+ static void sp_timer_swap(size_t a, size_t b) {
296
+ sp_sched_timer *tmp = g_timer_heap[a];
297
+ g_timer_heap[a] = g_timer_heap[b];
298
+ g_timer_heap[b] = tmp;
299
+ g_timer_heap[a]->index = a;
300
+ g_timer_heap[b]->index = b;
301
+ }
302
+
303
+ static void sp_timer_bubble_up(size_t i) {
304
+ while (i > 0) {
305
+ size_t parent = (i - 1) / 2;
306
+ if (g_timer_heap[parent]->deadline <= g_timer_heap[i]->deadline) break;
307
+ sp_timer_swap(i, parent);
308
+ i = parent;
309
+ }
310
+ }
311
+
312
+ static void sp_timer_bubble_down(size_t i) {
313
+ for (;;) {
314
+ size_t left = i * 2 + 1;
315
+ if (left >= g_timer_heap_len) return;
316
+ size_t right = left + 1;
317
+ size_t child = right < g_timer_heap_len &&
318
+ g_timer_heap[right]->deadline < g_timer_heap[left]->deadline ? right : left;
319
+ if (g_timer_heap[i]->deadline <= g_timer_heap[child]->deadline) return;
320
+ sp_timer_swap(i, child);
321
+ i = child;
322
+ }
323
+ }
324
+
325
+ static void sp_timer_heapify(void) {
326
+ for (size_t i = 0; i < g_timer_heap_len; i++) {
327
+ g_timer_heap[i]->in_heap = 1;
328
+ g_timer_heap[i]->index = i;
329
+ }
330
+ if (g_timer_heap_len > 1) {
331
+ for (size_t i = g_timer_heap_len / 2; i > 0; i--) sp_timer_bubble_down(i - 1);
332
+ }
333
+ }
334
+
335
+ static sp_sched_timer *sp_timer_pop_root(void) {
336
+ if (!g_timer_heap_len) return NULL;
337
+ sp_sched_timer *root = g_timer_heap[0];
338
+ sp_sched_timer *last = g_timer_heap[--g_timer_heap_len];
339
+ if (g_timer_heap_len) {
340
+ g_timer_heap[0] = last;
341
+ last->index = 0;
342
+ sp_timer_bubble_down(0);
343
+ }
344
+ root->in_heap = 0;
345
+ return root;
346
+ }
347
+
348
+ static sp_sched_timer *sp_timer_schedule(sp_thread *thread, double deadline, int kind) {
349
+ sp_sched_timer *timer = (sp_sched_timer *)malloc(sizeof(sp_sched_timer));
350
+ if (!timer) return NULL;
351
+ if (!sp_timer_reserve(&g_timer_scheduled, &g_timer_scheduled_cap, g_timer_scheduled_len + 1) ||
352
+ !sp_timer_reserve(&g_timer_heap, &g_timer_heap_cap,
353
+ g_timer_heap_len + g_timer_scheduled_len + 1)) {
354
+ free(timer);
355
+ return NULL;
356
+ }
357
+ timer->deadline = deadline;
358
+ timer->thread = thread;
359
+ timer->kind = kind;
360
+ timer->active = 1;
361
+ timer->in_heap = 0;
362
+ timer->index = 0;
363
+ g_timer_scheduled[g_timer_scheduled_len++] = timer;
364
+ thread->timer = timer;
365
+ g_timer_pending++;
366
+ sp_deadline_added(deadline);
367
+ return timer;
368
+ }
369
+
370
+ static void sp_timer_cancel(sp_thread *thread) {
371
+ sp_sched_timer *timer = thread->timer;
372
+ if (!timer) return;
373
+ thread->timer = NULL;
374
+ if (timer->active) {
375
+ timer->active = 0;
376
+ if (g_timer_pending) g_timer_pending--;
377
+ if (timer->in_heap) g_timer_cancelled++;
378
+ if (timer->deadline == g_nearest) {
379
+ g_nearest = 0.0;
380
+ sp_sysmon_wake();
381
+ }
382
+ }
383
+ }
384
+
385
+ static void sp_timer_flush(void) {
386
+ if (!g_timer_scheduled_len &&
387
+ !(g_timer_cancelled >= SP_TIMER_COMPACT_MINIMUM && g_timer_cancelled * 2 > g_timer_heap_len)) return;
388
+
389
+ if (g_timer_cancelled >= SP_TIMER_COMPACT_MINIMUM && g_timer_cancelled * 2 > g_timer_heap_len) {
390
+ size_t write = 0;
391
+ for (size_t i = 0; i < g_timer_heap_len; i++) {
392
+ sp_sched_timer *timer = g_timer_heap[i];
393
+ if (timer->active) g_timer_heap[write++] = timer;
394
+ else free(timer);
395
+ }
396
+ g_timer_heap_len = write;
397
+ g_timer_cancelled = 0;
398
+ for (size_t i = 0; i < g_timer_scheduled_len; i++) {
399
+ sp_sched_timer *timer = g_timer_scheduled[i];
400
+ if (timer->active) g_timer_heap[g_timer_heap_len++] = timer;
401
+ else free(timer);
402
+ }
403
+ g_timer_scheduled_len = 0;
404
+ sp_timer_heapify();
405
+ return;
406
+ }
407
+
408
+ size_t live = 0;
409
+ for (size_t i = 0; i < g_timer_scheduled_len; i++)
410
+ if (g_timer_scheduled[i]->active) live++;
411
+ int rebuild = g_timer_heap_len == 0 || live > g_timer_heap_len * SP_TIMER_HEAPIFY_INSERT_RATIO;
412
+ for (size_t i = 0; i < g_timer_scheduled_len; i++) {
413
+ sp_sched_timer *timer = g_timer_scheduled[i];
414
+ if (!timer->active) { free(timer); continue; }
415
+ size_t index = g_timer_heap_len++;
416
+ g_timer_heap[index] = timer;
417
+ timer->in_heap = 1;
418
+ timer->index = index;
419
+ if (!rebuild) sp_timer_bubble_up(index);
420
+ }
421
+ g_timer_scheduled_len = 0;
422
+ if (rebuild) sp_timer_heapify();
423
+ }
424
+
425
+ static sp_sched_timer *sp_timer_peek(void) {
426
+ sp_timer_flush();
427
+ while (g_timer_heap_len && !g_timer_heap[0]->active) {
428
+ sp_sched_timer *timer = sp_timer_pop_root();
429
+ if (g_timer_cancelled) g_timer_cancelled--;
430
+ free(timer);
431
+ }
432
+ return g_timer_heap_len ? g_timer_heap[0] : NULL;
433
+ }
434
+
435
+ static double sp_timer_next_deadline(void) {
436
+ sp_sched_timer *timer = sp_timer_peek();
437
+ return timer ? timer->deadline : 0.0;
438
+ }
439
+
440
+ static void sp_timer_fire_due(double now) {
441
+ sp_sched_timer *timer;
442
+ while ((timer = sp_timer_peek()) && timer->deadline <= now) {
443
+ sp_timer_pop_root();
444
+ sp_thread *thread = timer->thread;
445
+ timer->active = 0;
446
+ if (thread->timer == timer) thread->timer = NULL;
447
+ if (g_timer_pending) g_timer_pending--;
448
+ sp_sched_timer_expire(timer, now);
449
+ free(timer);
450
+ }
451
+ }
452
+
453
+ /* ---- preemption (design §5): timeslice tracking + the one safepoint flag ----
454
+ * The monitor watches how long each worker has run its current green thread; past
455
+ * a quantum it sets that thread's preempt_request, raises the safepoint flag, and
456
+ * SIGURGs the worker. The thread yields at its next safepoint poll -- which the
457
+ * codegen emits at loop back-edges, i.e. points that hold no runtime lock, so a
458
+ * preempting thread always parks at a safe point. g_npreempt counts outstanding
459
+ * requests so the flag (shared with GC stop-the-world) is cleared only when no
460
+ * reason remains. All of this is touched solely under g_sched_lock. */
461
+ #define SP_PREEMPT_QUANTUM 0.010 /* a green thread runs ~10ms before it must yield */
462
+ #define SP_PREEMPT_TICK 0.005 /* monitor re-checks worker timeslices this often while busy */
463
+ static int g_npreempt = 0; /* preempt_requests set but not yet consumed */
464
+ static int g_preempt_sig = SIGURG; /* the signal the monitor sends; SPINEL_PREEMPT_SIGNAL overrides */
465
+ typedef struct { pthread_t tid; sp_thread *cur; double since; int active;
466
+ /* A STARTED green thread is pinned to its home worker, so the wake
467
+ that readies it has exactly one worker to reach. Waiting on one
468
+ shared condvar meant that wake had to BROADCAST -- every idle
469
+ worker rose, serialised on the scheduler lock, found nothing and
470
+ slept again, with the monitor (the only thing that readies I/O
471
+ waiters) queued behind them. Adding workers then subtracted
472
+ throughput (#4305). Each helper waits on its own condvar instead;
473
+ `idle` says it is on it, and is written only under the lock, so a
474
+ wake that lands between the enqueue and the wait is not lost. */
475
+ pthread_cond_t cv; int idle;
476
+ /* (b), #4306: the worker waits on its OWN readiness set, holding the
477
+ descriptors of the threads pinned to it -- so a thread it can run
478
+ is one it wakes for itself, with no monitor round trip and no
479
+ condvar hand-off. `kick` is a self-pipe in that set: it is how a
480
+ stop-the-world, a shutdown, or work enqueued for this worker
481
+ breaks it out of the wait, since a signal on `cv` no longer
482
+ reaches it there. `ev_waiting` says which of the two it is on,
483
+ and like `idle` it is written only under the lock. */
484
+ int evfd; int kick[2]; int kick_armed; int ev_waiting;
485
+ /* Green threads whose home this worker is (pinned here for life, see
486
+ home_wid). A thread's first run fixes its home, so where an
487
+ unstarted thread is first run decides the balance for as long as
488
+ it lives -- for a server, one connection per thread, that is the
489
+ whole run. Counted so sched_pick can place a new thread on the
490
+ worker with the fewest. */
491
+ int pinned; } sp_wslot;
492
+ static sp_wslot g_wslot[SP_MAX_WORKERS]; /* per-worker: the green thread it runs + when it started */
493
+ static void sp_recompute_safepoint_flag(void) { /* PRE: g_sched_lock held */
494
+ SP_SAFEPOINT_SET(g_stw_active || g_npreempt > 0);
495
+ }
496
+ /* The preemption signal lands on the target worker's own stack. Re-assert the
497
+ flag so the worker sees it even if it was about to clear the lock-free read;
498
+ the actual yield happens cooperatively at the next safepoint poll (kept minimal
499
+ and async-signal-safe -- a lone relaxed atomic store). */
500
+ static void sp_preempt_handler(int sig) { (void)sig; SP_SAFEPOINT_SET(1); }
501
+ /* Wake one helper that can take unpinned work; main (worker 0, on g_sched_work)
502
+ is the fallback when no helper is idle. */
503
+ /* Main waits on its condvar OR, once it has a readiness set, inside that --
504
+ and a condvar signal does not reach it there. Every wake aimed at main goes
505
+ through here, because the one that did not (quiescence) left it sitting out
506
+ its 50ms backstop, which is a ping-pong at 8k hops a second instead of 190k
507
+ (#4306). */
508
+ /* Wake one worker, whichever kind of wait it is on.
509
+ The kick BYTE is durable and the ev_waiting FLAG is not: a worker sets the
510
+ flag under the lock and enters its wait after releasing it, so a kick
511
+ conditioned on the flag is lost in exactly that window -- the worker then
512
+ sleeps its whole backstop. That is how a stop-the-world came to wait one
513
+ out (the collector holds the lock and waits for every worker to park), and
514
+ it is the wake that went missing about once in four hundred hops. A byte
515
+ written before the wait is still there when it starts, because the kick
516
+ descriptor is armed for the whole time the worker has a set. */
517
+ static void sched_kick_worker(int wid) { /* PRE: sched lock held */
518
+ if (wid < 0 || wid >= SP_MAX_WORKERS) return;
519
+ #ifdef SP_EV_BACKEND
520
+ if (g_wslot[wid].evfd > 0) {
521
+ if (wid != sp_worker_id) sp_ev_kick(wid); /* ourselves: we loop and re-pick */
522
+ return;
523
+ }
524
+ #endif
525
+ if (wid == 0) { pthread_cond_broadcast(&g_sched_work); return; }
526
+ if (g_wslot[wid].idle) pthread_cond_signal(&g_wslot[wid].cv);
527
+ }
528
+
529
+ static void sched_wake_main(void) { /* PRE: sched lock held */
530
+ #ifdef SP_EV_BACKEND
531
+ if (g_wslot[0].evfd > 0) { sched_kick_worker(0); return; }
532
+ #endif
533
+ pthread_cond_broadcast(&g_sched_work);
534
+ }
535
+
536
+ static void sched_wake_idle_helper(void) { /* PRE: sched lock held */
537
+ for (int i = 1; i < sp_active_workers; i++) {
538
+ #ifdef SP_EV_BACKEND
539
+ if (g_wslot[i].evfd > 0) { sched_kick_worker(i); return; }
540
+ #endif
541
+ if (g_wslot[i].idle) { pthread_cond_signal(&g_wslot[i].cv); return; }
542
+ }
543
+ sched_wake_main();
544
+ }
545
+ /* Every waiter re-checks state: helpers on their own condvars, main on its
546
+ pump. Used where the state change is not one thread becoming runnable --
547
+ shutdown, the STW barrier, quiescence. */
548
+ static unsigned char g_native_out[SP_MAX_WORKERS]; /* out of the world, roots published: SP_OUT_IDLE / SP_OUT_NATIVE, or 0 */
549
+ #define SP_OUT_IDLE 1 /* in the scheduler's idle wait: can be kicked to sweep its own lists */
550
+ #define SP_OUT_NATIVE 2 /* in a blocking native call: a sweeper thread takes its lists */
551
+ /* mode 0: everyone (shutdown). 1: a barrier being raised -- a worker out of
552
+ the world is counted already and stays asleep. 2: a barrier released --
553
+ the idle ones are kicked so each sweeps its own young lists now, while
554
+ its cache is warm, rather than at its next wake or by a sweeper thread. */
555
+ static void sched_wake_all_workers(int mode) { /* PRE: sched lock held */
556
+ for (int i = 1; i < sp_active_workers; i++) {
557
+ unsigned char out = SP_ATOMIC_LOAD(&g_native_out[i], __ATOMIC_RELAXED);
558
+ if (mode == 1 && out) continue;
559
+ if (mode == 2 && out != SP_OUT_IDLE) continue;
560
+ #ifdef SP_EV_BACKEND
561
+ if (g_wslot[i].evfd > 0) { sched_kick_worker(i); continue; }
562
+ #endif
563
+ if (g_wslot[i].idle) pthread_cond_signal(&g_wslot[i].cv);
564
+ }
565
+ { unsigned char out0 = SP_ATOMIC_LOAD(&g_native_out[0], __ATOMIC_RELAXED);
566
+ if (!((mode == 1 && out0) || (mode == 2 && out0 != SP_OUT_IDLE))) sched_wake_main(); }
567
+ }
568
+ /* Wake the one worker that can run a thread pinned to `wid` (see home_wid).
569
+ Main's worker (0) waits in its pump on the shared condvar, and a signal
570
+ there could be taken by a helper instead, so it gets the broadcast. */
571
+ static void sched_wake_home(int wid) { /* PRE: sched lock held */
572
+ if (wid >= 0) { sched_kick_worker(wid); return; }
573
+ sched_wake_idle_helper(); /* unpinned: any worker will do */
574
+ }
575
+ #define SCHED_WAKE() sched_wake_idle_helper()
576
+ #define SCHED_WAKE_ALL() sched_wake_all_workers(0)
577
+ #define SCHED_WAKE_MAIN() sched_wake_main() /* main's pump alone */
578
+
579
+ /* Park the calling worker at the barrier until the collection finishes,
580
+ publishing its running green thread's roots first. PRE: g_sched_lock held. */
581
+ /* A worker inside a blocking native call (an ffi_func declared `blocking:
582
+ true`) has left the world: it runs no Ruby, touches no Ruby object, and
583
+ its roots were published on the way in, so a collection raised while it
584
+ is out counts it as parked and marks the fibers it recorded, and the
585
+ barrier does not wait for the call to return. On the way back, a
586
+ collection in progress is waited out. The count of such workers joins
587
+ g_nparked in the collector's wait; a worker that returns while the
588
+ barrier is up moves itself from one count to the other before waiting,
589
+ so the collector's condition never goes backwards. */
590
+ static int g_nnative = 0; /* workers out in a blocking native call */
591
+ static SP_TLS int g_native_depth = 0; /* this worker: nested enter/leave */
592
+ static sp_Fiber *g_native_fiber[SP_MAX_WORKERS][2]; /* per worker: the fibers to mark while out */
593
+ static int g_native_nfiber[SP_MAX_WORKERS];
594
+ static void sp_stw_publish_locked(void);
595
+ static void sp_stw_park_locked(void);
596
+ static int sp_cs_chunks_settled(int wid); /* the concurrent sweep is out of this slot's chunks (below) */
597
+ static SP_TLS int g_native_noop = 0; /* entered before the pool existed: nothing to undo */
598
+ /* Leave the world (PRE: sched lock held, no collection active): publish, record
599
+ the fibers to mark, and count this worker as out. Shared by a blocking
600
+ native call and by the idle wait in the scheduler loop -- an idle worker
601
+ has nothing running either, and waking thirty of them so each could park
602
+ was most of what the barrier waited for. */
603
+ static void sp_out_enter_locked(int wid, int how) {
604
+ sp_stw_publish_locked();
605
+ if (wid >= 0 && wid < SP_MAX_WORKERS) {
606
+ sp_Fiber *root = sp_fiber_worker_root();
607
+ g_native_nfiber[wid] = 0;
608
+ if (sp_fiber_current) g_native_fiber[wid][g_native_nfiber[wid]++] = sp_fiber_current;
609
+ if (root && root != sp_fiber_current) g_native_fiber[wid][g_native_nfiber[wid]++] = root;
610
+ SP_ATOMIC_STORE(&g_native_out[wid], (unsigned char)how, __ATOMIC_RELEASE);
611
+ }
612
+ g_nnative++;
613
+ }
614
+ /* Back in the world (PRE: sched lock held). A collection in progress counted
615
+ this worker as out; it becomes a parker of this epoch and waits for the
616
+ release like one (the fibers it recorded stay valid until then: nothing
617
+ has run on it). */
618
+ static void sp_out_leave_locked(int wid) {
619
+ if (g_stw_active) {
620
+ g_nnative--;
621
+ unsigned my_epoch = g_stw_epoch;
622
+ g_nparked++;
623
+ if (g_nparked + g_nnative >= sp_active_workers - 1) pthread_cond_signal(&g_stw_request);
624
+ while (g_stw_active && g_stw_epoch == my_epoch) {
625
+ if (g_cs_help && SP_ATOMIC_LOAD(&g_cs_unclaimed, __ATOMIC_RELAXED) > 0) {
626
+ SCHED_UNLOCK(); sp_cs_help_run(); SCHED_LOCK(); continue;
627
+ }
628
+ pthread_cond_wait(&g_stw_release, &g_sched_lock);
629
+ }
630
+ if (g_stw_epoch == my_epoch) g_nparked--;
631
+ }
632
+ else g_nnative--;
633
+ if (wid >= 0 && wid < SP_MAX_WORKERS) { SP_ATOMIC_STORE(&g_native_out[wid], 0, __ATOMIC_RELEASE); g_native_nfiber[wid] = 0; }
634
+ /* the collection that ran while this worker was out left it its own young
635
+ lists to sweep, and after a full cycle its slab chunks to release */
636
+ if (SP_ATOMIC_LOAD(&g_cs_unclaimed, __ATOMIC_RELAXED) > 0 || (g_cs_full && sp_slab_on > 0)) {
637
+ SCHED_UNLOCK();
638
+ if (SP_ATOMIC_LOAD(&g_cs_unclaimed, __ATOMIC_RELAXED) > 0) sp_cs_owner_run(sp_worker_id);
639
+ if (g_cs_full && sp_slab_on > 0 && sp_cs_chunks_settled(sp_worker_id)) sp_slab_release_worker(sp_worker_id);
640
+ SCHED_LOCK();
641
+ }
642
+ }
643
+ void sp_native_enter(void) {
644
+ if (g_native_depth++ > 0) return;
645
+ if (!g_workers_started) { g_native_noop = 1; return; } /* one worker, no collector to leave the world for */
646
+ SCHED_LOCK();
647
+ /* a collection already running: park through it first, as a poll would */
648
+ if (g_stw_active) sp_stw_park_locked();
649
+ sp_out_enter_locked(sp_worker_id, SP_OUT_NATIVE);
650
+ SCHED_UNLOCK();
651
+ }
652
+ void sp_native_leave(void) {
653
+ if (--g_native_depth > 0) return;
654
+ if (g_native_noop) { g_native_noop = 0; return; }
655
+ SCHED_LOCK();
656
+ sp_out_leave_locked(sp_worker_id);
657
+ SCHED_UNLOCK();
658
+ }
659
+ /* What a parking worker publishes for the collector: the shadow-stack roots
660
+ plus this worker's live match registers (TLS, so the collector's globals
661
+ hook does not reach them) into the green thread's saved snapshot, then
662
+ the worker's own root depth is restored -- the snapshot keeps them. The
663
+ pointer-keyed string length cache goes too: the collection about to run
664
+ may recycle a string's address. */
665
+ static void sp_stw_publish_locked(void) {
666
+ int saved_nroots = sp_gc_nroots;
667
+ sp_re_push_match_roots();
668
+ if (sp_safepoint_publish_hook) sp_safepoint_publish_hook(); /* TU in-flight exc / proc homes */
669
+ sp_fiber_publish_current_roots();
670
+ sp_gc_nroots = saved_nroots;
671
+ sp_str_lcache_clear();
672
+ }
673
+ static void sp_stw_park_locked(void) {
674
+ /* Publish the shadow-stack roots plus this worker's live match registers (TLS,
675
+ so the collector's globals hook does not reach them) into the green thread's
676
+ saved snapshot, then restore our own root depth -- the snapshot keeps them. */
677
+ sp_stw_publish_locked();
678
+ /* Record the fibers the collector must mark for this worker: the green thread
679
+ it is running (sp_fiber_current) AND its root fiber. The root fiber holds the
680
+ worker's own suspended context -- for the main thread that is the top-level
681
+ locals, saved when it transferred into the green thread it is pumping -- and
682
+ it is not on the global fiber list, so without this it would be missed. */
683
+ sp_Fiber *root = sp_fiber_worker_root();
684
+ if (sp_fiber_current && g_n_parked_fiber < 2 * SP_MAX_WORKERS)
685
+ g_parked_fiber[g_n_parked_fiber++] = sp_fiber_current; /* collector marks these */
686
+ if (root && root != sp_fiber_current && g_n_parked_fiber < 2 * SP_MAX_WORKERS)
687
+ g_parked_fiber[g_n_parked_fiber++] = root;
688
+ /* Park for the current collection (epoch). g_nparked counts only this epoch's
689
+ parkers: when the next collection starts it bumps the epoch and resets the
690
+ count, and a straggler from a finished collection (epoch mismatch) must not
691
+ touch the new count -- otherwise the new collector could see a stale count
692
+ and proceed before anyone has actually parked, marking an incomplete root
693
+ set (then sweeping a still-live root). */
694
+ unsigned my_epoch = g_stw_epoch;
695
+ g_nparked++;
696
+ if (g_nparked + g_nnative >= sp_active_workers - 1) pthread_cond_signal(&g_stw_request);
697
+ while (g_stw_active && g_stw_epoch == my_epoch) {
698
+ /* Sweep our own slot if the collector has asked for it. Dropping the
699
+ scheduler lock is safe and necessary: nothing else touches this slot
700
+ (the collector claims only its own), the mutators are all parked here,
701
+ and holding the lock through a free-heavy walk would serialize exactly
702
+ what this phase exists to parallelize. */
703
+ if (g_sweep_go && SP_ATOMIC_LOAD(&g_sw_next, __ATOMIC_RELAXED) < g_sw_ntasks) {
704
+ SCHED_UNLOCK();
705
+ int ran = sp_sweep_run_tasks();
706
+ SCHED_LOCK();
707
+ g_sw_done += ran;
708
+ pthread_cond_signal(&g_sweep_cv);
709
+ continue;
710
+ }
711
+ /* The collector's drain wants helpers: take a seat if one is left, run
712
+ the drain to its end, and come back here. */
713
+ if (g_mk_go && g_mk_seen != g_mk_gen) {
714
+ g_mk_seen = g_mk_gen;
715
+ if (g_mk_joined < g_mk_max) {
716
+ g_mk_joined++; g_mk_active++;
717
+ SCHED_UNLOCK();
718
+ sp_gc_mark_par_run();
719
+ SCHED_LOCK();
720
+ if (--g_mk_active == 0) pthread_cond_broadcast(&g_mk_cv);
721
+ }
722
+ continue;
723
+ }
724
+ /* The previous concurrent sweep is not done and the collector asked for
725
+ hands: what the sweeper threads have not claimed yet is claimed here,
726
+ under the barrier, by everyone who is parked. */
727
+ if (g_cs_help && SP_ATOMIC_LOAD(&g_cs_unclaimed, __ATOMIC_RELAXED) > 0) {
728
+ SCHED_UNLOCK();
729
+ sp_cs_help_run();
730
+ SCHED_LOCK();
731
+ continue;
732
+ }
733
+ pthread_cond_wait(&g_stw_release, &g_sched_lock);
734
+ }
735
+ if (g_stw_epoch == my_epoch) g_nparked--;
736
+ /* Released: this worker's own young lists from the collection that just
737
+ ended are swept HERE, by their owner, before it runs any program. The
738
+ slots it frees are the ones it allocates from next, still in its own
739
+ cache from the walk; swept by another core they came back cold, and the
740
+ mutators measured slower than under the stop-the-world sweep. */
741
+ if (SP_ATOMIC_LOAD(&g_cs_unclaimed, __ATOMIC_RELAXED) > 0 || (g_cs_full && sp_slab_on > 0)) {
742
+ SCHED_UNLOCK();
743
+ if (SP_ATOMIC_LOAD(&g_cs_unclaimed, __ATOMIC_RELAXED) > 0) sp_cs_owner_run(sp_worker_id);
744
+ /* and, after a full cycle, its own slab chunks: the release that used
745
+ to run for every worker under the next barrier */
746
+ if (g_cs_full && sp_slab_on > 0 && sp_cs_chunks_settled(sp_worker_id)) sp_slab_release_worker(sp_worker_id);
747
+ SCHED_LOCK();
748
+ }
749
+ }
750
+ #else
751
+ #define SCHED_LOCK() ((void)0)
752
+ #define SCHED_UNLOCK() ((void)0)
753
+ #define SCHED_WAKE() ((void)0)
754
+ #define SCHED_WAKE_ALL() ((void)0)
755
+ #define SCHED_WAKE_MAIN() ((void)0)
756
+ #define sched_wake_home(w) ((void)(w))
757
+ #endif
758
+
759
+ #ifdef SP_THREADS
760
+ static void sp_safepoint_preempt(void); /* defined after the scheduler state below */
761
+ #endif
762
+
763
+ /* Safepoint poll body: codegen emits `if (sp_safepoint_flag) sp_safepoint();` at
764
+ loop back-edges. Park if a stop-the-world is in progress, then yield if the
765
+ monitor flagged this green thread as over its timeslice (design §5). */
766
+ void sp_safepoint(void) {
767
+ #ifdef SP_THREADS
768
+ SCHED_LOCK();
769
+ if (g_stw_active) sp_stw_park_locked();
770
+ sp_safepoint_preempt();
771
+ SCHED_UNLOCK();
772
+ #endif
773
+ }
774
+
775
+ /* Stop the world and collect (design 6.2). Called by sp_gc_alloc once over the
776
+ threshold, with the heap lock released. Either become the collector -- set the
777
+ barrier, wait for every other worker to park, then mark+sweep with exclusive
778
+ heap access -- or, if a collection is already running, just park through it.
779
+ At N=1 there are no other workers, so the wait is a no-op and this is exactly
780
+ today's inline collect, routed through the barrier. */
781
+ static void sp_stw_collect_impl(int force);
782
+
783
+ void sp_stw_collect(void) { sp_stw_collect_impl(0); }
784
+
785
+ /* An EXPLICIT collection (GC.start / GC.compact). It must take the same barrier
786
+ as a threshold-triggered one: the parallel sweep hands one slot to each other
787
+ worker and waits for them, which only happens if they are parked here. Run
788
+ straight from a mutator, the collector waited on sweeps nobody would do and
789
+ any threaded program calling GC.start hung (#3781). Forced: an explicit
790
+ request collects even when neither heap is over its trigger. */
791
+ void sp_gc_collect_request(void) {
792
+ #ifdef SP_THREADS
793
+ sp_stw_collect_impl(1);
794
+ #else
795
+ sp_gc_collect();
796
+ #endif
797
+ /* GC.start is a call of the program's, a safe point: what it freed is
798
+ finalized before it returns, after the barrier has lifted */
799
+ sp_fin_run_pending();
800
+ }
801
+
802
+ static void sp_stw_collect_impl(int force) {
803
+ #ifdef SP_THREADS
804
+ /* Re-entrancy guard: a finalizer run during the sweep may allocate and cross
805
+ the threshold again. We are already the collector with the world stopped
806
+ (exclusive heap access), so just let that allocation proceed -- re-entering
807
+ the barrier here would park the collector waiting on itself (deadlock). */
808
+ if (g_collector_active) return;
809
+ SCHED_LOCK();
810
+ if (g_stw_active) { sp_stw_park_locked(); SCHED_UNLOCK(); return; }
811
+ if (!force && !sp_gc_collection_wanted()) { SCHED_UNLOCK(); return; } /* another worker just collected */
812
+ g_stw_active = 1;
813
+ g_stw_epoch++; /* new epoch; a previous collection's stragglers won't be counted */
814
+ g_nparked = 0; /* this collection's park count starts fresh */
815
+ SP_SAFEPOINT_SET(1);
816
+ double bt0 = sp_gc_ph_on ? sp_monotonic_now() : 0;
817
+ /* SPINEL_GC_PHASES: how many of the workers this barrier waits for are in
818
+ their own sweep of the previous cycle's lists (a sweep does not look at
819
+ the safepoint), against how many are running the program */
820
+ int ph_sweeping = sp_gc_ph_on ? SP_ATOMIC_LOAD(&g_cs_running, __ATOMIC_RELAXED) : 0;
821
+ /* wake idle workers (and main waiting in its pump) so they park at the barrier
822
+ rather than sit through the collection without publishing their roots. */
823
+ sched_wake_all_workers(1); /* reaches an ev_waiting main and workers too, except those out */
824
+ while (g_nparked + g_nnative < sp_active_workers - 1) pthread_cond_wait(&g_stw_request, &g_sched_lock);
825
+ /* the workers out in a blocking native call: their roots were published
826
+ on the way out, and the fibers they recorded are marked like a parked
827
+ worker's (a worker that came back meanwhile is a parker of this epoch,
828
+ still with its slot recorded, since nothing has run on it) */
829
+ for (int w = 0; w < SP_MAX_WORKERS && w < sp_active_workers; w++) {
830
+ if (!g_native_out[w]) continue;
831
+ for (int f = 0; f < g_native_nfiber[w] && g_n_parked_fiber < 2 * SP_MAX_WORKERS; f++)
832
+ g_parked_fiber[g_n_parked_fiber++] = g_native_fiber[w][f];
833
+ }
834
+ if (sp_gc_ph_on) { double pw = sp_monotonic_now() - bt0; sp_gc_ph_park += pw;
835
+ if (ph_sweeping > 0) { sp_gc_ph_park_sweeping += pw; sp_gc_ph_park_sweeping_n++; } }
836
+ /* Our own root fiber holds this worker's suspended context (the main thread's
837
+ top-level locals if it triggered the collection while pumping a green
838
+ thread). We do not park, so record it here for the mark like a parked worker
839
+ does -- our current fiber's roots are reached directly from our TLS stack. */
840
+ {
841
+ sp_Fiber *croot = sp_fiber_worker_root();
842
+ if (croot && croot != sp_fiber_current && g_n_parked_fiber < 2 * SP_MAX_WORKERS)
843
+ g_parked_fiber[g_n_parked_fiber++] = croot;
844
+ }
845
+ SCHED_UNLOCK();
846
+ /* exclusive: every other worker is parked at a safepoint with roots published */
847
+ g_collector_active = 1;
848
+ sp_gc_collect_retune_all(); /* sweeps both heaps; marks parked fibers via sp_sched_globals_mark */
849
+ /* an explicit GC.start answers once everything unreachable is gone: the
850
+ concurrent sweep it started is finished here, still under the barrier */
851
+ if (force && sp_gc_conc_wait_hook) sp_gc_conc_wait_hook();
852
+ g_collector_active = 0;
853
+ SCHED_LOCK();
854
+ g_n_parked_fiber = 0;
855
+ g_stw_active = 0;
856
+ sp_recompute_safepoint_flag(); /* keep the flag set if a preempt is still pending */
857
+ pthread_cond_broadcast(&g_stw_release);
858
+ if (SP_ATOMIC_LOAD(&g_cs_unclaimed, __ATOMIC_RELAXED) > 0) sched_wake_all_workers(2); /* the idle owners sweep their own lists */
859
+ if (sp_gc_ph_on) sp_gc_ph_barrier += sp_monotonic_now() - bt0;
860
+ SCHED_UNLOCK();
861
+ /* the collector's own young lists, like every released worker's */
862
+ if (SP_ATOMIC_LOAD(&g_cs_unclaimed, __ATOMIC_RELAXED) > 0) sp_cs_owner_run(sp_worker_id);
863
+ if (g_cs_full && sp_slab_on > 0 && sp_cs_chunks_settled(sp_worker_id)) sp_slab_release_worker(sp_worker_id);
864
+ #else
865
+ (void)force;
866
+ sp_gc_collect_retune();
867
+ #endif
868
+ }
869
+
870
+ /* ---- scheduler state (single OS worker, so plain globals) ---- */
871
+ /* The main thread is a STATIC, not a GC allocation -- but `Thread.current`
872
+ hands it to user code and to the runtime's own thread ops, and both the mark
873
+ and the write barrier decide what a pointer is from the byte in FRONT of it.
874
+ Without a guard that read is one byte before a global (ASAN reports it), and
875
+ the barrier goes further: it fabricates a header there and writes a dirty bit
876
+ into whatever .bss happens to precede this one. Lay the same 0xfd skip byte
877
+ the root fiber uses, exactly one alignment unit wide so no padding can slip
878
+ in between. */
879
+ static struct { char guard[_Alignof(sp_thread)]; sp_thread t; } g_main_thread_box
880
+ = { .guard = { [_Alignof(sp_thread) - 1] = (char)0xfd }, .t = {0} };
881
+ #define g_main_thread (g_main_thread_box.t) /* the main thread: runs on root, fiber == NULL */
882
+ static SP_TLS sp_thread *g_current = NULL; /* per-worker: the green thread this worker runs now */
883
+ /* Run queues (design 3.1). A worker requeues a thread it just ran onto its OWN
884
+ local queue (g_lrq[wid]) so a yielding thread reruns on the same worker (warm
885
+ cache); spawns and wakeups, which have no worker affinity, land on the shared
886
+ global queue (g_grq). A worker picks local-first, then global, then steals one
887
+ from another worker before parking. All queues are guarded by g_sched_lock --
888
+ this gives locality and load balancing without a second lock; reducing the
889
+ lock itself would mean reworking the off-cpu handshake (deferred, see git log).
890
+ g_runnable is the total parked-runnable count across every queue, for the
891
+ quiescence/deadlock predicate. */
892
+ SP_TLS int sp_worker_id = 0; /* exported: sp_alloc.h indexes the per-worker string heaps */ /* this worker's run-queue slot (0 = main); both builds */
893
+ typedef struct { sp_thread *head, *tail; } sp_runq;
894
+ static sp_runq g_grq; /* global run queue: spawned + woken threads */
895
+ static sp_runq g_lrq[SP_MAX_WORKERS]; /* per-worker local run queues (rerun locality) */
896
+ static int g_runnable = 0; /* threads sitting in any run queue right now */
897
+ static int g_nrunning = 0; /* workers currently executing a green thread (quiescence) */
898
+ static sp_thread *g_sleepers = NULL; /* threads parked in Kernel#sleep, woken by deadline */
899
+ static sp_thread *g_io_waiters = NULL; /* threads parked on a fd, woken by the monitor's poll */
900
+ static struct pollfd *g_pfds = NULL; /* monitor's poll set, rebuilt from g_io_waiters each tick */
901
+ static sp_thread **g_pths = NULL; /* parallel to g_pfds: the thread waiting on each fd */
902
+ static int g_pcap = 0; /* capacity of g_pfds / g_pths */
903
+
904
+ /* ---- persistent I/O registration (#4306 / #4317) -----------------------
905
+ poll(2) is stateless: the monitor had to hand the WHOLE parked population to
906
+ the kernel on every turn, so a wake cost O(parked) -- 109 us per turn at
907
+ 5,000 parked, against epoll_wait's flat 0.35.
908
+
909
+ The kernel's interest set is keyed by DESCRIPTOR (epoll_ctl targets an fd,
910
+ and a second ADD on the same one is EEXIST), so this table is too, and a
911
+ readiness event fans out to every waiter on that descriptor. Two waiters on
912
+ one fd is a shape that already runs: IO.select over a #to_io wrapper and the
913
+ IO it wraps (test/io_select_to_io.rb).
914
+
915
+ ONE-SHOT registration. An armed descriptor with no waiter would report
916
+ readiness forever and spin the monitor, so the arm is EPOLLONESHOT: the
917
+ kernel disables it after the one delivery, and a park re-arms with MOD.
918
+ That is one syscall per park -- what the self-pipe write cost anyway -- and
919
+ it removes the O(parked) term entirely.
920
+
921
+ TEARDOWN is self-healing (matz's call), and the direction of failure is what
922
+ makes that safe: the arm is attempted on EVERY park rather than skipped on
923
+ the belief that a previous one still stands. A registration the kernel has
924
+ dropped (its descriptor closed, or the number reused) answers ENOENT and is
925
+ re-added; one that outlives its descriptor costs a wake nobody wants, which
926
+ is discarded. What must never happen -- a park that arms nothing and waits
927
+ forever -- cannot, because nothing is ever assumed still armed. */
928
+ #ifdef SP_EV_BACKEND
929
+ typedef struct { sp_ev_waiter *waiters; } sp_ev_slot; /* indexed by fd: the threads' entries parked on it */
930
+ static int g_ev_fd = -1;
931
+ static sp_ev_slot *g_ev_tab = NULL;
932
+ static int g_ev_cap = 0;
933
+ static unsigned long long g_ev_arms = 0, g_ev_adds = 0, g_ev_lost = 0, g_ev_timeouts = 0, g_ev_backstop = 0;
934
+ #endif
935
+
936
+ static sp_thread *g_all = NULL; /* registry of live threads, for GC rooting */
937
+
938
+ #ifdef SP_EV_BACKEND
939
+ /* Bring up the event set once, lazily: a program with no I/O park never pays
940
+ for it, and a kernel without epoll leaves g_ev_fd -1 and the poll path in
941
+ place. */
942
+ typedef struct { int fd; short rev; } sp_ev_ready;
943
+ static int sp_ev_backend_arm(int set, int fd, short want);
944
+ static void sp_ev_backend_del(int set, int fd);
945
+ static int sp_ev_backend_wait(int set, sp_ev_ready *out, int max, int tmo_ms);
946
+
947
+ /* The wait's own timeout. Every wake has a kick or an event behind it, so this
948
+ is a backstop and nothing routes through it; SPINEL_SCHED_STATS counts how
949
+ often it expires, which should be "rarely" and is how the missing kick was
950
+ found. */
951
+ #define SP_EV_BACKSTOP_MS 50
952
+ static int sp_ev_disabled(void) {
953
+ static int asked = 0, off = 0;
954
+ if (!asked) { const char *e = getenv("SPINEL_SCHED_POLL"); off = (e && *e && *e != '0'); asked = 1; }
955
+ return off;
956
+ }
957
+ /* Each worker owns a readiness set holding the descriptors of the threads
958
+ pinned to IT, so the worker that can run a ready thread is the one the
959
+ kernel wakes -- no monitor round trip, no condvar hand-off (#4306). The set
960
+ and its kick pipe are created on that worker's first park. */
961
+ static int sp_ev_worker_up(int wid) { /* PRE: sched lock held */
962
+ if (wid < 0 || wid >= SP_MAX_WORKERS) return 0;
963
+ if (g_wslot[wid].evfd > 0) return 1;
964
+ if (g_wslot[wid].evfd == -2 || sp_ev_disabled()) { g_wslot[wid].evfd = -2; return 0; }
965
+ #ifdef SP_EV_EPOLL
966
+ int fd = epoll_create1(EPOLL_CLOEXEC);
967
+ #else
968
+ int fd = kqueue();
969
+ if (fd >= 0) { int fl = fcntl(fd, F_GETFD); if (fl >= 0) fcntl(fd, F_SETFD, fl | FD_CLOEXEC); }
970
+ #endif
971
+ if (fd < 0) { g_wslot[wid].evfd = -2; return 0; }
972
+ if (pipe(g_wslot[wid].kick) != 0) { close(fd); g_wslot[wid].evfd = -2; return 0; }
973
+ for (int i = 0; i < 2; i++) {
974
+ int fl = fcntl(g_wslot[wid].kick[i], F_GETFL);
975
+ if (fl >= 0) fcntl(g_wslot[wid].kick[i], F_SETFL, fl | O_NONBLOCK);
976
+ fl = fcntl(g_wslot[wid].kick[i], F_GETFD);
977
+ if (fl >= 0) fcntl(g_wslot[wid].kick[i], F_SETFD, fl | FD_CLOEXEC);
978
+ }
979
+ g_wslot[wid].evfd = fd;
980
+ g_wslot[wid].kick_armed = 0;
981
+ g_ev_fd = fd; /* any set being up is what tells the monitor the backend is live */
982
+ return 1;
983
+ }
984
+ /* Re-arm the kick pipe. One-shot like everything else in the set. */
985
+ static void sp_ev_arm_kick(int wid) { /* PRE: sched lock held */
986
+ if (g_wslot[wid].evfd <= 0 || g_wslot[wid].kick_armed) return;
987
+ sp_ev_backend_arm(g_wslot[wid].evfd, g_wslot[wid].kick[0], POLLIN);
988
+ g_wslot[wid].kick_armed = 1;
989
+ }
990
+ /* Break a worker out of its readiness wait. A byte is enough; the reader
991
+ drains whatever accumulated. */
992
+ static void sp_ev_kick(int wid) { /* PRE: sched lock held */
993
+ if (wid < 0 || wid >= SP_MAX_WORKERS || g_wslot[wid].evfd <= 0) return;
994
+ char c = 1; ssize_t r = write(g_wslot[wid].kick[1], &c, 1); (void)r;
995
+ }
996
+
997
+ /* Arm one descriptor for `want` (POLLIN / POLLOUT), one-shot. 0 on success.
998
+ The three functions below are the whole platform surface; everything above
999
+ and below them is shared. */
1000
+ static int sp_ev_backend_arm(int set, int fd, short want) {
1001
+ #ifdef SP_EV_EPOLL
1002
+ struct epoll_event e;
1003
+ e.events = (uint32_t)((want & POLLIN ? EPOLLIN : 0) | (want & POLLOUT ? EPOLLOUT : 0)) | EPOLLONESHOT;
1004
+ e.data.fd = fd;
1005
+ if (epoll_ctl(set, EPOLL_CTL_MOD, fd, &e) == 0) return 0;
1006
+ /* Not in the set: epoll's one-shot only DISABLES, so MOD is the usual arm
1007
+ and ADD is the first one. */
1008
+ if (errno == ENOENT) { SP_STAT_ADD(g_ev_adds, 1); return epoll_ctl(set, EPOLL_CTL_ADD, fd, &e); }
1009
+ return -1;
1010
+ #else
1011
+ /* kqueue's one-shot DELETES the entry when it fires, so every arm is an
1012
+ EV_ADD -- and a filter the waiters no longer want stays armed until it
1013
+ fires once into nobody, which is the discarded wake the design allows. */
1014
+ struct kevent ch[2];
1015
+ int n = 0;
1016
+ if (want & POLLIN) EV_SET(&ch[n++], (uintptr_t)fd, EVFILT_READ, EV_ADD | EV_ONESHOT, 0, 0, NULL);
1017
+ if (want & POLLOUT) EV_SET(&ch[n++], (uintptr_t)fd, EVFILT_WRITE, EV_ADD | EV_ONESHOT, 0, 0, NULL);
1018
+ if (!n) return 0;
1019
+ SP_STAT_ADD(g_ev_adds, 1);
1020
+ return kevent(set, ch, n, NULL, 0, NULL) < 0 ? -1 : 0;
1021
+ #endif
1022
+ }
1023
+
1024
+ /* Drop a descriptor from the set outright (a handle is closing). */
1025
+ static void sp_ev_backend_del(int set, int fd) {
1026
+ #ifdef SP_EV_EPOLL
1027
+ epoll_ctl(set, EPOLL_CTL_DEL, fd, NULL);
1028
+ #else
1029
+ struct kevent ch[2];
1030
+ EV_SET(&ch[0], (uintptr_t)fd, EVFILT_READ, EV_DELETE, 0, 0, NULL);
1031
+ EV_SET(&ch[1], (uintptr_t)fd, EVFILT_WRITE, EV_DELETE, 0, 0, NULL);
1032
+ kevent(set, ch, 2, NULL, 0, NULL); /* ENOENT for a filter never armed */
1033
+ #endif
1034
+ }
1035
+
1036
+ /* Wait for readiness. Fills `out` with (descriptor, poll-style events) and
1037
+ answers how many, or a negative on error. */
1038
+ static int sp_ev_backend_wait(int set, sp_ev_ready *out, int max, int tmo_ms) {
1039
+ #ifdef SP_EV_EPOLL
1040
+ struct epoll_event evs[64];
1041
+ if (max > 64) max = 64;
1042
+ int n = epoll_wait(set, evs, max, tmo_ms);
1043
+ for (int i = 0; i < n; i++) {
1044
+ out[i].fd = evs[i].data.fd;
1045
+ out[i].rev = (short)(((evs[i].events & (EPOLLIN | EPOLLHUP | EPOLLERR)) ? POLLIN : 0) |
1046
+ ((evs[i].events & (EPOLLOUT | EPOLLHUP | EPOLLERR)) ? POLLOUT : 0));
1047
+ }
1048
+ return n;
1049
+ #else
1050
+ struct kevent evs[64];
1051
+ if (max > 64) max = 64;
1052
+ struct timespec ts;
1053
+ ts.tv_sec = tmo_ms / 1000;
1054
+ ts.tv_nsec = (long)(tmo_ms % 1000) * 1000000L;
1055
+ int n = kevent(set, NULL, 0, evs, max, &ts);
1056
+ for (int i = 0; i < n; i++) {
1057
+ out[i].fd = (int)evs[i].ident;
1058
+ /* EV_EOF is a peer that closed: readable (and writable, for a socket whose
1059
+ other end is gone) exactly as POLLHUP is. EV_ERROR answers both so the
1060
+ waiter's own read or write reports the error, which is what poll did. */
1061
+ short r = (evs[i].filter == EVFILT_WRITE) ? POLLOUT : POLLIN;
1062
+ if (evs[i].flags & (EV_EOF | EV_ERROR)) r = POLLIN | POLLOUT;
1063
+ out[i].rev = r;
1064
+ }
1065
+ return n;
1066
+ #endif
1067
+ }
1068
+ /* Arm `fd` for the union of what its waiters want. Attempted on every park --
1069
+ see the note above: never skipped on the belief that an earlier arm stands. */
1070
+ static int sp_ev_home_of(sp_thread *t) { return t->home_wid < 0 ? 0 : (int)t->home_wid; }
1071
+ /* What an entry waits for: the thread's own descriptor, or one of its set */
1072
+ static short sp_evw_events(sp_ev_waiter *e) { return e->idx < 0 ? e->t->io_events : e->t->io_set[e->idx].events; }
1073
+ static void sp_ev_arm_fd(int fd) { /* PRE: sched lock held */
1074
+ if (fd < 0 || fd >= g_ev_cap) return;
1075
+ /* One descriptor can be waited on by threads pinned to DIFFERENT workers --
1076
+ IO.select over a #to_io wrapper and the IO it wraps, from two threads --
1077
+ so it is armed in each of their sets, for the union of what that worker's
1078
+ waiters want. The list is one element in the ordinary case, which is why
1079
+ the quadratic shape here costs nothing. */
1080
+ int done[8]; int nd = 0;
1081
+ for (sp_ev_waiter *w = g_ev_tab[fd].waiters; w; w = w->next) {
1082
+ int h = sp_ev_home_of(w->t), seen = 0;
1083
+ for (int i = 0; i < nd; i++) if (done[i] == h) { seen = 1; break; }
1084
+ if (seen) continue;
1085
+ if (nd < 8) done[nd++] = h;
1086
+ short want = 0;
1087
+ for (sp_ev_waiter *x = g_ev_tab[fd].waiters; x; x = x->next)
1088
+ if (sp_ev_home_of(x->t) == h) want |= sp_evw_events(x);
1089
+ if (!want || g_wslot[h].evfd <= 0) continue;
1090
+ SP_STAT_ADD(g_ev_arms, 1);
1091
+ if (sp_ev_backend_arm(g_wslot[h].evfd, fd, want) == 0) continue;
1092
+ SP_STAT_ADD(g_ev_lost, 1);
1093
+ }
1094
+ return;
1095
+ {
1096
+ /* EBADF, or a descriptor the kernel will not watch (epoll refuses a regular
1097
+ file): the waiter falls back on its deadline, which is the same answer
1098
+ poll gave for an always-ready file. Counted, so SPINEL_SCHED_STATS shows
1099
+ it rather than leaving a silent gap. */
1100
+ } }
1101
+ /* Returns 0 when the descriptor could not be taken into the set. The waiter is
1102
+ then on its deadline alone -- the same degradation the poll path already had
1103
+ when its own array could not grow. */
1104
+ static int sp_ev_park_entry(sp_ev_waiter *e, int fd) { /* PRE: sched lock held */
1105
+ sp_thread *t = e->t;
1106
+ if (fd < 0 || !sp_ev_worker_up(sp_ev_home_of(t))) return 0;
1107
+ if (fd >= g_ev_cap) {
1108
+ int nc = g_ev_cap ? g_ev_cap : 64;
1109
+ while (nc <= fd) nc *= 2;
1110
+ sp_ev_slot *nt = (sp_ev_slot *)realloc(g_ev_tab, sizeof(sp_ev_slot) * (size_t)nc);
1111
+ if (!nt) return 0;
1112
+ memset(nt + g_ev_cap, 0, sizeof(sp_ev_slot) * (size_t)(nc - g_ev_cap));
1113
+ g_ev_tab = nt; g_ev_cap = nc;
1114
+ }
1115
+ e->next = g_ev_tab[fd].waiters;
1116
+ g_ev_tab[fd].waiters = e;
1117
+ sp_ev_arm_fd(fd);
1118
+ return 1;
1119
+ }
1120
+ static int sp_ev_park(sp_thread *t, int fd) { /* the thread's own descriptor */
1121
+ t->ev0.t = t; t->ev0.idx = -1;
1122
+ return sp_ev_park_entry(&t->ev0, fd);
1123
+ }
1124
+ /* Hand one readiness event to the threads waiting on that descriptor. Called
1125
+ by whichever worker's set produced it, so `only_home` filters to the threads
1126
+ that worker can actually run -- a descriptor shared by two homes is armed in
1127
+ both sets and each worker takes its own. Re-arms for whoever is left, since
1128
+ the arm is one-shot and a waiter still parked must not be stranded.
1129
+ Answers how many threads it readied. PRE: sched lock held. */
1130
+ static void sp_sched_wake_for(sp_thread *t);
1131
+ static void runq_requeue(sp_thread *t);
1132
+ static void sp_ev_drop(sp_thread *t);
1133
+ static int sp_ev_dispatch(int rfd, short rev, int only_home) {
1134
+ if (rfd < 0 || rfd >= g_ev_cap) return 0;
1135
+ int n = 0;
1136
+ for (sp_ev_waiter *e = g_ev_tab[rfd].waiters, *nx = NULL; e; e = nx) {
1137
+ nx = e->next;
1138
+ sp_thread *w = e->t;
1139
+ if (!(sp_evw_events(e) & rev)) continue;
1140
+ if (w->wait_head != &g_io_waiters) continue;
1141
+ if (only_home >= 0 && sp_ev_home_of(w) != only_home) continue;
1142
+ SP_STAT_ADD(g_mon_readied, 1);
1143
+ for (sp_thread **pp = &g_io_waiters; *pp; pp = &(*pp)->wait_next)
1144
+ if (*pp == w) { *pp = w->wait_next; break; }
1145
+ sp_timer_cancel(w);
1146
+ sp_ev_drop(w);
1147
+ w->wait_next = NULL; w->wait_head = NULL;
1148
+ w->io_revents = rev; w->io_fd = -1;
1149
+ n++;
1150
+ if (w == &g_main_thread) { w->state = SP_TH_RUNNABLE; SCHED_WAKE_ALL(); }
1151
+ else if (w->off_cpu) { w->state = SP_TH_RUNNABLE; runq_requeue(w); sp_sched_wake_for(w); }
1152
+ else w->wake_pending = 1;
1153
+ }
1154
+ sp_ev_arm_fd(rfd);
1155
+ return n;
1156
+ }
1157
+
1158
+ /* Wait on this worker's own readiness set for up to `tmo_ms`, then dispatch
1159
+ what came back. Releases and retakes the lock around the wait. Answers how
1160
+ many threads it readied. PRE/POST: sched lock held. */
1161
+ static int sp_ev_worker_wait(int wid, int tmo_ms) {
1162
+ if (g_wslot[wid].evfd <= 0) return 0;
1163
+ sp_ev_ready evs[64];
1164
+ sp_ev_arm_kick(wid);
1165
+ g_wslot[wid].ev_waiting = 1;
1166
+ int set = g_wslot[wid].evfd;
1167
+ /* the idle wait (not the per-turn zero-timeout drain) leaves the world: a
1168
+ collection raised meanwhile marks this worker's recorded fibers and does
1169
+ not wake it to park */
1170
+ int out = tmo_ms != 0 && !g_stw_active;
1171
+ if (out) sp_out_enter_locked(wid, SP_OUT_IDLE);
1172
+ SCHED_UNLOCK();
1173
+ int en = sp_ev_backend_wait(set, evs, 64, tmo_ms);
1174
+ SCHED_LOCK();
1175
+ if (out) sp_out_leave_locked(wid);
1176
+ g_wslot[wid].ev_waiting = 0;
1177
+ if (en == 0) SP_STAT_ADD(g_ev_backstop, 1);
1178
+ SP_STAT_ADD(g_mon_polls, 1); SP_STAT_ADD(g_mon_pollfds, en > 0 ? en : 0);
1179
+ int n = 0;
1180
+ for (int i = 0; i < en; i++) {
1181
+ if (evs[i].fd == g_wslot[wid].kick[0]) {
1182
+ char buf[64]; while (read(g_wslot[wid].kick[0], buf, sizeof buf) > 0) {}
1183
+ g_wslot[wid].kick_armed = 0;
1184
+ continue;
1185
+ }
1186
+ n += sp_ev_dispatch(evs[i].fd, evs[i].rev, wid);
1187
+ }
1188
+ return n;
1189
+ }
1190
+
1191
+ /* Take a thread off its descriptors' waiter lists -- its own, and every entry
1192
+ of a set wait. Every path that unlinks a waiter from g_io_waiters goes
1193
+ through here. */
1194
+ static void sp_ev_unlink(sp_ev_waiter *e, int fd) { /* PRE: sched lock held */
1195
+ if (fd < 0 || fd >= g_ev_cap) { e->next = NULL; return; }
1196
+ for (sp_ev_waiter **pp = &g_ev_tab[fd].waiters; *pp; pp = &(*pp)->next)
1197
+ if (*pp == e) { *pp = e->next; break; }
1198
+ e->next = NULL;
1199
+ }
1200
+ static void sp_ev_drop(sp_thread *t) { /* PRE: sched lock held */
1201
+ sp_ev_unlink(&t->ev0, t->io_fd);
1202
+ for (int i = 0; t->ev_set && i < t->io_nset; i++) sp_ev_unlink(&t->ev_set[i], t->io_set[i].fd);
1203
+ }
1204
+ /* A handle is closing: the descriptor is about to stop being ours, so drop the
1205
+ registration while the fd still names the right thing. Waiters parked on it
1206
+ keep their deadline. */
1207
+ void sp_sched_ev_forget(int fd) {
1208
+ if (fd < 0) return;
1209
+ SCHED_LOCK();
1210
+ /* Wake whoever is parked on the descriptor first: a thread blocked in a
1211
+ read has no deadline, so with its registration gone and its entry
1212
+ dropped nothing would ever ready it -- `sock.close` from another thread
1213
+ left the reader waiting forever, where CRuby raises IOError in it
1214
+ (#4546). Readied here as for any readiness event, from whatever worker
1215
+ it is pinned to; the read it retries finds the handle closed and raises.
1216
+ Then the registrations go, while the number still names this descriptor. */
1217
+ if (fd < g_ev_cap && g_ev_tab[fd].waiters) sp_ev_dispatch(fd, POLLIN | POLLOUT, -1);
1218
+ for (int wid = 0; wid < SP_MAX_WORKERS; wid++)
1219
+ if (g_wslot[wid].evfd > 0) sp_ev_backend_del(g_wslot[wid].evfd, fd);
1220
+ if (fd < g_ev_cap) {
1221
+ for (sp_ev_waiter *w = g_ev_tab[fd].waiters; w; ) { sp_ev_waiter *n = w->next; w->next = NULL; w = n; }
1222
+ g_ev_tab[fd].waiters = NULL;
1223
+ }
1224
+ SCHED_UNLOCK();
1225
+ }
1226
+ #else
1227
+ void sp_sched_ev_forget(int fd) { (void)fd; }
1228
+ #endif
1229
+
1230
+ static unsigned g_next_id = 1;
1231
+ static unsigned char g_report_default = 1; /* Thread.report_on_exception default */
1232
+
1233
+ #ifdef SP_THREADS
1234
+ static void sp_sched_maybe_grow(void); /* grow the helper pool toward demand */
1235
+ #endif
1236
+ /* SPINEL_SCHED_STATS=2: how long a readied thread waited on a run queue before
1237
+ a worker ran it, as a log2 histogram (bucket k: [2^k, 2^(k+1)) us), reported
1238
+ by the monitor every few seconds. The scheduling delay is what a request's
1239
+ tail latency is made of when the CPU is not the bottleneck. */
1240
+ static int g_sched_lat_on = -1;
1241
+ static unsigned long long g_sched_lat_hist[32];
1242
+ static double g_sched_lat_max = 0;
1243
+ static unsigned long long g_sched_lat_n = 0;
1244
+ static int g_lrq_len[SP_MAX_WORKERS], g_lrq_len_max = 0;
1245
+ static unsigned long long g_mtx_hist[32], g_mtx_n = 0, g_mtx_fast = 0, g_mtx_spin = 0; static double g_mtx_max = 0;
1246
+ static unsigned long long g_cv_hist[32], g_cv_n = 0; static double g_cv_max = 0;
1247
+ static void sched_hist_add(unsigned long long *h, unsigned long long *n, double *mx, double us) {
1248
+ int k = 0; while (k < 31 && us >= (double)(2u << k)) k++;
1249
+ h[k]++; (*n)++; if (us > *mx) *mx = us;
1250
+ }
1251
+ static void sched_hist_print(const char *tag, unsigned long long *h, unsigned long long n, double mx) {
1252
+ fprintf(stderr, "[%s] n=%llu max=%.0fus us:", tag, n, mx);
1253
+ for (int k = 0; k < 32; k++) if (h[k]) fprintf(stderr, " [%u]=%llu", 1u << k, h[k]);
1254
+ fprintf(stderr, "\n");
1255
+ }
1256
+ static int sched_lat_enabled(void) {
1257
+ if (g_sched_lat_on < 0) { const char *e = getenv("SPINEL_SCHED_STATS"); g_sched_lat_on = (e && *e == '2'); }
1258
+ return g_sched_lat_on;
1259
+ }
1260
+ static void runq_push(sp_runq *q, sp_thread *t) {
1261
+ t->state = SP_TH_RUNNABLE; t->rq_next = NULL;
1262
+ if (sched_lat_enabled()) t->readied_at = sp_monotonic_now();
1263
+ if (q->tail) q->tail->rq_next = t; else q->head = t;
1264
+ q->tail = t; g_runnable++;
1265
+ #ifdef SP_THREADS
1266
+ sp_sched_maybe_grow(); /* one execution helper per live green thread, up to the cap */
1267
+ #endif
1268
+ }
1269
+ static sp_thread *runq_pop(sp_runq *q) {
1270
+ sp_thread *t = q->head;
1271
+ if (t) { q->head = t->rq_next; if (!q->head) q->tail = NULL; t->rq_next = NULL; g_runnable--; }
1272
+ return t;
1273
+ }
1274
+ /* Spawn: the shared global queue (no worker affinity yet). */
1275
+ static void rq_push(sp_thread *t) { runq_push(&g_grq, t); }
1276
+ /* Rerun a thread on the worker that just ran it (locality). */
1277
+ static void lrq_push(int wid, sp_thread *t) { runq_push(&g_lrq[wid], t); }
1278
+ /* Requeue/wake a thread that may have already run. A STARTED thread must go
1279
+ back to its home worker's local queue -- never the global queue -- because
1280
+ its live frames cache that worker's __thread addresses (see home_wid). An
1281
+ unstarted thread has no affinity and takes the global queue. */
1282
+ static void runq_requeue(sp_thread *t) {
1283
+ #ifdef SP_THREADS
1284
+ if (t->home_wid >= 0) { runq_push(&g_lrq[t->home_wid], t); return; }
1285
+ #endif
1286
+ runq_push(&g_grq, t);
1287
+ }
1288
+ /* Next thread for worker `wid`: its own queue, then the global queue, then steal
1289
+ one from another worker. Every 61st pick checks the global queue first so a
1290
+ worker that keeps refilling its own queue (e.g. a thread spawning in a loop)
1291
+ cannot starve globally-requeued (preempted / woken) work. */
1292
+ static SP_TLS unsigned g_pick_tick = 0;
1293
+ #ifdef SP_THREADS
1294
+ /* An unstarted thread popped from the global queue is about to be pinned to
1295
+ whichever worker runs it first, for life. Under a helper pool that grows on
1296
+ demand the workers that exist when a server's connections arrive are the
1297
+ busy ones, so every connection was pinned to the first dozen workers and
1298
+ the rest sat idle for the run: 64 connections on 12 of 32 workers, with 7
1299
+ queued on one while 21 workers had nothing (campfire at c=64). Place it on
1300
+ the worker with the fewest pinned threads instead -- spawning one more
1301
+ helper when every worker already carries some and the cap allows -- and
1302
+ hand it over there. Returns 1 when t was given away. */
1303
+ static void sp_sched_spawn_helper(void);
1304
+ static int sched_place_unstarted(int wid, sp_thread *t) {
1305
+ if (t->home_wid >= 0 || sp_active_workers <= 1) return 0;
1306
+ int best = -1, bestn = 0;
1307
+ for (int i = 1; i < sp_active_workers; i++) { /* worker 0 (main) runs no general thread */
1308
+ if (!g_wslot[i].active) continue;
1309
+ if (best < 0 || g_wslot[i].pinned < bestn) { best = i; bestn = g_wslot[i].pinned; }
1310
+ }
1311
+ if (best < 0) return 0;
1312
+ if (bestn > 0 && g_helpers_spawned < g_worker_cap && !g_stw_active) {
1313
+ int before = g_helpers_spawned;
1314
+ sp_sched_spawn_helper();
1315
+ if (g_helpers_spawned > before) { best = g_helpers_spawned; bestn = 0; }
1316
+ }
1317
+ if (best == wid || (wid > 0 && g_wslot[wid].pinned <= bestn)) return 0; /* here is as good */
1318
+ t->home_wid = (short)best; g_wslot[best].pinned++;
1319
+ runq_push(&g_lrq[best], t);
1320
+ sched_wake_home(best);
1321
+ return 1;
1322
+ }
1323
+ #endif
1324
+ static sp_thread *sched_pick(int wid) {
1325
+ sp_thread *t;
1326
+ #ifdef SP_THREADS
1327
+ /* the global queue holds unstarted threads: place each on its home first */
1328
+ while ((t = g_grq.head) != NULL && t->home_wid < 0) {
1329
+ runq_pop(&g_grq);
1330
+ if (!sched_place_unstarted(wid, t)) { return t; }
1331
+ }
1332
+ #endif
1333
+ if ((++g_pick_tick % 61u) == 0) { t = runq_pop(&g_grq); if (t) return t; }
1334
+ t = runq_pop(&g_lrq[wid]);
1335
+ if (t) return t;
1336
+ t = runq_pop(&g_grq);
1337
+ if (t) return t;
1338
+ #ifdef SP_THREADS
1339
+ for (int i = 0; i < sp_active_workers; i++) { /* steal one from a busier worker */
1340
+ if (i == wid) continue;
1341
+ /* Only an UNSTARTED thread may be stolen: a started one is pinned to its
1342
+ home worker's TLS (home_wid) and must not resume elsewhere. Unstarted
1343
+ spawns sit at most one-per-spawner ahead of pinned reruns, so scan the
1344
+ queue rather than popping blindly. */
1345
+ sp_thread **pp = &g_lrq[i].head;
1346
+ while (*pp && (*pp)->home_wid >= 0) pp = &(*pp)->rq_next;
1347
+ if (*pp) {
1348
+ t = *pp;
1349
+ *pp = t->rq_next;
1350
+ if (g_lrq[i].tail == t) {
1351
+ g_lrq[i].tail = NULL;
1352
+ for (sp_thread *w = g_lrq[i].head; w; w = w->rq_next) g_lrq[i].tail = w;
1353
+ }
1354
+ t->rq_next = NULL; g_runnable--;
1355
+ return t;
1356
+ }
1357
+ }
1358
+ #endif
1359
+ return NULL;
1360
+ }
1361
+
1362
+ /* Wake worker(s) after enqueueing t. A STARTED thread is pinned to its home
1363
+ worker (see home_wid) and sched_pick's stealing skips pinned threads, so the
1364
+ wake has exactly one worker to reach; an unstarted thread can run anywhere.
1365
+ This used to have to BROADCAST for a pinned thread, because every idle
1366
+ worker waited on the one g_sched_work condvar and a plain signal could take
1367
+ a worker that cannot run t -- see the per-worker condvar in sp_wslot, which
1368
+ is what lets the wake be addressed (#4305). PRE: sched lock held. */
1369
+ static void sp_sched_wake_for(sp_thread *t) {
1370
+ sched_wake_home(t->home_wid);
1371
+ }
1372
+
1373
+ static void reg_add(sp_thread *t) {
1374
+ t->all_prev = NULL; t->all_next = g_all;
1375
+ if (g_all) g_all->all_prev = t;
1376
+ g_all = t;
1377
+ }
1378
+ /* Is any green thread other than the caller alive? A blocking syscall answers
1379
+ for the OS thread it runs on, and a started green thread is pinned to its
1380
+ worker, so blocking in one stalls every thread pinned there -- including the
1381
+ one that has to make progress before the syscall can return. Process.waitpid2
1382
+ and Kernel#system ask this before choosing between a blocking wait and a
1383
+ polling one (#4381).
1384
+
1385
+ Main is not in the registry, so a green thread asking this saw "nobody
1386
+ else" once every other spawned thread had ended and took the blocking
1387
+ wait -- with main alive and allocating, and a worker in a syscall never
1388
+ reaches a safepoint, so main's next collection waited out the child
1389
+ (#4528). From a green thread the answer is always yes. */
1390
+ int sp_sched_other_threads_live(void) {
1391
+ int other = 0;
1392
+ SCHED_LOCK();
1393
+ if (g_current != &g_main_thread) other = 1;
1394
+ else { sp_thread *t = g_all;
1395
+ while (t) { if (t != g_current) { other = 1; break; } t = t->all_next; } }
1396
+ SCHED_UNLOCK();
1397
+ return other;
1398
+ }
1399
+ /* Wait for one child without holding the OS worker while other green threads
1400
+ have to run. A blocking waitpid answers for the whole worker, and a started
1401
+ green thread is pinned to its worker, so `spin build --verbose` deadlocked:
1402
+ the parent waited for the compiler, the compiler filled the stderr pipe and
1403
+ blocked writing, and the reader thread that would have drained it could not
1404
+ be scheduled (#4381). The cooperative build has the same shape with one OS
1405
+ thread and several fibers.
1406
+
1407
+ Poll and hand the scheduler back between attempts, but only while another
1408
+ thread is alive -- a program with one thread keeps the blocking wait and its
1409
+ exact wake-up. sp_Thread_pass runs a runnable sibling immediately; the sleep
1410
+ only keeps a busy loop from burning the core while the child works, and it
1411
+ caps at 5ms, which is nothing against a wait long enough to matter. */
1412
+ int sp_sched_wait_child(int pid, int *status) {
1413
+ int r;
1414
+ if (!sp_sched_other_threads_live()) {
1415
+ do { r = (int)waitpid((pid_t)pid, status, 0); } while (r < 0 && errno == EINTR);
1416
+ return r;
1417
+ }
1418
+ { double back = 0.0002;
1419
+ for (;;) {
1420
+ do { r = (int)waitpid((pid_t)pid, status, WNOHANG); } while (r < 0 && errno == EINTR);
1421
+ if (r != 0) return r; /* reaped, or an error for the caller to report */
1422
+ sp_Thread_pass();
1423
+ #ifdef SP_THREADS
1424
+ sp_sched_sleep(back);
1425
+ #else
1426
+ { struct timespec rq; rq.tv_sec = 0; rq.tv_nsec = (long)(back * 1e9);
1427
+ while (nanosleep(&rq, &rq) == -1 && errno == EINTR) {} }
1428
+ #endif
1429
+ if (back < 0.005) back *= 2.0;
1430
+ } }
1431
+ }
1432
+ static void reg_remove(sp_thread *t) {
1433
+ if (t->all_prev) t->all_prev->all_next = t->all_next;
1434
+ else if (g_all == t) g_all = t->all_next;
1435
+ if (t->all_next) t->all_next->all_prev = t->all_prev;
1436
+ t->all_prev = t->all_next = NULL;
1437
+ }
1438
+
1439
+ /* GC: root every live green thread (and thus its fiber, stack roots, and
1440
+ pending result) so a fire-and-forget thread with no user reference is not
1441
+ collected mid-run. Chained ahead of whatever globals hook was installed. */
1442
+ static void (*g_prev_globals_hook)(void) = NULL;
1443
+ static sp_Fiber *g_main_root = NULL; /* worker 0's root fiber: the main thread's */
1444
+ static void sp_sched_globals_mark(void) {
1445
+ for (sp_thread *t = g_all; t; t = t->all_next) sp_gc_mark(t);
1446
+ /* The main thread is a static struct, not a registry entry, so nothing
1447
+ above marks what hangs off it -- and its thread-local map
1448
+ (Thread.current[:k] = v on the main thread) is a GC object. Unmarked, a
1449
+ collection freed the map while g_main_thread.tls still pointed at it,
1450
+ and the next Thread#[]= wrote into freed memory. */
1451
+ if (g_main_thread.tls) sp_gc_mark(g_main_thread.tls);
1452
+ /* The main thread runs on worker 0's root fiber, which is thread-local, so
1453
+ sp_mark_fiber_root_storage reaches it only when worker 0 collects. Its
1454
+ storage (Fiber[:k] = v, Fiber.current.storage = h at top level) is marked
1455
+ here for a collection run by any other worker. */
1456
+ if (g_main_root) {
1457
+ if (g_main_root->storage) sp_gc_mark(g_main_root->storage);
1458
+ if (g_main_root->attrs) sp_gc_mark(g_main_root->attrs);
1459
+ }
1460
+ #ifdef SP_THREADS
1461
+ /* Mark each parked worker's published roots. Reaches the per-worker root fibers
1462
+ (idle/main workers) that are not on sp_fiber_list_head; green-thread fibers
1463
+ are also covered here (harmless re-mark) and via the suspended-fibers hook. */
1464
+ for (int i = 0; i < g_n_parked_fiber; i++) sp_fiber_mark_chain(g_parked_fiber[i]); /* and the resumers waiting on it */
1465
+ #endif
1466
+ if (g_prev_globals_hook) g_prev_globals_hook();
1467
+ }
1468
+
1469
+ #ifdef SP_THREADS
1470
+ static int sp_sched_start_workers(void); /* defined after run_thread_once */
1471
+ #endif
1472
+
1473
+ void sp_sched_init(void) {
1474
+ /* Called from main() before any fiber/thread op. Adopt this OS thread (worker
1475
+ 0) as the main green thread: its native stack is the per-worker root fiber. */
1476
+ sp_fiber_worker_init();
1477
+ g_main_root = sp_fiber_worker_root();
1478
+ memset(&g_main_thread, 0, sizeof g_main_thread);
1479
+ g_main_thread.fiber = NULL;
1480
+ g_main_thread.state = SP_TH_RUNNING;
1481
+ g_main_thread.report_on_exception = 1;
1482
+ g_main_thread.arg = sp_box_nil();
1483
+ g_main_thread.retval = sp_box_nil();
1484
+ g_main_thread.name = sp_box_nil();
1485
+ g_current = &g_main_thread;
1486
+ g_prev_globals_hook = sp_gc_mark_globals_hook;
1487
+ sp_gc_mark_globals_hook = sp_sched_globals_mark;
1488
+ #ifdef SP_THREADS
1489
+ sp_worker_id = 0; /* main is worker 0 */
1490
+ g_wslot[0].tid = pthread_self();
1491
+ g_wslot[0].active = 1;
1492
+ /* Helpers + monitor are spawned lazily on the first Thread (sp_sched_ensure_workers),
1493
+ not here: a program that only uses Mutex/Queue/ConditionVariable/sleep for its
1494
+ structure -- but never spawns a second thread -- then runs entirely on main with
1495
+ no idle helper OS threads and no monitor. */
1496
+ #endif
1497
+ }
1498
+
1499
+ #ifdef SP_THREADS
1500
+ /* Spawn the helper pool + monitor the first time a green thread is created. The
1501
+ first Thread ever always originates on main (no helper exists to run user code
1502
+ before this), so this runs single-threaded the one time it does work; later
1503
+ calls (a green thread spawning another) see the flag set and return. */
1504
+ /* Drive the parallel sweep from inside sp_gc_collect, with the world already
1505
+ stopped and every other worker parked in sp_stw_park_locked. Waking them
1506
+ through g_stw_release is safe: their loop re-tests g_stw_active, so a
1507
+ wake that is not a release puts them back to sleep. */
1508
+ /* ---- The concurrent sweep ----
1509
+ The mark runs under the barrier and the sweep does not. At the end of the
1510
+ mark the collector detaches every worker's young lists (objects; the
1511
+ SP_STR_YSUB string lists) and, on a full cycle, the old lists, replaces
1512
+ them with empty ones, and makes the detached lists a task list; the world
1513
+ resumes while it is swept. Who sweeps what: a worker's own young lists are
1514
+ swept by that worker, right after it is released and before it runs any
1515
+ program (sp_cs_owner_run), so the slots it frees are the ones it allocates
1516
+ from next and are still in its own cache from the walk (swept by another
1517
+ core they came back cold, and the mutators measured slower than under the
1518
+ stop-the-world sweep); the old lists, whose slots nobody is about to
1519
+ reuse, go to a small pool of sweeper threads. Whatever is unclaimed when
1520
+ the next collection stops the world is finished there by everyone who is
1521
+ parked (sp_sched_conc_wait, the first thing sp_gc_collect does), so the
1522
+ apply that follows sees a complete sweep. What is applied under the
1523
+ barrier is only what the mutators must not see half-done: the survivors
1524
+ spliced onto the old lists and the string budget retune. The pooled dead
1525
+ are pushed onto their pools by the sweep itself (see sp_gc_sweep_list);
1526
+ done at the barrier they cost it more than the sweep had shed.
1527
+
1528
+ What makes the sweep safe beside the mutators: a mutator never reads a
1529
+ GC list link; the mark promoted every survivor before the world resumed
1530
+ (sp_gc_conc_promote), so the write barrier records stores into them and
1531
+ the sweeper never writes the flag word the barrier writes; a dead object is
1532
+ unreachable, so its finalizer races nothing; a string's mark byte is reset
1533
+ with a compare-and-swap (a freeze can land on the same byte); freed slab
1534
+ slots are pushed in per-chunk batches with a compare-and-swap that the
1535
+ owner's pop also uses; and the length caches were cleared when the workers
1536
+ parked, so no cache names a string the sweep frees. */
1537
+ #define CS_OBJ 0
1538
+ #define CS_OBJ_OLD 1
1539
+ #define CS_STR_YOUNG 2
1540
+ #define CS_STR_OLD 3
1541
+ #define CS_CHUNKS 4 /* a slot's slab chunks, by their bitmaps (objects and strings, both generations) */
1542
+ #define CS_TASK_MAX (SP_MAX_WORKERS * (3 + SP_STR_YSUB) + 1)
1543
+ #define CS_SWEEPER_MAX 32
1544
+ static pthread_mutex_t g_cs_lock = PTHREAD_MUTEX_INITIALIZER;
1545
+ static pthread_cond_t g_cs_go = PTHREAD_COND_INITIALIZER;
1546
+ static pthread_cond_t g_cs_done = PTHREAD_COND_INITIALIZER;
1547
+ static int g_cs_nsweepers = 0;
1548
+ static unsigned g_cs_gen = 0; /* bumped per sweep started; sweepers wait for a new one */
1549
+ static int g_cs_pending = 0; /* a sweep is in flight */
1550
+ static int g_cs_str_sweep = 0, g_cs_str_major = 0;
1551
+ static sp_sw_task g_cs_tasks[CS_TASK_MAX];
1552
+ /* the detached lists (inputs) and the per-slot results */
1553
+ static sp_gc_hdr *g_cs_obj[SP_MAX_WORKERS];
1554
+ static sp_gc_hdr *g_cs_obj_old = NULL, *g_cs_obj_old_tail = NULL;
1555
+ static sp_str_hdr *g_cs_str_young[SP_MAX_WORKERS][SP_STR_YSUB];
1556
+ static sp_str_hdr *g_cs_str_old[SP_MAX_WORKERS];
1557
+ static sp_gc_hdr *g_cs_pro_head[SP_MAX_WORKERS], *g_cs_pro_tail[SP_MAX_WORKERS];
1558
+ static sp_str_hdr *g_cs_sy_keep[SP_MAX_WORKERS][SP_STR_YSUB], *g_cs_sy_tail[SP_MAX_WORKERS][SP_STR_YSUB];
1559
+ static size_t g_cs_sy_moved[SP_MAX_WORKERS][SP_STR_YSUB];
1560
+ static size_t g_cs_so_freed[SP_MAX_WORKERS];
1561
+ static unsigned long g_cs_task_us = 0; /* SPINEL_GC_PHASES: sweeper time, summed */
1562
+ static int g_cs_hi = 1; /* slots in use, plus the sweepers' own */
1563
+ #define CS_SWEEPER_WID (SP_MAX_WORKERS - 1) /* a sweeper's worker id: its own slot, should a finalizer allocate */
1564
+
1565
+ static double g_cs_t0 = 0; /* SPINEL_GC_PHASES: when the sweep started */
1566
+ static unsigned long g_cs_task_max_us = 0;
1567
+ static void sp_cs_run_task(const sp_sw_task *t) {
1568
+ size_t dummy = 0;
1569
+ double t0 = sp_gc_ph_on ? sp_monotonic_now() : 0;
1570
+ switch (t->kind) {
1571
+ case CS_OBJ:
1572
+ sp_gc_sweep_list(&g_cs_obj[t->wid], 1, &g_cs_pro_head[t->wid], &g_cs_pro_tail[t->wid], &dummy);
1573
+ break;
1574
+ case CS_OBJ_OLD:
1575
+ sp_gc_sweep_old_list(&g_cs_obj_old, &dummy, &g_cs_obj_old_tail);
1576
+ break;
1577
+ case CS_CHUNKS:
1578
+ sp_gc_sweep_chunks(t->wid, g_cs_full);
1579
+ break;
1580
+ case CS_STR_YOUNG: {
1581
+ size_t held = 0;
1582
+ sp_str_sweep_young_list(&g_cs_str_young[t->wid][t->sub], &g_cs_sy_keep[t->wid][t->sub],
1583
+ &g_cs_sy_tail[t->wid][t->sub], &g_cs_sy_moved[t->wid][t->sub], &held);
1584
+ break; }
1585
+ default:
1586
+ g_cs_so_freed[t->wid] = sp_str_sweep_old_list(&g_cs_str_old[t->wid]);
1587
+ break;
1588
+ }
1589
+ if (sp_gc_ph_on) {
1590
+ unsigned long d = (unsigned long)((sp_monotonic_now() - t0) * 1e6), m;
1591
+ do { m = SP_ATOMIC_LOAD(&g_cs_task_max_us, __ATOMIC_RELAXED); if (d <= m) break; }
1592
+ while (!SP_ATOMIC_CAS(&g_cs_task_max_us, &m, d, 0, __ATOMIC_RELAXED, __ATOMIC_RELAXED));
1593
+ }
1594
+ }
1595
+ /* Who takes a task: a worker's own young lists (CS_OBJ, CS_STR_YOUNG of a
1596
+ live slot) are the owner's, so the freed slots come back warm to the core
1597
+ that allocates from them; the old lists and the sweepers' own slot are
1598
+ the sweeper threads'; under the barrier anyone takes what is left. */
1599
+ static unsigned char g_cs_claimed[CS_TASK_MAX];
1600
+ /* A young list is its owner's to sweep (warm in its cache, and it is the
1601
+ next to allocate from those slots). An idle owner is kicked at the release
1602
+ to do exactly that; one out in a blocking native call cannot be, so a
1603
+ sweeper thread takes its lists, or the next barrier would be spent
1604
+ finishing them. */
1605
+ static int sp_cs_task_is_owned(const sp_sw_task *t) {
1606
+ return (t->kind == CS_OBJ || t->kind == CS_STR_YOUNG || t->kind == CS_CHUNKS) && t->wid != CS_SWEEPER_WID &&
1607
+ !(t->wid >= 0 && t->wid < SP_MAX_WORKERS && SP_ATOMIC_LOAD(&g_native_out[t->wid], __ATOMIC_RELAXED) == SP_OUT_NATIVE);
1608
+ }
1609
+ #define CS_RUN_SWEEPER 0
1610
+ #define CS_RUN_OWNER 1
1611
+ #define CS_RUN_ANY 2
1612
+ static int sp_cs_run_tasks(int mode, int wid) {
1613
+ int ran = 0;
1614
+ double t0 = sp_gc_ph_on ? sp_monotonic_now() : 0;
1615
+ for (int i = 0; i < g_cs_ntasks; i++) {
1616
+ if (SP_ATOMIC_LOAD(&g_cs_claimed[i], __ATOMIC_RELAXED)) continue;
1617
+ const sp_sw_task *t = &g_cs_tasks[i];
1618
+ int owned = g_cs_owner_env && sp_cs_task_is_owned(t);
1619
+ if (mode == CS_RUN_SWEEPER && owned) continue;
1620
+ if (mode == CS_RUN_OWNER && (!owned || t->wid != wid)) continue;
1621
+ unsigned char z = 0;
1622
+ if (!SP_ATOMIC_CAS(&g_cs_claimed[i], &z, 1, 0, __ATOMIC_ACQ_REL, __ATOMIC_RELAXED)) continue;
1623
+ SP_ATOMIC_FETCH_SUB(&g_cs_unclaimed, 1, __ATOMIC_RELAXED);
1624
+ sp_cs_run_task(t);
1625
+ SP_ATOMIC_STORE(&g_cs_claimed[i], 2, __ATOMIC_RELEASE); /* done: the owner's release reads this */
1626
+ ran++;
1627
+ }
1628
+ if (sp_gc_ph_on && ran) SP_ATOMIC_FETCH_ADD(&g_cs_task_us, (unsigned long)((sp_monotonic_now() - t0) * 1e6), __ATOMIC_RELAXED);
1629
+ return ran;
1630
+ }
1631
+ /* May this worker hand its empty chunks back? Not while a sweeper thread is
1632
+ still in the chunk task of its slot (claimed, not done): that sweep clears
1633
+ bits of the very chunks the pool would carve for someone else. A slot with
1634
+ no task in this sweep, or whose task this worker ran itself, is settled. */
1635
+ static int sp_cs_chunks_settled(int wid) {
1636
+ if (!g_cs_pending) return 1;
1637
+ for (int i = 0; i < g_cs_ntasks; i++)
1638
+ if (g_cs_tasks[i].kind == CS_CHUNKS && g_cs_tasks[i].wid == wid)
1639
+ return SP_ATOMIC_LOAD(&g_cs_claimed[i], __ATOMIC_ACQUIRE) == 2;
1640
+ return 1;
1641
+ }
1642
+ /* Leaving the task list: count what was run, and count ourselves out.
1643
+ The collector reuses the task array for the next sweep only once nobody is
1644
+ scanning it (g_cs_running), not merely once every task is done: a thread
1645
+ still walking the array for something to claim would otherwise read the
1646
+ next sweep's entries as they are written. */
1647
+ static void sp_cs_finish(int ran) {
1648
+ pthread_mutex_lock(&g_cs_lock);
1649
+ /* release: the collector may find both counters at rest with an acquire
1650
+ load and proceed without the lock, and it then reads what the tasks wrote */
1651
+ int fin = SP_ATOMIC_ADD_FETCH(&g_cs_finished, ran, __ATOMIC_RELEASE);
1652
+ int running = SP_ATOMIC_SUB_FETCH(&g_cs_running, 1, __ATOMIC_RELEASE);
1653
+ if (fin >= g_cs_ntasks && running == 0) {
1654
+ if (sp_gc_ph_on) sp_gc_ph_conc_wall += sp_monotonic_now() - g_cs_t0;
1655
+ pthread_cond_broadcast(&g_cs_done);
1656
+ }
1657
+ pthread_mutex_unlock(&g_cs_lock);
1658
+ }
1659
+ static void sp_cs_enter(void) { pthread_mutex_lock(&g_cs_lock); SP_ATOMIC_FETCH_ADD(&g_cs_running, 1, __ATOMIC_RELAXED); pthread_mutex_unlock(&g_cs_lock); }
1660
+ /* The owner's share, run by the released worker with the world running. */
1661
+ static void sp_cs_owner_run(int wid) {
1662
+ int was = sp_gc_in_sweeper;
1663
+ sp_cs_enter();
1664
+ sp_gc_in_sweeper = 1;
1665
+ int ran = sp_cs_run_tasks(CS_RUN_OWNER, wid);
1666
+ sp_gc_in_sweeper = was;
1667
+ sp_cs_finish(ran);
1668
+ }
1669
+ /* A parked worker (or the collector) finishing the sweep under the barrier:
1670
+ it sweeps like a sweeper thread does, with the byte accounting off, since
1671
+ the collector recounts the bytes the mark saw. */
1672
+ static void sp_cs_help_run(void) {
1673
+ int was = sp_gc_in_sweeper;
1674
+ sp_cs_enter();
1675
+ sp_gc_in_sweeper = 1;
1676
+ int ran = sp_cs_run_tasks(CS_RUN_ANY, -1);
1677
+ sp_gc_in_sweeper = was;
1678
+ sp_cs_finish(ran);
1679
+ }
1680
+ static void *sp_cs_sweeper_main(void *arg) {
1681
+ (void)arg;
1682
+ sigset_t blk; sigemptyset(&blk); sigaddset(&blk, g_preempt_sig);
1683
+ pthread_sigmask(SIG_BLOCK, &blk, NULL);
1684
+ sp_gc_in_sweeper = 1;
1685
+ sp_worker_id = CS_SWEEPER_WID;
1686
+ #ifdef __linux__
1687
+ prctl(PR_SET_NAME, "sp-sweeper", 0, 0, 0); /* pthread_setname_np needs _GNU_SOURCE; this does not (#4469) */
1688
+ #endif
1689
+ unsigned seen = 0;
1690
+ for (;;) {
1691
+ pthread_mutex_lock(&g_cs_lock);
1692
+ /* g_shutdown is set under the sched lock at drain, which this thread
1693
+ does not hold: read it atomically, as the trim thread does */
1694
+ while (g_cs_gen == seen && !SP_ATOMIC_LOAD(&g_shutdown, __ATOMIC_RELAXED)) pthread_cond_wait(&g_cs_go, &g_cs_lock);
1695
+ if (SP_ATOMIC_LOAD(&g_shutdown, __ATOMIC_RELAXED)) { pthread_mutex_unlock(&g_cs_lock); break; }
1696
+ seen = g_cs_gen;
1697
+ SP_ATOMIC_FETCH_ADD(&g_cs_running, 1, __ATOMIC_RELAXED);
1698
+ pthread_mutex_unlock(&g_cs_lock);
1699
+ sp_cs_finish(sp_cs_run_tasks(CS_RUN_SWEEPER, -1));
1700
+ }
1701
+ return NULL;
1702
+ }
1703
+ /* Under the barrier, at the end of the mark: detach and hand off. */
1704
+ static void sp_sched_conc_start(int full, int str_sweep, int str_major) {
1705
+ int n = sp_active_workers; if (n < 1) n = 1; if (n > SP_MAX_WORKERS - 1) n = SP_MAX_WORKERS - 1;
1706
+ if (n > g_cs_hi) g_cs_hi = n;
1707
+ int nt = 0;
1708
+ g_cs_full = full; g_cs_str_sweep = str_sweep; g_cs_str_major = str_major;
1709
+ /* The collector's own length cache: every parked worker cleared its own at
1710
+ the park; this thread did not park, and it may name strings the sweep
1711
+ is about to free, whose addresses a later allocation reuses. */
1712
+ sp_str_lcache_clear();
1713
+ /* every slot ever used, plus the sweepers' own: a finalizer that
1714
+ allocated would have pushed onto CS_SWEEPER_WID's lists */
1715
+ int slots[SP_MAX_WORKERS]; int ns = 0;
1716
+ for (int i = 0; i < g_cs_hi; i++) slots[ns++] = i;
1717
+ slots[ns++] = CS_SWEEPER_WID;
1718
+ for (int k = 0; k < ns; k++) {
1719
+ int i = slots[k];
1720
+ g_cs_pro_head[i] = g_cs_pro_tail[i] = NULL;
1721
+ g_cs_obj[i] = sp_gc_wslot[i].young; sp_gc_wslot[i].young = NULL;
1722
+ if (g_cs_obj[i]) g_cs_tasks[nt++] = (sp_sw_task){ CS_OBJ, (short)i, 0 };
1723
+ /* the slot's chunks: the young slab generation every cycle, the old one
1724
+ at a full, strings with objects -- the string gate below governs only
1725
+ the lists of the strings too large for the slab (and their retune) */
1726
+ g_cs_tasks[nt++] = (sp_sw_task){ CS_CHUNKS, (short)i, 0 };
1727
+ if (str_sweep) {
1728
+ g_cs_so_freed[i] = 0;
1729
+ if (str_major) {
1730
+ g_cs_str_old[i] = sp_str_wslot[i].old; sp_str_wslot[i].old = NULL;
1731
+ if (g_cs_str_old[i]) g_cs_tasks[nt++] = (sp_sw_task){ CS_STR_OLD, (short)i, 0 };
1732
+ }
1733
+ for (int sub = 0; sub < SP_STR_YSUB; sub++) {
1734
+ g_cs_sy_keep[i][sub] = g_cs_sy_tail[i][sub] = NULL; g_cs_sy_moved[i][sub] = 0;
1735
+ g_cs_str_young[i][sub] = sp_str_wslot[i].young[sub]; sp_str_wslot[i].young[sub] = NULL;
1736
+ if (g_cs_str_young[i][sub]) g_cs_tasks[nt++] = (sp_sw_task){ CS_STR_YOUNG, (short)i, (short)sub };
1737
+ }
1738
+ /* the young generation left with its lists; the trigger counts what
1739
+ the slot holds from here */
1740
+ SP_GC_CTR_SET(sp_str_wslot[i].young_bytes, 0);
1741
+ sp_str_wslot[i].ask_at = 0;
1742
+ }
1743
+ }
1744
+ if (full) {
1745
+ g_cs_obj_old = sp_gc_old_detach();
1746
+ if (g_cs_obj_old) g_cs_tasks[nt++] = (sp_sw_task){ CS_OBJ_OLD, 0, 0 };
1747
+ }
1748
+ pthread_mutex_lock(&g_cs_lock);
1749
+ if (sp_gc_ph_on) g_cs_t0 = sp_monotonic_now();
1750
+ g_cs_ntasks = nt; SP_ATOMIC_STORE(&g_cs_finished, 0, __ATOMIC_RELAXED);
1751
+ memset(g_cs_claimed, 0, (size_t)nt);
1752
+ SP_ATOMIC_STORE(&g_cs_unclaimed, nt, __ATOMIC_RELEASE);
1753
+ g_cs_pending = 1;
1754
+ g_cs_gen++;
1755
+ pthread_cond_broadcast(&g_cs_go);
1756
+ pthread_mutex_unlock(&g_cs_lock);
1757
+ }
1758
+ /* Under the barrier, at the start of the next collection (or the end of an
1759
+ explicit one): join the sweepers and apply what they produced. */
1760
+ static void sp_sched_conc_wait(void) {
1761
+ if (!g_cs_pending) return;
1762
+ if (SP_ATOMIC_LOAD(&g_cs_finished, __ATOMIC_ACQUIRE) < g_cs_ntasks || SP_ATOMIC_LOAD(&g_cs_running, __ATOMIC_ACQUIRE) > 0) {
1763
+ /* Not done yet: the world is stopped, so every parked worker is idle.
1764
+ Hand them the unclaimed tasks (thirty hands finish in a fraction of
1765
+ what eight sweepers need), take some ourselves, then join. */
1766
+ double t0 = sp_gc_ph_on ? sp_monotonic_now() : 0;
1767
+ SCHED_LOCK();
1768
+ g_cs_help = 1;
1769
+ pthread_cond_broadcast(&g_stw_release);
1770
+ SCHED_UNLOCK();
1771
+ sp_cs_help_run();
1772
+ pthread_mutex_lock(&g_cs_lock);
1773
+ while (SP_ATOMIC_LOAD(&g_cs_finished, __ATOMIC_RELAXED) < g_cs_ntasks || SP_ATOMIC_LOAD(&g_cs_running, __ATOMIC_RELAXED) > 0) pthread_cond_wait(&g_cs_done, &g_cs_lock);
1774
+ pthread_mutex_unlock(&g_cs_lock);
1775
+ SCHED_LOCK();
1776
+ g_cs_help = 0;
1777
+ SCHED_UNLOCK();
1778
+ if (sp_gc_ph_on) { sp_gc_ph_conc_wait += sp_monotonic_now() - t0; sp_gc_ph_conc_waits++; }
1779
+ }
1780
+ if (sp_gc_ph_on) { sp_gc_ph_task_sum += (double)g_cs_task_us * 1e-6; g_cs_task_us = 0;
1781
+ sp_gc_ph_slot_max += (double)g_cs_task_max_us * 1e-6; g_cs_task_max_us = 0; }
1782
+ double at0 = sp_gc_ph_on ? sp_monotonic_now() : 0;
1783
+ /* the swept old list comes back first, then the survivors go in front of it */
1784
+ if (g_cs_full) { sp_gc_old_attach(g_cs_obj_old, g_cs_obj_old_tail); g_cs_obj_old = g_cs_obj_old_tail = NULL; }
1785
+ int slots[SP_MAX_WORKERS]; int ns = 0;
1786
+ for (int i = 0; i < g_cs_hi; i++) slots[ns++] = i;
1787
+ slots[ns++] = CS_SWEEPER_WID;
1788
+ for (int k = 0; k < ns; k++) {
1789
+ int i = slots[k];
1790
+ /* bytes: the mark counted them into the old total already */
1791
+ sp_gc_promote_slot(g_cs_pro_head[i], g_cs_pro_tail[i], 0);
1792
+ g_cs_pro_head[i] = g_cs_pro_tail[i] = NULL;
1793
+ }
1794
+ if (sp_gc_ph_on) { double t = sp_monotonic_now(); sp_gc_ph_apply_obj += t - at0; at0 = t; }
1795
+ if (g_cs_str_sweep) {
1796
+ size_t promoted = 0;
1797
+ size_t young_now = sp_str_bytes_total();
1798
+ for (int k = 0; k < ns; k++) {
1799
+ int i = slots[k];
1800
+ if (g_cs_str_major) {
1801
+ /* the old list was detached whole; what the sweep left of it is
1802
+ the old list again, and the young survivors go in front */
1803
+ sp_str_wslot[i].old = g_cs_str_old[i]; g_cs_str_old[i] = NULL;
1804
+ sp_str_wslot[i].old_bytes = sp_str_wslot[i].old_bytes > g_cs_so_freed[i] ? sp_str_wslot[i].old_bytes - g_cs_so_freed[i] : 0;
1805
+ }
1806
+ for (int sub = 0; sub < SP_STR_YSUB; sub++)
1807
+ sp_str_sweep_young_done(i, g_cs_sy_keep[i][sub], g_cs_sy_tail[i][sub], g_cs_sy_moved[i][sub], 0, &promoted);
1808
+ }
1809
+ sp_str_sweep_end_excluding(g_cs_str_major, promoted, young_now);
1810
+ }
1811
+ if (sp_gc_ph_on) { double t = sp_monotonic_now(); sp_gc_ph_apply_str += t - at0; at0 = t; }
1812
+ /* the owners released their own chunks beside the program (sp_stw_park_locked's
1813
+ exit); what is left for the barrier is the slots no active worker owns */
1814
+ if (g_cs_full) sp_slab_release_from(sp_active_workers);
1815
+ if (sp_gc_ph_on) sp_gc_ph_apply_release += sp_monotonic_now() - at0;
1816
+ g_cs_pending = 0;
1817
+ }
1818
+ /* Under the barrier, at the end of the root walk: lend the parked workers
1819
+ to the drain, drain with them, and join them before the mark is declared
1820
+ done (a late helper that found nothing must still be out of the drain). */
1821
+ static void sp_sched_par_mark(void) {
1822
+ sp_gc_mark_par_begin();
1823
+ SCHED_LOCK();
1824
+ g_mk_gen++; g_mk_joined = 0; g_mk_go = 1;
1825
+ /* as many wake-ups as there are seats: a broadcast had thirty parked
1826
+ workers take the scheduler lock in turn to find no seat, and that
1827
+ procession cost more than the drain */
1828
+ for (int i = 0; i < g_mk_max; i++) pthread_cond_signal(&g_stw_release);
1829
+ SCHED_UNLOCK();
1830
+ double t0 = sp_gc_ph_on ? sp_monotonic_now() : 0;
1831
+ sp_gc_mark_par_run();
1832
+ double t1 = sp_gc_ph_on ? sp_monotonic_now() : 0;
1833
+ SCHED_LOCK();
1834
+ g_mk_go = 0;
1835
+ while (g_mk_active > 0) pthread_cond_wait(&g_mk_cv, &g_sched_lock);
1836
+ if (sp_gc_ph_on) { double t2 = sp_monotonic_now(); sp_gc_ph_mk_drain += t1 - t0; sp_gc_ph_mk_join += t2 - t1; }
1837
+ if (sp_gc_ph_on) { sp_gc_ph_mk_helpers += (unsigned long long)g_mk_joined; sp_gc_ph_mk_drains++; }
1838
+ SCHED_UNLOCK();
1839
+ }
1840
+ static void sp_cs_start_sweepers(void) {
1841
+ { const char *o = getenv("SPINEL_GC_OWNER"); g_cs_owner_env = !(o && *o == '0'); }
1842
+ const char *e = getenv("SPINEL_GC_SWEEPERS");
1843
+ int want = e && *e ? atoi(e) : 0;
1844
+ if (want <= 0) { want = g_worker_cap > 0 ? g_worker_cap : 1; if (want > 8) want = 8; }
1845
+ if (want > CS_SWEEPER_MAX) want = CS_SWEEPER_MAX;
1846
+ pthread_attr_t at; pthread_attr_init(&at);
1847
+ pthread_attr_setstacksize(&at, 1024 * 1024);
1848
+ pthread_attr_setdetachstate(&at, PTHREAD_CREATE_DETACHED);
1849
+ for (int i = 0; i < want; i++) {
1850
+ pthread_t t;
1851
+ if (pthread_create(&t, &at, sp_cs_sweeper_main, NULL) == 0) g_cs_nsweepers++;
1852
+ }
1853
+ pthread_attr_destroy(&at);
1854
+ if (g_cs_nsweepers > 0) {
1855
+ sp_gc_conc_sweep_hook = sp_sched_conc_start;
1856
+ sp_gc_conc_wait_hook = sp_sched_conc_wait;
1857
+ }
1858
+ }
1859
+
1860
+ static int g_sw_hi = 1; /* largest pool seen; bounds the per-collection loops */
1861
+ static void sp_sched_par_sweep(void) {
1862
+ int n = sp_active_workers; if (n < 1) n = 1; if (n > SP_MAX_WORKERS) n = SP_MAX_WORKERS;
1863
+ /* Only the live slots: SP_MAX_WORKERS is 256 and this runs on every
1864
+ collection, so clearing the whole array cost more than the sweep saved.
1865
+ g_sw_hi is the largest pool ever seen, which bounds the orphan scan below
1866
+ without walking 256 empty slots each time. */
1867
+ if (n > g_sw_hi) g_sw_hi = n;
1868
+ /* The string gate is one decision over the whole heap, so make it here --
1869
+ once, before anyone starts -- and let each worker apply it to its own
1870
+ slot. Taking it per worker would let them disagree about `major`. */
1871
+ g_str_major = 0;
1872
+ g_str_sweep = sp_str_sweep_begin(&g_str_major);
1873
+ /* Same gate the serial sweep applies: on a minor cycle the old string list
1874
+ holds exactly the strings the mark could not reach, so sweeping it frees
1875
+ live ones. Without this a threaded program corrupted a Hash the moment a
1876
+ minor cycle landed on a string-heap trigger. */
1877
+ if (sp_gc_str_minor_only) g_str_major = 0;
1878
+ sp_str_par_done = 1; /* the collector's serial pass must not repeat this */
1879
+ /* Every slot up to the largest pool seen is on the list: a slot no live
1880
+ worker owns -- past the current pool, or a worker that exited -- still
1881
+ has to be swept or its lists leak, and its string lists count too. The
1882
+ heavy tasks go first so the tail of the phase is short ones. */
1883
+ memset(g_str_promoted, 0, (size_t)g_sw_hi * sizeof g_str_promoted[0]);
1884
+ memset(g_sw_head, 0, (size_t)g_sw_hi * sizeof g_sw_head[0]);
1885
+ memset(g_sw_tail, 0, (size_t)g_sw_hi * sizeof g_sw_tail[0]);
1886
+ memset(g_sw_bytes, 0, (size_t)g_sw_hi * sizeof g_sw_bytes[0]);
1887
+ int nt = 0;
1888
+ if (g_str_sweep) {
1889
+ if (g_str_major)
1890
+ for (int i = 0; i < g_sw_hi; i++)
1891
+ if (sp_str_wslot[i].old) g_sw_tasks[nt++] = (sp_sw_task){ SW_STR_OLD, (short)i, 0 };
1892
+ for (int i = 0; i < g_sw_hi; i++)
1893
+ for (int sub = 0; sub < SP_STR_YSUB; sub++) {
1894
+ g_sy_keep[i][sub] = g_sy_tail[i][sub] = NULL; g_sy_moved[i][sub] = g_sy_held[i][sub] = 0;
1895
+ if (sp_str_wslot[i].young[sub]) g_sw_tasks[nt++] = (sp_sw_task){ SW_STR_YOUNG, (short)i, (short)sub };
1896
+ }
1897
+ }
1898
+ for (int i = 0; i < g_sw_hi; i++)
1899
+ if (sp_gc_wslot[i].young) g_sw_tasks[nt++] = (sp_sw_task){ SW_OBJ, (short)i, 0 };
1900
+ /* every slot's slab chunks, the slots no worker runs included: a chunk is
1901
+ swept exactly once a cycle, and a second pass over one at a full cycle
1902
+ would read its cleared marks as "nothing reached" */
1903
+ for (int i = 0; i < SP_MAX_WORKERS; i++)
1904
+ g_sw_tasks[nt++] = (sp_sw_task){ SW_CHUNKS, (short)i, 0 };
1905
+ /* The collector's own length cache: every parked worker cleared its own
1906
+ at the park, and until now the collector dropped its entries one by one
1907
+ as it swept its own strings. Its strings may now be swept by any worker,
1908
+ which drops them from THAT worker's (empty) cache, so the collector's
1909
+ entries would name freed strings. Clear it whole, like a park does. */
1910
+ sp_str_lcache_clear();
1911
+ SCHED_LOCK();
1912
+ g_sw_ntasks = nt;
1913
+ SP_ATOMIC_STORE(&g_sw_next, 0, __ATOMIC_RELEASE);
1914
+ g_sw_done = 0;
1915
+ g_sw_slot_max_us = 0;
1916
+ g_sweep_go = 1;
1917
+ pthread_cond_broadcast(&g_stw_release);
1918
+ SCHED_UNLOCK();
1919
+ int ran = sp_sweep_run_tasks();
1920
+ SCHED_LOCK();
1921
+ g_sw_done += ran;
1922
+ while (g_sw_done < nt) pthread_cond_wait(&g_sweep_cv, &g_sched_lock);
1923
+ g_sweep_go = 0;
1924
+ SCHED_UNLOCK();
1925
+ for (int i = 0; i < g_sw_hi; i++)
1926
+ sp_gc_promote_slot(g_sw_head[i], g_sw_tail[i], g_sw_bytes[i]);
1927
+ if (sp_gc_ph_on) {
1928
+ sp_gc_ph_slot_max += (double)g_sw_slot_max_us * 1e-6;
1929
+ sp_gc_ph_task_sum += (double)g_sw_task_sum_us * 1e-6;
1930
+ sp_gc_ph_task_obj += (double)g_sw_task_max_kind[SW_OBJ] * 1e-6;
1931
+ sp_gc_ph_task_sold += (double)g_sw_task_max_kind[SW_STR_OLD] * 1e-6;
1932
+ sp_gc_ph_task_syoung += (double)g_sw_task_max_kind[SW_STR_YOUNG] * 1e-6;
1933
+ g_sw_task_sum_us = 0; memset(g_sw_task_max_kind, 0, sizeof g_sw_task_max_kind);
1934
+ }
1935
+ if (g_str_sweep) {
1936
+ size_t promoted = 0;
1937
+ for (int i = 0; i < g_sw_hi; i++)
1938
+ for (int sub = 0; sub < SP_STR_YSUB; sub++)
1939
+ sp_str_sweep_young_done(i, g_sy_keep[i][sub], g_sy_tail[i][sub], g_sy_moved[i][sub], g_sy_held[i][sub], &promoted);
1940
+ sp_str_sweep_end(g_str_major, promoted);
1941
+ g_str_sweep = 0;
1942
+ }
1943
+ }
1944
+
1945
+ /* The trimmer: malloc_trim beside the running program rather than under
1946
+ stop-the-world (see the full-cycle tail of sp_gc_collect). It wakes once a
1947
+ second and trims when a full cycle has asked since the last one. */
1948
+ #if defined(__GLIBC__)
1949
+ #include <malloc.h>
1950
+ /* The trimmer sleeps in short steps and trims as soon as a full cycle has
1951
+ asked (the cadence, once a second, is the requester's): a trim a second
1952
+ after the request found the arena mid-cycle, with the freed memory of the
1953
+ last sweep only partly in it, and the process kept 260 MB more resident
1954
+ than the inline trim used to leave. Trimming promptly after the request,
1955
+ which follows the sweep, hands back what the sweep freed. */
1956
+ static void *sp_trim_thread_main(void *arg) {
1957
+ (void)arg;
1958
+ sigset_t blk; sigemptyset(&blk); sigaddset(&blk, g_preempt_sig);
1959
+ pthread_sigmask(SIG_BLOCK, &blk, NULL);
1960
+ for (;;) {
1961
+ struct timespec ts = { 0, 20 * 1000 * 1000 };
1962
+ nanosleep(&ts, NULL);
1963
+ if (SP_ATOMIC_LOAD(&g_shutdown, __ATOMIC_RELAXED)) break; /* set under the sched lock at drain; read here without it */
1964
+ if (SP_ATOMIC_EXCHANGE(&sp_gc_trim_wanted, 0, __ATOMIC_ACQ_REL)) malloc_trim(0);
1965
+ }
1966
+ return NULL;
1967
+ }
1968
+ static void sp_trim_thread_start(void) {
1969
+ pthread_t t;
1970
+ pthread_attr_t at; pthread_attr_init(&at);
1971
+ /* 1 MB, like the sweepers: a thread's stack also carries the runtime's
1972
+ static TLS, and the mark stack there alone is half a megabyte, so the
1973
+ 256 KB this asked for was refused by pthread_create (EINVAL). The
1974
+ trimmer then never started, and every full cycle's malloc_trim ran
1975
+ inline under the barrier -- 40 ms each, the whole reason the thread
1976
+ exists -- with nothing saying so. */
1977
+ pthread_attr_setstacksize(&at, 1024 * 1024);
1978
+ pthread_attr_setdetachstate(&at, PTHREAD_CREATE_DETACHED);
1979
+ int rc = pthread_create(&t, &at, sp_trim_thread_main, NULL);
1980
+ if (rc == 0) sp_gc_trimmer_on = 1;
1981
+ else if (sp_gc_ph_on) fprintf(stderr, "[gcph] trimmer thread did not start: %s\n", strerror(rc));
1982
+ pthread_attr_destroy(&at);
1983
+ }
1984
+ #else
1985
+ static void sp_trim_thread_start(void) {}
1986
+ #endif
1987
+
1988
+ static int sp_worker_count(void); /* defined below; the pool size */
1989
+ static int sp_sched_ensure_workers(void) {
1990
+ if (g_workers_started) return 1;
1991
+ sp_alloc_stress_init(); /* set the stress flags once here, before any helper reads them */
1992
+ sp_alloc_worker_tune(sp_worker_count()); /* and size the GC budget for the pool */
1993
+ if (!sp_sched_start_workers()) return 0;
1994
+ g_workers_started = 1;
1995
+ /* Hand parked workers their own slots from here on. Workers spawn lazily, so
1996
+ the pool may still be one at this point; the driver itself falls back to
1997
+ the serial sweep whenever there is nobody parked to help. */
1998
+ sp_gc_par_sweep_hook = sp_sched_par_sweep;
1999
+ /* Seats for the parallel mark. Four helpers took a server's drain from
2000
+ 2.0 ms to 0.7 ms; eight did no better and sixteen were slower, since
2001
+ every helper is a parked worker woken from a futex and the wake-ups
2002
+ and the chunk list's lock are the drain's fixed cost. */
2003
+ { const char *e = getenv("SPINEL_GC_MARKERS"); int m = e && *e ? atoi(e) : 0;
2004
+ if (m <= 0) { m = g_worker_cap > 0 ? g_worker_cap : 1; if (m > 4) m = 4; }
2005
+ g_mk_max = m; sp_gc_mark_par_markers(m + 1); }
2006
+ sp_gc_hdr_flags_check();
2007
+ sp_gc_par_mark_hook = sp_sched_par_mark;
2008
+ sp_trim_thread_start();
2009
+ sp_cs_start_sweepers();
2010
+ return 1;
2011
+ }
2012
+ #endif
2013
+
2014
+ /* CRuby's two-line report: the thread's own #inspect, then the same tail an
2015
+ uncaught exception prints. The old one-line form named the thread by its
2016
+ small serial, which says nothing about WHICH thread in a program that
2017
+ spawns several -- #inspect carries the spawn site. sp_Thread_inspect
2018
+ allocates, and the caller has already dropped t from the registry, so root
2019
+ t across it. */
2020
+ static void sp_thread_report(sp_thread *t) {
2021
+ SP_GC_ROOT(t);
2022
+ const char *cls = t->exc_cls ? t->exc_cls : "Exception";
2023
+ const char *msg = t->exc_msg ? t->exc_msg : "";
2024
+ SP_GC_ROOT_STR(msg);
2025
+ const char *ins = sp_Thread_inspect(t);
2026
+ fprintf(stderr, "%s terminated with exception (report_on_exception is true):\n", ins);
2027
+ fprintf(stderr, "%s (%s)\n", (msg && *msg) ? msg : cls, cls);
2028
+ }
2029
+
2030
+ /* Park/wake primitives (defined below; used by join here). */
2031
+ static void sp_sched_block(sp_thread **waitlist, int defer_inject);
2032
+ static sp_thread *sp_sched_wake_one(sp_thread **waitlist);
2033
+ #ifdef SP_THREADS
2034
+ static int sp_sched_block_timeout(sp_thread **waitlist, double deadline, sp_mutex *mutex);
2035
+ #endif
2036
+
2037
+ /* A finished thread's parked joiners become runnable again (the main thread,
2038
+ if it was waiting, is released by the pump's target check, not the queue). */
2039
+ static void sp_thread_wake_joiners(sp_thread *t) {
2040
+ while (sp_sched_wake_one(&t->joiners)) { }
2041
+ }
2042
+
2043
+ /* Run runnable green threads until `target` is DEAD, until the main thread is
2044
+ woken back to RUNNABLE (it had blocked on a primitive), or until the run queue
2045
+ drains. Runs on the root fiber (the main thread). */
2046
+ /* Run thread t for one timeslice: transfer into its fiber until it yields back
2047
+ (parked on a wait-list, or re-queued itself via Thread.pass) or its body
2048
+ returns. On termination, publish the result/exception and wake joiners. */
2049
+ /* Quiescence: nothing left to run anywhere. A main thread parked in its pump
2050
+ (#join / drain) waits on exactly this, so the worker that empties the last of
2051
+ the work wakes it. Counting RUNNING workers (not idle ones) makes this robust:
2052
+ a worker that merely wakes spuriously and re-idles never touches g_nrunning,
2053
+ so it cannot momentarily perturb the predicate the way an idle count would. */
2054
+ static void sp_sched_signal_if_quiescent(void) {
2055
+ /* Only main is waiting on this. Quiescent means there is nothing for a
2056
+ helper to find, so waking the helpers here was pure cost -- and in a
2057
+ park-heavy program (every green thread blocked on I/O between hops) it
2058
+ fired on every hop, which is most of what adding workers cost (#4305). */
2059
+ if (g_nrunning == 0 && g_runnable == 0) SCHED_WAKE_MAIN();
2060
+ }
2061
+
2062
+ /* The fiber a green thread switches back to when it yields, blocks or is
2063
+ preempted: the one that ran it on this worker (run_thread_once). That is
2064
+ the worker's root fiber, except when the main thread runs a green thread
2065
+ from inside a Fiber it resumed (a Thread.pass sweep, or the pump): the root
2066
+ is then suspended in that #resume, its context saved in the fiber's
2067
+ caller_ctx rather than its own, so a switch to it lands on a context that
2068
+ was never saved. run_thread_once never nests on a worker, so it sets the
2069
+ home and clears it after. */
2070
+ static SP_TLS sp_Fiber *g_sched_home = NULL;
2071
+ static sp_Fiber *sp_sched_home(void) { return g_sched_home ? g_sched_home : sp_fiber_worker_root(); }
2072
+ sp_Fiber *sp_sched_home_fiber(void) { return sp_sched_home(); }
2073
+ /* A green thread yields, blocks or is preempted: back to the fiber that ran
2074
+ it. Inside a Fiber it resumed, that fiber is where it stops, so note it for
2075
+ run_thread_once to switch back to. */
2076
+ static void sp_sched_switch_out(void) {
2077
+ sp_thread *self = g_current;
2078
+ if (self && sp_fiber_current != self->fiber) self->at = sp_fiber_current;
2079
+ sp_fiber_sched_switch(sp_sched_home());
2080
+ }
2081
+
2082
+ static void run_thread_once(sp_thread *t) { sp_gc_wb((void*)t); /* PRE/POST: sched lock held */
2083
+ if (sched_lat_enabled() && t->readied_at > 0) {
2084
+ double d = (sp_monotonic_now() - t->readied_at) * 1e6; /* us */
2085
+ int k = 0; while (k < 31 && d >= (double)(2u << k)) k++;
2086
+ g_sched_lat_hist[k]++; g_sched_lat_n++;
2087
+ if (d > g_sched_lat_max) g_sched_lat_max = d;
2088
+ t->readied_at = 0;
2089
+ int len = 0; for (sp_thread *w = g_lrq[sp_worker_id].head; w; w = w->rq_next) len++;
2090
+ if (len > g_lrq_len_max) g_lrq_len_max = len;
2091
+ }
2092
+ sp_thread *saved = g_current;
2093
+ g_current = t;
2094
+ g_nrunning++;
2095
+ t->state = SP_TH_RUNNING;
2096
+ t->off_cpu = 0; /* on-cpu now: no other worker may pick it up */
2097
+ if (t->home_wid < 0) { /* pin to this worker (TLS affinity) */
2098
+ t->home_wid = (short)sp_worker_id;
2099
+ #ifdef SP_THREADS
2100
+ g_wslot[sp_worker_id].pinned++;
2101
+ #endif
2102
+ }
2103
+ #ifdef SP_THREADS
2104
+ /* Publish to the monitor that this worker is now running t, and when -- it uses
2105
+ this to enforce the timeslice. Nudge the monitor if it is idle so it starts
2106
+ ticking. */
2107
+ g_wslot[sp_worker_id].cur = t;
2108
+ g_wslot[sp_worker_id].since = sp_monotonic_now();
2109
+ sp_sysmon_wake();
2110
+ #endif
2111
+ int raised = 0;
2112
+ const char *ec = NULL, *em = NULL;
2113
+ void *eo = NULL;
2114
+ /* On the body's first entry the block's param reads the fiber's resumed
2115
+ value, so hand it Thread.new's argument then; later resumes pass nil. */
2116
+ sp_RbVal in = (t->fiber->state == 0) ? t->arg : sp_box_nil();
2117
+ /* Run the green thread with the lock dropped: it executes Ruby (and may park
2118
+ on, or wake, other threads, which re-take the lock themselves). */
2119
+ g_sched_home = sp_fiber_current;
2120
+ sp_Fiber *at = t->at; /* it stopped inside a Fiber it resumed */
2121
+ t->at = NULL;
2122
+ SCHED_UNLOCK();
2123
+ sp_Fiber_transfer_catch(t->fiber, at, in, &raised, &ec, &em, &eo);
2124
+ SCHED_LOCK();
2125
+ g_sched_home = NULL;
2126
+ g_current = saved;
2127
+ #ifdef SP_THREADS
2128
+ g_wslot[sp_worker_id].cur = NULL; /* no longer timing t on this worker */
2129
+ /* If the monitor flagged t but it yielded/blocked/died before reaching a poll,
2130
+ retire the request here so the flag does not stay stuck set. */
2131
+ if (t->preempt_request) { t->preempt_request = 0; g_npreempt--; sp_recompute_safepoint_flag(); }
2132
+ #endif
2133
+ if (t->fiber->state == 3) { /* the body returned (terminated) */
2134
+ t->retval = t->fiber->yielded_value;
2135
+ t->state = SP_TH_DEAD;
2136
+ #ifdef SP_THREADS
2137
+ if (t->home_wid >= 0 && g_wslot[t->home_wid].pinned > 0) g_wslot[t->home_wid].pinned--;
2138
+ #endif
2139
+ int do_report = 0;
2140
+ if (raised) {
2141
+ t->has_exc = 1; t->exc_cls = ec; t->exc_msg = em; t->exc_obj = eo;
2142
+ /* A SystemExit is not a thread dying badly, it is the program being
2143
+ asked to end: CRuby reports every other class here and stays silent
2144
+ for this one, then lets the exception surface at join and terminate
2145
+ with its status. Reporting it printed a scary line for an ordinary
2146
+ `exit` inside a thread. */
2147
+ do_report = t->report_on_exception &&
2148
+ !(ec && strcmp(ec, "SystemExit") == 0);
2149
+ }
2150
+ /* Record t AFTER the stores above, not only on the way into this function.
2151
+ The barrier at the top has been consumed by then: the transfer runs the
2152
+ thread's body, which allocates, and any collection in there clears every
2153
+ old object's dirty bit and empties the remembered set. retval and the
2154
+ exception fields are all allocated by that body, so an old t was left
2155
+ holding young values nothing had recorded -- and a minor mark does not
2156
+ walk the old list. Thread#value read a recycled string this way.
2157
+
2158
+ A barrier before a call that can collect only covers the stores before
2159
+ the call; gc_wb_cells makes the same choice on the codegen side, and
2160
+ says why in its own comment. */
2161
+ sp_gc_wb((void *)t);
2162
+ sp_thread_wake_joiners(t);
2163
+ reg_remove(t); /* collectable once no user reference remains */
2164
+ g_nrunning--;
2165
+ SCHED_WAKE_ALL(); /* wake a pump-waiting main (joining on this thread) and idle workers */
2166
+ if (do_report) { SCHED_UNLOCK(); sp_thread_report(t); SCHED_LOCK(); }
2167
+ return;
2168
+ }
2169
+ /* t yielded back and is now fully off its stack. Only NOW is it safe for
2170
+ another worker to run it, so this is where it (re-)enters the run queue:
2171
+ - BLOCKED: it parked on a wait list. If a waker raced in while it was still
2172
+ switching out, it deferred the enqueue to us (wake_pending); do it now.
2173
+ - otherwise (still RUNNING): it yielded via Thread.pass and wants to keep
2174
+ running, so requeue it. */
2175
+ t->off_cpu = 1;
2176
+ if (t->state == SP_TH_BLOCKED) {
2177
+ if (t->wake_pending) {
2178
+ t->wake_pending = 0;
2179
+ t->state = SP_TH_RUNNABLE;
2180
+ runq_requeue(t);
2181
+ sched_wake_home(t->home_wid); /* pinned to us: our own loop re-picks it */
2182
+ }
2183
+ }
2184
+ else {
2185
+ /* Thread.pass / preempt: requeue. A started thread is pinned to its home
2186
+ worker (TLS affinity), so it lands on our own local queue TAIL -- other
2187
+ queued work still runs first, and the 61-tick global check keeps the
2188
+ global queue from starving. */
2189
+ runq_requeue(t);
2190
+ sched_wake_home(t->home_wid); /* pinned to us: our own loop re-picks it */
2191
+ }
2192
+ g_nrunning--;
2193
+ sp_sched_signal_if_quiescent(); /* this thread blocked/passed; if nothing else runs, wake a waiting main */
2194
+ }
2195
+
2196
+ #ifdef SP_THREADS
2197
+ /* Cooperative preemption point (called from sp_safepoint with the lock held). If
2198
+ the monitor flagged the running green thread as over its timeslice, yield to
2199
+ the fiber that ran it (sp_sched_home) exactly as Thread.pass does for a
2200
+ spawned thread: stay RUNNING so run_thread_once requeues us at the tail and
2201
+ the worker runs a sibling. The main thread is never flagged (the monitor
2202
+ only times green threads it runs via run_thread_once), so this only ever
2203
+ preempts a spawned thread. PRE/POST: lock held. */
2204
+ static void sp_safepoint_preempt(void) {
2205
+ sp_thread *self = g_current;
2206
+ if (!self || self == &g_main_thread || !self->preempt_request) return;
2207
+ self->preempt_request = 0;
2208
+ g_npreempt--;
2209
+ sp_recompute_safepoint_flag();
2210
+ /* Inside a fiber the thread resumed (an Enumerator's), switching out
2211
+ would park that fiber, and the thread is resumed at its own. Skip this
2212
+ slice; the monitor asks again on the next one. */
2213
+ if (sp_fiber_current != self->fiber) return;
2214
+ SCHED_UNLOCK();
2215
+ sp_sched_switch_out();
2216
+ sp_fiber_fire_inject_if_pending(); /* a #kill/#raise delivered while we were off-cpu */
2217
+ SCHED_LOCK();
2218
+ }
2219
+ #endif
2220
+
2221
+ /* Run runnable green threads on this (the main) worker. Returns when `target`
2222
+ dies, when the main thread is woken back to RUNNABLE, or when the run queue is
2223
+ empty. When may_wait is set and a helper worker is still busy (so it may yet
2224
+ enqueue work or wake us), block on g_sched_work instead of returning on an
2225
+ empty queue -- otherwise main would falsely declare a deadlock while a helper
2226
+ runs the very thread that will wake it. At N=1 g_nrunning is 0 between runs, so
2227
+ this returns on an empty queue exactly as before. PRE/POST: sched lock held. */
2228
+ /* may_wait: 0 never waits, 1 waits for anything outstanding, 2 is the EXIT
2229
+ DRAIN -- it waits for work that can still run and NOT for threads that are
2230
+ merely blocked. main() finishing is the end of the program in CRuby, where
2231
+ the other threads are killed where they stand; waiting on a sleeper there
2232
+ meant `Thread.new { sleep 30 }` at the top of a script hung the process
2233
+ after its last statement, and `exit 0` was the difference between a program
2234
+ that ended and one that did not (#4394, #4397, rofl0r). */
2235
+ static void sp_sched_pump(sp_thread *target, int may_wait) {
2236
+ for (;;) {
2237
+ #ifdef SP_THREADS
2238
+ if (g_stw_active) { sp_stw_park_locked(); continue; } /* park main through STW too */
2239
+ #endif
2240
+ if (target && target->state == SP_TH_DEAD) return;
2241
+ #ifdef SP_EV_BACKEND
2242
+ if (g_wslot[0].evfd > 0) sp_ev_worker_wait(0, 0); /* see sp_worker_main */
2243
+ #endif
2244
+ /* main blocked on a Queue/Mutex and a runnable thread just woke it */
2245
+ if (g_main_thread.state == SP_TH_RUNNABLE) { g_main_thread.state = SP_TH_RUNNING; return; }
2246
+ /* Run a green thread on the main worker only at N=1. With helpers present,
2247
+ main does NOT pump general green threads: running one means transferring
2248
+ off the root and back, which leaves the root fiber's published top-level
2249
+ roots a stale snapshot a collector could later mark. Main instead waits
2250
+ and lets a helper run the work. At N=1 there are no helpers, so it must
2251
+ pump. EXCEPTION: a thread pinned to main's worker (home_wid 0 -- it first
2252
+ ran here via a Thread.pass sweep, sp_sched_pass) can run NOWHERE else:
2253
+ helpers must not resume it (TLS affinity) and stealing skips it. If one
2254
+ is requeued while main pumps (a #kill/#raise or Queue wake unblocked it),
2255
+ main is the only worker that can finish it, so run it here -- otherwise
2256
+ the pump and that thread deadlock waiting on each other. */
2257
+ #ifdef SP_THREADS
2258
+ if (sp_active_workers > 1) {
2259
+ sp_thread *t = runq_pop(&g_lrq[0]);
2260
+ if (t) { run_thread_once(t); continue; }
2261
+ }
2262
+ else
2263
+ #endif
2264
+ {
2265
+ sp_thread *t = sched_pick(sp_worker_id);
2266
+ if (t) { run_thread_once(t); continue; }
2267
+ }
2268
+ #ifdef SP_THREADS
2269
+ /* Queue empty for us. Wait while a helper is still running a green thread or
2270
+ work sits in the queue (a helper will pick it up) -- it may enqueue more or
2271
+ wake us; the worker that drops g_nrunning to zero with an empty queue
2272
+ broadcasts (sp_sched_signal_if_quiescent). Only when nothing runs and the
2273
+ queue is empty do we fall through -- drained, or a deadlock the caller
2274
+ observes. */
2275
+ int outstanding = (g_nrunning > 0 || g_runnable > 0);
2276
+ /* A timed wait or an I/O waiter is work that may yet become runnable, so an
2277
+ ordinary wait counts it. The exit drain does not: nothing is going to
2278
+ ask for it after main has returned. */
2279
+ if (may_wait == 1) outstanding = outstanding || g_sleepers || g_io_waiters || g_timer_pending;
2280
+ if (may_wait && outstanding) {
2281
+ #ifdef SP_EV_BACKEND
2282
+ /* Main is worker 0, and threads pin to it -- at SPINEL_WORKERS=1 all of
2283
+ them do. So main waits on worker 0's readiness set like any other
2284
+ worker, or nothing would ever deliver to the threads pinned here
2285
+ (#4306). The timeout keeps the pump's own predicates -- target death,
2286
+ main made runnable -- checked on a bound. */
2287
+ if (g_wslot[0].evfd > 0) { sp_ev_worker_wait(0, SP_EV_BACKSTOP_MS); continue; }
2288
+ #endif
2289
+ sp_out_enter_locked(0, SP_OUT_IDLE);
2290
+ pthread_cond_wait(&g_sched_work, &g_sched_lock);
2291
+ sp_out_leave_locked(0);
2292
+ continue;
2293
+ }
2294
+ #else
2295
+ (void)may_wait;
2296
+ #endif
2297
+ return;
2298
+ }
2299
+ }
2300
+
2301
+ /* One round-robin sweep for Thread.pass from the main thread: give each thread
2302
+ that is runnable *right now* exactly one timeslice, then return to main.
2303
+ Threads that re-queue themselves (their own Thread.pass) during the sweep land
2304
+ after the snapshot boundary and wait for the next sweep, so a sibling looping
2305
+ on Thread.pass cannot starve main (which would happen if main drained the
2306
+ queue here). */
2307
+ static void sp_sched_pass(void) {
2308
+ /* Snapshot the runnable count and run exactly that many: a thread that
2309
+ re-queues itself (its own Thread.pass) during the sweep lands beyond the
2310
+ snapshot and waits for the next sweep, so it cannot starve main. */
2311
+ int n = g_runnable;
2312
+ while (n-- > 0) {
2313
+ sp_thread *t = sched_pick(sp_worker_id);
2314
+ if (!t) return;
2315
+ run_thread_once(t);
2316
+ }
2317
+ }
2318
+
2319
+ static void sp_thread_scan(void *p) {
2320
+ sp_thread *t = (sp_thread *)p;
2321
+ if (t->fiber) sp_gc_mark(t->fiber);
2322
+ if (t->at) sp_gc_mark(t->at);
2323
+ sp_mark_rbval(t->arg);
2324
+ sp_mark_rbval(t->retval);
2325
+ sp_mark_rbval(t->name);
2326
+ if (t->exc_obj) sp_gc_mark(t->exc_obj);
2327
+ if (t->tls) sp_gc_mark(t->tls);
2328
+ }
2329
+
2330
+ sp_thread *sp_Thread_spawn_fiber_at(sp_Fiber *f, sp_RbVal arg, const char *file, sp_int line) {SP_GC_ROOT_RBVAL(arg);SP_GC_ROOT(f);SP_GC_ROOT_STR(file);
2331
+ sp_thread *t = sp_Thread_spawn_fiber(f, arg);
2332
+ t->birth_file = file;
2333
+ t->birth_line = line;
2334
+ return t;
2335
+ }
2336
+ sp_thread *sp_Thread_spawn_fiber(sp_Fiber *f, sp_RbVal arg) {
2337
+ #ifdef SP_THREADS
2338
+ if (!sp_sched_ensure_workers())
2339
+ sp_raise_cls("ThreadError", "failed to start scheduler monitor");
2340
+ #endif
2341
+ SP_GC_ROOT(f); /* root the freshly-built fiber across the allocation below */
2342
+ SP_GC_ROOT_RBVAL(arg);
2343
+ f->owner = 0; /* the thread's own fiber: the scheduler runs it on any worker */
2344
+ f->thread_main = 1;
2345
+ sp_thread *volatile t = (sp_thread *)sp_gc_alloc(sizeof(sp_thread), NULL, sp_thread_scan);
2346
+ memset(t, 0, sizeof *t);
2347
+ t->home_wid = -1; /* not yet started: any worker may pick it up */
2348
+ t->fiber = f;
2349
+ t->arg = arg;
2350
+ t->retval = sp_box_nil();
2351
+ t->name = sp_box_nil();
2352
+ t->report_on_exception = g_report_default;
2353
+ SCHED_LOCK();
2354
+ t->id = g_next_id++;
2355
+ reg_add(t);
2356
+ /* Prefer the spawning worker's local queue (locality) -- but only when that
2357
+ worker actually drains it. Main is a coordinator: it hands work to helpers
2358
+ and only runs a thread itself in the sole-worker fallback (no helper could
2359
+ be spawned), which drains the global queue too. So a main-spawned thread
2360
+ always goes to the global queue -- never main's local queue, where it would
2361
+ wait on a steal. (g_worker_cap > 0 whenever threads are in use.) */
2362
+ #ifdef SP_THREADS
2363
+ if (g_current == &g_main_thread && g_worker_cap > 0) rq_push(t);
2364
+ else
2365
+ #endif
2366
+ lrq_push(sp_worker_id, t);
2367
+ SCHED_WAKE(); /* a helper worker may be idle: hand it the new thread */
2368
+ SCHED_UNLOCK();
2369
+ return t;
2370
+ }
2371
+
2372
+ /* Joining the current thread can never finish. Reject it before attempting to
2373
+ park, including on the main thread before the timer monitor has started. */
2374
+ static void sp_thread_check_join_target(sp_thread *t) {
2375
+ if (t == g_current)
2376
+ sp_raise_cls("ThreadError", "Target thread must not be current thread");
2377
+ }
2378
+
2379
+ /* Block the calling thread until `t` is dead. The main thread pumps the queue;
2380
+ a spawned thread parks on t's joiners and yields to the scheduler. */
2381
+ static void sp_thread_await(sp_thread *t) {
2382
+ sp_thread_check_join_target(t);
2383
+ SCHED_LOCK();
2384
+ if (t->state == SP_TH_DEAD) { SCHED_UNLOCK(); return; }
2385
+ sp_thread *self = g_current;
2386
+ if (self == &g_main_thread) {
2387
+ sp_sched_pump(t, 1);
2388
+ int dead = (t->state == SP_TH_DEAD);
2389
+ SCHED_UNLOCK();
2390
+ if (!dead) sp_raise_cls("ThreadError", "deadlock detected: no runnable thread");
2391
+ }
2392
+ else {
2393
+ sp_sched_block(&t->joiners, 0); /* parks on t's joiners; resumes once t is dead */
2394
+ SCHED_UNLOCK();
2395
+ }
2396
+ }
2397
+
2398
+ /* CRuby: #join and #value re-raise the thread's unhandled exception in the
2399
+ joining thread. */
2400
+ static void sp_thread_reraise_if_exc(sp_thread *t) {
2401
+ if (t->has_exc) sp_fiber_reraise(t->exc_cls, t->exc_msg, t->exc_obj);
2402
+ }
2403
+
2404
+ sp_thread *sp_Thread_join(sp_thread *t) {
2405
+ sp_thread_await(t);
2406
+ sp_thread_reraise_if_exc(t);
2407
+ return t;
2408
+ }
2409
+
2410
+ /* CRuby's Thread#join(limit): wait at most `seconds` for the thread to
2411
+ finish, answering the thread when it does and NULL (nil) on timeout.
2412
+ Same return type as the no-arg join so one variable can hold either
2413
+ call's result. */
2414
+ sp_thread *sp_Thread_join_timeout(sp_thread *t, double seconds) {
2415
+ sp_thread_check_join_target(t);
2416
+ SCHED_LOCK();
2417
+ if (t->state == SP_TH_DEAD) {
2418
+ SCHED_UNLOCK();
2419
+ sp_thread_reraise_if_exc(t);
2420
+ return t;
2421
+ }
2422
+ if (!(seconds > 0)) {
2423
+ SCHED_UNLOCK();
2424
+ return NULL; /* also catches a NaN limit */
2425
+ }
2426
+
2427
+ #ifdef SP_THREADS
2428
+ int woken = sp_sched_block_timeout(&t->joiners, sp_monotonic_now() + seconds, NULL);
2429
+ if (woken < 0) {
2430
+ SCHED_UNLOCK();
2431
+ sp_raise_cls("NoMemoryError", "failed to schedule thread join timeout");
2432
+ }
2433
+ SCHED_UNLOCK();
2434
+ if (!woken) return NULL;
2435
+ sp_thread_reraise_if_exc(t);
2436
+ return t;
2437
+ #else
2438
+ SCHED_UNLOCK();
2439
+ /* Without the monitor thread there is no timer queue to park on. Keep the
2440
+ single-worker build responsive by yielding in short sleeps until either
2441
+ the target finishes or the deadline passes. */
2442
+ const double poll_slice = 0.002;
2443
+ double deadline = sp_monotonic_now() + seconds;
2444
+ for (;;) {
2445
+ double left = deadline - sp_monotonic_now();
2446
+ if (left <= 0) break;
2447
+ sp_sleep(left < poll_slice ? (sp_float)left : (sp_float)poll_slice);
2448
+ SCHED_LOCK();
2449
+ int dead = (t->state == SP_TH_DEAD);
2450
+ SCHED_UNLOCK();
2451
+ if (dead) { sp_thread_reraise_if_exc(t); return t; }
2452
+ }
2453
+ return NULL;
2454
+ #endif
2455
+ }
2456
+
2457
+ sp_RbVal sp_Thread_value(sp_thread *t) {
2458
+ sp_thread_await(t);
2459
+ sp_thread_reraise_if_exc(t);
2460
+ return t->retval;
2461
+ }
2462
+
2463
+ void sp_Thread_pass(void) {
2464
+ SCHED_LOCK();
2465
+ sp_thread *self = g_current;
2466
+ if (self == &g_main_thread) {
2467
+ sp_sched_pass(); /* one round-robin sweep, then main resumes (not a drain) */
2468
+ SCHED_UNLOCK();
2469
+ }
2470
+ else {
2471
+ /* Yield but stay runnable. Do NOT enqueue ourselves here: a second worker
2472
+ could pop and run our fiber while we are still mid-context-switch. We keep
2473
+ our state RUNNING and transfer to the fiber that ran us; run_thread_once
2474
+ requeues us once we are fully off-cpu. */
2475
+ SCHED_UNLOCK();
2476
+ sp_sched_switch_out();
2477
+ sp_fiber_fire_inject_if_pending(); /* a #kill/#raise delivered while paused */
2478
+ }
2479
+ }
2480
+
2481
+ sp_thread *sp_Thread_current(void) { return g_current; }
2482
+ /* The fiber a finished transferred fiber hands back to: the green thread's
2483
+ own fiber, or NULL on the main thread (its root fiber). */
2484
+ sp_Fiber *sp_thread_main_fiber(void) { return g_current ? g_current->fiber : NULL; }
2485
+ /* The current thread as a fiber owner. An id is never reused, so a fiber
2486
+ can't be taken over by a later thread at the same address. */
2487
+ unsigned sp_thread_owner_id(void) { return g_current ? g_current->id + 1 : 0; }
2488
+
2489
+ sp_bool sp_Thread_alive(sp_thread *t) { return t->state != SP_TH_DEAD; }
2490
+
2491
+ /* Thread.report_on_exception=(v): set the default for threads spawned after.
2492
+ Thread.report_on_exception: read the default. Per-thread #report_on_exception
2493
+ reads/sets the thread's own flag. */
2494
+ sp_bool sp_Thread_set_report_default(sp_bool v) { g_report_default = v ? 1 : 0; return v; }
2495
+ sp_bool sp_Thread_get_report_default(void) { return g_report_default; }
2496
+ sp_bool sp_Thread_set_report(sp_thread *t, sp_bool v) { t->report_on_exception = v ? 1 : 0; return v; }
2497
+ sp_bool sp_Thread_get_report(sp_thread *t) { return t->report_on_exception; }
2498
+
2499
+ sp_thread *sp_Thread_main(void) { return &g_main_thread; }
2500
+
2501
+ sp_RbVal sp_Thread_get_name(sp_thread *t) { return t->name; }
2502
+ sp_RbVal sp_Thread_set_name(sp_thread *t, sp_RbVal v) { sp_gc_wb((void*)t); t->name = v; return v; }
2503
+
2504
+ /* Thread.list enumeration: the main thread followed by every live spawned
2505
+ thread (dead ones are off the registry). The generated TU builds the array
2506
+ over these accessors since it owns sp_PolyArray. */
2507
+ sp_int sp_Thread_list_count(void) {
2508
+ sp_int n = 1; /* the main thread */
2509
+ for (sp_thread *t = g_all; t; t = t->all_next) n++;
2510
+ return n;
2511
+ }
2512
+ sp_thread *sp_Thread_list_at(sp_int i) {
2513
+ if (i <= 0) return &g_main_thread;
2514
+ sp_thread *t = g_all;
2515
+ for (sp_int k = 1; t && k < i; k++) t = t->all_next;
2516
+ return t ? t : &g_main_thread;
2517
+ }
2518
+
2519
+ /* #status: "run" while runnable/running, "sleep" while blocked, false when it
2520
+ finished normally, nil when it died with an unhandled exception. */
2521
+ sp_RbVal sp_Thread_status(sp_thread *t) {
2522
+ switch (t->state) {
2523
+ case SP_TH_RUNNING: case SP_TH_RUNNABLE: return sp_box_str(&("\xff" "run")[1]);
2524
+ case SP_TH_BLOCKED: return sp_box_str(&("\xff" "sleep")[1]);
2525
+ default: return t->has_exc ? sp_box_nil() : sp_box_bool(0);
2526
+ }
2527
+ }
2528
+
2529
+ /* ---- thread-local storage (Thread#[] / #[]=), a small sym->value map ---- */
2530
+ typedef struct { sp_sym *keys; sp_RbVal *vals; sp_int len, cap; } sp_tls_map;
2531
+ static void sp_tls_scan(void *p) { sp_tls_map *m = (sp_tls_map *)p; for (sp_int i = 0; i < m->len; i++) sp_mark_rbval(m->vals[i]); }
2532
+ static void sp_tls_fin(void *p) { sp_tls_map *m = (sp_tls_map *)p; free(m->keys); free(m->vals); }
2533
+
2534
+ sp_RbVal sp_Thread_tls_get(sp_thread *t, sp_sym k) {
2535
+ sp_tls_map *m = (sp_tls_map *)t->tls;
2536
+ if (m) for (sp_int i = 0; i < m->len; i++) if (m->keys[i] == k) return m->vals[i];
2537
+ return sp_box_nil();
2538
+ }
2539
+ sp_bool sp_Thread_tls_key(sp_thread *t, sp_sym k) {
2540
+ /* A READ needs no barrier, and this one was pointed at the thread anyway --
2541
+ which for the main thread is a static struct, so the barrier read the byte
2542
+ in front of a global to decide whether it had a header. */
2543
+ sp_tls_map *m = (sp_tls_map *)t->tls;
2544
+ if (m) for (sp_int i = 0; i < m->len; i++) if (m->keys[i] == k) return 1;
2545
+ return 0;
2546
+ }
2547
+ /* The barrier belongs on the MAP, which is the object that ends up holding the
2548
+ new reference. It was on the thread: a minor mark then reached the map
2549
+ through the thread's scan and MARKED it, but an old object is not re-scanned
2550
+ unless it is in the remembered set, so a young value stored into a long-lived
2551
+ map was swept out from under it. `Thread.current[:slots] = {}` per request,
2552
+ on a main thread whose map has been alive since boot, is exactly that shape:
2553
+ the next read of the slot faulted in sp_PolyPolyHash_get (#4311). */
2554
+ sp_RbVal sp_Thread_tls_set(sp_thread *t, sp_sym k, sp_RbVal v) {
2555
+ sp_tls_map *m = (sp_tls_map *)t->tls;
2556
+ if (m) for (sp_int i = 0; i < m->len; i++) if (m->keys[i] == k) {
2557
+ if (v.tag == SP_TAG_NIL) {
2558
+ memmove(&m->keys[i], &m->keys[i + 1], sizeof(sp_sym) * (size_t)(m->len - i - 1));
2559
+ memmove(&m->vals[i], &m->vals[i + 1], sizeof(sp_RbVal) * (size_t)(m->len - i - 1));
2560
+ m->len--;
2561
+ return v;
2562
+ }
2563
+ m->vals[i] = v; sp_gc_wb((void *)m); return v;
2564
+ }
2565
+ if (v.tag == SP_TAG_NIL) return v;
2566
+ if (!m) {
2567
+ SP_GC_ROOT(t); SP_GC_ROOT_RBVAL(v);
2568
+ m = (sp_tls_map *)sp_gc_alloc(sizeof(sp_tls_map), sp_tls_fin, sp_tls_scan);
2569
+ m->cap = 4; m->len = 0;
2570
+ m->keys = (sp_sym *)malloc(sizeof(sp_sym) * m->cap);
2571
+ m->vals = (sp_RbVal *)malloc(sizeof(sp_RbVal) * m->cap);
2572
+ if (!m->keys || !m->vals) sp_raise_cls("NoMemoryError", "failed to allocate thread storage");
2573
+ t->tls = m;
2574
+ /* The map is a second reference store, into the THREAD, and the barrier
2575
+ below covers only the map. sp_gc_alloc above is what makes this needed:
2576
+ it can collect, which clears t's dirty bit, so a thread that was already
2577
+ old holds a young map that nothing recorded and a minor mark never walks
2578
+ it. The whole map went away and the next read answered nil. */
2579
+ sp_gc_wb((void *)t);
2580
+ }
2581
+ if (m->len == m->cap) {
2582
+ sp_int nc = m->cap * 2;
2583
+ sp_sym *nk = (sp_sym *)realloc(m->keys, sizeof(sp_sym) * nc);
2584
+ if (!nk) sp_raise_cls("NoMemoryError", "failed to grow thread storage");
2585
+ m->keys = nk;
2586
+ sp_RbVal *nv = (sp_RbVal *)realloc(m->vals, sizeof(sp_RbVal) * nc);
2587
+ if (!nv) sp_raise_cls("NoMemoryError", "failed to grow thread storage");
2588
+ m->vals = nv; m->cap = nc;
2589
+ }
2590
+ m->keys[m->len] = k; m->vals[m->len] = v; m->len++;
2591
+ sp_gc_wb((void *)m);
2592
+ return v;
2593
+ }
2594
+ sp_PolyArray *sp_Thread_tls_keys(sp_thread *t) {
2595
+ SP_GC_ROOT(t);
2596
+ sp_PolyArray *a = sp_PolyArray_new();
2597
+ SP_GC_ROOT(a);
2598
+ sp_tls_map *m = (sp_tls_map *)t->tls;
2599
+ if (m) for (sp_int i = 0; i < m->len; i++) sp_PolyArray_push(a, sp_box_sym(m->keys[i]));
2600
+ return a;
2601
+ }
2602
+
2603
+ #ifdef SP_THREADS
2604
+ /* ---- helper OS workers (design 3.2, Appendix B) ---- */
2605
+ static pthread_t g_worker_threads[SP_MAX_WORKERS];
2606
+
2607
+ /* min(online cores, SPINEL_WORKERS); the env var overrides the autodetect. */
2608
+ static int sp_worker_count(void) {
2609
+ const char *e = getenv("SPINEL_WORKERS");
2610
+ int n;
2611
+ if (e && *e) { n = atoi(e); if (n < 1) n = 1; }
2612
+ else { long c = sysconf(_SC_NPROCESSORS_ONLN); n = (c > 0) ? (int)c : 1; }
2613
+ if (n > SP_MAX_WORKERS) n = SP_MAX_WORKERS;
2614
+ return n;
2615
+ }
2616
+
2617
+ /* The monitor thread (sysmon, design §5). Two jobs, both off the workers' backs:
2618
+ (1) fire scheduler timers (sleep, timed I/O, and timed condition waits), and (2) enforce the
2619
+ timeslice -- when a worker has run the same green thread past the quantum, flag
2620
+ it for preemption and SIGURG the worker so it yields at its next safepoint. It
2621
+ idles on g_sysmon_cv when nothing sleeps and no worker runs a green thread, and
2622
+ otherwise ticks on a short nanosleep. It never participates in GC. */
2623
+ static void *sp_sysmon_main(void *arg) {
2624
+ (void)arg;
2625
+ /* The monitor must never field a preemption signal itself. */
2626
+ sigset_t blk; sigemptyset(&blk); sigaddset(&blk, g_preempt_sig);
2627
+ pthread_sigmask(SIG_BLOCK, &blk, NULL);
2628
+ SCHED_LOCK();
2629
+ for (;;) {
2630
+ if (g_shutdown) break;
2631
+ /* Stay out of the scheduler state while a collection has the world stopped:
2632
+ the monitor is not a GC participant (it holds no roots) but it must not
2633
+ move threads between lists concurrently with the collector. */
2634
+ if (g_stw_active) { pthread_cond_wait(&g_stw_release, &g_sched_lock); continue; }
2635
+ SP_STAT_ADD(g_mon_iters, 1);
2636
+ double now = sp_monotonic_now();
2637
+ if (sched_lat_enabled()) {
2638
+ static double last_report = 0;
2639
+ if (now - last_report >= 5.0) {
2640
+ last_report = now;
2641
+ fprintf(stderr, "[sched-lat] n=%llu max=%.0fus lrq_max=%d us:", g_sched_lat_n, g_sched_lat_max, g_lrq_len_max);
2642
+ for (int k = 0; k < 32; k++) if (g_sched_lat_hist[k]) fprintf(stderr, " [%u]=%llu", 1u << k, g_sched_lat_hist[k]);
2643
+ fprintf(stderr, "\n");
2644
+ int busyw = 0; for (int i = 0; i < sp_active_workers; i++) if (g_wslot[i].active && g_wslot[i].cur) busyw++;
2645
+ fprintf(stderr, "[sched-lat] workers=%d busy=%d runnable=%d nrunning=%d\n", sp_active_workers, busyw, g_runnable, g_nrunning);
2646
+ { int pinned[SP_MAX_WORKERS] = {0}; int unpinned = 0;
2647
+ for (sp_thread *a = g_all; a; a = a->all_next) { if (a->home_wid >= 0) pinned[a->home_wid]++; else unpinned++; }
2648
+ fprintf(stderr, "[sched-lat] pinned per worker:"); for (int i = 0; i < sp_active_workers; i++) fprintf(stderr, " %d", pinned[i]); fprintf(stderr, " unpinned=%d\n", unpinned); }
2649
+ sched_hist_print("mutex-wait", g_mtx_hist, g_mtx_n, g_mtx_max);
2650
+ fprintf(stderr, "[mutex-wait] fast-path locks=%llu spin-acquired=%llu\n", g_mtx_fast, g_mtx_spin);
2651
+ sched_hist_print("cv-wait", g_cv_hist, g_cv_n, g_cv_max);
2652
+ g_lrq_len_max = 0;
2653
+ }
2654
+ }
2655
+ sp_timer_flush();
2656
+ sp_timer_fire_due(now);
2657
+ double nearest = sp_timer_next_deadline();
2658
+ int npf = 1, nio = 0, busy = 0;
2659
+ /* Timeslice enforcement, on every turn: it reads the worker slots, not the
2660
+ wait lists, so it is O(workers) and does not belong behind the skip
2661
+ below. (An earlier cut jumped over its declarations, which is how `busy`
2662
+ came to be read uninitialised.) */
2663
+ for (int i = 0; i < sp_active_workers; i++) {
2664
+ sp_thread *r = g_wslot[i].active ? g_wslot[i].cur : NULL;
2665
+ if (!r) continue;
2666
+ busy = 1;
2667
+ if (!r->preempt_request && (now - g_wslot[i].since) >= SP_PREEMPT_QUANTUM) {
2668
+ r->preempt_request = 1;
2669
+ g_npreempt++;
2670
+ sp_recompute_safepoint_flag();
2671
+ pthread_kill(g_wslot[i].tid, g_preempt_sig); /* nudge it to its next safepoint poll */
2672
+ }
2673
+ }
2674
+ /* Rebuilding the poll set walks the I/O waiters and grows with the
2675
+ population. The event backend holds descriptors in the kernel, so skip
2676
+ that walk until a deadline is due; the timer heap still fires due
2677
+ handles above without scanning parked threads. */
2678
+ int skip_walks = 0;
2679
+ #ifdef SP_EV_BACKEND
2680
+ skip_walks = (g_ev_fd >= 0 && !(g_nearest != 0.0 && now >= g_nearest));
2681
+ #endif
2682
+ if (skip_walks) {
2683
+ if (g_nearest != 0.0 && (nearest == 0.0 || g_nearest < nearest)) nearest = g_nearest;
2684
+ }
2685
+ else {
2686
+ /* Build the I/O poll set: slot 0 is the wake pipe (a registering thread
2687
+ writes a byte to break us out of poll early), the rest are parked fds.
2688
+ Timed I/O waiters use the same timer heap as sleeps and timed condition
2689
+ waits; this list is only for readiness registration. */
2690
+ for (sp_thread **wp = &g_io_waiters; *wp; ) {
2691
+ sp_thread *w = *wp;
2692
+ wp = &w->wait_next;
2693
+ nio++;
2694
+ #ifdef SP_EV_BACKEND
2695
+ if (g_ev_fd >= 0) continue; /* the kernel holds the interest set */
2696
+ #endif
2697
+ /* one slot per descriptor: the thread's own, or each of its set */
2698
+ int nslot = w->io_fd >= 0 ? 1 : w->io_nset;
2699
+ for (int si = 0; si < nslot; si++) {
2700
+ if (npf >= g_pcap) {
2701
+ int nc = g_pcap ? g_pcap * 2 : 16;
2702
+ struct pollfd *np = (struct pollfd *)realloc(g_pfds, sizeof(struct pollfd) * nc);
2703
+ sp_thread **nh = (sp_thread **)realloc(g_pths, sizeof(sp_thread *) * nc);
2704
+ if (np) g_pfds = np; if (nh) g_pths = nh;
2705
+ if (!np || !nh) break;
2706
+ g_pcap = nc;
2707
+ }
2708
+ if (w->io_fd >= 0) { g_pfds[npf].fd = w->io_fd; g_pfds[npf].events = w->io_events; }
2709
+ else { g_pfds[npf].fd = w->io_set[si].fd; g_pfds[npf].events = w->io_set[si].events; }
2710
+ g_pfds[npf].revents = 0;
2711
+ g_pths[npf] = w; npf++;
2712
+ }
2713
+ }
2714
+ if (g_pcap < 1) { /* ensure room for slot 0 even with no I/O waiters */
2715
+ g_pfds = (struct pollfd *)realloc(g_pfds, sizeof(struct pollfd) * 16);
2716
+ g_pths = (sp_thread **)realloc(g_pths, sizeof(sp_thread *) * 16);
2717
+ if (g_pfds && g_pths) g_pcap = 16;
2718
+ }
2719
+ g_nearest = nearest; /* re-derived from the heap and current I/O waiters */
2720
+ }
2721
+ int have_io = (nio > 0) || (g_io_waiters != NULL);
2722
+ if (nearest == 0.0 && !busy && !have_io) {
2723
+ /* nothing to time or watch: sleep until a thread sleeps / waits on I/O /
2724
+ is picked up by a worker (a registrant signals g_sysmon_cv). */
2725
+ g_sysmon_idle = 1;
2726
+ pthread_cond_wait(&g_sysmon_cv, &g_sched_lock);
2727
+ g_sysmon_idle = 0;
2728
+ }
2729
+ else {
2730
+ /* Poll the parked fds, timing out at the quantum (while preempting), near
2731
+ the nearest sleeper deadline, or 50ms otherwise (poll returns earlier on
2732
+ fd activity or a wake-pipe byte). */
2733
+ double dt = busy ? SP_PREEMPT_TICK : (nearest != 0.0 ? nearest - now : 0.05);
2734
+ if (nearest != 0.0 && nearest - now < dt) dt = nearest - now;
2735
+ if (dt > 0.05) dt = 0.05;
2736
+ if (dt < 0.0005) dt = 0.0005;
2737
+ int tmo = (int)(dt * 1000.0); if (tmo < 1) tmo = 1;
2738
+ #ifdef SP_EV_BACKEND
2739
+ if (g_ev_fd > 0) {
2740
+ /* The descriptors are in the WORKERS' sets now, so the monitor has no
2741
+ fds of its own to watch: it is a timer, for deadlines and the
2742
+ timeslice, and a condvar is the right thing to wait on. Its own wake
2743
+ pipe goes with them -- sp_sysmon_wake signals the condvar. */
2744
+ /* pthread_cond_timedwait's deadline is on CLOCK_REALTIME, and every
2745
+ other clock in this file is CLOCK_MONOTONIC. Handing it a monotonic
2746
+ stamp names a moment decades in the past, so the wait returns at
2747
+ once, every time: the monitor spun 1.5 MILLION turns where it should
2748
+ have taken 4,400, and burned a quarter of a core doing nothing. */
2749
+ struct timespec ts;
2750
+ clock_gettime(CLOCK_REALTIME, &ts);
2751
+ ts.tv_sec += (time_t)dt;
2752
+ ts.tv_nsec += (long)((dt - (double)(time_t)dt) * 1e9);
2753
+ if (ts.tv_nsec >= 1000000000L) { ts.tv_sec++; ts.tv_nsec -= 1000000000L; }
2754
+ SP_STAT_ADD(g_mon_polls, 1);
2755
+ g_sysmon_idle = 1;
2756
+ pthread_cond_timedwait(&g_sysmon_cv, &g_sched_lock, &ts);
2757
+ g_sysmon_idle = 0;
2758
+ continue;
2759
+ }
2760
+ #endif
2761
+ /* Only the poll path needs the array; the degraded wait below is its
2762
+ fallback, not the event path's. Placed after it, the event path was
2763
+ never reached when the array had not been allocated -- which is the
2764
+ case whenever the walks are skipped, so a monitor that started with
2765
+ nothing parked nanosleeps 50ms a turn and delivers nothing. 102 turns
2766
+ of that is the five-second stall this cost. */
2767
+ if (!g_pfds) { /* allocation failed: degrade to a plain timed wait */
2768
+ SCHED_UNLOCK();
2769
+ struct timespec req = { (time_t)dt, (long)((dt - (time_t)dt) * 1e9) };
2770
+ nanosleep(&req, NULL);
2771
+ SCHED_LOCK();
2772
+ continue;
2773
+ }
2774
+ g_pfds[0].fd = g_sysmon_pipe[0]; g_pfds[0].events = POLLIN; g_pfds[0].revents = 0;
2775
+ SCHED_UNLOCK();
2776
+ int pr = poll(g_pfds, (nfds_t)npf, tmo);
2777
+ SCHED_LOCK();
2778
+ SP_STAT_ADD(g_mon_polls, 1); SP_STAT_ADD(g_mon_pollfds, npf);
2779
+ if (g_pfds[0].revents & POLLIN) { /* drain the wake pipe */
2780
+ char buf[64]; while (read(g_sysmon_pipe[0], buf, sizeof buf) > 0) {}
2781
+ }
2782
+ if (pr > 0) {
2783
+ for (int i = 1; i < npf; i++) {
2784
+ if (!g_pfds[i].revents) continue;
2785
+ SP_STAT_ADD(g_mon_readied, 1);
2786
+ sp_thread *t = g_pths[i];
2787
+ if (t->wait_head != &g_io_waiters) continue; /* unparked meanwhile (e.g. #kill) */
2788
+ for (sp_thread **pp = &g_io_waiters; *pp; pp = &(*pp)->wait_next)
2789
+ if (*pp == t) { *pp = t->wait_next; break; }
2790
+ sp_timer_cancel(t);
2791
+ t->wait_next = NULL; t->wait_head = NULL; t->io_revents = g_pfds[i].revents; t->io_fd = -1;
2792
+ if (t == &g_main_thread) { t->state = SP_TH_RUNNABLE; SCHED_WAKE_ALL(); }
2793
+ else if (t->off_cpu) { t->state = SP_TH_RUNNABLE; runq_requeue(t); sp_sched_wake_for(t); }
2794
+ else t->wake_pending = 1;
2795
+ }
2796
+ }
2797
+ }
2798
+ }
2799
+ SCHED_UNLOCK();
2800
+ return NULL;
2801
+ }
2802
+
2803
+ static void sp_sched_unpark(sp_thread *t); /* below */
2804
+
2805
+ /* Kernel#sleep, Thread.stop and Mutex#sleep park here: on g_sleepers, with a
2806
+ timer unless seconds < 0 (until #wakeup). Answers 1 when Thread#wakeup
2807
+ ended the sleep early, 0 when the time ran out, -1 when its timer could
2808
+ not be allocated (the caller raises, after any cleanup of its own). */
2809
+ static int sp_sched_sleep_park(double seconds) {
2810
+ if (!g_sysmon_started) { /* the monitor was not started: plain blocking sleep */
2811
+ if (seconds < 0.0) for (;;) pause(); /* nothing else could wake us */
2812
+ struct timespec req; req.tv_sec = (time_t)seconds;
2813
+ req.tv_nsec = (long)((seconds - (double)req.tv_sec) * 1e9);
2814
+ if (req.tv_nsec < 0) req.tv_nsec = 0; if (req.tv_nsec >= 1000000000L) req.tv_nsec = 999999999L;
2815
+ while (nanosleep(&req, &req) == -1 && errno == EINTR) {}
2816
+ return 0;
2817
+ }
2818
+ SCHED_LOCK();
2819
+ sp_thread *self = g_current;
2820
+ /* Same pending-inject check as sp_sched_block: a #kill/#raise that arrived
2821
+ while we were RUNNING must fire now, not after the full sleep. */
2822
+ if (self != &g_main_thread && self->fiber && sp_fiber_inject_pending(self->fiber)) {
2823
+ SCHED_UNLOCK();
2824
+ sp_fiber_fire_inject_if_pending(); /* raises; does not return */
2825
+ SCHED_LOCK();
2826
+ }
2827
+ if (seconds >= 0.0 && !sp_timer_schedule(self, sp_monotonic_now() + seconds, SP_TIMER_SLEEP)) {
2828
+ SCHED_UNLOCK();
2829
+ return -1;
2830
+ }
2831
+ self->state = SP_TH_BLOCKED;
2832
+ self->off_cpu = 0;
2833
+ self->wake_pending = 0;
2834
+ self->woken = 0;
2835
+ self->wait_next = g_sleepers; self->wait_head = &g_sleepers; g_sleepers = self;
2836
+ int woken;
2837
+ if (self == &g_main_thread) {
2838
+ sp_sched_pump(NULL, 1); /* main waits (and pumps at N=1) until the monitor wakes it */
2839
+ woken = self->woken;
2840
+ SCHED_UNLOCK();
2841
+ }
2842
+ else {
2843
+ /* Same exception-context snapshot as sp_sched_block: the symmetric transfer
2844
+ clobbers our handler stack on resume. */
2845
+ void *exc_snap = sp_exc_ctx_new();
2846
+ sp_exc_ctx_save(exc_snap);
2847
+ SCHED_UNLOCK();
2848
+ sp_sched_switch_out();
2849
+ sp_exc_ctx_load(exc_snap);
2850
+ sp_exc_ctx_free(exc_snap);
2851
+ woken = self->woken;
2852
+ sp_fiber_fire_inject_if_pending(); /* a #kill/#raise delivered while sleeping */
2853
+ }
2854
+ return woken;
2855
+ }
2856
+
2857
+ static void sp_sched_sleep_park_or_raise(double seconds) {
2858
+ if (sp_sched_sleep_park(seconds) < 0) sp_raise_cls("NoMemoryError", "failed to schedule thread timer");
2859
+ }
2860
+
2861
+ void sp_sched_sleep(double seconds) {
2862
+ if (!(seconds > 0.0)) return;
2863
+ sp_sched_sleep_park_or_raise(seconds);
2864
+ }
2865
+
2866
+ void sp_sched_sleep_forever(void) { sp_sched_sleep_park_or_raise(-1.0); }
2867
+
2868
+ void sp_Thread_stop(void) {
2869
+ if (sp_Thread_list_count() <= 1)
2870
+ sp_raise_cls("ThreadError", "stopping only thread\n\tnote: use sleep to stop forever");
2871
+ sp_sched_sleep_park_or_raise(-1.0);
2872
+ }
2873
+
2874
+ /* #wakeup ends a sleep (Kernel#sleep, Thread.stop, Mutex#sleep) early. A thread
2875
+ blocked on anything else is left alone, and one that is running loses the
2876
+ wakeup, as in CRuby. */
2877
+ sp_thread *sp_Thread_wakeup(sp_thread *t) {
2878
+ SCHED_LOCK();
2879
+ if (t->state == SP_TH_DEAD) { SCHED_UNLOCK(); sp_raise_cls("ThreadError", "killed thread"); }
2880
+ if (t->wait_head == &g_sleepers) {
2881
+ t->woken = 1;
2882
+ sp_sched_unpark(t); /* off g_sleepers, timer cancelled */
2883
+ if (t == &g_main_thread) { t->state = SP_TH_RUNNABLE; SCHED_WAKE_ALL(); }
2884
+ else if (t->off_cpu) { t->state = SP_TH_RUNNABLE; runq_requeue(t); sp_sched_wake_for(t); }
2885
+ else t->wake_pending = 1; /* mid-switch: its worker enqueues it */
2886
+ }
2887
+ SCHED_UNLOCK();
2888
+ return t;
2889
+ }
2890
+
2891
+ sp_thread *sp_Thread_run(sp_thread *t) {
2892
+ sp_Thread_wakeup(t);
2893
+ sp_Thread_pass();
2894
+ return t;
2895
+ }
2896
+
2897
+ sp_bool sp_Thread_stop_p(sp_thread *t) {
2898
+ SCHED_LOCK();
2899
+ sp_bool r = t->state == SP_TH_DEAD || t->state == SP_TH_BLOCKED;
2900
+ SCHED_UNLOCK();
2901
+ return r;
2902
+ }
2903
+
2904
+ /* Mutex#sleep: unlock, sleep until the timeout or a #wakeup, lock again.
2905
+ nil when the timeout ran out, else the whole seconds slept. A #kill or
2906
+ #raise waits for the lock too, as CRuby re-locks before it unwinds. */
2907
+ sp_RbVal sp_Mutex_sleep(sp_mutex *m, int has_timeout, double timeout) {
2908
+ if (has_timeout && timeout != timeout) sp_raise_cls("RangeError", "NaN out of Time range");
2909
+ if (has_timeout && timeout < 0.0) sp_raise_cls("ArgumentError", "time interval must not be negative");
2910
+ if (!has_timeout) timeout = -1.0;
2911
+ sp_Mutex_unlock(m); /* raises ThreadError when we don't hold it */
2912
+ double t0 = sp_monotonic_now();
2913
+ /* a #raise or #kill is held until we own the mutex again, which the
2914
+ contended lock path would otherwise fire before we get it */
2915
+ sp_fiber_defer_inject();
2916
+ int woken = sp_sched_sleep_park(timeout);
2917
+ sp_Mutex_lock(m);
2918
+ sp_fiber_undefer_inject();
2919
+ if (woken < 0) sp_raise_cls("NoMemoryError", "failed to schedule thread timer");
2920
+ sp_fiber_fire_inject_if_pending();
2921
+ if (!woken && has_timeout) return sp_box_nil();
2922
+ return sp_box_int((sp_int)(sp_monotonic_now() - t0 + 0.5));
2923
+ }
2924
+
2925
+ int sp_sched_wait_io(int fd, short events) {
2926
+ return sp_sched_wait_io_timeout(fd, events, -1.0);
2927
+ }
2928
+
2929
+ /* One descriptor (set == NULL) or a set of n: the park is the same, the
2930
+ registration is per descriptor. */
2931
+ static int sp_poll_plain(struct pollfd *pp, nfds_t np, double timeout_s) {
2932
+ if (timeout_s >= 0.0) {
2933
+ double until = sp_monotonic_now() + timeout_s;
2934
+ for (;;) {
2935
+ double left = until - sp_monotonic_now();
2936
+ int ms = left > 0.0 ? (int)(left * 1000.0) + 1 : 0;
2937
+ int pr = poll(pp, np, ms);
2938
+ if (pr > 0) return 1; if (pr == 0) return 0; if (errno == EINTR) continue; return 0;
2939
+ }
2940
+ }
2941
+ for (;;) { int pr = poll(pp, np, 1000); if (pr > 0) return 1; if (pr == 0 || errno == EINTR) continue; return 0; }
2942
+ }
2943
+ static int sp_sched_wait_io_impl(int fd, short events, struct pollfd *set, int n, double timeout_s,
2944
+ const unsigned char *cancel) {
2945
+ if ((set ? n <= 0 : fd < 0) || !g_sysmon_started) { /* no monitor: plain blocking poll */
2946
+ struct pollfd pf; pf.fd = fd; pf.events = events; pf.revents = 0;
2947
+ return sp_poll_plain(set ? set : &pf, set ? (nfds_t)n : 1, timeout_s);
2948
+ }
2949
+ SCHED_LOCK();
2950
+ sp_thread *self = g_current;
2951
+ /* Same pending-inject check as sp_sched_block. */
2952
+ if (self != &g_main_thread && self->fiber && sp_fiber_inject_pending(self->fiber)) {
2953
+ SCHED_UNLOCK();
2954
+ sp_fiber_fire_inject_if_pending(); /* raises; does not return */
2955
+ SCHED_LOCK();
2956
+ }
2957
+ /* A close from another thread that landed between the caller's readiness
2958
+ probe and this lock. The handle's closed flag is written before
2959
+ sp_sched_ev_forget takes the lock, so a park that takes it afterwards
2960
+ sees the flag here, and one that took it first is readied by the forget
2961
+ itself. Without this the registration went in after the forget (or the
2962
+ kernel dropped it at the close that followed) and a read with no
2963
+ deadline waited forever: the #4546 test hung on one macOS run in three. */
2964
+ if (cancel && *cancel) { SCHED_UNLOCK(); return 1; }
2965
+ sp_ev_waiter *es = NULL;
2966
+ if (set) {
2967
+ es = (sp_ev_waiter *)malloc(sizeof(sp_ev_waiter) * (size_t)n);
2968
+ if (!es) { SCHED_UNLOCK(); return sp_poll_plain(set, (nfds_t)n, timeout_s); }
2969
+ for (int i = 0; i < n; i++) { es[i].t = self; es[i].idx = i; es[i].next = NULL; }
2970
+ }
2971
+ if (timeout_s >= 0.0 &&
2972
+ !sp_timer_schedule(self, sp_monotonic_now() + timeout_s, SP_TIMER_IO)) {
2973
+ free(es);
2974
+ SCHED_UNLOCK();
2975
+ return sp_poll_plain(set ? set : &(struct pollfd){.fd = fd, .events = events},
2976
+ set ? (nfds_t)n : 1, timeout_s);
2977
+ }
2978
+ if (set) { self->io_fd = -1; self->io_events = 0; self->io_set = set; self->io_nset = n; self->ev_set = es; }
2979
+ else { self->io_fd = fd; self->io_events = events; }
2980
+ self->io_revents = 0;
2981
+ self->state = SP_TH_BLOCKED;
2982
+ self->off_cpu = 0;
2983
+ self->wake_pending = 0;
2984
+ self->wait_next = g_io_waiters; self->wait_head = &g_io_waiters; g_io_waiters = self;
2985
+ SP_STAT_ADD(g_mon_regs, 1);
2986
+ #ifdef SP_EV_BACKEND
2987
+ int registered = 1;
2988
+ if (set) { for (int i = 0; i < n; i++) if (!sp_ev_park_entry(&self->ev_set[i], set[i].fd)) registered = 0; }
2989
+ else registered = sp_ev_park(self, fd);
2990
+ if (registered) {
2991
+ /* Registered: the kernel holds the interest set, so the monitor needs no
2992
+ word about the descriptor -- only about a deadline that moved earlier.
2993
+ That is what takes the self-pipe write off the park path.
2994
+
2995
+ The idle check is NOT part of that saving and must not be folded into
2996
+ it. Timer scheduling signals only when this deadline is the new
2997
+ earliest, and a monitor asleep on its condvar is woken by nothing else:
2998
+ park with a deadline LATER than a stale g_nearest, while it sleeps, and
2999
+ it sleeps through every event that follows. That is a whole-scheduler
3000
+ stall, and it is what the first cut of this did -- 16 of 16 threads
3001
+ waiting out their select timeout, about one run in twenty. */
3002
+ if (g_sysmon_idle) sp_sysmon_wake();
3003
+ }
3004
+ else
3005
+ #endif
3006
+ {
3007
+ sp_sysmon_wake(); /* let the monitor rebuild its poll set */
3008
+ }
3009
+ if (self == &g_main_thread) {
3010
+ sp_sched_pump(NULL, 1); /* main waits (and pumps at N=1) until the monitor wakes it */
3011
+ int rev = self->io_revents; self->io_revents = 0; self->io_fd = -1;
3012
+ if (set) { free(self->ev_set); self->ev_set = NULL; self->io_set = NULL; self->io_nset = 0; }
3013
+ SCHED_UNLOCK();
3014
+ return rev ? 1 : 0;
3015
+ }
3016
+ /* Same exception-context snapshot as sp_sched_block/sleep: the symmetric
3017
+ transfer clobbers our handler stack on resume. */
3018
+ void *exc_snap = sp_exc_ctx_new();
3019
+ sp_exc_ctx_save(exc_snap);
3020
+ SCHED_UNLOCK();
3021
+ sp_sched_switch_out();
3022
+ sp_exc_ctx_load(exc_snap);
3023
+ sp_exc_ctx_free(exc_snap);
3024
+ int rev = self->io_revents; self->io_revents = 0; self->io_fd = -1;
3025
+ /* the set entries are off every list (whoever unparked us dropped them);
3026
+ released before an inject can raise past this frame */
3027
+ if (set) { free(self->ev_set); self->ev_set = NULL; self->io_set = NULL; self->io_nset = 0; }
3028
+ sp_fiber_fire_inject_if_pending(); /* a #kill/#raise delivered while waiting on I/O */
3029
+ return rev ? 1 : 0;
3030
+ }
3031
+ int sp_sched_wait_io_timeout(int fd, short events, double timeout_s) {
3032
+ return sp_sched_wait_io_impl(fd, events, NULL, 0, timeout_s, NULL);
3033
+ }
3034
+ int sp_sched_wait_io_unless(int fd, short events, const unsigned char *cancel) {
3035
+ return sp_sched_wait_io_impl(fd, events, NULL, 0, -1.0, cancel);
3036
+ }
3037
+ int sp_sched_wait_io_set(struct pollfd *set, int n, double timeout_s) {
3038
+ if (n == 1) return sp_sched_wait_io_impl(set[0].fd, set[0].events, NULL, 0, timeout_s, NULL);
3039
+ return sp_sched_wait_io_impl(-1, 0, set, n, timeout_s, NULL);
3040
+ }
3041
+
3042
+ /* A helper worker: adopt its native stack as a per-worker root fiber, then pull
3043
+ runnable green threads off the GRQ forever. It parks at the GC barrier when a
3044
+ collection is in progress and exits when main signals shutdown at drain. */
3045
+ static void *sp_worker_main(void *arg) {
3046
+ int wid = (int)(intptr_t)arg;
3047
+ sp_worker_id = wid; /* TLS: read only by this worker */
3048
+ sp_fiber_worker_init();
3049
+ SCHED_LOCK();
3050
+ g_wslot[wid].tid = pthread_self(); /* publish under the lock; the monitor reads it there */
3051
+ g_wslot[wid].active = 1;
3052
+ for (;;) {
3053
+ if (g_stw_active) { sp_stw_park_locked(); continue; }
3054
+ if (g_shutdown) break;
3055
+ #ifdef SP_EV_BACKEND
3056
+ /* Drain what is ready before picking, every turn. A worker with runnable
3057
+ work never reaches the blocking wait below, and its OWN set is the only
3058
+ place its parked threads are delivered from -- so without this a busy
3059
+ worker starves them until it happens to idle. That showed as a
3060
+ ping-pong that ran at full speed on one worker and fell to a fifth of it
3061
+ on four, run to run. One zero-timeout wait per scheduling turn is what
3062
+ every netpoll scheduler pays for the same reason. */
3063
+ if (g_wslot[wid].evfd > 0) sp_ev_worker_wait(wid, 0);
3064
+ #endif
3065
+ sp_thread *t = sched_pick(wid); /* own queue, then global, then steal */
3066
+ if (t) { run_thread_once(t); continue; } /* run_thread_once signals quiescence on the last one */
3067
+ #ifdef SP_EV_BACKEND
3068
+ /* Nothing to run: wait on THIS worker's readiness set, so a descriptor
3069
+ belonging to a thread pinned here wakes the worker that can run it --
3070
+ no monitor round trip and no condvar hand-off (#4306). The kick pipe in
3071
+ the same set is how a stop-the-world, a shutdown, or work enqueued for
3072
+ us gets through. The timeout is a backstop, not the mechanism. */
3073
+ if (g_wslot[wid].evfd > 0) {
3074
+ sp_ev_worker_wait(wid, SP_EV_BACKSTOP_MS);
3075
+ continue;
3076
+ }
3077
+ #endif
3078
+ /* `idle` is set under the lock the enqueue also holds, so a wake issued
3079
+ between our sched_pick and this wait cannot be lost. */
3080
+ g_wslot[wid].idle = 1;
3081
+ sp_out_enter_locked(wid, SP_OUT_IDLE);
3082
+ pthread_cond_wait(&g_wslot[wid].cv, &g_sched_lock); /* woken for work meant for us, or shutdown */
3083
+ sp_out_leave_locked(wid);
3084
+ g_wslot[wid].idle = 0;
3085
+ }
3086
+ SCHED_UNLOCK();
3087
+ return NULL;
3088
+ }
3089
+
3090
+ #ifndef NSIG
3091
+ #define NSIG 65
3092
+ #endif
3093
+
3094
+ /* Resolve SPINEL_PREEMPT_SIGNAL to the signal the monitor sends. Accepts a number
3095
+ (so real-time signals work, e.g. `kill -l SIGRTMIN`) or a name with or without
3096
+ the SIG prefix (URG/USR1/USR2/IO/WINCH). The default, SIGURG, is chosen because
3097
+ real programs essentially never use it (its nominal job is TCP out-of-band data)
3098
+ and its default disposition is "ignore", so it is safe to repurpose; override it
3099
+ only if the program itself needs SIGURG, or wants a real-time signal. An
3100
+ unrecognized or uncatchable value warns and falls back to SIGURG. */
3101
+ static int sp_resolve_preempt_signal(void) {
3102
+ const char *e = getenv("SPINEL_PREEMPT_SIGNAL");
3103
+ if (!e || !*e) return SIGURG;
3104
+ char *end;
3105
+ long n = strtol(e, &end, 10);
3106
+ if (*end == '\0') {
3107
+ if (n > 0 && n < NSIG) return (int)n;
3108
+ }
3109
+ else {
3110
+ const char *name = e;
3111
+ if (strncasecmp(name, "SIG", 3) == 0) name += 3;
3112
+ static const struct { const char *n; int s; } tab[] = {
3113
+ { "URG", SIGURG }, { "USR1", SIGUSR1 }, { "USR2", SIGUSR2 },
3114
+ { "IO", SIGIO }, { "WINCH", SIGWINCH },
3115
+ };
3116
+ for (size_t i = 0; i < sizeof tab / sizeof tab[0]; i++)
3117
+ if (strcasecmp(name, tab[i].n) == 0) return tab[i].s;
3118
+ }
3119
+ fprintf(stderr, "spinel: ignoring unrecognized SPINEL_PREEMPT_SIGNAL=%s; using SIGURG\n", e);
3120
+ return SIGURG;
3121
+ }
3122
+
3123
+ static int sp_sched_start_workers(void) {
3124
+ /* Spawn the monitor thread (scheduler timers) before any worker. Without it,
3125
+ timed waits cannot be woken by the deadline queue, so do not permit helper
3126
+ workers to run concurrently with the blocking fallback. */
3127
+ /* Fix the helper cap before spawning anything, so the monitor and helpers read
3128
+ it through the pthread_create happens-before edge (no lock needed). Helpers
3129
+ themselves are spawned on demand (sp_sched_maybe_grow), not here -- main
3130
+ stays worker 0 and starts as the only participant (sp_active_workers 1). */
3131
+ g_worker_cap = sp_worker_count();
3132
+ /* main is worker 0 and the last slot belongs to the sweeper threads */
3133
+ if (g_worker_cap > SP_MAX_WORKERS - 2) g_worker_cap = SP_MAX_WORKERS - 2;
3134
+ /* Install the preemption signal handler before any worker can be targeted.
3135
+ SA_RESTART so an in-flight library syscall resumes rather than failing with
3136
+ EINTR -- the yield itself is cooperative (at the next safepoint poll), the
3137
+ signal only nudges the worker there. Resolve and pin g_preempt_sig here,
3138
+ before the monitor reads it through the pthread_create edge. */
3139
+ g_preempt_sig = sp_resolve_preempt_signal();
3140
+ struct sigaction sa; memset(&sa, 0, sizeof sa);
3141
+ sa.sa_handler = sp_preempt_handler;
3142
+ sigemptyset(&sa.sa_mask);
3143
+ sa.sa_flags = SA_RESTART;
3144
+ if (sigaction(g_preempt_sig, &sa, NULL) != 0) { /* uncatchable signal: fall back */
3145
+ fprintf(stderr, "spinel: SPINEL_PREEMPT_SIGNAL=%d cannot be caught; using SIGURG\n", g_preempt_sig);
3146
+ g_preempt_sig = SIGURG;
3147
+ sigaction(g_preempt_sig, &sa, NULL);
3148
+ }
3149
+ /* Self-pipe so a thread registering for sleep/I/O can break the monitor out of
3150
+ poll() immediately. Both ends non-blocking: the writer never stalls on a full
3151
+ pipe (the bytes only signal, they are drained wholesale), the reader drains
3152
+ without blocking. Created before the monitor so it sees valid fds. */
3153
+ if (pipe(g_sysmon_pipe) == 0) {
3154
+ for (int e = 0; e < 2; e++) { int fl = fcntl(g_sysmon_pipe[e], F_GETFL, 0); if (fl >= 0) fcntl(g_sysmon_pipe[e], F_SETFL, fl | O_NONBLOCK); }
3155
+ }
3156
+ else { g_sysmon_pipe[0] = g_sysmon_pipe[1] = -1; }
3157
+ if (pthread_create(&g_sysmon, NULL, sp_sysmon_main, NULL) == 0) {
3158
+ g_sysmon_started = 1;
3159
+ /* SPINEL_SCHED_STATS reports at any end of the program -- `exit`, an
3160
+ uncaught exception -- not only when main returns and drains */
3161
+ atexit(sp_sched_report_stats);
3162
+ return 1;
3163
+ }
3164
+ if (g_sysmon_pipe[0] >= 0) close(g_sysmon_pipe[0]);
3165
+ if (g_sysmon_pipe[1] >= 0) close(g_sysmon_pipe[1]);
3166
+ g_sysmon_pipe[0] = g_sysmon_pipe[1] = -1;
3167
+ return 0;
3168
+ }
3169
+
3170
+ /* Create one helper worker (next id). PRE: g_sched_lock held, below the cap, and
3171
+ no collection in progress (sp_sched_maybe_grow enforces the last two). The id
3172
+ count (sp_active_workers) is bumped only after pthread_create succeeds, so the
3173
+ STW barrier never waits on a worker that failed to start. */
3174
+ static void sp_sched_spawn_helper(void) {
3175
+ int wid = g_helpers_spawned + 1;
3176
+ if (wid > g_worker_cap || wid >= SP_MAX_WORKERS) return;
3177
+ g_wslot[wid].idle = 0;
3178
+ if (pthread_cond_init(&g_wslot[wid].cv, NULL) != 0) return;
3179
+ if (pthread_create(&g_worker_threads[wid], NULL, sp_worker_main, (void *)(intptr_t)wid) != 0) return;
3180
+ g_helpers_spawned = wid;
3181
+ sp_active_workers = g_helpers_spawned + 1; /* participants: main (0) + helpers */
3182
+ }
3183
+
3184
+ /* Grow the helper pool toward one execution worker per live green thread, capped
3185
+ at g_worker_cap. Called under the lock wherever a thread becomes runnable; a
3186
+ no-op once at the cap or while a collection is reading the participant count
3187
+ (we never change it mid-STW). This turns "SPINEL_WORKERS=N" into up to N real
3188
+ parallel execution threads while never starting a helper the workload has no
3189
+ runnable thread for -- a lone background thread brings up exactly one. */
3190
+ static void sp_sched_maybe_grow(void) {
3191
+ if (!g_workers_started || g_stw_active) return;
3192
+ int live = g_runnable + g_nrunning;
3193
+ int want = live < g_worker_cap ? live : g_worker_cap;
3194
+ while (g_helpers_spawned < want) {
3195
+ int before = g_helpers_spawned;
3196
+ sp_sched_spawn_helper();
3197
+ if (g_helpers_spawned == before) break; /* spawn failed: stop, main will cope */
3198
+ }
3199
+ }
3200
+ #endif
3201
+
3202
+ /* SPINEL_SCHED_STATS=1: what the monitor did, on the way out. A wake costs
3203
+ O(parked) three times over, so what sizes a deployment is how OFTEN the
3204
+ monitor turned and how BIG its poll set was each time -- and neither is
3205
+ visible from outside the process, which is what left a 6x gap between a
3206
+ standalone and a real server unexplained (#4317). */
3207
+ static void sp_sched_report_stats(void) {
3208
+ #ifdef SP_THREADS
3209
+ /* main's drain and the atexit hook both call it. From the hook (an
3210
+ `exit` before the drain) the monitor may still be running, so the
3211
+ counters are a best-effort snapshot -- read without the lock, which the
3212
+ exiting code may hold. */
3213
+ static int reported = 0;
3214
+ const char *e = getenv("SPINEL_SCHED_STATS");
3215
+ if (reported || !e || !*e || *e == '0') return;
3216
+ reported = 1;
3217
+ unsigned long long iters = SP_STAT_GET(g_mon_iters), polls = SP_STAT_GET(g_mon_polls),
3218
+ pollfds = SP_STAT_GET(g_mon_pollfds), regs = SP_STAT_GET(g_mon_regs),
3219
+ readied = SP_STAT_GET(g_mon_readied);
3220
+ double avg = polls ? (double)pollfds / (double)polls : 0.0;
3221
+ fprintf(stderr,
3222
+ "[sched] monitor: %llu turns, %llu polls, %.1f fds/poll avg, "
3223
+ "%llu fds total; %llu io parks registered, %llu waiters readied\n",
3224
+ iters, polls, avg, pollfds, regs, readied);
3225
+ #ifdef SP_EV_BACKEND
3226
+ if (g_ev_fd >= 0 || SP_STAT_GET(g_ev_arms))
3227
+ fprintf(stderr, "[sched] events: %llu arms (%llu adds), %llu refused, "
3228
+ "%llu deadline expiries, %llu backstop expiries\n",
3229
+ SP_STAT_GET(g_ev_arms), SP_STAT_GET(g_ev_adds), SP_STAT_GET(g_ev_lost),
3230
+ SP_STAT_GET(g_ev_timeouts), SP_STAT_GET(g_ev_backstop));
3231
+ #endif
3232
+ #endif
3233
+ }
3234
+
3235
+ void sp_sched_drain(void) {
3236
+ /* main() is finishing: run remaining runnable threads so fire-and-forget side
3237
+ effects happen, then shut the helper workers down. Only the main thread
3238
+ drains. pump(NULL, 1) returns once the queue is empty and every helper is
3239
+ idle -- i.e. all runnable work is done (at N=1 it returns on an empty queue
3240
+ exactly as before). */
3241
+ if (g_current != &g_main_thread) return;
3242
+ SCHED_LOCK();
3243
+ sp_sched_pump(NULL, 2); /* exit drain: runnable work only, not sleepers */
3244
+ #ifdef SP_THREADS
3245
+ /* stored atomically: the sweeper and the trim thread read it without the
3246
+ sched lock (the workers and the monitor read it under it) */
3247
+ SP_ATOMIC_STORE(&g_shutdown, 1, __ATOMIC_RELAXED);
3248
+ sched_wake_all_workers(0);
3249
+ sp_sysmon_wake(); /* wake the monitor (idle or in poll) so it sees shutdown */
3250
+ int sysmon_running = g_sysmon_started;
3251
+ SCHED_UNLOCK();
3252
+ for (int i = 1; i < sp_active_workers; i++) pthread_join(g_worker_threads[i], NULL);
3253
+ if (sysmon_running) pthread_join(g_sysmon, NULL);
3254
+ sp_sched_report_stats();
3255
+ return;
3256
+ #endif
3257
+ SCHED_UNLOCK();
3258
+ }
3259
+
3260
+ /* ---- generic park / wake on a primitive wait list ---- */
3261
+
3262
+ /* Block the current green thread on `*waitlist` until a wake moves it back to
3263
+ runnable. The main thread pumps the scheduler (it cannot transfer away from
3264
+ root); a spawned thread transfers back to the scheduler hub. `defer_inject`
3265
+ keeps a condition-wait interruption pending until its caller reacquires m. */
3266
+ static void sp_sched_block(sp_thread **waitlist, int defer_inject) { /* PRE/POST: sched lock held */
3267
+ sp_thread *self = g_current;
3268
+ /* A #kill/#raise delivered while this thread was RUNNING left its inject
3269
+ pending on the fiber (sp_thread_deliver found no wait list to unpark).
3270
+ Parking now would sleep through it forever -- nobody will wake us. Fire it
3271
+ here instead (it raises out of the blocking call, ensures running). The
3272
+ deliver and this check both run under the sched lock, so no window. */
3273
+ if (self != &g_main_thread && self->fiber && sp_fiber_inject_pending(self->fiber)) {
3274
+ SCHED_UNLOCK();
3275
+ sp_fiber_fire_inject_if_pending(); /* raises; does not return */
3276
+ SCHED_LOCK();
3277
+ }
3278
+ self->state = SP_TH_BLOCKED;
3279
+ self->off_cpu = 0; /* still on-cpu until our worker confirms the switch-out */
3280
+ self->wake_pending = 0;
3281
+ /* Append at the TAIL: sp_sched_wake_one takes the head, so the list is a
3282
+ FIFO and a waiter is served in the order it arrived. Pushed at the head
3283
+ it was a LIFO -- the newest waiter woke first -- and under sustained
3284
+ contention an early waiter never reached the front: campfire's DB-pool
3285
+ condvar and fragment-cache mutexes held requests for seconds at p99 (a
3286
+ 2.9 s mutex wait, an 8 s condvar wait at 64 connections) while the median
3287
+ was 4 ms. The walk is O(waiters) under the scheduler lock, which is what
3288
+ one park already costs. */
3289
+ self->wait_head = waitlist; /* so #kill/#raise can unlink it */
3290
+ if (self->repark_front) { /* a mutex waiter that lost the race after its wake: keep its turn */
3291
+ self->repark_front = 0;
3292
+ self->wait_next = *waitlist; *waitlist = self;
3293
+ }
3294
+ else {
3295
+ self->wait_next = NULL;
3296
+ sp_thread **pp = waitlist; while (*pp) pp = &(*pp)->wait_next; *pp = self;
3297
+ }
3298
+ if (self == &g_main_thread) {
3299
+ sp_sched_pump(NULL, 1); /* returns (lock held) once a waker marks main RUNNABLE */
3300
+ if (self->state != SP_TH_RUNNING) {
3301
+ SCHED_UNLOCK();
3302
+ sp_raise_cls("ThreadError", "deadlock detected: all threads blocked");
3303
+ }
3304
+ }
3305
+ else {
3306
+ /* The symmetric fiber transfer's exc bookkeeping clobbers this thread's
3307
+ handler stack when root resumes from the block, so snapshot it here and
3308
+ restore it on wake -- otherwise a #raise/#kill delivered while blocked
3309
+ would find an empty handler stack and escape unhandled. */
3310
+ void *exc_snap = sp_exc_ctx_new();
3311
+ sp_exc_ctx_save(exc_snap);
3312
+ if (defer_inject) sp_fiber_defer_inject();
3313
+ SCHED_UNLOCK(); /* drop the lock across the transfer (we run no metadata while parked) */
3314
+ sp_sched_switch_out();
3315
+ sp_exc_ctx_load(exc_snap);
3316
+ sp_exc_ctx_free(exc_snap);
3317
+ /* resumed: a pending #kill/#raise fires here, in this thread's context (lock
3318
+ not held), so its ensure/rescue blocks unwind on its own stack. On the
3319
+ normal resume we re-take the lock so the caller continues holding it. */
3320
+ sp_fiber_fire_inject_if_pending();
3321
+ SCHED_LOCK();
3322
+ }
3323
+ }
3324
+
3325
+ #ifdef SP_THREADS
3326
+ /* A timed wait reserves its timer and publishes the waiter under the scheduler
3327
+ lock, preserving wake-vs-park atomicity. If mutex is non-NULL, it is checked
3328
+ and released as part of parking (ConditionVariable#wait); interruption stays
3329
+ pending until the caller reacquires it. Other waiters such as Queue#pop pass
3330
+ NULL. Returns -1 if timer storage could not be allocated,
3331
+ 0 if the deadline expired, or 1 if another event woke the thread. The caller
3332
+ holds the scheduler lock on return. */
3333
+ static int sp_sched_block_timeout(sp_thread **waitlist, double deadline, sp_mutex *mutex) {
3334
+ sp_thread *self = g_current;
3335
+ if (mutex && SP_ATOMIC_LOAD(&mutex->owner, __ATOMIC_SEQ_CST) != self) {
3336
+ SCHED_UNLOCK();
3337
+ sp_raise_cls("ThreadError", "Attempt to unlock a mutex which is not locked");
3338
+ }
3339
+ if (self != &g_main_thread && self->fiber && sp_fiber_inject_pending(self->fiber)) {
3340
+ SCHED_UNLOCK();
3341
+ sp_fiber_fire_inject_if_pending();
3342
+ SCHED_LOCK();
3343
+ }
3344
+ self->timer_expired = 0;
3345
+ if (!sp_timer_schedule(self, deadline, SP_TIMER_WAIT)) return -1;
3346
+
3347
+ if (mutex) {
3348
+ SP_ATOMIC_STORE(&mutex->owner, NULL, __ATOMIC_SEQ_CST);
3349
+ if (mutex->waiters) sp_sched_wake_one(&mutex->waiters);
3350
+ }
3351
+ self->state = SP_TH_BLOCKED;
3352
+ self->off_cpu = 0;
3353
+ self->wake_pending = 0;
3354
+ self->wait_head = waitlist;
3355
+ if (self->repark_front) {
3356
+ self->repark_front = 0;
3357
+ self->wait_next = *waitlist;
3358
+ *waitlist = self;
3359
+ }
3360
+ else {
3361
+ self->wait_next = NULL;
3362
+ sp_thread **pp = waitlist;
3363
+ while (*pp) pp = &(*pp)->wait_next;
3364
+ *pp = self;
3365
+ }
3366
+ if (self == &g_main_thread) {
3367
+ sp_sched_pump(NULL, 1);
3368
+ if (self->state != SP_TH_RUNNING) {
3369
+ SCHED_UNLOCK();
3370
+ sp_raise_cls("ThreadError", "deadlock detected: all threads blocked");
3371
+ }
3372
+ }
3373
+ else {
3374
+ void *exc_snap = sp_exc_ctx_new();
3375
+ sp_exc_ctx_save(exc_snap);
3376
+ if (mutex) sp_fiber_defer_inject();
3377
+ SCHED_UNLOCK();
3378
+ sp_sched_switch_out();
3379
+ sp_exc_ctx_load(exc_snap);
3380
+ sp_exc_ctx_free(exc_snap);
3381
+ if (!mutex) sp_fiber_fire_inject_if_pending();
3382
+ SCHED_LOCK();
3383
+ }
3384
+ return self->timer_expired ? 0 : 1;
3385
+ }
3386
+ #endif
3387
+
3388
+ /* Move one thread off `*waitlist` back onto the run queue (or mark the main
3389
+ thread runnable so its pump returns). Returns the woken thread, or NULL. */
3390
+ static sp_thread *sp_sched_wake_one(sp_thread **waitlist) {
3391
+ sp_thread *t = *waitlist;
3392
+ if (!t) return NULL;
3393
+ *waitlist = t->wait_next;
3394
+ #ifdef SP_THREADS
3395
+ sp_timer_cancel(t);
3396
+ #endif
3397
+ t->wait_next = NULL;
3398
+ t->wait_head = NULL;
3399
+ if (t == &g_main_thread) { t->state = SP_TH_RUNNABLE; SCHED_WAKE_ALL(); return t; } /* broadcast: a signal could wake a helper instead of main */
3400
+ if (t->off_cpu) { t->state = SP_TH_RUNNABLE; runq_requeue(t); } /* fully parked: enqueue now */
3401
+ else { t->wake_pending = 1; } /* still switching out: its worker enqueues it once off-cpu */
3402
+ sp_sched_wake_for(t); /* wake an idle worker to run it (all of them if t is pinned) */
3403
+ return t;
3404
+ }
3405
+
3406
+ /* Remove a parked thread from whatever wait list it sits on (for #kill/#raise). */
3407
+ static void sp_sched_unpark(sp_thread *t) {
3408
+ if (!t->wait_head) return;
3409
+ #ifdef SP_THREADS
3410
+ sp_timer_cancel(t);
3411
+ #endif
3412
+ #ifdef SP_EV_BACKEND
3413
+ if (t->wait_head == &g_io_waiters) sp_ev_drop(t);
3414
+ #endif
3415
+ for (sp_thread **pp = t->wait_head; *pp; pp = &(*pp)->wait_next)
3416
+ if (*pp == t) { *pp = t->wait_next; break; }
3417
+ t->wait_next = NULL;
3418
+ t->wait_head = NULL;
3419
+ }
3420
+
3421
+ #ifdef SP_THREADS
3422
+ static void sp_sched_timer_expire(sp_sched_timer *timer, double now) {
3423
+ (void)now;
3424
+ sp_thread *t = timer->thread;
3425
+ if (!t->wait_head) return;
3426
+ if (timer->kind == SP_TIMER_SLEEP && t->wait_head != &g_sleepers) return;
3427
+ if (timer->kind == SP_TIMER_IO && t->wait_head != &g_io_waiters) return;
3428
+
3429
+ if (timer->kind == SP_TIMER_WAIT) t->timer_expired = 1;
3430
+ if (timer->kind == SP_TIMER_IO) {
3431
+ #ifdef SP_EV_BACKEND
3432
+ SP_STAT_ADD(g_ev_timeouts, 1);
3433
+ #endif
3434
+ }
3435
+ sp_sched_unpark(t);
3436
+ if (timer->kind == SP_TIMER_IO) {
3437
+ t->io_revents = 0;
3438
+ t->io_fd = -1;
3439
+ }
3440
+ if (t == &g_main_thread) {
3441
+ t->state = SP_TH_RUNNABLE;
3442
+ SCHED_WAKE_ALL();
3443
+ }
3444
+ else if (t->off_cpu) {
3445
+ t->state = SP_TH_RUNNABLE;
3446
+ runq_requeue(t);
3447
+ sp_sched_wake_for(t);
3448
+ }
3449
+ else {
3450
+ t->wake_pending = 1;
3451
+ }
3452
+ }
3453
+ #endif
3454
+
3455
+ /* #kill / #raise: deliver an inject to the target so it terminates (running its
3456
+ ensures) or raises. The current thread acts on itself immediately; another
3457
+ thread gets the inject queued on its fiber and is made runnable, so the inject
3458
+ fires when the scheduler next runs it -- at body entry (never-run) or right
3459
+ after it unblocks (sp_sched_block / Thread.pass). */
3460
+ static void sp_thread_deliver(sp_thread *t, int is_kill,
3461
+ const char *cls, const char *msg, void *obj) {
3462
+ if (t == g_current) {
3463
+ if (t == &g_main_thread) return; /* killing the main thread from itself: unsupported, no-op */
3464
+ if (is_kill) sp_fiber_raise_kill_self(); /* noreturn */
3465
+ sp_fiber_reraise(cls, msg, obj); /* noreturn */
3466
+ }
3467
+ SCHED_LOCK();
3468
+ if (t->state == SP_TH_DEAD) { SCHED_UNLOCK(); return; } /* raced with its death: no-op */
3469
+ /* #kill/#raise on the main thread from a sibling: main runs on the root
3470
+ fiber (t->fiber == NULL), which has no inject delivery points, so this is
3471
+ unsupported -- no-op rather than a NULL deref, matching the self-kill case
3472
+ above. (CRuby delivers to main; see the Thread row in docs/limitations.md.) */
3473
+ if (t == &g_main_thread || !t->fiber) { SCHED_UNLOCK(); return; }
3474
+ if (is_kill) sp_fiber_set_kill_inject(t->fiber);
3475
+ else sp_fiber_set_raise_inject(t->fiber, cls, msg, obj);
3476
+ if (t->state == SP_TH_BLOCKED) {
3477
+ sp_sched_unpark(t);
3478
+ /* runq_requeue, not rq_push: a STARTED thread is pinned to its home
3479
+ worker's TLS (home_wid) and must not resume on another worker. */
3480
+ if (t->off_cpu) { t->state = SP_TH_RUNNABLE; runq_requeue(t); sp_sched_wake_for(t); }
3481
+ else { t->wake_pending = 1; } /* mid-switch: its worker enqueues it (see run_thread_once) */
3482
+ }
3483
+ /* RUNNABLE threads are already queued; the inject fires when they run. */
3484
+ SCHED_UNLOCK();
3485
+ }
3486
+
3487
+ /* The DEAD check lives inside sp_thread_deliver, under the sched lock: t may be
3488
+ running on another worker and die concurrently, so an unlocked read here
3489
+ would race with run_thread_once's state write. */
3490
+ sp_thread *sp_Thread_kill(sp_thread *t) {
3491
+ sp_thread_deliver(t, 1, NULL, NULL, NULL);
3492
+ return t;
3493
+ }
3494
+ sp_thread *sp_Thread_raise(sp_thread *t, const char *cls, const char *msg, void *obj) {SP_GC_ROOT_STR(msg);
3495
+ sp_thread_deliver(t, 0, cls, msg, obj);
3496
+ return t;
3497
+ }
3498
+
3499
+ /* ---- Queue ---- */
3500
+
3501
+ static void sp_queue_scan(void *p) {
3502
+ sp_queue *q = (sp_queue *)p;
3503
+ for (sp_int i = 0; i < q->len; i++)
3504
+ sp_mark_rbval(q->buf[(q->head + i) % q->cap]);
3505
+ }
3506
+ static void sp_queue_fin(void *p) { sp_queue *q = (sp_queue *)p; free(q->buf); }
3507
+
3508
+ sp_queue *sp_Queue_new(void) {
3509
+ sp_queue *q = (sp_queue *)sp_gc_alloc(sizeof(sp_queue), sp_queue_fin, sp_queue_scan);
3510
+ q->cap = 8;
3511
+ q->head = q->len = 0;
3512
+ q->max = 0;
3513
+ q->pop_waiters = NULL;
3514
+ q->push_waiters = NULL;
3515
+ q->closed = 0;
3516
+ q->buf = (sp_RbVal *)malloc(sizeof(sp_RbVal) * q->cap);
3517
+ if (!q->buf) sp_raise_cls("NoMemoryError", "failed to allocate queue");
3518
+ return q;
3519
+ }
3520
+
3521
+ sp_queue *sp_SizedQueue_new(sp_int max) {
3522
+ if (max <= 0) sp_raise_cls("ArgumentError", "queue size must be positive");
3523
+ sp_queue *q = sp_Queue_new();
3524
+ q->max = max;
3525
+ return q;
3526
+ }
3527
+
3528
+ void sp_Queue_push_options_check(sp_queue *q, int has_non_block, int has_timeout) {
3529
+ if (q->max > 0) return;
3530
+ /* CRuby's Queue#push takes one argument; a timeout: hash counts as one more. */
3531
+ if (has_non_block && has_timeout) sp_raise_cls("ArgumentError", "wrong number of arguments (given 3, expected 1)");
3532
+ if (has_non_block || has_timeout) sp_raise_cls("ArgumentError", "wrong number of arguments (given 2, expected 1)");
3533
+ }
3534
+
3535
+ static int sp_queue_push_impl(sp_queue *q, sp_RbVal v, int non_block,
3536
+ int timed, double seconds) { sp_gc_wb((void*)q);
3537
+ /* On a full SizedQueue, block until a #pop frees a slot. Root v across the
3538
+ block: it lives in this (possibly suspended) frame, and the parking
3539
+ thread's saved roots only cover the shadow stack. */
3540
+ SP_GC_ROOT_RBVAL(v);
3541
+ SP_GC_ROOT(q);
3542
+ /* A negative timeout is already expired (CRuby): no ArgumentError. */
3543
+ double deadline = timed ? sp_monotonic_now() + seconds : 0.0;
3544
+ SCHED_LOCK();
3545
+ for (;;) {
3546
+ if (q->closed) { SCHED_UNLOCK(); sp_raise_cls("ClosedQueueError", "queue closed"); }
3547
+ if (q->max <= 0 || q->len < q->max) break;
3548
+ if (non_block) { SCHED_UNLOCK(); sp_raise_cls("ThreadError", "queue full"); }
3549
+ if (timed && !(seconds > 0.0)) { SCHED_UNLOCK(); return 0; }
3550
+ if (timed) {
3551
+ #ifdef SP_THREADS
3552
+ if (!g_sysmon_started) {
3553
+ /* Before the first Thread, no green thread can free a slot. There is
3554
+ no monitor to fire a timer, so wait directly and check the queue. */
3555
+ SCHED_UNLOCK();
3556
+ double remaining = deadline - sp_monotonic_now();
3557
+ if (remaining > 0.0) sp_sched_sleep(remaining);
3558
+ SCHED_LOCK();
3559
+ if (q->max > 0 && q->len >= q->max) { SCHED_UNLOCK(); return 0; }
3560
+ }
3561
+ else {
3562
+ int woken = sp_sched_block_timeout(&q->push_waiters, deadline, NULL);
3563
+ if (woken < 0) {
3564
+ SCHED_UNLOCK();
3565
+ sp_raise_cls("NoMemoryError", "failed to schedule queue timeout");
3566
+ }
3567
+ if (!woken) { SCHED_UNLOCK(); return 0; }
3568
+ }
3569
+ #else
3570
+ SCHED_UNLOCK();
3571
+ double remaining = deadline - sp_monotonic_now();
3572
+ if (remaining > 0.0) sp_sleep((sp_float)remaining);
3573
+ SCHED_LOCK();
3574
+ if (q->max > 0 && q->len >= q->max) { SCHED_UNLOCK(); return 0; }
3575
+ #endif
3576
+ }
3577
+ else sp_sched_block(&q->push_waiters, 0); /* releases+reacquires lock around transfer */
3578
+ }
3579
+ if (q->len == q->cap) {
3580
+ sp_int nc = q->cap * 2;
3581
+ sp_RbVal *nb = (sp_RbVal *)malloc(sizeof(sp_RbVal) * nc);
3582
+ if (!nb) { SCHED_UNLOCK(); sp_raise_cls("NoMemoryError", "failed to grow queue"); }
3583
+ for (sp_int i = 0; i < q->len; i++) nb[i] = q->buf[(q->head + i) % q->cap];
3584
+ free(q->buf);
3585
+ q->buf = nb; q->cap = nc; q->head = 0;
3586
+ }
3587
+ q->buf[(q->head + q->len) % q->cap] = v;
3588
+ q->len++;
3589
+ sp_sched_wake_one(&q->pop_waiters); /* hand the new value to a waiting popper */
3590
+ SCHED_UNLOCK();
3591
+ return 1;
3592
+ }
3593
+
3594
+ void sp_Queue_push(sp_queue *q, sp_RbVal v) {
3595
+ (void)sp_queue_push_impl(q, v, 0, 0, 0.0);
3596
+ }
3597
+
3598
+ void sp_Queue_push_nb(sp_queue *q, sp_RbVal v) {
3599
+ (void)sp_queue_push_impl(q, v, 1, 0, 0.0);
3600
+ }
3601
+
3602
+ sp_bool sp_Queue_push_timeout(sp_queue *q, sp_RbVal v, double seconds) {
3603
+ return sp_queue_push_impl(q, v, 0, 1, seconds);
3604
+ }
3605
+
3606
+ sp_RbVal sp_Queue_pop(sp_queue *q) {
3607
+ /* Block until an element is available. A closed, drained queue returns nil
3608
+ rather than blocking forever (CRuby behaviour). */
3609
+ SP_GC_ROOT(q);
3610
+ SCHED_LOCK();
3611
+ while (q->len == 0) {
3612
+ if (q->closed) { SCHED_UNLOCK(); return sp_box_nil(); }
3613
+ sp_sched_block(&q->pop_waiters, 0);
3614
+ }
3615
+ sp_RbVal v = q->buf[q->head];
3616
+ q->head = (q->head + 1) % q->cap;
3617
+ q->len--;
3618
+ if (q->max > 0) sp_sched_wake_one(&q->push_waiters); /* a slot freed up */
3619
+ SCHED_UNLOCK();
3620
+ return v;
3621
+ }
3622
+
3623
+ sp_RbVal sp_Queue_pop_timeout(sp_queue *q, double seconds) {
3624
+ SP_GC_ROOT(q);
3625
+ /* A negative timeout is already expired (CRuby): no ArgumentError. */
3626
+ double deadline = sp_monotonic_now() + seconds;
3627
+ SCHED_LOCK();
3628
+ while (q->len == 0) {
3629
+ if (q->closed || !(seconds > 0.0)) { SCHED_UNLOCK(); return sp_box_nil(); }
3630
+ #ifdef SP_THREADS
3631
+ if (!g_sysmon_started) {
3632
+ /* Before the first Thread, no green thread can add an item. */
3633
+ SCHED_UNLOCK();
3634
+ double remaining = deadline - sp_monotonic_now();
3635
+ if (remaining > 0.0) sp_sched_sleep(remaining);
3636
+ SCHED_LOCK();
3637
+ if (q->len == 0) { SCHED_UNLOCK(); return sp_box_nil(); }
3638
+ }
3639
+ else {
3640
+ int woken = sp_sched_block_timeout(&q->pop_waiters, deadline, NULL);
3641
+ if (woken < 0) {
3642
+ SCHED_UNLOCK();
3643
+ sp_raise_cls("NoMemoryError", "failed to schedule queue timeout");
3644
+ }
3645
+ if (!woken) { SCHED_UNLOCK(); return sp_box_nil(); }
3646
+ }
3647
+ #else
3648
+ SCHED_UNLOCK();
3649
+ double remaining = deadline - sp_monotonic_now();
3650
+ if (remaining > 0.0) sp_sleep((sp_float)remaining);
3651
+ SCHED_LOCK();
3652
+ if (q->len == 0) { SCHED_UNLOCK(); return sp_box_nil(); }
3653
+ #endif
3654
+ }
3655
+ sp_RbVal v = q->buf[q->head];
3656
+ q->head = (q->head + 1) % q->cap;
3657
+ q->len--;
3658
+ if (q->max > 0) sp_sched_wake_one(&q->push_waiters);
3659
+ SCHED_UNLOCK();
3660
+ return v;
3661
+ }
3662
+
3663
+ sp_RbVal sp_Queue_pop_nb(sp_queue *q) {
3664
+ /* Queue#pop(truthy): no_wait. Raise ThreadError on an empty queue, return
3665
+ the value otherwise. A closed queue returns nil. */
3666
+ SCHED_LOCK();
3667
+ if (q->len == 0) {
3668
+ if (q->closed) { SCHED_UNLOCK(); return sp_box_nil(); }
3669
+ SCHED_UNLOCK();
3670
+ sp_raise_cls("ThreadError", "queue empty");
3671
+ }
3672
+ sp_RbVal v = q->buf[q->head];
3673
+ q->head = (q->head + 1) % q->cap;
3674
+ q->len--;
3675
+ if (q->max > 0) sp_sched_wake_one(&q->push_waiters);
3676
+ SCHED_UNLOCK();
3677
+ return v;
3678
+ }
3679
+
3680
+ sp_int sp_Queue_size(sp_queue *q) { SCHED_LOCK(); sp_int n = q->len; SCHED_UNLOCK(); return n; }
3681
+ sp_bool sp_Queue_empty(sp_queue *q) { SCHED_LOCK(); sp_bool e = q->len == 0; SCHED_UNLOCK(); return e; }
3682
+ sp_int sp_Queue_max(sp_queue *q) { SCHED_LOCK(); sp_int m = q->max; SCHED_UNLOCK(); return m; }
3683
+ sp_bool sp_Queue_closed(sp_queue *q) { SCHED_LOCK(); sp_bool c = q->closed != 0; SCHED_UNLOCK(); return c; }
3684
+ /* Threads parked in #pop, plus in #push on a full SizedQueue. */
3685
+ sp_int sp_Queue_num_waiting(sp_queue *q) {
3686
+ SCHED_LOCK();
3687
+ sp_int n = 0;
3688
+ for (sp_thread *t = q->pop_waiters; t; t = t->wait_next) n++;
3689
+ for (sp_thread *t = q->push_waiters; t; t = t->wait_next) n++;
3690
+ SCHED_UNLOCK();
3691
+ return n;
3692
+ }
3693
+ void sp_Queue_clear(sp_queue *q) {
3694
+ SCHED_LOCK();
3695
+ q->head = q->len = 0;
3696
+ if (q->max > 0) while (sp_sched_wake_one(&q->push_waiters)) { } /* all slots free */
3697
+ SCHED_UNLOCK();
3698
+ }
3699
+
3700
+ void sp_Queue_close(sp_queue *q) {
3701
+ SCHED_LOCK();
3702
+ q->closed = 1;
3703
+ /* wake every blocked popper (they return nil) and pusher (they raise) */
3704
+ while (sp_sched_wake_one(&q->pop_waiters)) { }
3705
+ while (sp_sched_wake_one(&q->push_waiters)) { }
3706
+ SCHED_UNLOCK();
3707
+ }
3708
+
3709
+ /* ---- Mutex ----
3710
+ * A non-recursive lock. At N=1 there is no preemption, so a Mutex only matters
3711
+ * across a yield: a green thread holding the lock blocks (Queue/CondVar/IO) and
3712
+ * another tries to acquire it. unlock hands ownership directly to the next
3713
+ * waiter, so the wakeup is a clean transfer (no re-contention). */
3714
+
3715
+ sp_mutex *sp_Mutex_new(void) {
3716
+ sp_mutex *m = (sp_mutex *)sp_gc_alloc(sizeof(sp_mutex), NULL, NULL);
3717
+ m->owner = NULL;
3718
+ m->waiters = NULL;
3719
+ m->nwaiters = 0;
3720
+ return m;
3721
+ }
3722
+
3723
+ sp_mutex *sp_Monitor_new(void) {
3724
+ sp_mutex *m = sp_Mutex_new();
3725
+ if (m) m->reentrant = 1;
3726
+ return m;
3727
+ }
3728
+
3729
+ const char *sp_Mutex_class_name(sp_mutex *m) {
3730
+ return (m && m->reentrant) ? "Monitor" : "Thread::Mutex";
3731
+ }/* A bounded queue is a SizedQueue; an unbounded one is a Queue. One object
3732
+ backs both, so the class name reads the bound rather than a separate tag. */
3733
+ const char *sp_Queue_class_name(sp_queue *q) {
3734
+ return (q && q->max > 0) ? "Thread::SizedQueue" : "Thread::Queue";
3735
+ }
3736
+
3737
+
3738
+ /* An UNCONTENDED lock/unlock does not touch the global scheduler lock.
3739
+ *
3740
+ * Both entries used to take it unconditionally, so every Ruby-level Mutex
3741
+ * operation was a round trip through the one lock that also guards every run
3742
+ * queue. A server taking a single lock per request stopped scaling at two OS
3743
+ * workers: 12 workers served no more than 2 did, on 1.5 cores, with two of
3744
+ * them holding most of the CPU (#4346). At one worker the same lock is free,
3745
+ * which is the signature of contention rather than cost.
3746
+ *
3747
+ * The waiter list still lives under the scheduler lock, since parking and
3748
+ * waking are its business. What the fast paths need is agreement about
3749
+ * whether anyone is parked, and that is `nwaiters`. The two sides publish in
3750
+ * opposite orders -- a waiter counts itself and then re-reads `owner`, an
3751
+ * unlocker clears `owner` and then re-reads `nwaiters` -- so with sequential
3752
+ * consistency at least one of them observes the other and no wake is lost. */
3753
+ /* A contended lock spins briefly before it parks. The slow path below is a
3754
+ scheduler-lock round trip to park plus another to be woken, and the wake
3755
+ itself is a scheduling latency (the [mutex-wait] histogram put the median
3756
+ contended acquisition at 64-128 us). A critical section guarding a hash
3757
+ read holds the mutex for a microsecond, so an owner that is RUNNING on
3758
+ another worker will let go long before a park would complete; wait for it
3759
+ in place, for a bounded number of pauses. The spin stops as soon as the
3760
+ owner is not on a CPU: an owner parked on I/O or preempted inside the
3761
+ section will not release it soon, and the only thread running on THIS
3762
+ worker is us, so a running owner is by construction elsewhere. campfire's
3763
+ messages page at 64 connections parked 58k times a second on its
3764
+ fragment-cache shards (3,200 req/s); spinning first restores most of the
3765
+ throughput the parks took. */
3766
+ /* SP_CPU_RELAX: the architecture's pause instruction, sp_compat.h */
3767
+ #ifndef SP_MUTEX_SPIN
3768
+ #define SP_MUTEX_SPIN 256
3769
+ #endif
3770
+ static inline int sp_mutex_spin_acquire(sp_mutex *m, sp_thread *self) {
3771
+ for (int i = 0; i < SP_MUTEX_SPIN; i++) {
3772
+ sp_thread *o = SP_ATOMIC_LOAD(&m->owner, __ATOMIC_SEQ_CST);
3773
+ if (o == NULL) {
3774
+ sp_thread *expect = NULL;
3775
+ if (SP_ATOMIC_CAS(&m->owner, &expect, self, 0,
3776
+ __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST)) return 1;
3777
+ continue;
3778
+ }
3779
+ if (o == self) return 0; /* reentrancy or a deadlock: the slow path decides */
3780
+ if (SP_ATOMIC_LOAD(&o->state, __ATOMIC_SEQ_CST) != SP_TH_RUNNING) return 0;
3781
+ SP_CPU_RELAX();
3782
+ }
3783
+ return 0;
3784
+ }
3785
+
3786
+ void sp_Mutex_lock(sp_mutex *m) {
3787
+ sp_thread *self = g_current;
3788
+ sp_thread *expect = NULL;
3789
+ if (SP_ATOMIC_CAS(&m->owner, &expect, self, 0,
3790
+ __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST))
3791
+ { if (sched_lat_enabled()) SP_ATOMIC_FETCH_ADD(&g_mtx_fast, 1, __ATOMIC_RELAXED); return; } /* was unlocked: ours, no lock taken */
3792
+ if (sp_mutex_spin_acquire(m, self))
3793
+ { if (sched_lat_enabled()) SP_ATOMIC_FETCH_ADD(&g_mtx_spin, 1, __ATOMIC_RELAXED); return; }
3794
+ SCHED_LOCK();
3795
+ /* the owner re-entering its own Monitor just goes deeper */
3796
+ if (SP_ATOMIC_LOAD(&m->owner, __ATOMIC_SEQ_CST) == self && m->reentrant) { m->depth++; SCHED_UNLOCK(); return; }
3797
+ if (SP_ATOMIC_LOAD(&m->owner, __ATOMIC_SEQ_CST) == self) { SCHED_UNLOCK(); sp_raise_cls("ThreadError", "deadlock; recursive locking"); }
3798
+ expect = NULL;
3799
+ if (SP_ATOMIC_CAS(&m->owner, &expect, self, 0,
3800
+ __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST))
3801
+ { SCHED_UNLOCK(); return; }
3802
+ /* Count ourselves BEFORE the last look at `owner`: an unlocker that clears
3803
+ it after this point re-reads the count and finds us. */
3804
+ m->nwaiters++;
3805
+ SP_ATOMIC_FENCE(__ATOMIC_SEQ_CST);
3806
+ expect = NULL;
3807
+ if (SP_ATOMIC_CAS(&m->owner, &expect, self, 0,
3808
+ __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST)) {
3809
+ m->nwaiters--; /* released under us; took it instead */
3810
+ SCHED_UNLOCK();
3811
+ return;
3812
+ }
3813
+ /* The unlocker clears `owner` and wakes us; we take the mutex ourselves
3814
+ with the same exchange the fast path uses, and park again if a running
3815
+ thread took it first. No hand-off: handing a contended mutex to a PARKED
3816
+ waiter made the critical section's throughput the scheduling latency of
3817
+ a wake (milliseconds on a busy worker), and every thread behind it paid
3818
+ that per acquisition -- a lock convoy, 16-64 ms waits on campfire's
3819
+ fragment-cache shards at 64 connections. A waiter that loses the race
3820
+ goes back to the FRONT of the list, so the order of arrival still
3821
+ decides who is offered the mutex next. */
3822
+ double mt0 = sched_lat_enabled() ? sp_monotonic_now() : 0;
3823
+ for (;;) {
3824
+ sp_sched_block(&m->waiters, 0);
3825
+ expect = NULL;
3826
+ if (SP_ATOMIC_CAS(&m->owner, &expect, self, 0,
3827
+ __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST)) break;
3828
+ self->repark_front = 1; /* re-park at the head (sp_sched_block honours this) */
3829
+ }
3830
+ if (mt0 > 0) sched_hist_add(g_mtx_hist, &g_mtx_n, &g_mtx_max, (sp_monotonic_now() - mt0) * 1e6);
3831
+ m->nwaiters--;
3832
+ SCHED_UNLOCK();
3833
+ }
3834
+
3835
+ void sp_Mutex_unlock(sp_mutex *m) {
3836
+ sp_thread *self = g_current;
3837
+ /* depth is the owner's own field, so reading it as the owner needs no lock;
3838
+ a non-owner fails the exchange below and takes the slow path, which is
3839
+ where the ThreadError is raised. */
3840
+ if (m->depth == 0 && SP_ATOMIC_LOAD(&m->nwaiters, __ATOMIC_SEQ_CST) == 0) {
3841
+ sp_thread *expect = self;
3842
+ if (SP_ATOMIC_CAS(&m->owner, &expect, NULL, 0,
3843
+ __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST)) {
3844
+ if (SP_ATOMIC_LOAD(&m->nwaiters, __ATOMIC_SEQ_CST) == 0) return;
3845
+ /* A waiter counted itself while we were releasing. It is parked, or is
3846
+ about to look at `owner` one more time and take it; either way the
3847
+ list is the authority, so finish the hand-off under the lock.
3848
+
3849
+ The hand-off has to be a compare-exchange, not a load and a store.
3850
+ The lock's own fast path takes a free mutex WITHOUT the scheduler
3851
+ lock, so a third thread can claim it between the load that finds it
3852
+ free and the store that gives it to the waiter -- and the store then
3853
+ puts the waiter's name over that owner, leaving TWO threads inside
3854
+ the critical section. Which of them notices is whichever unlocks
3855
+ second: its fast-path exchange fails, and it raises "Attempt to
3856
+ unlock a mutex which is not locked" against a mutex it really did
3857
+ hold. Reproduced with eight workers on a hot lock, roughly one run in
3858
+ five.
3859
+
3860
+ The waiter is peeked rather than woken first, so a failed exchange
3861
+ leaves it on the list where it was. Nothing is lost by declining: a
3862
+ listed waiter is a counted one, so the thread that won the race sees
3863
+ nwaiters > 0 and cannot take this fast path when it unlocks. */
3864
+ SCHED_LOCK();
3865
+ if (m->waiters) sp_sched_wake_one(&m->waiters); /* it takes the mutex itself, or re-parks */
3866
+ SCHED_UNLOCK();
3867
+ return;
3868
+ }
3869
+ }
3870
+ SCHED_LOCK();
3871
+ if (m->depth > 0 && SP_ATOMIC_LOAD(&m->owner, __ATOMIC_SEQ_CST) == self) { m->depth--; SCHED_UNLOCK(); return; }
3872
+ if (SP_ATOMIC_LOAD(&m->owner, __ATOMIC_SEQ_CST) != self) {
3873
+ SCHED_UNLOCK();
3874
+ sp_raise_cls("ThreadError", "Attempt to unlock a mutex which is not locked");
3875
+ }
3876
+ SP_ATOMIC_STORE(&m->owner, NULL, __ATOMIC_SEQ_CST);
3877
+ if (m->waiters) sp_sched_wake_one(&m->waiters);
3878
+ SCHED_UNLOCK();
3879
+ }
3880
+
3881
+ /* Every read and write of `owner` outside the scheduler lock is atomic, since
3882
+ the uncontended lock/unlock paths above no longer take it. */
3883
+ sp_bool sp_Mutex_try_lock(sp_mutex *m) {
3884
+ SCHED_LOCK();
3885
+ sp_bool r;
3886
+ if (SP_ATOMIC_LOAD(&m->owner, __ATOMIC_SEQ_CST) == g_current && m->reentrant) { m->depth++; r = 1; }
3887
+ else {
3888
+ sp_thread *expect = NULL;
3889
+ r = SP_ATOMIC_CAS(&m->owner, &expect, g_current, 0,
3890
+ __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST) ? 1 : 0;
3891
+ }
3892
+ SCHED_UNLOCK();
3893
+ return r;
3894
+ }
3895
+ sp_bool sp_Mutex_locked(sp_mutex *m) { return SP_ATOMIC_LOAD(&m->owner, __ATOMIC_SEQ_CST) != NULL; }
3896
+ sp_bool sp_Mutex_owned(sp_mutex *m) { return SP_ATOMIC_LOAD(&m->owner, __ATOMIC_SEQ_CST) == g_current; }
3897
+
3898
+ /* ---- ConditionVariable ----
3899
+ * #wait releases the mutex, parks on the CV, and re-acquires the mutex on
3900
+ * wake; #signal wakes one waiter, #broadcast wakes all. */
3901
+
3902
+ sp_condvar *sp_CondVar_new(void) {
3903
+ sp_condvar *cv = (sp_condvar *)sp_gc_alloc(sizeof(sp_condvar), NULL, NULL);
3904
+ cv->waiters = NULL;
3905
+ return cv;
3906
+ }
3907
+
3908
+ sp_RbVal sp_CondVar_wait(sp_condvar *cv, sp_mutex *m) {
3909
+ time_t beg = time(NULL);
3910
+ /* Release the mutex and park on the CV atomically under the one lock, so a
3911
+ concurrent #signal cannot slip in between and be lost. The mutex unlock is
3912
+ inlined (its hand-off + ownership check) since sp_Mutex_unlock would take
3913
+ the lock again on its own. */
3914
+ SCHED_LOCK();
3915
+ if (SP_ATOMIC_LOAD(&m->owner, __ATOMIC_SEQ_CST) != g_current) {
3916
+ SCHED_UNLOCK();
3917
+ sp_raise_cls("ThreadError", "Attempt to unlock a mutex which is not locked");
3918
+ }
3919
+ if (g_current != &g_main_thread && g_current->fiber &&
3920
+ sp_fiber_inject_pending(g_current->fiber)) {
3921
+ SCHED_UNLOCK();
3922
+ sp_fiber_fire_inject_if_pending();
3923
+ SCHED_LOCK();
3924
+ }
3925
+ SP_ATOMIC_STORE(&m->owner, NULL, __ATOMIC_SEQ_CST);
3926
+ if (m->waiters) sp_sched_wake_one(&m->waiters);
3927
+ double ct0 = sched_lat_enabled() ? sp_monotonic_now() : 0;
3928
+ sp_sched_block(&cv->waiters, 1); /* park (drops+retakes the lock) */
3929
+ if (ct0 > 0) sched_hist_add(g_cv_hist, &g_cv_n, &g_cv_max, (sp_monotonic_now() - ct0) * 1e6);
3930
+ SCHED_UNLOCK();
3931
+ sp_Mutex_lock(m); /* re-acquire (may block again on the mutex) */
3932
+ sp_fiber_undefer_inject();
3933
+ sp_fiber_fire_inject_if_pending();
3934
+ return sp_box_int((sp_int)(time(NULL) - beg));
3935
+ }
3936
+
3937
+ /* The single-threaded runtime (or a threaded program before its monitor
3938
+ starts) has no timer heap to park on. */
3939
+ static void sp_CondVar_wait_timeout_blocking(sp_mutex *m, double seconds) {
3940
+ SCHED_LOCK();
3941
+ if (SP_ATOMIC_LOAD(&m->owner, __ATOMIC_SEQ_CST) != g_current) {
3942
+ SCHED_UNLOCK();
3943
+ sp_raise_cls("ThreadError", "Attempt to unlock a mutex which is not locked");
3944
+ }
3945
+ SP_ATOMIC_STORE(&m->owner, NULL, __ATOMIC_SEQ_CST);
3946
+ if (m->waiters) sp_sched_wake_one(&m->waiters);
3947
+ SCHED_UNLOCK();
3948
+ while (seconds > 86400.0) {
3949
+ struct timespec day = {86400, 0};
3950
+ while (nanosleep(&day, &day) == -1 && errno == EINTR) {}
3951
+ seconds -= 86400.0;
3952
+ }
3953
+ struct timespec req = {(time_t)seconds, (long)((seconds - (time_t)seconds) * 1e9)};
3954
+ if (req.tv_nsec >= 1000000000L) { req.tv_sec++; req.tv_nsec -= 1000000000L; }
3955
+ while (nanosleep(&req, &req) == -1 && errno == EINTR) {}
3956
+ sp_Mutex_lock(m);
3957
+ }
3958
+
3959
+ sp_RbVal sp_CondVar_wait_timeout(sp_condvar *cv, sp_mutex *m, double seconds) {
3960
+ /* CRuby checks the interval before it lets go of the mutex */
3961
+ if (seconds < 0.0) sp_raise_cls("ArgumentError", "time interval must not be negative");
3962
+ if (!(seconds > 0.0)) return sp_CondVar_wait_nb(cv, m);
3963
+ #ifdef SP_THREADS
3964
+ /* With no spawned green threads there is no monitor yet. Release the mutex
3965
+ and sleep directly; the main thread is the only possible signaler in this
3966
+ state, so there cannot be a signal to lose, and the wait always times out. */
3967
+ if (!g_sysmon_started) {
3968
+ sp_CondVar_wait_timeout_blocking(m, seconds);
3969
+ return sp_box_nil();
3970
+ }
3971
+ time_t beg = time(NULL);
3972
+ SCHED_LOCK();
3973
+ double ct0 = sched_lat_enabled() ? sp_monotonic_now() : 0;
3974
+ int woken = sp_sched_block_timeout(&cv->waiters, sp_monotonic_now() + seconds, m);
3975
+ if (woken < 0) {
3976
+ SCHED_UNLOCK();
3977
+ sp_raise_cls("NoMemoryError", "failed to schedule condition variable timeout");
3978
+ }
3979
+ if (ct0 > 0) sched_hist_add(g_cv_hist, &g_cv_n, &g_cv_max, (sp_monotonic_now() - ct0) * 1e6);
3980
+ SCHED_UNLOCK();
3981
+ sp_Mutex_lock(m);
3982
+ sp_fiber_undefer_inject();
3983
+ sp_fiber_fire_inject_if_pending();
3984
+ return woken ? sp_box_int((sp_int)(time(NULL) - beg)) : sp_box_nil();
3985
+ #else
3986
+ sp_CondVar_wait_timeout_blocking(m, seconds);
3987
+ return sp_box_nil();
3988
+ #endif
3989
+ }
3990
+
3991
+ /* CRuby's `wait(mutex, 0)`: release the mutex and return without parking.
3992
+ The cv->waiters list is threads blocked on this CV, not pending signals, so
3993
+ this path does not touch it. */
3994
+ sp_RbVal sp_CondVar_wait_nb(sp_condvar *cv, sp_mutex *m) {
3995
+ (void)cv;
3996
+ SCHED_LOCK();
3997
+ if (SP_ATOMIC_LOAD(&m->owner, __ATOMIC_SEQ_CST) != g_current) {
3998
+ SCHED_UNLOCK();
3999
+ sp_raise_cls("ThreadError", "Attempt to unlock a mutex which is not locked");
4000
+ }
4001
+ if (g_current != &g_main_thread && g_current->fiber &&
4002
+ sp_fiber_inject_pending(g_current->fiber)) {
4003
+ SCHED_UNLOCK();
4004
+ sp_fiber_fire_inject_if_pending();
4005
+ SCHED_LOCK();
4006
+ }
4007
+ sp_fiber_defer_inject();
4008
+ SP_ATOMIC_STORE(&m->owner, NULL, __ATOMIC_SEQ_CST);
4009
+ if (m->waiters) sp_sched_wake_one(&m->waiters);
4010
+ SCHED_UNLOCK();
4011
+ sp_Mutex_lock(m);
4012
+ sp_fiber_undefer_inject();
4013
+ sp_fiber_fire_inject_if_pending();
4014
+ return sp_box_nil();
4015
+ }
4016
+
4017
+ void sp_CondVar_signal(sp_condvar *cv) { SCHED_LOCK(); sp_sched_wake_one(&cv->waiters); SCHED_UNLOCK(); }
4018
+ void sp_CondVar_broadcast(sp_condvar *cv) { SCHED_LOCK(); while (sp_sched_wake_one(&cv->waiters)) { } SCHED_UNLOCK(); }
4019
+
4020
+ /* Thread#inspect / #to_s: CRuby's "#<Thread:0xADDR <status>>" shape (the
4021
+ source-location segment CRuby inserts is not carried) (#2977). */
4022
+ const char *sp_Thread_inspect(sp_thread *t) {
4023
+ extern const char *sp_sprintf(const char *fmt, ...);
4024
+ /* NULL is this type's nil, and nil inspects as "nil". Without this a
4025
+ `Thread#join(limit)` that TIMED OUT -- which answers NULL, correctly --
4026
+ printed `#<Thread:0x0000000000000000 dead>`, so the one thing the return
4027
+ value is there to tell you read as the opposite of what it said (#4394).
4028
+ The status word compounded it: the thread it named was still running. */
4029
+ if (!t) return "nil";
4030
+ const char *st = "dead";
4031
+ if (t) {
4032
+ switch (t->state) {
4033
+ case SP_TH_RUNNING: case SP_TH_RUNNABLE: st = "run"; break;
4034
+ case SP_TH_BLOCKED: st = "sleep"; break;
4035
+ default: st = t->has_exc ? "aborting" : "dead"; break;
4036
+ }
4037
+ }
4038
+ /* CRuby carries the creation site between the address and the status */
4039
+ if (t && t->birth_file)
4040
+ return sp_sprintf("#<Thread:0x%016llx %s:%lld %s>",
4041
+ (unsigned long long)(uintptr_t)t, t->birth_file,
4042
+ (long long)t->birth_line, st);
4043
+ return sp_sprintf("#<Thread:0x%016llx %s>", (unsigned long long)(uintptr_t)t, st);
4044
+ }