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.
- checksums.yaml +7 -0
- data/LICENSE-mruby.txt +22 -0
- data/LICENSE-spinel.txt +19 -0
- data/LICENSE.txt +21 -0
- data/README.md +61 -0
- data/ext/qr_reader_native/SPINEL_VERSION +1 -0
- data/ext/qr_reader_native/extconf.rb +8 -0
- data/ext/qr_reader_native/mruby/error.h +13 -0
- data/ext/qr_reader_native/mruby/internal.h +145 -0
- data/ext/qr_reader_native/mruby/string.h +22 -0
- data/ext/qr_reader_native/mruby.h +93 -0
- data/ext/qr_reader_native/mruby_shim.h +118 -0
- data/ext/qr_reader_native/qr_reader_native.c +13513 -0
- data/ext/qr_reader_native/qr_reader_native.h +29 -0
- data/ext/qr_reader_native/qr_reader_native_ext.c +598 -0
- data/ext/qr_reader_native/re_cased.h +71 -0
- data/ext/qr_reader_native/re_compile.c +5784 -0
- data/ext/qr_reader_native/re_ctype.h +3540 -0
- data/ext/qr_reader_native/re_exec.c +2190 -0
- data/ext/qr_reader_native/re_internal.h +671 -0
- data/ext/qr_reader_native/re_prop.h +4629 -0
- data/ext/qr_reader_native/re_spinel.c +363 -0
- data/ext/qr_reader_native/re_spinel.h +22 -0
- data/ext/qr_reader_native/re_spinel_utf8.inc +97 -0
- data/ext/qr_reader_native/re_utf8.c +156 -0
- data/ext/qr_reader_native/sp_alloc.c +1447 -0
- data/ext/qr_reader_native/sp_alloc.h +1196 -0
- data/ext/qr_reader_native/sp_argf.h +33 -0
- data/ext/qr_reader_native/sp_array.c +1052 -0
- data/ext/qr_reader_native/sp_array.h +456 -0
- data/ext/qr_reader_native/sp_bigint.c +5826 -0
- data/ext/qr_reader_native/sp_bigint.h +117 -0
- data/ext/qr_reader_native/sp_cold.c +5259 -0
- data/ext/qr_reader_native/sp_compat.h +402 -0
- data/ext/qr_reader_native/sp_core.c +818 -0
- data/ext/qr_reader_native/sp_core.h +56 -0
- data/ext/qr_reader_native/sp_crypto.c +877 -0
- data/ext/qr_reader_native/sp_crypto.h +169 -0
- data/ext/qr_reader_native/sp_dtoa.c +1533 -0
- data/ext/qr_reader_native/sp_dtoa.h +29 -0
- data/ext/qr_reader_native/sp_encoding_names.h +172 -0
- data/ext/qr_reader_native/sp_enum.h +86 -0
- data/ext/qr_reader_native/sp_exc.c +1185 -0
- data/ext/qr_reader_native/sp_exc.h +186 -0
- data/ext/qr_reader_native/sp_exc_ctx.h +56 -0
- data/ext/qr_reader_native/sp_fiber.c +889 -0
- data/ext/qr_reader_native/sp_fiber.h +162 -0
- data/ext/qr_reader_native/sp_fiber_ctx.h +84 -0
- data/ext/qr_reader_native/sp_format.c +695 -0
- data/ext/qr_reader_native/sp_format.h +83 -0
- data/ext/qr_reader_native/sp_gc.c +1793 -0
- data/ext/qr_reader_native/sp_gc.h +762 -0
- data/ext/qr_reader_native/sp_hash.c +131 -0
- data/ext/qr_reader_native/sp_hash.h +131 -0
- data/ext/qr_reader_native/sp_inspect.c +215 -0
- data/ext/qr_reader_native/sp_inspect.h +128 -0
- data/ext/qr_reader_native/sp_io.c +1676 -0
- data/ext/qr_reader_native/sp_io.h +272 -0
- data/ext/qr_reader_native/sp_iobuffer.c +1014 -0
- data/ext/qr_reader_native/sp_iobuffer.h +167 -0
- data/ext/qr_reader_native/sp_marshal.c +487 -0
- data/ext/qr_reader_native/sp_marshal.h +60 -0
- data/ext/qr_reader_native/sp_net.c +1029 -0
- data/ext/qr_reader_native/sp_net.h +205 -0
- data/ext/qr_reader_native/sp_pack.c +1254 -0
- data/ext/qr_reader_native/sp_poly_cold.c +1220 -0
- data/ext/qr_reader_native/sp_proc.c +410 -0
- data/ext/qr_reader_native/sp_proc.h +327 -0
- data/ext/qr_reader_native/sp_process.c +547 -0
- data/ext/qr_reader_native/sp_process_status.c +211 -0
- data/ext/qr_reader_native/sp_process_status.h +66 -0
- data/ext/qr_reader_native/sp_random.c +287 -0
- data/ext/qr_reader_native/sp_random.h +52 -0
- data/ext/qr_reader_native/sp_range.h +144 -0
- data/ext/qr_reader_native/sp_re.c +1379 -0
- data/ext/qr_reader_native/sp_re.h +186 -0
- data/ext/qr_reader_native/sp_re_names.h +59 -0
- data/ext/qr_reader_native/sp_sched.c +4044 -0
- data/ext/qr_reader_native/sp_sched.h +295 -0
- data/ext/qr_reader_native/sp_slab.c +1507 -0
- data/ext/qr_reader_native/sp_str.c +1462 -0
- data/ext/qr_reader_native/sp_str.h +306 -0
- data/ext/qr_reader_native/sp_str_crypt.c +23 -0
- data/ext/qr_reader_native/sp_string.c +92 -0
- data/ext/qr_reader_native/sp_string.h +253 -0
- data/ext/qr_reader_native/sp_system.c +78 -0
- data/ext/qr_reader_native/sp_system.h +18 -0
- data/ext/qr_reader_native/sp_time.c +860 -0
- data/ext/qr_reader_native/sp_time.h +100 -0
- data/ext/qr_reader_native/sp_types.h +292 -0
- data/ext/qr_reader_native/spinel_rt.h +19017 -0
- data/ext/qr_reader_native/unicase.c +399 -0
- data/ext/qr_reader_native/unicase.h +901 -0
- data/lib/qr_reader/native.rb +6 -0
- 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
|
+
}
|