qr_reader-native 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE-mruby.txt +22 -0
  3. data/LICENSE-spinel.txt +19 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +61 -0
  6. data/ext/qr_reader_native/SPINEL_VERSION +1 -0
  7. data/ext/qr_reader_native/extconf.rb +8 -0
  8. data/ext/qr_reader_native/mruby/error.h +13 -0
  9. data/ext/qr_reader_native/mruby/internal.h +145 -0
  10. data/ext/qr_reader_native/mruby/string.h +22 -0
  11. data/ext/qr_reader_native/mruby.h +93 -0
  12. data/ext/qr_reader_native/mruby_shim.h +118 -0
  13. data/ext/qr_reader_native/qr_reader_native.c +13513 -0
  14. data/ext/qr_reader_native/qr_reader_native.h +29 -0
  15. data/ext/qr_reader_native/qr_reader_native_ext.c +598 -0
  16. data/ext/qr_reader_native/re_cased.h +71 -0
  17. data/ext/qr_reader_native/re_compile.c +5784 -0
  18. data/ext/qr_reader_native/re_ctype.h +3540 -0
  19. data/ext/qr_reader_native/re_exec.c +2190 -0
  20. data/ext/qr_reader_native/re_internal.h +671 -0
  21. data/ext/qr_reader_native/re_prop.h +4629 -0
  22. data/ext/qr_reader_native/re_spinel.c +363 -0
  23. data/ext/qr_reader_native/re_spinel.h +22 -0
  24. data/ext/qr_reader_native/re_spinel_utf8.inc +97 -0
  25. data/ext/qr_reader_native/re_utf8.c +156 -0
  26. data/ext/qr_reader_native/sp_alloc.c +1447 -0
  27. data/ext/qr_reader_native/sp_alloc.h +1196 -0
  28. data/ext/qr_reader_native/sp_argf.h +33 -0
  29. data/ext/qr_reader_native/sp_array.c +1052 -0
  30. data/ext/qr_reader_native/sp_array.h +456 -0
  31. data/ext/qr_reader_native/sp_bigint.c +5826 -0
  32. data/ext/qr_reader_native/sp_bigint.h +117 -0
  33. data/ext/qr_reader_native/sp_cold.c +5259 -0
  34. data/ext/qr_reader_native/sp_compat.h +402 -0
  35. data/ext/qr_reader_native/sp_core.c +818 -0
  36. data/ext/qr_reader_native/sp_core.h +56 -0
  37. data/ext/qr_reader_native/sp_crypto.c +877 -0
  38. data/ext/qr_reader_native/sp_crypto.h +169 -0
  39. data/ext/qr_reader_native/sp_dtoa.c +1533 -0
  40. data/ext/qr_reader_native/sp_dtoa.h +29 -0
  41. data/ext/qr_reader_native/sp_encoding_names.h +172 -0
  42. data/ext/qr_reader_native/sp_enum.h +86 -0
  43. data/ext/qr_reader_native/sp_exc.c +1185 -0
  44. data/ext/qr_reader_native/sp_exc.h +186 -0
  45. data/ext/qr_reader_native/sp_exc_ctx.h +56 -0
  46. data/ext/qr_reader_native/sp_fiber.c +889 -0
  47. data/ext/qr_reader_native/sp_fiber.h +162 -0
  48. data/ext/qr_reader_native/sp_fiber_ctx.h +84 -0
  49. data/ext/qr_reader_native/sp_format.c +695 -0
  50. data/ext/qr_reader_native/sp_format.h +83 -0
  51. data/ext/qr_reader_native/sp_gc.c +1793 -0
  52. data/ext/qr_reader_native/sp_gc.h +762 -0
  53. data/ext/qr_reader_native/sp_hash.c +131 -0
  54. data/ext/qr_reader_native/sp_hash.h +131 -0
  55. data/ext/qr_reader_native/sp_inspect.c +215 -0
  56. data/ext/qr_reader_native/sp_inspect.h +128 -0
  57. data/ext/qr_reader_native/sp_io.c +1676 -0
  58. data/ext/qr_reader_native/sp_io.h +272 -0
  59. data/ext/qr_reader_native/sp_iobuffer.c +1014 -0
  60. data/ext/qr_reader_native/sp_iobuffer.h +167 -0
  61. data/ext/qr_reader_native/sp_marshal.c +487 -0
  62. data/ext/qr_reader_native/sp_marshal.h +60 -0
  63. data/ext/qr_reader_native/sp_net.c +1029 -0
  64. data/ext/qr_reader_native/sp_net.h +205 -0
  65. data/ext/qr_reader_native/sp_pack.c +1254 -0
  66. data/ext/qr_reader_native/sp_poly_cold.c +1220 -0
  67. data/ext/qr_reader_native/sp_proc.c +410 -0
  68. data/ext/qr_reader_native/sp_proc.h +327 -0
  69. data/ext/qr_reader_native/sp_process.c +547 -0
  70. data/ext/qr_reader_native/sp_process_status.c +211 -0
  71. data/ext/qr_reader_native/sp_process_status.h +66 -0
  72. data/ext/qr_reader_native/sp_random.c +287 -0
  73. data/ext/qr_reader_native/sp_random.h +52 -0
  74. data/ext/qr_reader_native/sp_range.h +144 -0
  75. data/ext/qr_reader_native/sp_re.c +1379 -0
  76. data/ext/qr_reader_native/sp_re.h +186 -0
  77. data/ext/qr_reader_native/sp_re_names.h +59 -0
  78. data/ext/qr_reader_native/sp_sched.c +4044 -0
  79. data/ext/qr_reader_native/sp_sched.h +295 -0
  80. data/ext/qr_reader_native/sp_slab.c +1507 -0
  81. data/ext/qr_reader_native/sp_str.c +1462 -0
  82. data/ext/qr_reader_native/sp_str.h +306 -0
  83. data/ext/qr_reader_native/sp_str_crypt.c +23 -0
  84. data/ext/qr_reader_native/sp_string.c +92 -0
  85. data/ext/qr_reader_native/sp_string.h +253 -0
  86. data/ext/qr_reader_native/sp_system.c +78 -0
  87. data/ext/qr_reader_native/sp_system.h +18 -0
  88. data/ext/qr_reader_native/sp_time.c +860 -0
  89. data/ext/qr_reader_native/sp_time.h +100 -0
  90. data/ext/qr_reader_native/sp_types.h +292 -0
  91. data/ext/qr_reader_native/spinel_rt.h +19017 -0
  92. data/ext/qr_reader_native/unicase.c +399 -0
  93. data/ext/qr_reader_native/unicase.h +901 -0
  94. data/lib/qr_reader/native.rb +6 -0
  95. metadata +153 -0
@@ -0,0 +1,1447 @@
1
+ /* sp_alloc.c -- the single, shared definitions backing sp_alloc.h.
2
+
3
+ Owns the string heap so that both the generated program and every standalone
4
+ lib/*.c allocate onto one heap. sp_str_sweep is registered with the object GC
5
+ via a constructor, so a collection triggered from any TU also reaps strings. */
6
+ #include <time.h>
7
+ #include <signal.h> /* SPINEL_ALLOC_REPORT_SIGNAL: dump without exiting */
8
+ #include <strings.h> /* strcasecmp (that signal named rather than numbered) */
9
+ #include <unistd.h> /* write/read: the handler's only safe way to say "dump" */
10
+ #include <fcntl.h> /* the wake pipe's O_NONBLOCK and FD_CLOEXEC */
11
+ #include <errno.h> /* EINTR on the reader's park */
12
+ #ifdef SP_THREADS
13
+ #include <pthread.h> /* the thread that does the writing */
14
+ #endif
15
+ #include "sp_alloc.h"
16
+ #include "sp_dtoa.h" /* sp_format_float for locale-independent Float#to_s */
17
+ /* Per-site allocation attribution (SPINEL_ALLOC_SITES=1, on top of
18
+ SPINEL_ALLOC_REPORT). The site is the raw return address of the frame that
19
+ asked for the allocation -- captured here, symbolised only at dump time, so
20
+ nothing allocates on the counted path. execinfo is optional; without it the
21
+ report stays per-type. */
22
+ #if defined(__has_include)
23
+ # if __has_include(<execinfo.h>)
24
+ # include <execinfo.h>
25
+ # define SP_ALLOC_SITE_AVAILABLE 1
26
+ # endif
27
+ #endif
28
+ #ifndef SP_ALLOC_SITE_AVAILABLE
29
+ # define SP_ALLOC_SITE_AVAILABLE 0
30
+ #endif
31
+
32
+ #ifdef SP_THREADS
33
+ sp_str_wslot_t sp_str_wslot[SP_MAX_WORKERS]; /* zero-init: NULL lists, 0 bytes */
34
+ /* Aggregate live string bytes across every worker's list. Called only off the
35
+ fast path (collection trigger uses the per-worker slice; sweep/retune here). */
36
+ size_t sp_str_bytes_total(void) {
37
+ size_t s = 0;
38
+ int n = sp_active_workers; if (n < 1) n = 1; if (n > SP_MAX_WORKERS) n = SP_MAX_WORKERS;
39
+ for (int i = 0; i < n; i++) s += SP_GC_CTR_GET(sp_str_wslot[i].young_bytes);
40
+ return s;
41
+ }
42
+ #else
43
+ sp_str_hdr *sp_str_heap = NULL;
44
+ size_t sp_str_heap_bytes = 0;
45
+ sp_str_hdr *sp_str_old = NULL;
46
+ size_t sp_str_old_bytes = 0;
47
+ #endif
48
+ /* SPINEL_GC_OBJ_BUDGET: how much of the mark set the object collection budget
49
+ is priced from. 0 = the object heap alone (`obj`), 1 = the whole set a mark
50
+ walks (`walk`), 2 = gated on what the last collection cost (the default).
51
+ Read once, beside the other boot-time GC modes. */
52
+ int sp_gc_obj_budget_mode = 2;
53
+ /* The last gate decision, in 1024ths, so the stats line can report it and a
54
+ test can read it. 1024 is `walk`, 0 is `obj`. */
55
+ size_t sp_gc_obj_alpha1024 = 1024;
56
+ /* SPINEL_GC_OBJ_BUDGET=fixed / SPINEL_GC_STR_BUDGET=fixed: hold that heap's
57
+ budget at its floor instead of re-aiming it after every collection. Read
58
+ once beside the other boot-time GC modes; see the comment there. */
59
+ int sp_gc_obj_budget_fixed = 0;
60
+ int sp_gc_str_budget_fixed = 0;
61
+ /* SPINEL_GC_STR_MAJOR=fixed: hold the string old generation's gate at its floor
62
+ instead of re-aiming it, adapting nothing. */
63
+ int sp_gc_str_major_fixed = 0;
64
+ /* The major runs on a SCHEDULE, with the size test demoted to a backstop, the
65
+ way the object heap has always run its full collection. On by default;
66
+ SPINEL_GC_STR_MAJOR=size is the way back to the gate that shipped before it.
67
+ See the block above sp_str_major_interval for what it is measured to cost
68
+ and save. */
69
+ int sp_gc_str_major_sched = 1;
70
+ /* String majors run on their own gate, so the object collector's `full` count
71
+ does not describe them: pinning that gate changed the old generation from
72
+ 57.2 MB to 11.5 MB with the reported full count identical at 6. */
73
+ size_t sp_gc_str_majors = 0;
74
+ size_t sp_str_old_threshold = 1024 * 1024;
75
+ size_t sp_str_old_threshold_init = 1024 * 1024;
76
+ /* How many string sweeps between majors, and the count that drives it. The cadence is a COUNT rather than a size because a
77
+ size gate re-aimed from the old list is aimed at a number the same gate
78
+ produced: a small budget promotes early, promotion is one-way until a major,
79
+ and "twice what the last major left" then sets the next gate from what early
80
+ promotion inflated (#4407). The object heap has never gated its full
81
+ collection that way -- sp_gc.c runs it on an interval and keeps the size test
82
+ as a backstop for growth between scheduled fulls -- and this is that policy,
83
+ on the heap it was missing from. The bounds are the object heap's, for the
84
+ same reason its comment gives.
85
+
86
+ Measured on four shapes (#4407). On a real application at a 16 MB string
87
+ floor -- ONCE Campfire, measured by the reporter -- PSS went 280 to 145 MB
88
+ and throughput 345 to 365 req/s: the schedule went PAST the 64 MB control on
89
+ memory and was faster. On a 12 MB live set under a 4 MB floor it cut the old
90
+ generation from 67.4 MB to 21.4 MB and peak RSS from 135 MB to 84 MB, and ran
91
+ no slower. On the adversarial shape -- 400,000 retained strings that no major
92
+ can free, where a cadence firing too often would be pure cost -- there is no
93
+ difference at all in either wall time or RSS, because the survival ratio
94
+ stretches the interval to 16 and then to 128. The one cost is
95
+ benchmark/bm_threaded_render.rb, where twelve order-flipped passes a side put
96
+ median RSS at 776 MB against 736, with wall time identical (1.93s against
97
+ 1.92s). Five percent of one benchmark's median memory, no time, against
98
+ halving a real application's. */
99
+ #define SP_STR_MAJOR_INTERVAL 8
100
+ #define SP_STR_MAJOR_INTERVAL_MAX 128
101
+ static int sp_str_major_interval = SP_STR_MAJOR_INTERVAL;
102
+ static unsigned sp_str_sweep_cycle = 0;
103
+ static int sp_str_major_forced = 0;
104
+ /* The old generation's size at every string sweep, so the run can be described
105
+ by its shape rather than by whichever instant a per-second line happened to
106
+ catch. rubys' reading on #4407 is that the ratio of the MEDIAN to the MINIMUM
107
+ says whether the gate is holding garbage: the minimum is about what a major
108
+ can actually leave, so the ratio is how far above that the gate keeps the
109
+ heap. It could not be checked on a two-second benchmark, because the [gcph]
110
+ line prints once a second and a median of three samples is not a median.
111
+ Sampling at the sweep removes that limit: the cadence becomes the
112
+ collector's, not the clock's. A ring, so a long run costs no more than a
113
+ short one and the samples are the most recent SP_STR_SHAPE_MAX. */
114
+ #define SP_STR_SHAPE_MAX 8192
115
+ static size_t sp_str_shape[SP_STR_SHAPE_MAX];
116
+ static unsigned sp_str_shape_n = 0; /* total sweeps seen */
117
+ static int sp_str_shape_cmp(const void *a, const void *b) {
118
+ size_t x = *(const size_t *)a, y = *(const size_t *)b;
119
+ return x < y ? -1 : (x > y ? 1 : 0);
120
+ }
121
+ /* The [gcph] line names whichever policy is running, so the number after it is
122
+ never read as the other one's: the default's is a size to cross, the
123
+ schedule's is a cadence with the size demoted to a backstop. */
124
+ static const char *sp_str_major_label(void) {
125
+ static char buf[64];
126
+ if (!sp_gc_str_major_sched) return "at ";
127
+ snprintf(buf, sizeof buf, "every %d sweeps, backstop ", sp_str_major_interval);
128
+ return buf;
129
+ }
130
+
131
+ /* The slab strings' old generation: not a list anybody walks but bits in
132
+ the chunks (lib/sp_slab.c), so its bytes are what the MARK counted --
133
+ everything it reached at a major, plus what it promoted at each minor --
134
+ settled at sp_str_sweep_end. The lists below hold only the strings too
135
+ large for the slab. */
136
+ static size_t sp_str_old_slab_bytes = 0;
137
+ extern size_t sp_gc_mk_str_bytes, sp_gc_mk_str_young_bytes; /* lib/sp_gc.c: this cycle's mark */
138
+ /* After every mark (lib/sp_gc.c): a full cycle's mark reached every live
139
+ slab string, and the sweep of the bitmaps leaves exactly those; a minor's
140
+ promoted what it reached of the young. Every cycle, whatever the string
141
+ gate decided for the lists: the chunk sweep frees young slab strings on
142
+ every cycle and old ones on every full one. */
143
+ void sp_str_mark_settle(int full) {
144
+ if (full) sp_str_old_slab_bytes = sp_gc_mk_str_bytes;
145
+ else sp_str_old_slab_bytes += sp_gc_mk_str_young_bytes;
146
+ }
147
+ /* Live bytes in the old generation: the slab's, and every worker's list. */
148
+ static size_t sp_str_old_total(void) {
149
+ #ifdef SP_THREADS
150
+ size_t t = sp_str_old_slab_bytes;
151
+ int n = sp_active_workers; if (n < 1) n = 1; if (n > SP_MAX_WORKERS) n = SP_MAX_WORKERS;
152
+ for (int i = 0; i < n; i++) t += SP_GC_CTR_GET(sp_str_wslot[i].old_bytes);
153
+ return t;
154
+ #else
155
+ return sp_str_old_slab_bytes + SP_GC_CTR_GET(sp_str_old_bytes);
156
+ #endif
157
+ }
158
+
159
+ /* Every live string byte, young and old, in either build. Both retunes need it
160
+ and the young half is spelled differently with and without threads. */
161
+ static size_t sp_str_live_total(void) {
162
+ #ifdef SP_THREADS
163
+ return sp_str_bytes_total() + sp_str_old_total();
164
+ #else
165
+ return sp_str_heap_bytes + sp_str_old_total();
166
+ #endif
167
+ }
168
+ size_t sp_str_threshold = 256 * 1024;
169
+ size_t sp_str_threshold_init = 256 * 1024;
170
+ int sp_str_stress_checked = 0;
171
+
172
+ const char sp_str_empty_data[] = "\xff";
173
+ /* nil.to_s, true.to_s and false.to_s answer FROZEN Strings in CRuby: a
174
+ header-carrying static object marked 0xf1, as a frozen literal is */
175
+ static struct { sp_str_hdr h; unsigned char m; char d[1]; } sp_fz_empty_s = { { NULL, 1 | SP_STR_SIZE_ASCII7, 0, 0 }, 0xf1, "" };
176
+ static struct { sp_str_hdr h; unsigned char m; char d[5]; } sp_fz_true_s = { { NULL, 5 | SP_STR_SIZE_ASCII7, 4, 0 }, 0xf1, "true" };
177
+ static struct { sp_str_hdr h; unsigned char m; char d[6]; } sp_fz_false_s = { { NULL, 6 | SP_STR_SIZE_ASCII7, 5, 0 }, 0xf1, "false" };
178
+ const char *const sp_str_frozen_empty = sp_fz_empty_s.d;
179
+ const char *const sp_str_frozen_true = sp_fz_true_s.d;
180
+ const char *const sp_str_frozen_false = sp_fz_false_s.d;
181
+ /* the link a chilled String's header carries (sp_str_is_chilled) */
182
+ const sp_str_hdr sp_str_chilled_tag = { NULL, 0, 0, 0 };
183
+
184
+ SP_TLS int sp_ffi_bin_len = 0; /* see sp_alloc.h: byte count for :binstr / :cbinstr */
185
+
186
+ /* Object-heap collection threshold (was per-TU static in spinel_rt.h; now
187
+ shared so sp_gc_alloc can live in sp_alloc.h and lib TUs allocate too). */
188
+ size_t sp_gc_threshold = 256 * 1024;
189
+ size_t sp_gc_threshold_init = 256 * 1024;
190
+ int sp_gc_stress_checked = 0;
191
+ /* Stress pins the threshold instead of merely seeding it: the retunes float
192
+ the trigger to live*4 with the base as a FLOOR, so on any program whose
193
+ live set outgrows the base, stress stopped stressing after the first
194
+ collection -- request-time bugs sat behind a cadence identical to the
195
+ default's while boot-time ones reproduced instantly (#3513). */
196
+ int sp_gc_stress_pin = 0;
197
+
198
+ #ifdef SP_THREADS
199
+ pthread_mutex_t sp_heap_lock = PTHREAD_MUTEX_INITIALIZER; /* see sp_alloc.h */
200
+
201
+ /* One-time SPINEL_GC_STRESS check, run single-threaded before the first helper
202
+ worker spawns (sp_sched_ensure_workers). The alloc fast paths keep their lazy
203
+ `if (!checked)` guard for the single-threaded build, but under threads letting
204
+ workers race to first-write that flag on the hot path is a data race; doing it
205
+ here once means every worker only ever reads it (the pthread_create of the
206
+ helpers is the happens-before edge). Idempotent: safe if main already tripped
207
+ the lazy guard during startup. */
208
+ void sp_alloc_stress_init(void) {
209
+ const char *e = getenv("SPINEL_GC_STRESS");
210
+ int stress = (e && *e && *e != '0');
211
+ if (!sp_str_stress_checked) {
212
+ sp_str_stress_checked = 1;
213
+ if (stress) { sp_str_threshold = 2048; sp_str_threshold_init = 2048; sp_gc_stress_pin = 1; }
214
+ }
215
+ if (!sp_gc_stress_checked) {
216
+ sp_gc_stress_checked = 1;
217
+ if (stress) { SP_GC_CTR_SET(sp_gc_threshold, 2048); sp_gc_threshold_init = 2048; sp_gc_stress_pin = 1; }
218
+ }
219
+ }
220
+
221
+ /* Size the collection budget for the worker count, once, before any helper
222
+ spawns (same single-threaded window as the stress check above).
223
+
224
+ The object-heap trigger compares the GLOBAL live-byte total against one
225
+ threshold, so N workers cross it N times faster in wall clock -- and every
226
+ crossing now stops N workers instead of one. Measured on an
227
+ allocation-heavy program: the collection COUNT is flat across worker counts
228
+ (the total allocated is what it is), but 8 workers ran 1.7x slower than 1
229
+ while burning 2.9x the CPU, all of it in park/mark/unpark. The string heap
230
+ already avoids this by comparing per-worker bytes, which makes its aggregate
231
+ bound N * threshold; this gives the object heap the same bound.
232
+
233
+ The cost is bounded and small: N * 256 KB of garbage retained between
234
+ collections, 2 MB at eight workers. SPINEL_GC_THRESHOLD_KB overrides the
235
+ base for a program that wants to trade more memory for fewer stops.
236
+
237
+ Not scaled under GC stress: that mode exists to maximize collections, and
238
+ multiplying its 2 KB budget would quietly weaken every stress run. */
239
+ #endif /* SP_THREADS -- the floors below are read by EVERY program */
240
+ /* Set one heap's floor from an environment variable, leaving the other alone.
241
+ SPINEL_GC_THRESHOLD_KB moves both together, and moving them together cannot
242
+ answer WHICH heap's trigger paces the collections. On a server whose string
243
+ live set grows 26x across a concurrency ladder while its object live set
244
+ grows 4.9x, the mark walks both and only one of them decides when to look:
245
+ raising just the object floor says whether that is the pacer, and raising
246
+ just the string floor is the same question from the other side (#4384).
247
+ The per-heap variable wins when both are set, being the more specific. */
248
+ static void sp_alloc_floor_from_env(const char *name, size_t *cur, size_t *init) {
249
+ const char *e = getenv(name);
250
+ if (!e || !*e) return;
251
+ long v = atol(e);
252
+ if (v <= 0) return;
253
+ size_t base = (size_t)v * 1024;
254
+ SP_GC_CTR_SET(*cur, base);
255
+ *init = base;
256
+ }
257
+ /* The three floors, read from the environment. Called twice on purpose and
258
+ idempotent: once before main for EVERY program, and again from
259
+ sp_alloc_worker_tune, which only a threaded one reaches and which scales
260
+ what it finds by the worker count.
261
+
262
+ It used to live only in the second, and so did nothing at all in a
263
+ single-threaded program -- `SPINEL_GC_THRESHOLD_KB=65536 ./prog` collected
264
+ at 256 KB and said `trigger 0.25 MB` while the operator read the manual.
265
+ The budget MODE was moved out of here for the same reason and with the same
266
+ sentence (see sp_gc.c): the pacing is not a threads-only question. */
267
+ void sp_alloc_floors_from_env(void) {
268
+ const char *e = getenv("SPINEL_GC_THRESHOLD_KB");
269
+ if (e && *e) {
270
+ long v = atol(e);
271
+ if (v > 0) {
272
+ size_t base = (size_t)v * 1024;
273
+ SP_GC_CTR_SET(sp_gc_threshold, base); sp_gc_threshold_init = base;
274
+ SP_GC_CTR_SET(sp_str_threshold, base); sp_str_threshold_init = base;
275
+ }
276
+ }
277
+ sp_alloc_floor_from_env("SPINEL_GC_THRESHOLD_OBJ_KB", &sp_gc_threshold, &sp_gc_threshold_init);
278
+ sp_alloc_floor_from_env("SPINEL_GC_THRESHOLD_STR_KB", &sp_str_threshold, &sp_str_threshold_init);
279
+ /* The string heap's OLD generation has its own gate, and it is the one
280
+ nothing could reach. A string is promoted the first sweep it survives, and
281
+ an old string is reclaimed only by a MAJOR. SPINEL_GC_FULL_INTERVAL does
282
+ not touch it -- that gates the OBJECT full cycle, which is why forcing
283
+ every collection full changed nothing on a program whose memory was all in
284
+ the string old list (#4407). This sets the floor under the growth backstop,
285
+ and is the control that lets the cadence be measured against a pinned one. */
286
+ sp_alloc_floor_from_env("SPINEL_GC_STR_MAJOR_KB", &sp_str_old_threshold, &sp_str_old_threshold_init);
287
+ /* SPINEL_GC_STRESS=2 last, over any floor above: the two thresholds are
288
+ pinned at zero, so every allocation that can collect does. Here because
289
+ both callers must end on it -- a floor read after it put the trigger
290
+ back, and the level then collected as seldom as the floor said -- and
291
+ marked as checked, so the allocators' own one-shot (2048) does not run
292
+ after it. */
293
+ { const char *st = getenv("SPINEL_GC_STRESS");
294
+ if (st && atoi(st) >= 2) {
295
+ SP_GC_CTR_SET(sp_gc_threshold, 0); sp_gc_threshold_init = 0;
296
+ SP_GC_CTR_SET(sp_str_threshold, 0); sp_str_threshold_init = 0;
297
+ sp_gc_stress_pin = 1; sp_gc_stress_checked = 1; sp_str_stress_checked = 1;
298
+ } }
299
+ }
300
+ #ifdef SP_THREADS
301
+ void sp_alloc_worker_tune(int workers) {
302
+ sp_alloc_floors_from_env();
303
+ {
304
+ const char *st = getenv("SPINEL_GC_STRESS");
305
+ if (st && *st && *st != '0') return;
306
+ }
307
+ if (workers < 1) workers = 1;
308
+ if (workers > SP_MAX_WORKERS) workers = SP_MAX_WORKERS;
309
+ if (workers == 1) return;
310
+ sp_gc_threshold_init *= (size_t)workers;
311
+ /* Raise the CURRENT threshold to the new base, do not multiply it. The cost
312
+ budgeted above -- N * 256 KB retained between collections -- is what the
313
+ multiply costs when it lands on the base, which is where it lands for a
314
+ program that creates its threads before doing any work. A program that
315
+ creates its first thread after its heap has grown was handed N * whatever
316
+ the adaptive threshold had become: 76 MB -> 2.4 GB on a 32-core machine,
317
+ with no collection involved, and the churn that followed then ran to a
318
+ 997 MB heap against a 66 MB live set without collecting once. The
319
+ multiplier is the POOL size (min(cores, SPINEL_WORKERS)), not the thread
320
+ count the program asked for, so the damage scales with the machine
321
+ (#4146). */
322
+ { size_t cur = SP_GC_CTR_GET(sp_gc_threshold);
323
+ if (sp_gc_threshold_init > cur) SP_GC_CTR_SET(sp_gc_threshold, sp_gc_threshold_init); }
324
+ }
325
+ #endif
326
+
327
+ /* Re-tune the object / string GC thresholds from the pre-collect live bytes
328
+ (the heuristic mirrors the original inline code in sp_gc_alloc / sp_str_alloc). */
329
+ /* size_t multiply that stops at the top instead of wrapping. A threshold that
330
+ wraps is not a large threshold, it is an OFF switch: `bytes >= threshold`
331
+ never fires again, nothing collects, and the retune that would correct it is
332
+ only reached by a collection (#4073). */
333
+ static size_t sp_gc_sat_mul(size_t v, size_t k) {
334
+ return (k && v > (size_t)-1 / k) ? (size_t)-1 : v * k;
335
+ }
336
+ /* SPINEL_GC_STATS=1: a line on stderr, at most once a second, saying how many
337
+ collections have run and what they cost. A server whose GC share of CPU
338
+ climbs with concurrency and one that simply collects more often are the same
339
+ picture from a profile; separating them needs the COUNT beside the total
340
+ time, and spinel exposed neither (#4352). Reported from the object retune
341
+ because that runs at the end of every collection and this file is where both
342
+ thresholds and the string heap are visible. */
343
+ static void sp_gc_stats_emit(void);
344
+ static void sp_gc_stats_report(void) {
345
+ static int on = -1;
346
+ static double last = 0;
347
+ if (on < 0) {
348
+ const char *e = getenv("SPINEL_GC_STATS"); on = (e && *e && *e != '0') ? 1 : 0;
349
+ /* SPINEL_GC_PHASES arms the same reporter on its own, so the breakdown does
350
+ not also require SPINEL_GC_STATS to be set. */
351
+ if (sp_gc_ph_on) on = 1;
352
+ /* A program that exits before the next tick would otherwise report nothing
353
+ but its first collection, so the totals are also printed on the way out.
354
+ A server is killed rather than returning from main, which is why the
355
+ periodic line exists at all. */
356
+ if (on) atexit(sp_gc_stats_emit);
357
+ }
358
+ if (!on) return;
359
+ struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts);
360
+ double now = (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9;
361
+ if (now - last < 1.0) return;
362
+ last = now;
363
+ sp_gc_stats_emit();
364
+ }
365
+ static void sp_gc_stats_emit(void) {
366
+ static double first = 0;
367
+ struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts);
368
+ double now = (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9;
369
+ if (first == 0) first = now;
370
+ double wall = now - first;
371
+ unsigned long long n = sp_gc_stat_collections;
372
+ #ifdef SP_THREADS
373
+ int nw = sp_active_workers; if (nw < 1) nw = 1;
374
+ /* Both generations: survivors are promoted, so the young total alone reads
375
+ as ~0 right after a collection and would say the string heap is empty. */
376
+ size_t sbytes = sp_str_bytes_total() + sp_str_old_total();
377
+ #else
378
+ int nw = 1;
379
+ size_t sbytes = SP_GC_CTR_GET(sp_str_heap_bytes) + SP_GC_CTR_GET(sp_str_old_bytes);
380
+ #endif
381
+ fprintf(stderr,
382
+ "[gc] %llu collections (%llu full) in %.2fs of %.1fs wall (%.1f%%), %.2fms avg; "
383
+ "live %.1f MB obj + %.1f MB str; trigger %.1f MB obj + %.2f MB str/worker x %d; "
384
+ "mark share %.2f\n",
385
+ n, sp_gc_stat_fulls, sp_gc_stat_seconds, wall,
386
+ wall > 0 ? 100.0 * sp_gc_stat_seconds / wall : 0.0,
387
+ n ? 1000.0 * sp_gc_stat_seconds / (double)n : 0.0,
388
+ (double)SP_GC_CTR_GET(sp_gc_bytes) / 1048576.0, (double)sbytes / 1048576.0,
389
+ (double)SP_GC_CTR_GET(sp_gc_threshold) / 1048576.0,
390
+ (double)SP_GC_CTR_GET(sp_str_threshold) / 1048576.0, nw,
391
+ (double)sp_gc_obj_alpha1024 / 1024.0);
392
+ if (!sp_gc_ph_on) return;
393
+ /* Which part of a collection cost that time. The names are the ones the
394
+ collector's own comments use, so a number leads to the code that spent it.
395
+ Under SP_THREADS the per-worker string sweep runs inside the slot sweep
396
+ (sp_sweep_one_slot), so `string sweep` is the serial path's figure and
397
+ reads zero on the threaded one. */
398
+ /* Which generation the string live set is in. The [gc] line's `str` is the
399
+ two added together, and they answer different questions: young is what the
400
+ next sweep can reclaim, old is what only a MAJOR can, and a budget that
401
+ promotes early can grow the second while the first looks healthy. */
402
+ fprintf(stderr,
403
+ "[gcph] string live %.1f MB young + %.1f MB old "
404
+ "(major %s%.1f MB old, %llu so far)\n",
405
+ (double)(sp_str_live_total() - sp_str_old_total()) / 1048576.0,
406
+ (double)sp_str_old_total() / 1048576.0,
407
+ sp_str_major_label(),
408
+ (double)sp_str_old_threshold / 1048576.0,
409
+ (unsigned long long)sp_gc_str_majors);
410
+ /* median / min over the sweeps, and their ratio. One number for the sawtooth
411
+ the per-second line can only show a slice of. */
412
+ if (sp_str_shape_n > 0) {
413
+ unsigned n = sp_str_shape_n < SP_STR_SHAPE_MAX ? sp_str_shape_n : SP_STR_SHAPE_MAX;
414
+ size_t *cp = (size_t *)malloc((size_t)n * sizeof *cp);
415
+ if (cp) {
416
+ memcpy(cp, sp_str_shape, (size_t)n * sizeof *cp);
417
+ qsort(cp, n, sizeof *cp, sp_str_shape_cmp);
418
+ double med = (double)cp[n / 2], mn = (double)cp[0];
419
+ fprintf(stderr,
420
+ "[gcph] string old over %u sweeps: min %.1f MB median %.1f MB max %.1f MB"
421
+ " (median/min %.1fx)\n",
422
+ n, mn / 1048576.0, med / 1048576.0, (double)cp[n - 1] / 1048576.0,
423
+ mn > 0 ? med / mn : 0.0);
424
+ free(cp);
425
+ }
426
+ }
427
+ fprintf(stderr,
428
+ "[gcph] marked %llu objs swept %llu slots\n",
429
+ (unsigned long long)SP_GC_CTR_GET(sp_gc_ct_marked), (unsigned long long)SP_GC_CTR_GET(sp_gc_ct_swept));
430
+ fprintf(stderr,
431
+ "[gcph] mark %.3fs old sweep %.3fs slot sweep %.3fs (longest task %.3fs; tasks total %.3fs = obj %.3fs + str old %.3fs + str young %.3fs) "
432
+ "remembered clear %.3fs string sweep %.3fs trim %.3fs of %.3fs total\n",
433
+ sp_gc_ph_mark, sp_gc_ph_oldsweep, sp_gc_ph_slotsweep, sp_gc_ph_slot_max,
434
+ sp_gc_ph_task_sum, sp_gc_ph_task_obj, sp_gc_ph_task_sold, sp_gc_ph_task_syoung,
435
+ sp_gc_ph_rembclear, sp_gc_ph_strsweep, sp_gc_ph_trim,
436
+ sp_gc_stat_seconds);
437
+ { extern double sp_gc_ph_park_sweeping; extern unsigned long long sp_gc_ph_park_sweeping_n;
438
+ if (sp_gc_ph_park_sweeping_n > 0)
439
+ fprintf(stderr, "[gcph] park wait: %.3fs of it on %llu barriers raised while an owner sweep of the previous cycle was still running\n",
440
+ sp_gc_ph_park_sweeping, sp_gc_ph_park_sweeping_n); }
441
+ if (sp_gc_ph_barrier > 0)
442
+ fprintf(stderr, "[gcph] barrier: %.3fs from stop to release, of which %.3fs waiting for the workers to park; parallel mark on %llu drains, %.1f helpers each\n",
443
+ sp_gc_ph_barrier, sp_gc_ph_park, sp_gc_ph_mk_drains,
444
+ sp_gc_ph_mk_drains ? (double)sp_gc_ph_mk_helpers / (double)sp_gc_ph_mk_drains : 0.0);
445
+ { extern unsigned long long sp_gc_ph_mk_by_helpers, sp_gc_ph_mk_spills, sp_gc_ph_mk_takes;
446
+ if (sp_gc_ph_mk_drains)
447
+ fprintf(stderr, "[gcph] parallel mark: %llu of %llu objects marked by helpers; %llu chunks spilled, %llu taken; collector drain %.3fs (idle in it %.3fs) join %.3fs\n",
448
+ sp_gc_ph_mk_by_helpers, (unsigned long long)SP_GC_CTR_GET(sp_gc_ct_marked), sp_gc_ph_mk_spills, sp_gc_ph_mk_takes,
449
+ sp_gc_ph_mk_drain, sp_gc_ph_mk_idle, sp_gc_ph_mk_join); }
450
+ if (sp_gc_ph_conc_wall > 0)
451
+ fprintf(stderr, "[gcph] concurrent sweep: %.3fs wall beside the program (longest task %.3fs); %llu collections waited %.3fs for the previous sweep (not in the total above)\n",
452
+ sp_gc_ph_conc_wall, sp_gc_ph_slot_max, sp_gc_ph_conc_waits, sp_gc_ph_conc_wait);
453
+ if (sp_gc_ph_conc_wall > 0)
454
+ { extern double sp_gc_ph_wait_top;
455
+ fprintf(stderr, "[gcph] concurrent sweep, applied under the barrier: objects %.3fs strings %.3fs slab release %.3fs (the join at the top of the collection: %.3fs)\n",
456
+ sp_gc_ph_apply_obj, sp_gc_ph_apply_str, sp_gc_ph_apply_release, sp_gc_ph_wait_top); }
457
+ { extern unsigned long long sp_gc_ph_trim_req, sp_gc_ph_trim_inline; extern double sp_gc_ph_trim_inline_t;
458
+ if (sp_gc_ph_trim_req > 0)
459
+ fprintf(stderr, "[gcph] trim: %llu requests, %llu run inline (%.3fs), trimmer thread %s\n",
460
+ sp_gc_ph_trim_req, sp_gc_ph_trim_inline, sp_gc_ph_trim_inline_t, sp_gc_trimmer_on ? "on" : "off"); }
461
+ { extern unsigned long long sp_slab_rel_calls, sp_slab_rel_walked, sp_slab_rel_madv; extern double sp_slab_rel_madv_t, sp_slab_rel_sort_t;
462
+ if (sp_slab_rel_calls > 0)
463
+ fprintf(stderr, "[gcph] slab release: %llu releases walked %llu chunks, handed back %llu (madvise %.3fs, sort %.3fs)\n",
464
+ sp_slab_rel_calls, sp_slab_rel_walked, sp_slab_rel_madv, sp_slab_rel_madv_t, sp_slab_rel_sort_t); }
465
+ /* The mark, one level down, because "mark grew" has two causes that want
466
+ different answers: more ROOTS to scan and more GRAPH to trace. `fibers` is
467
+ every live fiber's saved roots, walked serially, and it grows with the
468
+ number of in-flight fibers rather than with the worker count; `scan` is the
469
+ trace that drains what the roots found, and it grows because those fibers
470
+ hold live objects. Only the first is what handing the fiber list to the
471
+ parked workers would address (#4384). The four sum to `mark` above. */
472
+ fprintf(stderr,
473
+ "[gcph] mark: roots %.3fs fibers %.3fs globals %.3fs scan %.3fs\n",
474
+ sp_gc_ph_mk_roots, sp_gc_ph_mk_fibers,
475
+ sp_gc_ph_mk_globals, sp_gc_ph_mk_scan);
476
+ }
477
+
478
+ void sp_gc_retune_object(size_t before) {
479
+ sp_gc_stats_report();
480
+ if (sp_gc_stress_pin || sp_gc_obj_budget_fixed) { sp_gc_threshold = sp_gc_threshold_init; return; }
481
+ size_t live = sp_gc_bytes;
482
+ /* The budget is what may be ALLOCATED before the next collection, and what
483
+ pays for it is what that collection COSTS. A collection marks BOTH heaps,
484
+ so an object budget taken from the object live set alone is priced off
485
+ the wrong quantity: rubys measured a ladder where the string live set
486
+ grows 26x while the object set grows 4.9x, the collection rate falls with
487
+ the object set, and the mark per request rises 2.5x (#4384).
488
+ This is the default. SPINEL_GC_OBJ_BUDGET=obj restores pricing it off
489
+ the object heap alone.
490
+
491
+ It shipped opt-in first, because the argument was sound and the evidence
492
+ was not: what had been measured at +47% was a fixed 16 MB FLOOR, which is
493
+ a different policy -- a floor stops the budget getting small, this makes
494
+ it proportional. rubys then ran both, on two emits at two concurrencies,
495
+ twice each. Pricing it off the walk reproduces the floor's throughput
496
+ (+26% to +44%) at within 5-10% of the floor's memory, and the reason to
497
+ prefer it is neither of those: it settles at 70-78 MB where the floor
498
+ pins 128, and at 227-253 MB where the floor is too small, so it is right
499
+ at both ends of a 3.3x concurrency swing. A fixed number cannot be.
500
+ Our own 61 benchmarks and optcarrot are neutral on it: same wall, RSS
501
+ within 0.5%, fps inside its spread.
502
+ What it cost, before the gate below: it widened the budget by the mark
503
+ set whether or not the mark was what the program paid for. Two synthetics
504
+ here that hold a large live string set while collecting cheaply paid
505
+ memory for nothing. */
506
+
507
+ /* ---- the gate: widen by the share of the collection the MARK is ----
508
+
509
+ alpha is that share, and the budget widens by alpha x the string live
510
+ set. A program whose collections are nearly all mark gets `walk`; one
511
+ whose collections are nearly all sweep gets `obj`; the two synthetics and
512
+ rubys' server sit at opposite ends of it rather than needing different
513
+ defaults.
514
+
515
+ COUNTS, not bytes. A cost ratio taken per byte does not carry between
516
+ programs: a cache of large strings and a churn of small arrays hold the
517
+ same megabytes with slot counts fifty times apart, which is how the first
518
+ attempt at this failed (#4384). The sweep's cost is per SLOT and the
519
+ mark's is per LIVE OBJECT, so counted, the coefficients are properties of
520
+ this code rather than of a program's allocation sizes.
521
+
522
+ And the coefficient is an ORDER, not a measurement. Measured here, mark
523
+ is ~360-560 ns an object and sweep is ~8 ns a slot serially against ~120
524
+ ns across eight workers, where the parked-worker coordination and the
525
+ string sweep fold in. Writing those numbers down would pin this machine's
526
+ ratio into the collector and be wrong on the next one. Written as the
527
+ order they sit at -- Cm/Cs is about 64 serially and about 4 in parallel --
528
+ the arithmetic is a shift and the answer barely moves: on the pair that
529
+ motivated the gate, the measured coefficients give alpha 0.012 and 0.57,
530
+ the orders give 0.016 and 0.55. The decision was never close enough for
531
+ the precision to matter, which is the argument for not claiming it. */
532
+ static size_t prev_marked = 0, prev_swept = 0;
533
+ size_t cmk = SP_GC_CTR_GET(sp_gc_ct_marked), csw = SP_GC_CTR_GET(sp_gc_ct_swept);
534
+ size_t marked = cmk > prev_marked ? cmk - prev_marked : 0;
535
+ size_t swept = csw > prev_swept ? csw - prev_swept : 0;
536
+ prev_marked = cmk; prev_swept = csw;
537
+ size_t alpha = 1024; /* nothing measured yet: widen, which is what `walk` did */
538
+ {
539
+ int nw = 1;
540
+ #ifdef SP_THREADS
541
+ nw = sp_active_workers; if (nw < 1) nw = 1;
542
+ #endif
543
+ /* The parallel sweep pays for parking and waking the workers that help it,
544
+ and folds the string sweep in, so a slot costs an order more there. */
545
+ size_t k = (nw > 1) ? 4u : 64u;
546
+ if (marked || swept) {
547
+ size_t num = k * marked, den = num + swept;
548
+ alpha = den ? (num * 1024) / den : 1024;
549
+ if (alpha > 1024) alpha = 1024;
550
+ }
551
+ }
552
+ if (sp_gc_obj_budget_mode == 0) alpha = 0;
553
+ else if (sp_gc_obj_budget_mode == 1) alpha = 1024;
554
+ sp_gc_obj_alpha1024 = alpha;
555
+ size_t walk = live;
556
+ { size_t str = sp_str_live_total();
557
+ walk += (str / 1024) * alpha + ((str % 1024) * alpha) / 1024; }
558
+ /* saturating: the live counter is a heuristic and is allowed to lag, so it
559
+ can read above the pre-collect total. Wrapping made `freed` enormous, the
560
+ productive-sweep test went false, and the threshold was taken from a live
561
+ count that had itself wrapped. */
562
+ size_t freed = before > live ? before - live : 0;
563
+ /* The productivity test stays on the OBJECT numbers -- this sweep is what
564
+ frees object bytes, and whether it was worth running is a question about
565
+ those. Only the budget it sets can be sized from the whole walk. */
566
+ if (freed < before / 4) { sp_gc_threshold = sp_gc_sat_mul(before + (walk - live), 2); }
567
+ else if (live > 0) { sp_gc_threshold = sp_gc_sat_mul(walk, 2); if (sp_gc_threshold < sp_gc_threshold_init) sp_gc_threshold = sp_gc_threshold_init; }
568
+ else { sp_gc_threshold = sp_gc_threshold_init; }
569
+ }
570
+ /* `before` and `after` are both the WHOLE live string set -- young plus old --
571
+ the way sp_gc_retune_object reads the whole object heap. Sizing from the
572
+ young generation alone left this budget blind to the old one: a render
573
+ promotes what it keeps, so `after` read as ~0 however much string data the
574
+ process was holding, and the trigger fell back to its floor after every
575
+ sweep. What it gates is a whole-heap stop-the-world, old generation
576
+ included, so the budget that pays for a collection has to see the bytes the
577
+ mark walks. Both sides have to move together -- a whole-heap `after` against
578
+ a young-only `before` reads as an unproductive sweep every time.
579
+
580
+ The threshold is the PER-WORKER budget (each worker triggers on its own
581
+ list, so the aggregate heap is bounded by N * threshold). Retune on the
582
+ per-worker average so the budget tracks a single worker's share and does NOT
583
+ inflate by N each cycle -- retuning on the aggregate would grow it
584
+ geometrically for long-lived strings. The single-threaded build works in
585
+ absolute bytes (N == 1). */
586
+ static size_t sp_str_gate_old = 0; /* the old total at the gate, for `before` */
587
+ /* Young bytes to leave out of the retune's "after": the concurrent sweep
588
+ retunes at the next barrier, by which time the young lists hold a cycle of
589
+ new allocation that the swept generation never contained. */
590
+ static size_t sp_str_retune_young_exclude = 0;
591
+ static void sp_str_retune(size_t before, size_t promoted) {
592
+ if (sp_gc_stress_pin || sp_gc_str_budget_fixed) { sp_str_threshold = sp_str_threshold_init; return; }
593
+ #ifdef SP_THREADS
594
+ int nw = sp_active_workers; if (nw < 1) nw = 1;
595
+ size_t yb = sp_str_bytes_total();
596
+ yb = yb > sp_str_retune_young_exclude ? yb - sp_str_retune_young_exclude : 0;
597
+ size_t after = (yb + sp_str_old_total()) / (size_t)nw;
598
+ before = (before + sp_str_gate_old) / (size_t)nw;
599
+ (void)promoted; /* already inside old_total by the time we run */
600
+ #else
601
+ /* sp_str_old_total() already carries what this sweep promoted, so a promoted
602
+ string is counted once, as the survivor it is: leaving it out would read as
603
+ a very productive sweep and shrink the trigger, collecting harder and
604
+ harder as the old generation grows. */
605
+ size_t after = sp_str_heap_bytes + sp_str_old_total();
606
+ before += sp_str_gate_old;
607
+ #endif
608
+ size_t freed = before > after ? before - after : 0; /* saturating; see sp_gc_retune_object */
609
+ if (freed < before / 4) { sp_str_threshold = sp_gc_sat_mul(before, 2); }
610
+ else if (after > 0) { sp_str_threshold = sp_gc_sat_mul(after, 2); if (sp_str_threshold < sp_str_threshold_init) sp_str_threshold = sp_str_threshold_init; }
611
+ else { sp_str_threshold = sp_str_threshold_init; }
612
+ }
613
+
614
+ /* Collect and re-tune. The caller guarantees exclusive heap access: the
615
+ single-threaded allocators hold sp_heap_lock; the threaded build runs the
616
+ _all variant under stop-the-world (every other worker parked), via
617
+ sp_stw_collect, so neither heap is mutated during the sweep. The object and
618
+ string variants retune only their own threshold, matching the original
619
+ per-heap inline collection so the single-threaded path stays byte-identical;
620
+ _all retunes both since one stop-the-world sweeps both heaps. */
621
+ void sp_gc_collect_retune(void) {
622
+ /* the retune hook inside sp_gc_collect adjusts the object threshold */
623
+ sp_gc_collect();
624
+ sp_gc_enforce_mem_limit();
625
+ }
626
+ void sp_str_collect_retune(void) {
627
+ /* the gated sweep hook inside sp_gc_collect retunes the string threshold */
628
+ sp_gc_collect();
629
+ }
630
+ void sp_gc_collect_retune_all(void) {
631
+ sp_gc_collect();
632
+ sp_gc_enforce_mem_limit();
633
+ }
634
+ /* Either heap over its trigger? Used by sp_stw_collect to skip a redundant
635
+ stop-the-world when another worker just collected. */
636
+ int sp_gc_collection_wanted(void) {
637
+ /* Everything read atomically: this runs before the world is stopped
638
+ (sp_stw_collect's early-out), concurrent with other workers' relaxed
639
+ counter adds AND with the allocators' one-shot GC-stress threshold
640
+ write (heap-locked, but this reader holds only the sched lock). The
641
+ retune writes are plain but never overlap: they run while g_stw_active
642
+ is set, and this is only called with it clear, under the same lock
643
+ that publishes it. A stale read at worst skips one redundant
644
+ collection. */
645
+ #ifdef SP_THREADS
646
+ /* The string trigger is PER WORKER (sp_str_alloc compares this worker's own
647
+ bytes), so the justified-now condition on the aggregate is N * threshold.
648
+ Comparing the aggregate against the bare threshold made this true almost
649
+ immediately at N > 1, so the early-out never suppressed a redundant stop
650
+ and workers queued up behind each other's collections. */
651
+ { int nw = sp_active_workers; if (nw < 1) nw = 1; if (nw > SP_MAX_WORKERS) nw = SP_MAX_WORKERS;
652
+ return SP_GC_CTR_GET(sp_gc_bytes) > SP_GC_CTR_GET(sp_gc_threshold) ||
653
+ sp_str_bytes_total() > SP_GC_CTR_GET(sp_str_threshold) * (size_t)nw; }
654
+ #else
655
+ return SP_GC_CTR_GET(sp_gc_bytes) > SP_GC_CTR_GET(sp_gc_threshold) ||
656
+ SP_GC_CTR_GET(sp_str_heap_bytes) > SP_GC_CTR_GET(sp_str_threshold);
657
+ #endif
658
+ }
659
+
660
+ /* sp_gc_alloc lives in lib/sp_slab.c: the allocation fast path is one
661
+ function there, the slab's claim and the collector's bookkeeping in one
662
+ frame. */
663
+ void *sp_gc_alloc_nogc(size_t sz, void (*fin)(void *), void (*scn)(void *)) {
664
+ size_t need = sizeof(sp_gc_hdr) + sz;
665
+ sp_gc_hdr *h = (sp_gc_hdr *)sp_slab_alloc(need);
666
+ h->finalize = fin; h->scan = scn; h->size = need; h->marked = 0; h->old = 0; h->dirty = 0;
667
+ if (fin) sp_slab_set_fin(h);
668
+ if (sp_alloc_report_on) sp_alloc_report_count((void *)scn, sz);
669
+ SP_HEAP_LOCK();
670
+ SP_GC_HEAP_PUSH(h); sp_gc_bytes_add(need);
671
+ SP_HEAP_UNLOCK();
672
+ return (char *)h + sizeof(sp_gc_hdr);
673
+ }
674
+
675
+ SP_TLS struct sp_str_lcache_entry sp_str_lcache[SP_STR_LCACHE_SIZE];
676
+ SP_TLS void *_sp_ret_strbuf;
677
+
678
+ void sp_str_lcache_clear(void) {
679
+ for (unsigned i = 0; i < SP_STR_LCACHE_SIZE; i++) sp_str_lcache[i].s = NULL;
680
+ }
681
+
682
+ /* sp_mark_string (sp_gc.h) flips a live string's marker 0xfe->0xfc during the
683
+ mark phase; sweep keeps the marked ones and frees the rest. A frozen heap
684
+ string (0xf1) is kept across sweeps (a live frozen global must survive, and
685
+ frozen literals are immortal). A collectable frozen one goes 0xfa->0xf8 and
686
+ is swept like 0xfe. */
687
+ /* Sweep one worker's list head (or the single st list). Runs under stop-the-
688
+ world (threaded) or the held heap lock (st), so no concurrent push races it.
689
+ `bytes` is decremented per freed string to keep the live-byte count in step. */
690
+ /* Sweep the YOUNG list: free what the mark phase did not reach, and move every
691
+ survivor onto the old list. The mark reset (0xfc -> 0xfe) happens at the move,
692
+ so a promoted string behaves exactly as it did before -- the next mark phase
693
+ re-marks it if it is still reachable, and the next MAJOR sweep frees it if
694
+ not. Survival of a single sweep is the whole promotion test: a string still
695
+ alive when the young generation filled is, empirically, one the program is
696
+ holding rather than one it is churning through.
697
+ `promoted` accumulates the moved bytes so the caller's threshold retune can
698
+ count them as survivors and not mistake promotion for reclamation. */
699
+ /* Sweep one young list into LOCAL results -- the survivors as a list with
700
+ its tail, the bytes they carry, and the bytes the list held in all -- so
701
+ that several young lists of one worker can be swept at once by different
702
+ workers and their results spliced into the slot afterwards
703
+ (sp_str_sweep_young_apply). */
704
+ static void sp_str_sweep_young(sp_str_hdr **head, sp_str_hdr **keep_head, sp_str_hdr **keep_tail,
705
+ size_t *moved_out, size_t *held_out) {
706
+ sp_str_hdr *h = *head;
707
+ sp_str_hdr *keep = NULL, *tail = NULL;
708
+ size_t moved = 0, held = 0;
709
+ while (h) {
710
+ sp_str_hdr *next = h->next;
711
+ SP_PREFETCH(next);
712
+ char *body = (char *)(h + 1);
713
+ unsigned char m = (unsigned char)body[0];
714
+ held += h->size & SP_STR_SIZE_MASK;
715
+ if (m == 0xfc || m == 0xf1 || m == 0xf8) {
716
+ /* Beside the mutators (a sweeper thread) the reset is a compare-and-
717
+ swap: a `freeze` that lands on the same byte in the same moment wins,
718
+ where a plain store could put its 0xfe over the 0xf1. */
719
+ if (m == 0xfc) {
720
+ if (sp_gc_in_sweeper) { unsigned char ex = 0xfc; SP_ATOMIC_CAS((unsigned char *)body, &ex, (unsigned char)0xfe, 0, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE); }
721
+ else body[0] = (char)0xfe;
722
+ }
723
+ else if (m == 0xf8) SP_ATOMIC_STORE((unsigned char *)body, (unsigned char)0xfa, __ATOMIC_RELAXED);
724
+ h->next = keep;
725
+ if (!keep) tail = h;
726
+ keep = h;
727
+ moved += h->size & SP_STR_SIZE_MASK;
728
+ }
729
+ else {
730
+ sp_str_lcache_drop(body + 1);
731
+ sp_slab_free(h);
732
+ }
733
+ h = next;
734
+ }
735
+ *head = NULL;
736
+ *keep_head = keep; *keep_tail = tail;
737
+ *moved_out = moved; *held_out = held;
738
+ }
739
+ /* Splice one young list's survivors into the slot's old generation and
740
+ settle the counters. Single-threaded per slot: the caller serializes. */
741
+ static void sp_str_sweep_young_apply(sp_str_hdr *keep, sp_str_hdr *tail, size_t moved, size_t held,
742
+ size_t *bytes, sp_str_hdr **old_head, size_t *old_bytes,
743
+ size_t *promoted) {
744
+ if (keep) { tail->next = *old_head; *old_head = keep; }
745
+ *bytes -= held;
746
+ *old_bytes += moved;
747
+ *promoted += moved;
748
+ }
749
+ static void sp_str_sweep_young_into(sp_str_hdr **head, size_t *bytes,
750
+ sp_str_hdr **old_head, size_t *old_bytes,
751
+ size_t *promoted) {
752
+ sp_str_hdr *keep, *tail; size_t moved, held;
753
+ sp_str_sweep_young(head, &keep, &tail, &moved, &held);
754
+ sp_str_sweep_young_apply(keep, tail, moved, held, bytes, old_head, old_bytes, promoted);
755
+ }
756
+
757
+ /* ---- Generational verifier, string side (SPINEL_GC_VERIFY_GEN=1) ----
758
+ The object verifier snapshots young OBJECTS a minor mark did not reach and
759
+ re-marks whole-heap to see which of them the full mark does; each one is
760
+ held only through an old object whose barrier is missing. Strings need the
761
+ same check and cannot share that machinery: their mark is a byte on the
762
+ string itself (0xfe unmarked, 0xfc marked), not a generation stamp on a
763
+ header. Snapshot the young strings still unmarked after the minor, then read
764
+ the same byte back after the whole-heap mark. */
765
+ static const char **sp_str_vcand = NULL;
766
+ static size_t sp_str_vcand_n = 0, sp_str_vcand_cap = 0;
767
+ static void sp_str_vcand_push(const char *body) {
768
+ if (sp_str_vcand_n == sp_str_vcand_cap) {
769
+ size_t c = sp_str_vcand_cap ? sp_str_vcand_cap * 2 : 1024;
770
+ const char **n = (const char **)realloc(sp_str_vcand, c * sizeof(const char *));
771
+ if (!n) return;
772
+ sp_str_vcand = n; sp_str_vcand_cap = c;
773
+ }
774
+ sp_str_vcand[sp_str_vcand_n++] = body;
775
+ }
776
+ static void sp_str_vscan(sp_str_hdr *h) {
777
+ for (; h; h = h->next) {
778
+ const char *body = (const char *)(h + 1);
779
+ if ((unsigned char)body[0] == 0xfe) sp_str_vcand_push(body);
780
+ }
781
+ }
782
+ /* a slab string's mark is a bit in its chunk, a list string's the byte */
783
+ static int sp_str_vmarked(const char *body) {
784
+ const sp_str_hdr *h = ((const sp_str_hdr *)body) - 1;
785
+ if (sp_slab_owns(h)) return sp_slab_is_marked(h);
786
+ return (unsigned char)body[0] == 0xfc;
787
+ }
788
+ static void sp_str_vunmark(const char *body) {
789
+ const sp_str_hdr *h = ((const sp_str_hdr *)body) - 1;
790
+ if (sp_slab_owns(h)) sp_slab_unmark(h);
791
+ else ((char *)body)[0] = (char)0xfe;
792
+ }
793
+ static void sp_str_vscan_slab_cb(void *hdr, void *arg) {
794
+ (void)arg;
795
+ const char *body = (const char *)((sp_str_hdr *)hdr + 1);
796
+ if (!sp_str_vmarked(body)) sp_str_vcand_push(body);
797
+ }
798
+ void sp_str_verify_begin(void) {
799
+ sp_str_vcand_n = 0;
800
+ sp_slab_each_string(1, 0, sp_str_vscan_slab_cb, NULL);
801
+ #ifdef SP_THREADS
802
+ { int n = sp_active_workers; if (n < 1) n = 1; if (n > SP_MAX_WORKERS) n = SP_MAX_WORKERS;
803
+ for (int i = 0; i < n; i++) for (int sub = 0; sub < SP_STR_YSUB; sub++) sp_str_vscan(sp_str_wslot[i].young[sub]); }
804
+ #else
805
+ sp_str_vscan(sp_str_heap);
806
+ #endif
807
+ }
808
+ size_t sp_str_verify_end(void) {
809
+ size_t leaked = 0;
810
+ for (size_t i = 0; i < sp_str_vcand_n; i++)
811
+ if (sp_str_vmarked(sp_str_vcand[i])) sp_str_vcand[leaked++] = sp_str_vcand[i];
812
+ sp_str_vcand_n = leaked; /* keep just the leaked ones, for the holder probe */
813
+ return leaked;
814
+ }
815
+ /* Holder probe: unmark the leaked strings, let one old object's scan run, and
816
+ see whether it re-marks any. Same shape as the object-side probe, and with
817
+ the same limit -- it names the DIRECT holder, since sp_gc_mark is inert
818
+ while the probe is armed. */
819
+ void sp_str_verify_probe_arm(void) {
820
+ for (size_t i = 0; i < sp_str_vcand_n; i++) sp_str_vunmark(sp_str_vcand[i]);
821
+ }
822
+ int sp_str_verify_probe_hit(void) {
823
+ for (size_t i = 0; i < sp_str_vcand_n; i++)
824
+ if (sp_str_vmarked(sp_str_vcand[i])) return 1;
825
+ return 0;
826
+ }
827
+ void sp_str_verify_probe_done(void) { sp_str_vcand_n = 0; }
828
+
829
+ /* Sweep the OLD list in place. Survivors stay old; nothing is demoted. */
830
+ static void sp_str_sweep_old(sp_str_hdr **head, size_t *bytes) {
831
+ sp_str_hdr **pp = head;
832
+ while (*pp) {
833
+ sp_str_hdr *h = *pp;
834
+ SP_PREFETCH(h->next);
835
+ char *body = (char *)(h + 1);
836
+ unsigned char m = (unsigned char)body[0];
837
+ if (m == 0xfc) {
838
+ if (sp_gc_in_sweeper) { unsigned char ex = 0xfc; SP_ATOMIC_CAS((unsigned char *)body, &ex, (unsigned char)0xfe, 0, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE); }
839
+ else body[0] = (char)0xfe;
840
+ pp = &h->next;
841
+ }
842
+ else if (m == 0xf1) { pp = &h->next; }
843
+ else if (m == 0xf8) { SP_ATOMIC_STORE((unsigned char *)body, (unsigned char)0xfa, __ATOMIC_RELAXED); pp = &h->next; }
844
+ else {
845
+ *pp = h->next;
846
+ *bytes -= h->size & SP_STR_SIZE_MASK;
847
+ sp_str_lcache_drop(body + 1);
848
+ sp_slab_free(h);
849
+ }
850
+ }
851
+ }
852
+
853
+ /* `major` also walks the old generation. Returns the bytes promoted, for the
854
+ threshold retune. */
855
+ static size_t sp_str_sweep_gen(int major) {
856
+ size_t promoted = 0;
857
+ #ifdef SP_THREADS
858
+ int n = sp_active_workers; if (n < 1) n = 1; if (n > SP_MAX_WORKERS) n = SP_MAX_WORKERS;
859
+ for (int i = 0; i < n; i++) {
860
+ if (major) sp_str_sweep_old(&sp_str_wslot[i].old, &sp_str_wslot[i].old_bytes);
861
+ for (int sub = 0; sub < SP_STR_YSUB; sub++)
862
+ sp_str_sweep_young_into(&sp_str_wslot[i].young[sub], &sp_str_wslot[i].young_bytes,
863
+ &sp_str_wslot[i].old, &sp_str_wslot[i].old_bytes, &promoted);
864
+ /* the young counter also carried the slab strings, swept by the chunk
865
+ sweep of this same stop-the-world cycle: the generation is empty now */
866
+ SP_GC_CTR_SET(sp_str_wslot[i].young_bytes, 0);
867
+ sp_str_wslot[i].ask_at = 0;
868
+ }
869
+ #else
870
+ if (major) sp_str_sweep_old(&sp_str_old, &sp_str_old_bytes);
871
+ sp_str_sweep_young_into(&sp_str_heap, &sp_str_heap_bytes,
872
+ &sp_str_old, &sp_str_old_bytes, &promoted);
873
+ SP_GC_CTR_SET(sp_str_heap_bytes, 0);
874
+ #endif
875
+ return promoted;
876
+ }
877
+
878
+ /* Full sweep of both generations. GC.start and the shutdown paths want every
879
+ unreachable string gone, not just the young ones. */
880
+ void sp_str_sweep(void) {
881
+ (void)sp_str_sweep_gen(1);
882
+ }
883
+
884
+ /* PolyArray free-list pool (see sp_alloc.h). Bounded so a burst does not pin
885
+ memory forever; an over-cap or oversized-buffer entry frees normally. The
886
+ scan/finalize hooks stay valid on recycled headers -- only `next` and the
887
+ heap-byte accounting change hands.
888
+ PER THREAD. It was one Treiber stack for the process, pushed by every
889
+ owner's sweep and popped by every allocating worker, and on a 32-core
890
+ server the exchange on its head was the single hottest symbol (8.6% of
891
+ the process, ahead of memmove). A worker sweeps its own young list, so
892
+ what it recycles it can hand back to itself with no atomics at all; the
893
+ sweeper threads and the barrier helpers recycle into their own, which
894
+ the cap bounds. The cap is per thread for the same reason. */
895
+ SP_TLS sp_gc_hdr *sp_polyarr_pool_head = NULL;
896
+ SP_TLS long sp_polyarr_pool_count = 0;
897
+ /* The cap is per thread, so threaded it is a fraction of what the one
898
+ process-wide pool held (up to 64k, which a tree benchmark churning tens
899
+ of thousands of arrays a cycle leaned on): 8k a worker bounds thirty
900
+ workers where 64k each would not, and single-threaded the one pool keeps
901
+ the old cap. */
902
+ #ifdef SP_THREADS
903
+ #define SP_POLYARR_POOL_MAX 8192
904
+ #else
905
+ #define SP_POLYARR_POOL_MAX 65536
906
+ #endif
907
+ #define SP_POLYARR_POOL_KEEP_CAP 64 /* don't retain unusually large buffers */
908
+ void sp_PolyArray_pool_recycle(sp_gc_hdr *h) {
909
+ sp_PolyArray *a = (sp_PolyArray *)((char *)h + sizeof(sp_gc_hdr));
910
+ if (sp_polyarr_pool_count >= SP_POLYARR_POOL_MAX || a->cap > SP_POLYARR_POOL_KEEP_CAP) {
911
+ if (a->data != a->inl) sp_pl_free(a->data);
912
+ sp_slab_free(h);
913
+ return;
914
+ }
915
+ h->next = sp_polyarr_pool_head;
916
+ sp_polyarr_pool_head = h;
917
+ sp_polyarr_pool_count++;
918
+ }
919
+
920
+ /* String sweep, gated on the string heap's own trigger. The object collector
921
+ used to run the full live-string walk on EVERY collection, making each one
922
+ O(live strings) -- the dominant cost of allocation-heavy programs (#2922
923
+ profiling on BabyStark: 2.9s of an 8.0s GC total). Skipping is safe:
924
+ string marks accumulate, so a dead string at worst survives until the next
925
+ string sweep (delayed reclamation, not a leak); the sweep itself resets
926
+ marks for the next cycle. Retuning here (with the collector-side retunes
927
+ removed) keeps the trigger tracking the live size in one place. */
928
+ /* The gate, split so the per-worker middle can run on the workers themselves.
929
+ `begin` decides (and remembers `before` for the retune), `one` sweeps one
930
+ worker's two lists, `end` re-aims the thresholds. The serial driver below
931
+ still calls all three in a row; the scheduler interleaves the middle across
932
+ the parked workers instead. */
933
+ static size_t sp_str_gate_before = 0;
934
+ #ifdef SP_THREADS
935
+ int sp_str_par_done = 0; /* the workers already did it for this collection */
936
+ #endif
937
+ /* The decision sp_str_sweep_begin will make this cycle, without its side
938
+ effects (the cycle counter, the shape sample): over the trigger, and a major
939
+ on schedule or forced by the old generation's growth. */
940
+ static int sp_str_major_due(void) {
941
+ #ifdef SP_THREADS
942
+ size_t before = sp_str_bytes_total();
943
+ #else
944
+ size_t before = SP_GC_CTR_GET(sp_str_heap_bytes);
945
+ #endif
946
+ if (before <= SP_GC_CTR_GET(sp_str_threshold)) return 0;
947
+ if (sp_str_old_total() > sp_str_old_threshold) return 1;
948
+ if (!sp_gc_str_major_sched) return 0;
949
+ return (sp_str_sweep_cycle % (unsigned)sp_str_major_interval) == 0;
950
+ }
951
+ int sp_str_sweep_begin(int *major) {
952
+ #ifdef SP_THREADS
953
+ size_t before = sp_str_bytes_total();
954
+ #else
955
+ size_t before = SP_GC_CTR_GET(sp_str_heap_bytes);
956
+ #endif
957
+ if (before <= SP_GC_CTR_GET(sp_str_threshold)) return 0;
958
+ sp_str_gate_before = before;
959
+ sp_str_gate_old = sp_str_old_total();
960
+ /* Only once a major has run: before that the minimum is the empty heap, not
961
+ "what a major can leave", and a min of zero makes the ratio meaningless
962
+ (it reads 0.0x rather than large). */
963
+ if (sp_gc_str_majors > 0) {
964
+ sp_str_shape[sp_str_shape_n % SP_STR_SHAPE_MAX] = sp_str_gate_old;
965
+ sp_str_shape_n++;
966
+ }
967
+ /* Walk the old generation only once it has itself grown past a threshold,
968
+ then re-aim that threshold at what survived. Between majors, old strings
969
+ that die are reclaimed late -- the same delayed-reclamation trade this
970
+ gate already makes for the whole heap, one level up. */
971
+ /* On schedule, or forced by growth the schedule did not keep up with. The
972
+ forced arm is what the size test used to be on its own; behind a schedule
973
+ it is a backstop, which is the whole difference. */
974
+ if (!sp_gc_str_major_sched) { *major = sp_str_old_total() > sp_str_old_threshold; }
975
+ else {
976
+ int sched = (sp_str_sweep_cycle % (unsigned)sp_str_major_interval) == 0;
977
+ sp_str_sweep_cycle++;
978
+ sp_str_major_forced = 0;
979
+ if (!sched && sp_str_old_total() > sp_str_old_threshold) {
980
+ sched = 1; sp_str_major_forced = 1;
981
+ }
982
+ *major = sched;
983
+ }
984
+ return 1;
985
+ }
986
+ void sp_str_sweep_end_excluding(int major, size_t promoted, size_t young_exclude) {
987
+ sp_str_retune_young_exclude = young_exclude;
988
+ sp_str_sweep_end(major, promoted);
989
+ sp_str_retune_young_exclude = 0;
990
+ }
991
+ void sp_str_sweep_end(int major, size_t promoted) {
992
+ /* the slab strings this cycle's mark promoted joined old too (their
993
+ generation is swept every cycle, gate or no gate; sp_str_mark_settle) */
994
+ promoted += sp_gc_mk_str_young_bytes;
995
+ if (major) {
996
+ sp_gc_str_majors++;
997
+ size_t old_after = sp_str_old_total();
998
+ /* SPINEL_GC_STR_MAJOR=fixed holds both the backstop and the cadence where
999
+ the floor put them, which is what makes a policy measurable against
1000
+ itself. SPINEL_GC_STR_MAJOR=size turns the schedule off entirely and
1001
+ leaves the size test as the whole policy, which is what shipped before. */
1002
+ /* The old set this major walked is what it KEPT: sp_str_old_total() after
1003
+ the sweep also carries what the same sweep promoted out of young, and
1004
+ that is not a survivor of anything yet. Both re-aims below are taken
1005
+ over kept, for the reason the ratio's note gives. */
1006
+ size_t kept = old_after > promoted ? old_after - promoted : 0;
1007
+ if (!sp_gc_str_major_fixed) {
1008
+ /* Re-baseline the backstop: twice what this major KEPT, the bound
1009
+ sp_gc_collect keeps for the object old generation. Aiming it at
1010
+ old_after instead let the strings a request had in flight at the sweep
1011
+ set the gate: on Campfire every sweep promotes 50-70 MB of them, they
1012
+ die milliseconds later, and a gate of twice kept-plus-promoted held
1013
+ three sweeps of that garbage (old 290 MB against 20 MB kept) -- and the
1014
+ young trigger, which retunes on the whole string heap, followed it up
1015
+ to 12 MB a worker. Aimed at kept, the next sweep whose promotion
1016
+ outgrows the live set is a major, and the old generation stays within
1017
+ one sweep's promotion of what is live. */
1018
+ sp_str_old_threshold = sp_gc_sat_mul(kept, 2);
1019
+ if (sp_str_old_threshold < sp_str_old_threshold_init)
1020
+ sp_str_old_threshold = sp_str_old_threshold_init;
1021
+ /* Adapt the CADENCE from the survival RATIO. A ratio is scale-free, so
1022
+ unlike a size it cannot carry the last major's inflation into the next
1023
+ one. A major FORCED by the backstop is not a sample taken on schedule:
1024
+ growth that is still live reads as ~100% survival and would lengthen
1025
+ the cadence that was already too short to hold it. So a forced major
1026
+ shortens and does not adapt -- sp_gc.c says this for the object heap,
1027
+ and said it first. */
1028
+ if (!sp_gc_str_major_sched) { /* the size gate is the whole policy */ }
1029
+ else if (sp_str_major_forced) {
1030
+ if (sp_str_major_interval > SP_STR_MAJOR_INTERVAL) sp_str_major_interval /= 2;
1031
+ }
1032
+ else if (sp_str_gate_old > 0) {
1033
+ /* The ratio has to be taken over the OLD SET THIS MAJOR WALKED, and
1034
+ sp_str_old_total() is not that: it already carries what this same
1035
+ sweep promoted out of young (the note in sp_str_retune says so for
1036
+ the same reason). Counting promotions as survivors reads a healthy
1037
+ reclamation as ~100% survival and lengthens the cadence -- the size
1038
+ gate's contamination, arriving a second time in ratio form. Measured
1039
+ at a 4 MB floor it walked the interval up to 32 and left 61 MB of old
1040
+ against a 12 MB live set. */
1041
+ size_t before = sp_str_gate_old;
1042
+ if (kept > before - (before >> 2)) { /* >75% survived */
1043
+ if (sp_str_major_interval < SP_STR_MAJOR_INTERVAL_MAX) sp_str_major_interval *= 2;
1044
+ }
1045
+ else if (kept < (before >> 1)) { /* <50% survived */
1046
+ if (sp_str_major_interval > SP_STR_MAJOR_INTERVAL) sp_str_major_interval /= 2;
1047
+ }
1048
+ }
1049
+ }
1050
+ }
1051
+ sp_str_retune(sp_str_gate_before, promoted);
1052
+ }
1053
+ #ifdef SP_THREADS
1054
+ /* One worker's own string lists. Freeing a string on the worker that allocated
1055
+ it keeps the block in the arena it came from: the collector doing all eight
1056
+ workers' frees turned every one of them into a cross-arena free, which is
1057
+ the slow path in glibc and in every other thread-caching allocator. Each
1058
+ worker also clears its own length cache, whose entries are keyed by the
1059
+ addresses this sweep is about to recycle. */
1060
+ /* The list forms, for the concurrent driver (sp_sched.c): the lists were
1061
+ detached from their slots under the barrier, so they arrive as plain
1062
+ pointers, and the old sweep answers the bytes it freed for the slot's
1063
+ counter to take at the next barrier. */
1064
+ void sp_str_sweep_young_list(sp_str_hdr **head, sp_str_hdr **keep, sp_str_hdr **tail, size_t *moved, size_t *held) {
1065
+ sp_str_sweep_young(head, keep, tail, moved, held);
1066
+ }
1067
+ size_t sp_str_sweep_old_list(sp_str_hdr **head) {
1068
+ size_t bytes = (size_t)-1 / 2; /* a counter the sweep decrements; the difference is what it freed */
1069
+ size_t start = bytes;
1070
+ sp_str_sweep_old(head, &bytes);
1071
+ return start - bytes;
1072
+ }
1073
+ /* The sweep tasks of one slot, for the parallel driver (sp_sched.c): the old
1074
+ list on a major, and each young list into local results that
1075
+ sp_str_sweep_young_done splices in once every task of the slot is over. */
1076
+ void sp_str_sweep_old_one(int wid) {
1077
+ sp_str_sweep_old(&sp_str_wslot[wid].old, &sp_str_wslot[wid].old_bytes);
1078
+ }
1079
+ void sp_str_sweep_young_one(int wid, int sub, sp_str_hdr **keep, sp_str_hdr **tail,
1080
+ size_t *moved, size_t *held) {
1081
+ sp_str_sweep_young(&sp_str_wslot[wid].young[sub], keep, tail, moved, held);
1082
+ }
1083
+ void sp_str_sweep_young_done(int wid, sp_str_hdr *keep, sp_str_hdr *tail, size_t moved, size_t held,
1084
+ size_t *promoted) {
1085
+ sp_str_sweep_young_apply(keep, tail, moved, held, &sp_str_wslot[wid].young_bytes,
1086
+ &sp_str_wslot[wid].old, &sp_str_wslot[wid].old_bytes, promoted);
1087
+ }
1088
+ #endif
1089
+ static void sp_str_sweep_gated(void) {
1090
+ #ifdef SP_THREADS
1091
+ if (sp_str_par_done) { sp_str_par_done = 0; return; }
1092
+ #endif
1093
+ int major = 0;
1094
+ if (!sp_str_sweep_begin(&major)) return;
1095
+ /* the old list holds the strings the minor mark could not reach */
1096
+ if (sp_gc_str_minor_only) major = 0;
1097
+ size_t promoted = sp_str_sweep_gen(major);
1098
+ sp_str_sweep_end(major, promoted);
1099
+ }
1100
+
1101
+ /* Non-inline sp_str_alloc, for a TU that cannot include sp_alloc.h.
1102
+ lib/sp_bigint.c is the one: it pulls mruby_shim.h, whose sp_bool disagrees
1103
+ with sp_types.h's, so the header cannot be added alongside. Its Integer#to_s
1104
+ still has to answer a string-heap string like every other producer (#3396). */
1105
+ char *sp_str_alloc_ext(size_t len) { return sp_str_alloc(len); }
1106
+
1107
+ /* Wire string sweep into the object collector. Runs before main, so the hook is
1108
+ set before the first allocation can trigger a collection. */
1109
+ SP_CONSTRUCTOR static void sp_alloc_install_hooks(void) {
1110
+ sp_gc_str_sweep_hook = sp_str_sweep_gated;
1111
+ sp_gc_str_major_due_hook = sp_str_major_due;
1112
+ sp_gc_obj_retune_hook = sp_gc_retune_object;
1113
+ sp_alloc_floors_from_env();
1114
+ }
1115
+
1116
+ /* Float#to_s / #inspect (declared in sp_alloc.h): shortest round-trip decimal.
1117
+ sp_float_shortest gives the shortest significant digits + decimal exponent
1118
+ with no locale dependency (pure integer arithmetic; see sp_dtoa.c); the
1119
+ fixed vs scientific layout is Ruby's Float#to_s rule (which differs from
1120
+ %g's), preserved from the previous strtod-probe implementation. */
1121
+ const char *sp_float_to_s(sp_float f) {
1122
+ if(f!=f){char*r=sp_str_alloc_raw(4);r[0]='N';r[1]='a';r[2]='N';r[3]=0;return r;}
1123
+ if(f==HUGE_VAL||f==-HUGE_VAL){if(f<0){char*r=sp_str_alloc_raw(10);memcpy(r,"-Infinity",10);return r;}char*r=sp_str_alloc_raw(9);memcpy(r,"Infinity",9);return r;}
1124
+ if(f==0.0){if(signbit(f)){char*r=sp_str_alloc_raw(5);memcpy(r,"-0.0",5);return r;}char*r=sp_str_alloc_raw(4);memcpy(r,"0.0",4);return r;}
1125
+ int neg = signbit(f);
1126
+ char digits[32]; int dlen;
1127
+ int exp = sp_float_shortest(neg ? -f : f, digits, &dlen);
1128
+ int decpt = exp + 1; /* number of digits before the decimal point in fixed form */
1129
+ char *out=sp_str_alloc_raw(64);int o=0;
1130
+ if(neg)out[o++]='-';
1131
+ /* fixed notation when the point sits within the digits (a fractional part,
1132
+ dlen>decpt) OR the integer part is <= 15 digits; a longer integer-valued
1133
+ value (dlen<=decpt, decpt>15) prints scientific like CRuby (#2593). */
1134
+ if(decpt>0&&(decpt<=15||dlen>decpt)){
1135
+ if(decpt<dlen){memcpy(out+o,digits,decpt);o+=decpt;out[o++]='.';memcpy(out+o,digits+decpt,dlen-decpt);o+=(dlen-decpt);}
1136
+ else{memcpy(out+o,digits,dlen);o+=dlen;for(int i=dlen;i<decpt;i++)out[o++]='0';out[o++]='.';out[o++]='0';}
1137
+ }
1138
+ else if(decpt<=0&&decpt>-4){
1139
+ out[o++]='0';out[o++]='.';for(int i=decpt;i<0;i++)out[o++]='0';memcpy(out+o,digits,dlen);o+=dlen;
1140
+ }
1141
+ else{
1142
+ out[o++]=digits[0];out[o++]='.';
1143
+ if(dlen==1)out[o++]='0';else{memcpy(out+o,digits+1,dlen-1);o+=(dlen-1);}
1144
+ out[o++]='e';int e=decpt-1;
1145
+ if(e>=0)out[o++]='+';else{out[o++]='-';e=-e;}
1146
+ if(e<10){out[o++]='0';out[o++]=(char)('0'+e);}
1147
+ else o+=snprintf(out+o,16,"%d",e);
1148
+ }
1149
+ out[o]=0;sp_str_set_len(out,(size_t)o);return out;
1150
+ }
1151
+
1152
+ /* ---- SPINEL_ALLOC_REPORT: deterministic allocation counters (#1336) ----
1153
+ Env-var gated (set to 1 or an output path); zero work when off beyond one
1154
+ predictable branch at each allocation entry point. Counters key on the
1155
+ object's scan callback (the de-facto type identity); sp_alloc_report_tag
1156
+ attaches human names (builtins + user classes, registered by the generated
1157
+ prologue when the gate is on). Strings count separately (no scan fn).
1158
+ Dump: folded `alloc;<Type> <count>` lines plus `# bytes` comments, to the
1159
+ env value as a path, or stderr when it is "1". No signals, no allocation
1160
+ in the hot path, portable (plain counters + atexit). */
1161
+ int sp_alloc_report_on = 0;
1162
+ static int sp_alloc_sites_on = 0;
1163
+ typedef struct { void *key; void *site; unsigned long long count, bytes; } sp_AllocStat;
1164
+ /* Sized for the per-SITE case, which is what fills this table: one entry per
1165
+ (type, site) pair rather than one per type. Strings alone reach into the
1166
+ hundreds of sites on a Rails-scale app, and a full table silently merges
1167
+ into the home slot -- the one failure mode that would quietly misattribute
1168
+ the numbers this feature exists to report. BSS, so the untouched tail costs
1169
+ nothing when the report is off. */
1170
+ #ifndef SP_ALLOC_STATS
1171
+ #define SP_ALLOC_STATS 8192
1172
+ #endif
1173
+ static sp_AllocStat sp_alloc_stats[SP_ALLOC_STATS];
1174
+ /* Type names live in their own table: one entry per scan fn, independent of
1175
+ how many sites allocate it. */
1176
+ typedef struct { void *key; const char *name; } sp_AllocName;
1177
+ #define SP_ALLOC_NAMES 512
1178
+ static sp_AllocName sp_alloc_names[SP_ALLOC_NAMES];
1179
+
1180
+ static const char *sp_alloc_name_of(void *key) {
1181
+ size_t h = ((size_t)(uintptr_t)key >> 4) % SP_ALLOC_NAMES;
1182
+ for (size_t i = 0; i < SP_ALLOC_NAMES; i++) {
1183
+ sp_AllocName *n = &sp_alloc_names[(h + i) % SP_ALLOC_NAMES];
1184
+ if (n->key == key) return n->name;
1185
+ if (n->key == NULL) return NULL;
1186
+ }
1187
+ return NULL;
1188
+ }
1189
+ /* Allocations the table had no room to attribute. They used to be added to
1190
+ the probe's home slot -- a row belonging to a DIFFERENT (type, site) pair --
1191
+ which reads exactly like a real count, so a saturated run reported plausible
1192
+ and wrong numbers with nothing to say it had happened (#3481). Everything
1193
+ that lands here is instead kept out of the per-row numbers entirely and
1194
+ reported as its own line: the rows that remain are all true, and the part
1195
+ that was lost is visible. */
1196
+ static sp_AllocStat sp_alloc_overflow;
1197
+ static sp_AllocStat *sp_alloc_stat_slot(void *key, void *site) {
1198
+ size_t h = (((size_t)(uintptr_t)key >> 4) ^ ((size_t)(uintptr_t)site >> 3)) % SP_ALLOC_STATS;
1199
+ for (size_t i = 0; i < SP_ALLOC_STATS; i++) {
1200
+ sp_AllocStat *s = &sp_alloc_stats[(h + i) % SP_ALLOC_STATS];
1201
+ if ((s->key == key && s->site == site) || s->key == NULL) { s->key = key; s->site = site; return s; }
1202
+ }
1203
+ return &sp_alloc_overflow;
1204
+ }
1205
+ /* The frame that asked for this allocation: skip this helper, the counter and
1206
+ the allocator itself. */
1207
+ static void *sp_alloc_site_now(void) {
1208
+ #if SP_ALLOC_SITE_AVAILABLE
1209
+ if (!sp_alloc_sites_on) return NULL;
1210
+ /* frame 0 is this counter (sp_alloc_site_now inlines into it), frame 1 the
1211
+ allocator, frame 2 the code that asked -- which is what we want. */
1212
+ void *fr[4];
1213
+ int n = backtrace(fr, 4);
1214
+ return n >= 3 ? fr[2] : (n > 0 ? fr[n - 1] : NULL);
1215
+ #else
1216
+ return NULL;
1217
+ #endif
1218
+ }
1219
+ /* NULL is the table's empty marker, so a scan-less object (an int array, a
1220
+ plain byte buffer) counts under this stand-in key -- which keeps it on the
1221
+ per-site path too. */
1222
+ #define SP_ALLOC_NOSCAN_KEY ((void *)(uintptr_t)1)
1223
+ /* Strings carry no scan fn, so they get a reserved key of their own rather
1224
+ than a pair of standalone counters. Same table means the same per-site
1225
+ path: with SPINEL_ALLOC_SITES off every string lands in one slot (site
1226
+ NULL) and the dump is byte-identical to the old aggregate line, and with
1227
+ it on they split by caller like every other type. Strings are the largest
1228
+ share of allocated bytes in a typical app, so leaving them off the site
1229
+ path left the biggest question the report raises unanswerable. */
1230
+ #define SP_ALLOC_STR_KEY ((void *)(uintptr_t)2)
1231
+ /* Defined below, beside the dump it calls: a signal asked for the report and
1232
+ this is the first place after it that is allowed to write one. */
1233
+ static void sp_alloc_report_poll(void);
1234
+ void sp_alloc_report_count(void *scan, size_t bytes) {
1235
+ sp_alloc_report_poll();
1236
+ sp_AllocStat *s = sp_alloc_stat_slot(scan ? scan : SP_ALLOC_NOSCAN_KEY, sp_alloc_site_now());
1237
+ s->count++; s->bytes += (unsigned long long)bytes;
1238
+ }
1239
+ void sp_alloc_report_str(size_t bytes) {
1240
+ sp_alloc_report_poll();
1241
+ sp_AllocStat *s = sp_alloc_stat_slot(SP_ALLOC_STR_KEY, sp_alloc_site_now());
1242
+ s->count++; s->bytes += (unsigned long long)bytes;
1243
+ }
1244
+ void sp_alloc_report_tag(void *scan, const char *name) {
1245
+ size_t h = ((size_t)(uintptr_t)scan >> 4) % SP_ALLOC_NAMES;
1246
+ for (size_t i = 0; i < SP_ALLOC_NAMES; i++) {
1247
+ sp_AllocName *n = &sp_alloc_names[(h + i) % SP_ALLOC_NAMES];
1248
+ if (n->key == scan || n->key == NULL) { n->key = scan; n->name = name; return; }
1249
+ }
1250
+ }
1251
+ /* A site's human name, resolved at dump time. Prefers the symbol name from
1252
+ the dynamic symbol table; falls back to the raw address. Caller frees. */
1253
+ static char *sp_alloc_site_name(void *site) {
1254
+ #if SP_ALLOC_SITE_AVAILABLE
1255
+ char **syms = backtrace_symbols(&site, 1);
1256
+ if (syms && syms[0]) {
1257
+ /* "path(sym+0x12) [0xaddr]" -> "sym" when the symbol is there */
1258
+ const char *o = strchr(syms[0], '(');
1259
+ const char *plus = o ? strchr(o, '+') : NULL;
1260
+ char *r;
1261
+ if (o && plus && plus > o + 1) {
1262
+ size_t n = (size_t)(plus - o - 1);
1263
+ r = (char *)malloc(n + 1);
1264
+ if (r) { memcpy(r, o + 1, n); r[n] = 0; free(syms); return r; }
1265
+ }
1266
+ r = strdup(syms[0]);
1267
+ free(syms);
1268
+ if (r) return r;
1269
+ }
1270
+ if (syms) free(syms);
1271
+ #endif
1272
+ { char *r = (char *)malloc(32); if (r) snprintf(r, 32, "%p", site); return r; }
1273
+ }
1274
+ static void sp_alloc_report_dump(void) {
1275
+ const char *out = getenv("SPINEL_ALLOC_REPORT");
1276
+ FILE *f = stderr;
1277
+ int close_f = 0;
1278
+ if (out && out[0] && strcmp(out, "1") != 0) {
1279
+ FILE *g = fopen(out, "w");
1280
+ if (g) { f = g; close_f = 1; }
1281
+ }
1282
+ /* Symbolise the sites once, here: `alloc;<site>;<Type> <count>` keeps the
1283
+ folded-stack shape a flamegraph consumer wants, with the site as the outer
1284
+ frame. Without site tracking the line is the old per-type one. */
1285
+ for (int pass = 0; pass < 2; pass++) {
1286
+ const char *lead = pass ? "# bytes " : "alloc;";
1287
+ for (size_t i = 0; i < SP_ALLOC_STATS; i++) {
1288
+ sp_AllocStat *s = &sp_alloc_stats[i];
1289
+ if (!s->key || !s->count) continue;
1290
+ const char *nm = s->key == SP_ALLOC_NOSCAN_KEY ? "(no-scan)"
1291
+ : s->key == SP_ALLOC_STR_KEY ? "String"
1292
+ : sp_alloc_name_of(s->key);
1293
+ char tybuf[64];
1294
+ if (!nm) { snprintf(tybuf, sizeof tybuf, "scan_%p", s->key); nm = tybuf; }
1295
+ unsigned long long v = pass ? s->bytes : s->count;
1296
+ if (s->site) {
1297
+ char *sym = sp_alloc_site_name(s->site);
1298
+ if (pass) fprintf(f, "# bytes %s;%s %llu\n", sym, nm, v);
1299
+ else fprintf(f, "alloc;%s;%s %llu\n", sym, nm, v);
1300
+ free(sym);
1301
+ }
1302
+ else fprintf(f, "%s%s %llu\n", lead, nm, v);
1303
+ }
1304
+ }
1305
+ /* Say it out loud when the table saturated: the rows above are complete and
1306
+ correct as far as they go, and this is what they do not cover. Silence
1307
+ here is the failure this report must not have. */
1308
+ if (sp_alloc_overflow.count) {
1309
+ fprintf(f, "alloc;(unattributed) %llu\n", sp_alloc_overflow.count);
1310
+ fprintf(f, "# bytes (unattributed) %llu\n", sp_alloc_overflow.bytes);
1311
+ fprintf(f, "# note the stats table (%d entries) was full: %llu allocation(s)"
1312
+ " could not be attributed and are NOT counted in the rows above\n",
1313
+ (int)SP_ALLOC_STATS, sp_alloc_overflow.count);
1314
+ }
1315
+ if (close_f) fclose(f);
1316
+ }
1317
+ /* ---- a dump from a program that is still running ----
1318
+ The dump above runs from atexit, which a long-lived program never reaches:
1319
+ a server is stopped by a signal, so the counters it spent the whole run
1320
+ filling are lost at the moment they are worth reading. Nothing about the
1321
+ table needed changing -- it is complete at every instant -- it just had no
1322
+ way out before the process ended.
1323
+
1324
+ The handler cannot write the report: fopen, malloc and backtrace_symbols
1325
+ are none of them async-signal-safe. What it can do is `write` one byte to a
1326
+ pipe, which is, and a thread parked on the other end does the writing. That
1327
+ thread is why the signal works on an IDLE server -- the first version polled
1328
+ the flag from the counting path, and a server with no traffic allocates
1329
+ nothing, so the dump waited for the next request instead of arriving when
1330
+ asked.
1331
+
1332
+ Without threads there is no such thread to park, and a single-threaded
1333
+ program that is idle is inside a syscall with nothing else to run: there
1334
+ the flag and the counting path are the only mechanism available, and the
1335
+ dump lands on the next allocation. The handler picks whichever is armed. */
1336
+ static volatile sig_atomic_t sp_alloc_report_pending = 0;
1337
+ static int sp_alloc_report_pipe[2] = { -1, -1 };
1338
+
1339
+ static void sp_alloc_report_handler(int sig) {
1340
+ (void)sig;
1341
+ if (sp_alloc_report_pipe[1] >= 0) {
1342
+ char b = 1;
1343
+ /* Non-blocking, so a full pipe (or a fork'd child, which inherited the
1344
+ handler but not the thread that reads it) degrades to the flag rather
1345
+ than blocking inside a signal handler. */
1346
+ if (write(sp_alloc_report_pipe[1], &b, 1) == 1) return;
1347
+ }
1348
+ sp_alloc_report_pending = 1;
1349
+ }
1350
+
1351
+ /* A number (so real-time signals work) or a name with or without the SIG
1352
+ prefix. SIGUSR1 by default because a program that wants it for itself can
1353
+ move this one, and the handler is installed only while the report is on.
1354
+ Kept as a small copy of sp_resolve_preempt_signal rather than a shared
1355
+ helper: the allocator sits below the scheduler and should not reach up into
1356
+ it for a dozen lines. */
1357
+ static int sp_resolve_report_signal(void) {
1358
+ const char *e = getenv("SPINEL_ALLOC_REPORT_SIGNAL");
1359
+ if (!e || !*e) return SIGUSR1;
1360
+ char *end;
1361
+ long n = strtol(e, &end, 10);
1362
+ if (*end == '\0') { if (n > 0 && n < 65) return (int)n; }
1363
+ else {
1364
+ const char *name = e;
1365
+ if (strncasecmp(name, "SIG", 3) == 0) name += 3;
1366
+ static const struct { const char *n; int s; } tab[] = {
1367
+ { "USR1", SIGUSR1 }, { "USR2", SIGUSR2 }, { "URG", SIGURG },
1368
+ { "IO", SIGIO }, { "WINCH", SIGWINCH },
1369
+ };
1370
+ for (size_t i = 0; i < sizeof tab / sizeof tab[0]; i++)
1371
+ if (strcasecmp(name, tab[i].n) == 0) return tab[i].s;
1372
+ }
1373
+ fprintf(stderr, "spinel: ignoring unrecognized SPINEL_ALLOC_REPORT_SIGNAL=%s;"
1374
+ " using SIGUSR1\n", e);
1375
+ return SIGUSR1;
1376
+ }
1377
+
1378
+ /* The flag path: the next counted allocation writes the report. Claimed with
1379
+ an exchange so that however many threads are allocating, one dump happens
1380
+ and the losers carry straight on. */
1381
+ static void sp_alloc_report_poll(void) {
1382
+ if (!sp_alloc_report_pending) return;
1383
+ #ifdef SP_THREADS
1384
+ if (!SP_ATOMIC_EXCHANGE(&sp_alloc_report_pending, 0, __ATOMIC_SEQ_CST)) return;
1385
+ #else
1386
+ sp_alloc_report_pending = 0;
1387
+ #endif
1388
+ sp_alloc_report_dump();
1389
+ }
1390
+
1391
+ #ifdef SP_THREADS
1392
+ /* Parked on the pipe for the life of the process. Every dump rewrites the
1393
+ whole cumulative table, the same one atexit writes, so a window is two
1394
+ dumps subtracted rather than a mode of its own -- which is also what
1395
+ excludes a server's boot from the profile. Two signals in quick succession
1396
+ write it twice; the second overwrites the first, which is harmless. */
1397
+ static void *sp_alloc_report_reader(void *arg) {
1398
+ (void)arg;
1399
+ for (;;) {
1400
+ char b;
1401
+ ssize_t n = read(sp_alloc_report_pipe[0], &b, 1);
1402
+ if (n == 0) return NULL;
1403
+ if (n < 0) { if (errno == EINTR) continue; return NULL; }
1404
+ sp_alloc_report_dump();
1405
+ }
1406
+ }
1407
+
1408
+ /* The read end stays blocking, which is how the thread parks for the life of
1409
+ the process at no cost; only the WRITE end is non-blocking, because that is
1410
+ the one a signal handler touches and it must never block. Both are
1411
+ close-on-exec, so an exec'd child does not inherit a pipe nobody reads.
1412
+ Failure here is not fatal: the handler falls back to the flag. */
1413
+ static void sp_alloc_report_start_reader(void) {
1414
+ if (pipe(sp_alloc_report_pipe) != 0) { sp_alloc_report_pipe[0] = sp_alloc_report_pipe[1] = -1; return; }
1415
+ fcntl(sp_alloc_report_pipe[0], F_SETFD, FD_CLOEXEC);
1416
+ fcntl(sp_alloc_report_pipe[1], F_SETFD, FD_CLOEXEC);
1417
+ fcntl(sp_alloc_report_pipe[1], F_SETFL,
1418
+ fcntl(sp_alloc_report_pipe[1], F_GETFL, 0) | O_NONBLOCK);
1419
+ pthread_t tid;
1420
+ if (pthread_create(&tid, NULL, sp_alloc_report_reader, NULL) != 0) {
1421
+ close(sp_alloc_report_pipe[0]); close(sp_alloc_report_pipe[1]);
1422
+ sp_alloc_report_pipe[0] = sp_alloc_report_pipe[1] = -1;
1423
+ return;
1424
+ }
1425
+ pthread_detach(tid);
1426
+ }
1427
+ #endif
1428
+
1429
+ SP_CONSTRUCTOR static void sp_alloc_report_boot(void) {
1430
+ const char *e = getenv("SPINEL_ALLOC_REPORT");
1431
+ if (e && *e && strcmp(e, "0") != 0) {
1432
+ sp_alloc_report_on = 1; sp_gc_alloc_fast_ok = 0;
1433
+ { const char *sv = getenv("SPINEL_ALLOC_SITES");
1434
+ sp_alloc_sites_on = (sv && *sv && strcmp(sv, "0") != 0) ? 1 : 0; }
1435
+ #ifdef SP_THREADS
1436
+ sp_alloc_report_start_reader();
1437
+ #endif
1438
+ /* SA_RESTART: asking for a report must not turn an in-flight read(2) into
1439
+ an EINTR the program never expected to handle. */
1440
+ { struct sigaction sa; memset(&sa, 0, sizeof sa);
1441
+ sa.sa_handler = sp_alloc_report_handler;
1442
+ sigemptyset(&sa.sa_mask);
1443
+ sa.sa_flags = SA_RESTART;
1444
+ sigaction(sp_resolve_report_signal(), &sa, NULL); }
1445
+ atexit(sp_alloc_report_dump);
1446
+ }
1447
+ }