@scriptc/runtime 0.0.21 → 0.0.23
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.
- package/package.json +1 -1
- package/src/scr_assert.c +23 -0
- package/src/scr_async.c +5 -2
- package/src/scr_bytes.c +7 -0
- package/src/scr_cycle.c +403 -62
- package/src/scr_dyn_invoke.c +58 -6
- package/src/scr_fetch.c +6200 -124
- package/src/scr_fetch_curl.c +21 -9
- package/src/scr_http.c +13 -0
- package/src/scr_inspect.c +6 -0
- package/src/scr_island.c +19 -0
- package/src/scr_json.c +413 -9
- package/src/scr_lib.c +277 -18
- package/src/scr_runtime.h +211 -27
- package/src/scr_tls.c +15 -11
- package/src/scr_tls_ca.c +35 -8
- package/src/scr_url.c +103 -15
- package/src/scr_web.c +77 -1
- package/vendor/README.md +4 -4
package/package.json
CHANGED
package/src/scr_assert.c
CHANGED
|
@@ -444,6 +444,8 @@ static bool scr_assert_dyn_same_value(const ScrDyn *a, const ScrDyn *b) {
|
|
|
444
444
|
* === — exact for SameValue too, since wrap-time scalar
|
|
445
445
|
* normalization leaves only reference kinds behind. */
|
|
446
446
|
return a == b || scr_dyn_jsval_ops()->strict_eq(a->v.jsval.cell, b->v.jsval.cell);
|
|
447
|
+
case SCR_DYN_TYPED_REF:
|
|
448
|
+
return scr_dyn_strict_eq(a, b);
|
|
447
449
|
default:
|
|
448
450
|
return a == b; /* ARR/OBJ/BYTES: node identity */
|
|
449
451
|
}
|
|
@@ -459,6 +461,18 @@ static bool scr_assert_dyn_same_value(const ScrDyn *a, const ScrDyn *b) {
|
|
|
459
461
|
* memo here where Node carries one (documented divergence). */
|
|
460
462
|
static bool scr_assert_dyn_deep_eq(const ScrDyn *a, const ScrDyn *b) {
|
|
461
463
|
if (a == b) return true;
|
|
464
|
+
if (a->kind == SCR_DYN_TYPED_REF || b->kind == SCR_DYN_TYPED_REF) {
|
|
465
|
+
ScrDyn *ma = a->kind == SCR_DYN_TYPED_REF
|
|
466
|
+
? scr_dyn_typed_ref_materialize(a)
|
|
467
|
+
: scr_dyn_retain((ScrDyn *)a);
|
|
468
|
+
ScrDyn *mb = b->kind == SCR_DYN_TYPED_REF
|
|
469
|
+
? scr_dyn_typed_ref_materialize(b)
|
|
470
|
+
: scr_dyn_retain((ScrDyn *)b);
|
|
471
|
+
bool same = scr_assert_dyn_deep_eq(ma, mb);
|
|
472
|
+
scr_dyn_release(ma);
|
|
473
|
+
scr_dyn_release(mb);
|
|
474
|
+
return same;
|
|
475
|
+
}
|
|
462
476
|
if (a->kind != b->kind) {
|
|
463
477
|
/* A MIXED comparison with an island side (wrapped engine object vs
|
|
464
478
|
* dyn data): Node walks both structurally — a plain `false` would
|
|
@@ -524,6 +538,8 @@ static bool scr_assert_dyn_deep_eq(const ScrDyn *a, const ScrDyn *b) {
|
|
|
524
538
|
if (scr_dyn_jsval_ops()->strict_eq(a->v.jsval.cell, b->v.jsval.cell)) return true;
|
|
525
539
|
scr_dyn_isl_fence(a, "deepStrictEqual");
|
|
526
540
|
return false;
|
|
541
|
+
case SCR_DYN_TYPED_REF:
|
|
542
|
+
return false; /* both mixed and same-kind capsules returned above */
|
|
527
543
|
}
|
|
528
544
|
return false; /* unreachable */
|
|
529
545
|
}
|
|
@@ -684,6 +700,12 @@ static void scr_assert_cf_value(ScrAssertBuf *b, const ScrDyn *d, size_t indent,
|
|
|
684
700
|
ab_cstr(b, "[island value]");
|
|
685
701
|
return;
|
|
686
702
|
}
|
|
703
|
+
case SCR_DYN_TYPED_REF: {
|
|
704
|
+
ScrDyn *materialized = scr_dyn_typed_ref_materialize(d);
|
|
705
|
+
scr_assert_cf_value(b, materialized, indent, depth);
|
|
706
|
+
scr_dyn_release(materialized);
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
687
709
|
case SCR_DYN_OBJ: {
|
|
688
710
|
/* The null-proto dictionary renders with Node's prefix in the
|
|
689
711
|
* failure diff too (assertion_error.js inspects both sides). */
|
|
@@ -928,6 +950,7 @@ static bool scr_assert_print_myers(ScrAssertBuf *b, const ScrDiffOp *diff, size_
|
|
|
928
950
|
static bool scr_assert_dyn_is_object(const ScrDyn *d) {
|
|
929
951
|
return d->kind == SCR_DYN_ARR || d->kind == SCR_DYN_OBJ || d->kind == SCR_DYN_BYTES ||
|
|
930
952
|
d->kind == SCR_DYN_HANDLE || d->kind == SCR_DYN_PROMISE ||
|
|
953
|
+
d->kind == SCR_DYN_TYPED_REF ||
|
|
931
954
|
scr_dyn_isl_typeof_is(d, "object"); /* engine-held: the engine's typeof */
|
|
932
955
|
}
|
|
933
956
|
|
package/src/scr_async.c
CHANGED
|
@@ -1802,8 +1802,11 @@ bool scr_loop_run(ScrPromise *top_level) {
|
|
|
1802
1802
|
}
|
|
1803
1803
|
if (scr_ready_len > 0 || scr_nt_head != NULL || scr_nunhandled > 0) continue;
|
|
1804
1804
|
/* Quiescent between turns (microtasks drained, nothing running):
|
|
1805
|
-
* collect any cycles the turn left behind.
|
|
1806
|
-
|
|
1805
|
+
* collect any cycles the turn left behind. One SCHEDULED pass — almost
|
|
1806
|
+
* always a nursery one, sized to the turn's own garbage. A full sweep
|
|
1807
|
+
* here would walk the whole live heap every turn, which is precisely
|
|
1808
|
+
* what the generations exist to avoid. No-op on an empty buffer. */
|
|
1809
|
+
scr_cyc_collect_scheduled();
|
|
1807
1810
|
/* Stream tick dispatch (scr_stream.c, when linked): the deferred
|
|
1808
1811
|
* next-tick emissions ('data' flow kicks, 'readable'/'end'/'finish'/
|
|
1809
1812
|
* 'drain'/'error'/'close') fire now, FIRST — the nextTick station.
|
package/src/scr_bytes.c
CHANGED
|
@@ -86,6 +86,13 @@ void scr_bytes_release_v(void *b) { scr_bytes_release((ScrBytes *)b); }
|
|
|
86
86
|
|
|
87
87
|
double scr_bytes_len(const ScrBytes *b) { return (double)b->len; }
|
|
88
88
|
|
|
89
|
+
void scr_bytes_copy_contents(ScrBytes *dst, const ScrBytes *src) {
|
|
90
|
+
if (dst->elem != src->elem || dst->len != src->len) {
|
|
91
|
+
scr_trap("scriptc: typed array snapshot shape changed\n");
|
|
92
|
+
}
|
|
93
|
+
memcpy(dst->data, src->data, dst->len * scr_bytes_elem_size(dst->elem));
|
|
94
|
+
}
|
|
95
|
+
|
|
89
96
|
double scr_bytes_byte_len(const ScrBytes *b) {
|
|
90
97
|
return (double)(b->len * scr_bytes_elem_size(b->elem));
|
|
91
98
|
}
|
package/src/scr_cycle.c
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* Reference-cycle collector:
|
|
1
|
+
/* Reference-cycle collector: generational Bacon–Rajan trial deletion (see
|
|
2
2
|
* "Concurrent Cycle Collection in Reference Counted Systems", the
|
|
3
3
|
* synchronous algorithm) over cycle-headered objects only — the object
|
|
4
4
|
* model and the trace/teardown contract live in scr_runtime.h.
|
|
@@ -29,6 +29,58 @@
|
|
|
29
29
|
* Recursion depth equals the traced structure's depth — same property as
|
|
30
30
|
* the existing recursive releases (deep lists recurse deeply; acceptable
|
|
31
31
|
* for now, revisit with an explicit stack if it ever traps).
|
|
32
|
+
*
|
|
33
|
+
* ── generations ──────────────────────────────────────────────────────
|
|
34
|
+
* A pass costs O(objects reachable from its candidates), and a candidate
|
|
35
|
+
* deep in a live structure reaches all of it — so collecting on a fixed
|
|
36
|
+
* candidate count makes total work QUADRATIC in a growing live heap. A
|
|
37
|
+
* splay tree is the worst case: re-linking a node buffers it, the walk
|
|
38
|
+
* from it drags in every node below, and none of it is ever garbage.
|
|
39
|
+
*
|
|
40
|
+
* So the collector is generational, in the shape CPython uses for this
|
|
41
|
+
* same algorithm. Each header carries a `gen`; a pass names the oldest
|
|
42
|
+
* generation it will walk and SKIPS every object above it, exactly as it
|
|
43
|
+
* skips NULL and immortals. Objects that survive a pass are promoted, so
|
|
44
|
+
* the next nursery pass never walks them again: the cost of a nursery pass
|
|
45
|
+
* is proportional to what has been allocated since the last one, not to
|
|
46
|
+
* the live heap.
|
|
47
|
+
*
|
|
48
|
+
* Restricting the walk stays sound because trial deletion is already
|
|
49
|
+
* conservative in the right direction. An edge from a skipped (older)
|
|
50
|
+
* object into the walked set is never trial-deleted, so its target keeps
|
|
51
|
+
* that count, reads as externally referenced, and survives — the same
|
|
52
|
+
* answer the collector gives for a reference held by the stack. Edges the
|
|
53
|
+
* other way are simply not followed; the older object was not a candidate
|
|
54
|
+
* for freeing in this pass. What this gives up is cycles that SPAN
|
|
55
|
+
* generations: those are invisible to a nursery pass and are found by the
|
|
56
|
+
* full pass, which walks every generation and so is exactly the old
|
|
57
|
+
* algorithm.
|
|
58
|
+
*
|
|
59
|
+
* Scheduling. One counter drives the release path: candidates buffered
|
|
60
|
+
* since the last pass. When it trips, the pass runs at the nursery level
|
|
61
|
+
* unless either full-pass condition holds:
|
|
62
|
+
* heap growth the live cycle-headered heap is a fraction past its size
|
|
63
|
+
* after the last full pass — the standard mature-generation
|
|
64
|
+
* rule, which catches garbage made from fresh allocation;
|
|
65
|
+
* mature backlog the mature candidate buffer has reached a fraction of the
|
|
66
|
+
* live heap. This one is not redundant: live data that is
|
|
67
|
+
* unlinked INTO a dead cycle grows no counter at all (it
|
|
68
|
+
* was tallied when it was allocated) and is invisible to a
|
|
69
|
+
* nursery pass, so without it a program that churns its
|
|
70
|
+
* long-lived structures would never collect anything.
|
|
71
|
+
* scheduled age a mature candidate has waited through a nursery's worth
|
|
72
|
+
* of event-loop checkpoints. Candidate COUNT cannot bound
|
|
73
|
+
* the garbage behind one root, and an idle heap does not
|
|
74
|
+
* trip growth, so this keeps a sparse mature backlog from
|
|
75
|
+
* floating forever without putting a full walk on every
|
|
76
|
+
* turn.
|
|
77
|
+
* The first two bound collection work to a constant factor of mutator work —
|
|
78
|
+
* one heap-sized walk per live/N candidates — while the scheduled age is the
|
|
79
|
+
* liveness backstop, limiting its forced full walks to one per nursery's worth
|
|
80
|
+
* of checkpoints while mature roots wait. That is the trade: a fixed root
|
|
81
|
+
* threshold bounds floating garbage tightly and pays unbounded time for it;
|
|
82
|
+
* this bounds ordinary mutator-triggered work and lets garbage float in
|
|
83
|
+
* proportion, but not forever.
|
|
32
84
|
*/
|
|
33
85
|
#include "scr_runtime.h"
|
|
34
86
|
|
|
@@ -42,70 +94,210 @@ static void scr_cyc_oom(void) {
|
|
|
42
94
|
scr_trap("scriptc: out of memory\n");
|
|
43
95
|
}
|
|
44
96
|
|
|
97
|
+
/* Live cycle-headered objects — what the full-pass trigger watches. */
|
|
98
|
+
static size_t scr_cyc_live = 0;
|
|
99
|
+
|
|
45
100
|
void *scr_cyc_alloc(size_t size, ScrTraceFn trace, ScrCycFreeFn free_fn) {
|
|
46
101
|
ScrCycHdr *h = calloc(1, sizeof(ScrCycHdr) + size);
|
|
47
102
|
if (!h) scr_cyc_oom();
|
|
48
103
|
h->trace = trace;
|
|
49
104
|
h->free_fn = free_fn;
|
|
50
105
|
h->color = SCR_CYC_BLACK;
|
|
106
|
+
h->gen = SCR_CYC_NURSERY;
|
|
107
|
+
scr_cyc_live++;
|
|
51
108
|
return h + 1;
|
|
52
109
|
}
|
|
53
110
|
|
|
54
|
-
void scr_cyc_free(void *obj) {
|
|
111
|
+
void scr_cyc_free(void *obj) {
|
|
112
|
+
scr_cyc_live--;
|
|
113
|
+
free(scr_cyc_hdr(obj));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* A pointer vector that only ever grows (these reuse their capacity across
|
|
117
|
+
* passes rather than churning it). Pointer, count and capacity live in ONE
|
|
118
|
+
* struct on purpose: buffering a candidate is the hot path, and splitting
|
|
119
|
+
* them across three arrays indexed by generation would touch three cache
|
|
120
|
+
* lines per release where the original single buffer touched one. */
|
|
121
|
+
typedef struct {
|
|
122
|
+
void **v;
|
|
123
|
+
size_t n, cap;
|
|
124
|
+
} ScrVec;
|
|
55
125
|
|
|
56
|
-
|
|
126
|
+
static void scr_cyc_grow(ScrVec *vec) {
|
|
127
|
+
vec->cap = vec->cap ? vec->cap * 2 : 64;
|
|
128
|
+
void **grown = realloc(vec->v, vec->cap * sizeof *grown);
|
|
129
|
+
if (!grown) scr_cyc_oom();
|
|
130
|
+
vec->v = grown;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
static void scr_cyc_push(ScrVec *vec, void *obj) {
|
|
134
|
+
if (vec->n == vec->cap) scr_cyc_grow(vec);
|
|
135
|
+
vec->v[vec->n++] = obj;
|
|
136
|
+
}
|
|
57
137
|
|
|
58
|
-
|
|
59
|
-
|
|
138
|
+
/* ── the candidate-root buffers (one per generation) ──────────────────── */
|
|
139
|
+
|
|
140
|
+
static ScrVec scr_roots[SCR_CYC_NGENS];
|
|
60
141
|
static bool scr_collecting = false;
|
|
61
142
|
|
|
62
|
-
|
|
143
|
+
/* The candidates of the pass in flight, gathered across the generations it
|
|
144
|
+
* collects so the phases can walk them without re-filtering the buffers. */
|
|
145
|
+
static ScrVec scr_cands;
|
|
146
|
+
|
|
147
|
+
/* Nursery survivors awaiting promotion (see scr_scan_black). */
|
|
148
|
+
static ScrVec scr_promote;
|
|
149
|
+
|
|
150
|
+
/* The gathered white set (freed after the walk completes). */
|
|
151
|
+
static ScrVec scr_white;
|
|
152
|
+
|
|
153
|
+
/* Cross-generation targets pinned by the white set (see scr_xg_pin_visit),
|
|
154
|
+
* one entry per skipped edge. Drained after the teardowns. */
|
|
155
|
+
static ScrVec scr_xgen;
|
|
156
|
+
|
|
157
|
+
/* Objects above this generation are invisible to the pass in flight. */
|
|
158
|
+
static unsigned scr_gen_limit = SCR_CYC_MATURE;
|
|
159
|
+
|
|
160
|
+
static size_t scr_cyc_pass(unsigned gen_limit);
|
|
161
|
+
|
|
162
|
+
/* ── when to collect ──────────────────────────────────────────────────── */
|
|
163
|
+
|
|
164
|
+
#define SCR_CYC_NURSERY_CANDIDATES 256 /* nursery trigger */
|
|
165
|
+
#define SCR_CYC_FULL_GROWTH_DIV 4 /* full pass per +1/4 of live heap */
|
|
166
|
+
#define SCR_CYC_FULL_FLOOR 4096 /* ...but not below this many objects */
|
|
167
|
+
|
|
168
|
+
/* Live count as of the end of the last full pass. */
|
|
169
|
+
static size_t scr_cyc_live_after_full = 0;
|
|
170
|
+
|
|
171
|
+
static size_t scr_cyc_nursery_threshold(void) {
|
|
63
172
|
static size_t cached = 0;
|
|
64
173
|
if (cached == 0) {
|
|
65
174
|
const char *env = getenv("SCR_CYCLE_THRESHOLD");
|
|
66
175
|
long v = env ? strtol(env, NULL, 10) : 0;
|
|
67
|
-
cached = v > 0 ? (size_t)v :
|
|
176
|
+
cached = v > 0 ? (size_t)v : SCR_CYC_NURSERY_CANDIDATES;
|
|
68
177
|
}
|
|
69
178
|
return cached;
|
|
70
179
|
}
|
|
71
180
|
|
|
181
|
+
/* Buffered candidates across both generations, and the count at which the
|
|
182
|
+
* release path next collects. Keeping a running total (rather than summing
|
|
183
|
+
* the buffers) is what holds the hot path to one load and one compare, as
|
|
184
|
+
* it was when there was a single buffer. The trigger is re-armed at the end
|
|
185
|
+
* of every pass to "whatever survived, plus a nursery's worth": without that
|
|
186
|
+
* hysteresis a large mature buffer — which a nursery pass cannot drain —
|
|
187
|
+
* would re-arm on every single release. Both start at zero, so the first
|
|
188
|
+
* release runs one trivial pass that arms them properly. */
|
|
189
|
+
static size_t scr_cyc_nbuffered = 0;
|
|
190
|
+
static size_t scr_cyc_trigger = 0;
|
|
191
|
+
|
|
192
|
+
/* Scheduled nursery passes for which at least one mature root was waiting.
|
|
193
|
+
* A full pass resets the age; with no mature backlog there is nothing to age. */
|
|
194
|
+
static size_t scr_cyc_scheduled_mature_age = 0;
|
|
195
|
+
|
|
196
|
+
static size_t scr_cyc_buffered(void) {
|
|
197
|
+
return scr_roots[SCR_CYC_NURSERY].n + scr_roots[SCR_CYC_MATURE].n;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* A full pass is due once the live heap has grown a fraction past what it
|
|
201
|
+
* was when the last one finished. Since a pass resets the baseline to the
|
|
202
|
+
* live count it leaves behind, an unproductive full pass cannot re-trigger
|
|
203
|
+
* itself — the next one waits for another fraction of growth. */
|
|
204
|
+
static bool scr_cyc_full_due(void) {
|
|
205
|
+
size_t base = scr_cyc_live_after_full;
|
|
206
|
+
if (base < SCR_CYC_FULL_FLOOR) base = SCR_CYC_FULL_FLOOR;
|
|
207
|
+
return scr_cyc_live > base + base / SCR_CYC_FULL_GROWTH_DIV;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/* Cold half of the release path: pick the generation and collect.
|
|
211
|
+
*
|
|
212
|
+
* Heap growth is not the only thing that owes a full pass. Live data that
|
|
213
|
+
* TURNS INTO garbage — a mature structure unlinked into a dead cycle —
|
|
214
|
+
* grows no counter at all: those objects were already tallied in
|
|
215
|
+
* scr_cyc_live when they were allocated, so scr_cyc_full_due stays false
|
|
216
|
+
* forever, and a nursery pass cannot see them because they are mature. Left
|
|
217
|
+
* at that, a program that churns its long-lived structures without
|
|
218
|
+
* allocating would never collect anything. So the mature buffer's own size
|
|
219
|
+
* is the second full-pass trigger, at a fraction of the live heap: that
|
|
220
|
+
* bounds uncollected mature candidates proportionally and keeps the
|
|
221
|
+
* amortized cost linear (one heap-sized walk per live/N candidates). */
|
|
222
|
+
static size_t scr_cyc_mature_threshold(void) {
|
|
223
|
+
size_t t = scr_cyc_live / SCR_CYC_FULL_GROWTH_DIV;
|
|
224
|
+
return t < SCR_CYC_NURSERY_CANDIDATES ? SCR_CYC_NURSERY_CANDIDATES : t;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
static void scr_cyc_collect_due(void) {
|
|
228
|
+
bool full = scr_cyc_full_due()
|
|
229
|
+
|| scr_roots[SCR_CYC_MATURE].n >= scr_cyc_mature_threshold();
|
|
230
|
+
scr_cyc_pass(full ? SCR_CYC_MATURE : SCR_CYC_NURSERY);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/* One scheduled pass, for callers that reach a natural collection point
|
|
234
|
+
* (the event loop between turns) rather than a threshold. Deliberately NOT
|
|
235
|
+
* scr_collect_cycles: that one is the exit-time full sweep, and running it
|
|
236
|
+
* per turn would walk the whole live heap every turn — which is exactly the
|
|
237
|
+
* cost the generations exist to avoid. */
|
|
238
|
+
void scr_cyc_collect_scheduled(void) {
|
|
239
|
+
if (scr_cyc_buffered() == 0) {
|
|
240
|
+
scr_cyc_scheduled_mature_age = 0;
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (scr_roots[SCR_CYC_MATURE].n == 0) {
|
|
244
|
+
scr_cyc_scheduled_mature_age = 0;
|
|
245
|
+
} else if (++scr_cyc_scheduled_mature_age
|
|
246
|
+
>= scr_cyc_nursery_threshold()) {
|
|
247
|
+
scr_cyc_pass(SCR_CYC_MATURE);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
scr_cyc_collect_due();
|
|
251
|
+
}
|
|
252
|
+
|
|
72
253
|
void scr_cyc_on_dead(void *obj) {
|
|
73
254
|
ScrCycHdr *h = scr_cyc_hdr(obj);
|
|
74
255
|
if (!h->buffered) return;
|
|
75
|
-
/* O(1) removal: swap the last entry into the hole.
|
|
256
|
+
/* O(1) removal: swap the last entry into the hole. A buffered object's
|
|
257
|
+
* generation never changes (promotion runs only on objects the pass has
|
|
258
|
+
* already drained from the buffers), so `gen` names the right one. */
|
|
259
|
+
ScrVec *b = &scr_roots[h->gen];
|
|
76
260
|
size_t i = h->buf_index;
|
|
77
|
-
void *last =
|
|
78
|
-
|
|
261
|
+
void *last = b->v[--b->n];
|
|
262
|
+
b->v[i] = last;
|
|
79
263
|
if (last != obj) scr_cyc_hdr(last)->buf_index = i;
|
|
80
264
|
h->buffered = 0;
|
|
265
|
+
scr_cyc_nbuffered--;
|
|
266
|
+
/* A death must not consume room reserved for a future candidate. */
|
|
267
|
+
scr_cyc_trigger--;
|
|
268
|
+
if (h->gen == SCR_CYC_MATURE && b->n == 0)
|
|
269
|
+
scr_cyc_scheduled_mature_age = 0;
|
|
81
270
|
}
|
|
82
271
|
|
|
272
|
+
/* THE hot path — every release that leaves an object alive lands here, so
|
|
273
|
+
* it stays what it has always been: buffer inline, then one compare. */
|
|
83
274
|
void scr_cyc_on_release(void *obj) {
|
|
84
275
|
ScrCycHdr *h = scr_cyc_hdr(obj);
|
|
85
276
|
h->color = SCR_CYC_PURPLE;
|
|
86
277
|
if (!h->buffered) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
scr_roots = realloc(scr_roots, scr_roots_cap * sizeof *scr_roots);
|
|
90
|
-
if (!scr_roots) scr_cyc_oom();
|
|
91
|
-
}
|
|
278
|
+
ScrVec *b = &scr_roots[h->gen];
|
|
279
|
+
if (b->n == b->cap) scr_cyc_grow(b);
|
|
92
280
|
h->buffered = 1;
|
|
93
|
-
h->buf_index =
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
/* Threshold trigger. Never re-entered: a teardown's releases of untraced
|
|
97
|
-
* children can buffer new candidates mid-collection, but they only wait
|
|
98
|
-
* for the next pass. */
|
|
99
|
-
if (!scr_collecting && scr_nroots >= scr_cyc_threshold()) {
|
|
100
|
-
scr_collect_cycles();
|
|
281
|
+
h->buf_index = b->n;
|
|
282
|
+
b->v[b->n++] = obj;
|
|
283
|
+
scr_cyc_nbuffered++;
|
|
101
284
|
}
|
|
285
|
+
/* Never re-entered: a teardown's releases of untraced children can buffer
|
|
286
|
+
* new candidates mid-collection, but they only wait for the next pass. */
|
|
287
|
+
if (!scr_collecting && scr_cyc_nbuffered >= scr_cyc_trigger)
|
|
288
|
+
scr_cyc_collect_due();
|
|
102
289
|
}
|
|
103
290
|
|
|
104
291
|
/* ── trial deletion ───────────────────────────────────────────────────── */
|
|
105
292
|
|
|
106
|
-
/* Child filter shared by every phase
|
|
107
|
-
*
|
|
108
|
-
|
|
293
|
+
/* Child filter shared by every phase — and it MUST be shared by every
|
|
294
|
+
* phase: scan restores exactly the trial decrements markGray made, so the
|
|
295
|
+
* two must agree on which edges are internal. Nothing to do for NULL
|
|
296
|
+
* (unassigned slots), immortal (interned statics have no header at all), or
|
|
297
|
+
* a generation this pass does not collect. */
|
|
298
|
+
#define SCR_CYC_SKIP(child) \
|
|
299
|
+
((child) == NULL || SCR_RC(child) == SIZE_MAX \
|
|
300
|
+
|| scr_cyc_hdr(child)->gen > scr_gen_limit)
|
|
109
301
|
|
|
110
302
|
static void scr_mark_gray(void *obj);
|
|
111
303
|
static void scr_mg_visit(void *child, void *ctx) {
|
|
@@ -129,8 +321,15 @@ static void scr_sb_visit(void *child, void *ctx) {
|
|
|
129
321
|
if (scr_cyc_hdr(child)->color != SCR_CYC_BLACK) scr_scan_black(child);
|
|
130
322
|
}
|
|
131
323
|
static void scr_scan_black(void *obj) {
|
|
132
|
-
scr_cyc_hdr(obj)
|
|
133
|
-
|
|
324
|
+
ScrCycHdr *h = scr_cyc_hdr(obj);
|
|
325
|
+
h->color = SCR_CYC_BLACK;
|
|
326
|
+
/* Blackening is what proves a walked object survives, so this is where
|
|
327
|
+
* promotion is decided — but it is only RECORDED here. Raising `gen` now
|
|
328
|
+
* would hide the object from SCR_CYC_SKIP partway through the pass, and
|
|
329
|
+
* this phase has trial decrements left to restore. */
|
|
330
|
+
if (h->gen < SCR_CYC_MATURE)
|
|
331
|
+
scr_cyc_push(&scr_promote, obj);
|
|
332
|
+
h->trace(obj, scr_sb_visit, NULL);
|
|
134
333
|
}
|
|
135
334
|
|
|
136
335
|
static void scr_scan(void *obj);
|
|
@@ -151,10 +350,6 @@ static void scr_scan(void *obj) {
|
|
|
151
350
|
h->trace(obj, scr_scan_visit, NULL);
|
|
152
351
|
}
|
|
153
352
|
|
|
154
|
-
/* The gathered white set (freed after the walk completes). */
|
|
155
|
-
static void **scr_white = NULL;
|
|
156
|
-
static size_t scr_nwhite = 0, scr_white_cap = 0;
|
|
157
|
-
|
|
158
353
|
static void scr_collect_white(void *obj);
|
|
159
354
|
static void scr_cw_visit(void *child, void *ctx) {
|
|
160
355
|
(void)ctx;
|
|
@@ -164,54 +359,200 @@ static void scr_cw_visit(void *child, void *ctx) {
|
|
|
164
359
|
static void scr_collect_white(void *obj) {
|
|
165
360
|
ScrCycHdr *h = scr_cyc_hdr(obj);
|
|
166
361
|
if (h->color != SCR_CYC_WHITE || h->buffered) return;
|
|
167
|
-
h->color =
|
|
362
|
+
h->color = SCR_CYC_DOOMED; /* gathered — don't gather twice */
|
|
168
363
|
h->trace(obj, scr_cw_visit, NULL);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
364
|
+
scr_cyc_push(&scr_white, obj);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/* ── cross-generation edges ───────────────────────────────────────────── */
|
|
368
|
+
|
|
369
|
+
/* A restricted pass leaves one edge unaccounted, and it is the one edge that
|
|
370
|
+
* nothing else accounts either. markGray does not trial-delete an edge into a
|
|
371
|
+
* generation it refused to walk (SCR_CYC_SKIP), and the teardown contract has
|
|
372
|
+
* free_fn release exactly the UNTRACED children — because every traced edge
|
|
373
|
+
* was supposed to have been decremented by markGray. So when a white object
|
|
374
|
+
* holds a traced edge into an older generation, freeing it drops the edge
|
|
375
|
+
* while the target keeps the count: a phantom reference that reads as an
|
|
376
|
+
* external one to every later pass, full ones included. The target and
|
|
377
|
+
* everything under it would never be reclaimable again.
|
|
378
|
+
*
|
|
379
|
+
* So the white set pays those edges off explicitly. The walk cannot do it
|
|
380
|
+
* inline — a release there could free a survivor that scr_promote still
|
|
381
|
+
* points at, or cascade into the white set mid-teardown — so it runs in two
|
|
382
|
+
* halves: PIN each target with a retain while `gen` still holds walk-time
|
|
383
|
+
* values, then DROP THE PIN AND GIVE UP THE EDGE after the teardowns, when
|
|
384
|
+
* generations have settled and the white set is gone. Two decrements per
|
|
385
|
+
* entry, because the pin is one of them — a single release would only undo
|
|
386
|
+
* the pin and leave the phantom edge exactly where it was. The pin is what
|
|
387
|
+
* makes the second decrement safe: a teardown's releases of untraced
|
|
388
|
+
* children can reach a cycle-headered object (that is how they re-buffer
|
|
389
|
+
* survivors), so without it a target could be freed before the drain runs.
|
|
390
|
+
*
|
|
391
|
+
* One entry per skipped edge, so several edges sharing a target settle it
|
|
392
|
+
* once each.
|
|
393
|
+
*
|
|
394
|
+
* Nothing pinned here can be in the white set: a skipped older object's
|
|
395
|
+
* edges were never trial-deleted, so anything it references kept that count
|
|
396
|
+
* and was scan-blacked rather than whitened — the same conservatism that
|
|
397
|
+
* makes restricting the walk sound in the first place. */
|
|
398
|
+
static void scr_xg_pin_visit(void *child, void *ctx) {
|
|
399
|
+
(void)ctx;
|
|
400
|
+
if (child == NULL || SCR_RC(child) == SIZE_MAX) return;
|
|
401
|
+
if (scr_cyc_hdr(child)->gen <= scr_gen_limit) return; /* markGray had it */
|
|
402
|
+
SCR_RC(child) += 1; /* pin across the teardowns */
|
|
403
|
+
scr_cyc_push(&scr_xgen, child);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/* Freed by the cross-generation drain, for the caller's fixpoint. */
|
|
407
|
+
static size_t scr_xg_freed = 0;
|
|
408
|
+
|
|
409
|
+
static void scr_xg_release(void *obj);
|
|
410
|
+
static void scr_xg_child_visit(void *child, void *ctx) {
|
|
411
|
+
(void)ctx;
|
|
412
|
+
if (child == NULL || SCR_RC(child) == SIZE_MAX) return;
|
|
413
|
+
scr_xg_release(child);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/* One genuine release, generically: the header carries everything needed. */
|
|
417
|
+
static void scr_xg_release(void *obj) {
|
|
418
|
+
ScrCycHdr *h = scr_cyc_hdr(obj);
|
|
419
|
+
if (SCR_RC(obj) > 1) {
|
|
420
|
+
SCR_RC(obj) -= 1;
|
|
421
|
+
scr_cyc_on_release(obj); /* lost a reference: a possible cycle root */
|
|
422
|
+
return;
|
|
173
423
|
}
|
|
174
|
-
|
|
424
|
+
SCR_RC(obj) = 0;
|
|
425
|
+
scr_cyc_on_dead(obj); /* out of its candidate buffer before the block goes */
|
|
426
|
+
h->trace(obj, scr_xg_child_visit, NULL); /* the traced children */
|
|
427
|
+
h->free_fn(obj); /* the untraced ones, then the block */
|
|
428
|
+
scr_xg_freed++;
|
|
175
429
|
}
|
|
176
430
|
|
|
177
|
-
|
|
178
|
-
|
|
431
|
+
/* One pass over every candidate at or below `gen_limit`; returns how many
|
|
432
|
+
* objects it freed. */
|
|
433
|
+
static size_t scr_cyc_pass(unsigned gen_limit) {
|
|
434
|
+
if (scr_collecting) return 0;
|
|
179
435
|
scr_collecting = true;
|
|
436
|
+
scr_gen_limit = gen_limit;
|
|
180
437
|
|
|
181
438
|
/* markRoots: keep live candidates (still purple), drop the rest — an
|
|
182
439
|
* object re-retained since buffering is black; one grayed by an earlier
|
|
183
|
-
* candidate's walk is already covered by that candidate's subgraph.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
scr_roots[
|
|
192
|
-
|
|
440
|
+
* candidate's walk is already covered by that candidate's subgraph. The
|
|
441
|
+
* buffers are drained up front (markGray only touches counts directly,
|
|
442
|
+
* so nothing can re-buffer underneath us) which leaves collectWhite free
|
|
443
|
+
* to gather members of an earlier root's cycle. Candidates ABOVE the
|
|
444
|
+
* limit keep their buffer slots and wait for a full pass. */
|
|
445
|
+
scr_cands.n = 0;
|
|
446
|
+
for (unsigned g = 0; g <= gen_limit; g++) {
|
|
447
|
+
for (size_t i = 0; i < scr_roots[g].n; i++) {
|
|
448
|
+
void *obj = scr_roots[g].v[i];
|
|
449
|
+
ScrCycHdr *h = scr_cyc_hdr(obj);
|
|
193
450
|
h->buffered = 0;
|
|
451
|
+
if (h->color == SCR_CYC_PURPLE)
|
|
452
|
+
scr_cyc_push(&scr_cands, obj);
|
|
194
453
|
}
|
|
454
|
+
scr_roots[g].n = 0;
|
|
195
455
|
}
|
|
196
|
-
|
|
456
|
+
for (size_t i = 0; i < scr_cands.n; i++) scr_mark_gray(scr_cands.v[i]);
|
|
457
|
+
|
|
458
|
+
for (size_t i = 0; i < scr_cands.n; i++) scr_scan(scr_cands.v[i]);
|
|
459
|
+
|
|
460
|
+
scr_white.n = 0;
|
|
461
|
+
for (size_t i = 0; i < scr_cands.n; i++) scr_collect_white(scr_cands.v[i]);
|
|
462
|
+
|
|
463
|
+
/* Pin the cross-generation targets of everything about to be freed, while
|
|
464
|
+
* `gen` still holds the values the walk filtered on (promotion below is
|
|
465
|
+
* what changes them). A full pass skips no edge, so it pins nothing. */
|
|
466
|
+
scr_xgen.n = 0;
|
|
467
|
+
if (gen_limit < SCR_CYC_MATURE) {
|
|
468
|
+
for (size_t i = 0; i < scr_white.n; i++) {
|
|
469
|
+
void *obj = scr_white.v[i];
|
|
470
|
+
scr_cyc_hdr(obj)->trace(obj, scr_xg_pin_visit, NULL);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/* Every phase that consults SCR_CYC_SKIP has run, so the recorded
|
|
475
|
+
* survivors can graduate now. They are externally referenced, so none of
|
|
476
|
+
* them is in the white set about to be freed. */
|
|
477
|
+
for (size_t i = 0; i < scr_promote.n; i++)
|
|
478
|
+
scr_cyc_hdr(scr_promote.v[i])->gen = SCR_CYC_MATURE;
|
|
479
|
+
scr_promote.n = 0;
|
|
197
480
|
|
|
198
|
-
|
|
481
|
+
/* A restricted pass may have spared a candidate only because the edge
|
|
482
|
+
* keeping it alive came from a generation it refused to walk — the
|
|
483
|
+
* deliberate conservatism above. But the candidate buffer is the ONLY
|
|
484
|
+
* root set this collector has, and markRoots consumed the entry. Dropping
|
|
485
|
+
* it would retire the one record that this object is a possible cycle
|
|
486
|
+
* root, and a dead cycle whose members all got spared that way would
|
|
487
|
+
* never be walked again by any pass, full ones included. So a restricted
|
|
488
|
+
* pass hands its survivors back as candidates in the generation they were
|
|
489
|
+
* just promoted into, where a full pass — which skips nothing, and so
|
|
490
|
+
* judges them on real reference counts — will settle them. A full pass
|
|
491
|
+
* needs none of this: it walked every edge, so rc > 0 there means a
|
|
492
|
+
* genuine outside reference and Bacon-Rajan's own reasoning retires the
|
|
493
|
+
* candidate. Re-buffering costs a walk, never correctness: an object that
|
|
494
|
+
* is truly live gets re-blackened by the next retain and dropped then. */
|
|
495
|
+
if (gen_limit < SCR_CYC_MATURE) {
|
|
496
|
+
for (size_t i = 0; i < scr_cands.n; i++) {
|
|
497
|
+
void *obj = scr_cands.v[i];
|
|
498
|
+
ScrCycHdr *h = scr_cyc_hdr(obj);
|
|
499
|
+
if (h->color == SCR_CYC_DOOMED) continue; /* being freed below */
|
|
500
|
+
h->color = SCR_CYC_PURPLE;
|
|
501
|
+
if (!h->buffered) {
|
|
502
|
+
h->buffered = 1;
|
|
503
|
+
h->buf_index = scr_roots[h->gen].n;
|
|
504
|
+
scr_cyc_push(&scr_roots[h->gen], obj);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
199
508
|
|
|
200
|
-
/* collectWhite over a drained buffer: clear every buffered flag first so
|
|
201
|
-
* the recursion can gather buffered members of an earlier root's cycle. */
|
|
202
|
-
size_t n = scr_nroots;
|
|
203
|
-
scr_nroots = 0;
|
|
204
|
-
for (size_t i = 0; i < n; i++) scr_cyc_hdr(scr_roots[i])->buffered = 0;
|
|
205
|
-
scr_nwhite = 0;
|
|
206
|
-
for (size_t i = 0; i < n; i++) scr_collect_white(scr_roots[i]);
|
|
207
509
|
/* Teardowns run after the full walk. They may release untraced children
|
|
208
510
|
* (plain RC) — which can re-buffer survivors for the NEXT pass — but
|
|
209
511
|
* never touch traced (white, already-accounted) edges. */
|
|
210
|
-
|
|
211
|
-
|
|
512
|
+
size_t freed = scr_white.n;
|
|
513
|
+
for (size_t i = 0; i < scr_white.n; i++) {
|
|
514
|
+
void *obj = scr_white.v[i];
|
|
212
515
|
scr_cyc_hdr(obj)->free_fn(obj);
|
|
213
516
|
}
|
|
214
|
-
|
|
517
|
+
scr_white.n = 0;
|
|
215
518
|
|
|
519
|
+
/* Now drain the pins: the white set is gone and generations have settled,
|
|
520
|
+
* so each of those edges can be given up for real. A target that survives
|
|
521
|
+
* lands back in the candidate buffer — it just lost a reference, which is
|
|
522
|
+
* exactly what makes an object a possible cycle root. */
|
|
523
|
+
scr_xg_freed = 0;
|
|
524
|
+
for (size_t i = 0; i < scr_xgen.n; i++) {
|
|
525
|
+
void *obj = scr_xgen.v[i];
|
|
526
|
+
/* Drop the pin first. The phantom edge is still counted, so this cannot
|
|
527
|
+
* reach zero and the release below is the one that settles the object. */
|
|
528
|
+
SCR_RC(obj) -= 1;
|
|
529
|
+
scr_xg_release(obj);
|
|
530
|
+
}
|
|
531
|
+
scr_xgen.n = 0;
|
|
532
|
+
freed += scr_xg_freed;
|
|
533
|
+
|
|
534
|
+
if (gen_limit >= SCR_CYC_MATURE) {
|
|
535
|
+
scr_cyc_live_after_full = scr_cyc_live;
|
|
536
|
+
scr_cyc_scheduled_mature_age = 0;
|
|
537
|
+
}
|
|
538
|
+
/* markRoots drained buffers in bulk and teardowns moved the count around;
|
|
539
|
+
* resync from the buffers themselves and re-arm. */
|
|
540
|
+
scr_cyc_nbuffered = scr_cyc_buffered();
|
|
541
|
+
scr_cyc_trigger = scr_cyc_nbuffered + scr_cyc_nursery_threshold();
|
|
216
542
|
scr_collecting = false;
|
|
543
|
+
return freed;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
void scr_collect_cycles(void) {
|
|
547
|
+
/* The full pass, run to a fixpoint. A teardown's releases can drop the
|
|
548
|
+
* last reference to a further cycle, so one pass does not always drain
|
|
549
|
+
* everything reclaimable — and the callers that matter (program exit,
|
|
550
|
+
* library session reset) are immediately followed by the RC audit and
|
|
551
|
+
* want nothing reclaimable left behind. Only a pass that freed something
|
|
552
|
+
* can have buffered anything new, so a pass that comes up empty ends
|
|
553
|
+
* this; and since each repeat needs a strictly smaller live heap to
|
|
554
|
+
* continue, it terminates. */
|
|
555
|
+
while (scr_cyc_pass(SCR_CYC_MATURE)
|
|
556
|
+
&& (scr_roots[SCR_CYC_NURSERY].n || scr_roots[SCR_CYC_MATURE].n)) {
|
|
557
|
+
}
|
|
217
558
|
}
|