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,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
|
+
}
|