Merge main

This commit is contained in:
Chris Tate
2026-10-18 13:24:16 -05:00
8 changed files with 5842 additions and 5482 deletions
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5980-cycle-components.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5996-rejections-between-macrotasks.cjs"
],
"diags": []
}
+11 -2
View File
@@ -4464,7 +4464,9 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) {
}
scr_firing_id = 0;
if (scr_exc_pending()) return false; /* uncaught throw in a callback: main handles it */
if (scr_ready_len > 0 || scr_nt_head != NULL) break; /* drain ticks and microtasks before more timers */
/* Drain ticks and microtasks, and report the rejections the callback
* left, before more timers (Node's runNextTicks between timers). */
if (scr_ready_len > 0 || scr_nt_head != NULL || scr_nunhandled > 0) break;
}
/* The check phase: immediates queued BEFORE this phase started run
* now, FIFO; ones a callback queues wait for the next turn (the end
@@ -4474,7 +4476,7 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) {
* every macrotask; pending ticks or a woken fiber restart the turn
* first (the timer loop's break above), so the phase only runs on a
* drained checkpoint. */
if (scr_ready_len == 0 && scr_nt_head == NULL && scr_pending_immediates > 0) {
if (scr_ready_len == 0 && scr_nt_head == NULL && scr_nunhandled == 0 && scr_pending_immediates > 0) {
size_t end = scr_nimmediates;
while (scr_immediates_head < end) {
size_t i = scr_immediates_head++;
@@ -4490,6 +4492,13 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) {
scr_closure_release(cb);
if (scr_exc_pending()) return false; /* uncaught throw: main handles it */
if (!scr_loop_run_checkpoint()) return false;
/* Node's runNextTicks between immediates also reports the
* rejections the callback left before the next one runs; a
* listener's jobs drain before the next report. */
while (scr_nunhandled > 0 && (top_level == NULL || top_level->state != SCR_PROM_REJECTED)) {
if (scr_report_unhandled_rejections()) return true;
if (!scr_loop_run_checkpoint()) return false;
}
if (top_level != NULL && top_level->state == SCR_PROM_REJECTED) break;
}
/* Fully drained: reset the cursor so the array reuses its slots. */
+261 -61
View File
@@ -26,14 +26,19 @@
* count was already given up (which would over-decrement and free live
* data).
*
* The main graph phases keep a bounded depth-first walk and defer deeper
* traces to reusable worklists. Ordinary RC destruction and cross-generation
* releases are separate paths. White candidates are recorded during scan
* and filtered after outside roots settle, avoiding a separate gather walk.
* markGray also records the objects it grays and counts those still
* externally referenced (rc > 0 after the trial decrements). When none is,
* every gray object is garbage, which is what scan would find, so the pass
* whitens the recorded set and skips scan.
* The graph phases keep a bounded depth-first walk and defer deeper traces
* to reusable worklists. Ordinary RC destruction and cross-generation
* releases are separate paths. markGray records the objects it grays (up
* to SCR_CYC_GRAY_LIST_MAX) and counts those still externally referenced
* (rc > 0 after the trial decrements), per pass and per component: the
* objects first reached from one candidate. A component with none is
* garbage, which is what scan would find, so the pass dooms it at once.
* For the others, scan blackens from each recorded object that is still
* externally referenced, which restores exactly the survivors, and what
* stays gray is the garbage: no walk whitens it. Only a pass too large to
* record walks scan from the candidates, whitening as it goes, and then
* filters the white set after outside roots settle, avoiding a separate
* gather walk.
*
* ── generations ──────────────────────────────────────────────────────
* A pass costs O(objects reachable from its candidates), and a candidate
@@ -225,6 +230,9 @@ typedef struct {
size_t trigger;
size_t alloc_trigger;
size_t gray_referenced;
size_t comp_referenced;
uint32_t gray_comp;
ScrVec live_ranges;
size_t scheduled_mature_age;
size_t xg_freed;
size_t mature_dead;
@@ -256,6 +264,9 @@ static _Thread_local ScrCycThread scr_cyc_thread = {.gen_limit = SCR_CYC_OLD};
#define scr_cyc_trigger (scr_cyc_thread.trigger)
#define scr_cyc_alloc_trigger (scr_cyc_thread.alloc_trigger)
#define scr_gray_referenced (scr_cyc_thread.gray_referenced)
#define scr_comp_referenced (scr_cyc_thread.comp_referenced)
#define scr_gray_comp (scr_cyc_thread.gray_comp)
#define scr_live_ranges (scr_cyc_thread.live_ranges)
#define scr_cyc_scheduled_mature_age (scr_cyc_thread.scheduled_mature_age)
#define scr_xg_freed (scr_cyc_thread.xg_freed)
#define scr_cyc_mature_dead (scr_cyc_thread.mature_dead)
@@ -347,7 +358,7 @@ void *scr_cyc_alloc(size_t size, ScrCycType *type) {
void scr_cyc_free(void *obj) { scr_cyc_free_inline(obj); }
static void scr_cyc_grow(ScrVec *vec) {
static __attribute__((noinline, cold)) void scr_cyc_grow(ScrVec *vec) {
/* Candidate positions are 31-bit (ScrCycHdr.buf). */
if (vec->cap > (size_t)(SCR_CYC_BUFFERED_BIT - 1) / 2) scr_cyc_oom();
vec->cap = vec->cap ? vec->cap * 2 : 64;
@@ -368,6 +379,16 @@ static inline void scr_cyc_push_work(ScrVec *vec, void *obj) {
if (vec->n > vec->used) vec->used = vec->n;
}
/* The same push inline, for the gray list markGray fills once per object;
* the pass records the list's high-water mark itself (scr_cyc_note_used). */
static inline void scr_cyc_push_gray(ScrVec *vec, void *obj) {
if (__builtin_expect(vec->n == vec->cap, 0)) scr_cyc_grow(vec);
vec->v[vec->n++] = obj;
}
static inline void scr_cyc_note_used(ScrVec *vec) {
if (vec->n > vec->used) vec->used = vec->n;
}
/* ── the candidate-root buffers (one per generation) ──────────────────── */
#ifndef SCR_WORKERS
@@ -392,7 +413,6 @@ static SCR_TL ScrVec scr_promote;
static SCR_TL ScrVec scr_pending;
static SCR_TL ScrVec scr_restore_pending;
#endif
#define SCR_CYC_WALK_DEPTH 64
/* The gathered white set (freed after the walk completes). */
#ifndef SCR_WORKERS
@@ -547,9 +567,19 @@ static SCR_TL size_t scr_cyc_alloc_trigger = 0;
#endif
/* Gray objects whose count is above zero after the trial decrements so far
* (see scr_mg_visit). markGray records the gray objects in scr_white while
* there are fewer than SCR_CYC_GRAY_LIST_MAX; a larger walk runs scan. */
* there are fewer than SCR_CYC_GRAY_LIST_MAX; a larger walk runs scan.
*
* The same count is kept per component (see "components" at scr_cyc_pass):
* the objects one candidate's walk grays first carry that walk's number in
* `buf` (gray objects are never buffered during a pass), and
* scr_comp_referenced counts those still externally referenced.
* scr_live_ranges holds (start, end) ranges of scr_white whose component
* was externally referenced and is left to scan. */
#ifndef SCR_WORKERS
static SCR_TL size_t scr_gray_referenced = 0;
static SCR_TL size_t scr_comp_referenced = 0;
static SCR_TL uint32_t scr_gray_comp = 0;
static SCR_TL ScrVec scr_live_ranges;
#endif
#define SCR_CYC_GRAY_LIST_MAX 65536
@@ -982,42 +1012,103 @@ void scr_cyc_on_release(void *obj) {
((child) == NULL || SCR_RC(child) == SIZE_MAX \
|| scr_cyc_gen(scr_cyc_hdr(child)) > scr_gen_limit)
/* Trace callbacks forward an integer depth through their opaque context;
* it is never dereferenced. The ABI already carries it through every edge. */
static void scr_mark_gray(void *obj, uintptr_t depth);
/* The walks are depth-first and recursive up to SCR_CYC_WALK_DEPTH, where
* an object is queued instead (scr_pending, or scr_restore_pending for the
* restoration, which can run inside scan) and traced from the queue. Trace
* callbacks forward that integer depth through their opaque context; it is
* never dereferenced. */
#define SCR_CYC_WALK_DEPTH 64
/* A walk visits a dense cycle-capable array's children itself: the present
* dense elements, which are all scr_arr_trace_v visits when the array has
* no sparse entries, properties or metadata (no `ext`). Each walk gets its
* own copy, out of line, so the per-edge visitors stay small. */
#define SCR_CYC_ARRAY_WALK(name, visit) \
static __attribute__((noinline)) void name(const ScrArr *arr, void *ctx) { \
size_t end = arr->len < arr->cap ? arr->len : arr->cap; \
size_t filled = arr->filled < end ? arr->filled : end; \
for (size_t i = 0; i < filled; i++) visit((void *)(uintptr_t)arr->data[i], ctx); \
for (size_t i = filled; i < end; i++) \
if (arr->present[i] == SCR_ARR_VALUE) visit((void *)(uintptr_t)arr->data[i], ctx); \
}
/* Visit every child of `obj` with VISIT, through ARRAY_WALK for a dense
* cycle-capable array and through the type's trace otherwise. */
#define SCR_CYC_CHILDREN(obj, type, ARRAY_WALK, VISIT, depth) \
do { \
if ((type)->kind == SCR_CYC_KIND_ARRAY && ((const ScrArr *)(obj))->ext == NULL) \
ARRAY_WALK((const ScrArr *)(obj), (void *)(depth)); \
else \
(type)->trace((obj), (VISIT), (void *)(depth)); \
} while (0)
static void scr_mg_visit(void *child, void *ctx);
static void scr_sb_visit(void *child, void *ctx);
static void scr_scan_visit(void *child, void *ctx);
SCR_CYC_ARRAY_WALK(scr_mg_array, scr_mg_visit)
SCR_CYC_ARRAY_WALK(scr_sb_array, scr_sb_visit)
SCR_CYC_ARRAY_WALK(scr_scan_array, scr_scan_visit)
static inline __attribute__((always_inline)) void scr_mark_gray(void *obj, uintptr_t depth);
static void scr_mg_visit(void *child, void *ctx) {
if (child == NULL || SCR_RC(child) == SIZE_MAX) return;
if (scr_cyc_gen(scr_cyc_hdr(child)) > scr_gen_limit) {
ScrCycHdr *h = scr_cyc_hdr(child);
if (scr_cyc_gen(h) > scr_gen_limit) {
scr_cyc_push_work(&scr_xgen_edges, scr_gray_holder);
scr_cyc_push_work(&scr_xgen_edges, child);
return;
}
size_t rc = --SCR_RC(child);
if (scr_cyc_color(scr_cyc_hdr(child)) == SCR_CYC_GRAY) {
if (rc == 0) scr_gray_referenced--;
if (scr_cyc_color(h) == SCR_CYC_GRAY) {
if (rc == 0) {
scr_gray_referenced--;
if (h->buf == scr_gray_comp) scr_comp_referenced--;
}
return;
}
scr_mark_gray(child, (uintptr_t)ctx);
}
static void scr_mark_gray(void *obj, uintptr_t depth) {
/* markGray reaches `obj` (its count already trial-decremented): gray it,
* number it with the walk's component, record it, count it while it is
* externally referenced, and walk its children. */
static inline __attribute__((always_inline)) void scr_mark_gray(void *obj, uintptr_t depth) {
ScrCycHdr *h = scr_cyc_hdr(obj);
if (scr_cyc_color(h) == SCR_CYC_GRAY) return;
scr_cyc_set_color(h, SCR_CYC_GRAY);
/* A full pass takes every buffer, so each buffered object it reaches
* leaves its buffer here, in the header write the walk makes anyway. */
if (scr_gen_limit == SCR_CYC_OLD && !scr_cyc_take) scr_cyc_clear_buffered(h);
if (scr_white.n < SCR_CYC_GRAY_LIST_MAX) scr_cyc_push_work(&scr_white, obj);
if (SCR_RC(obj) > 0) scr_gray_referenced++;
* leaves its buffer here, in the header write that numbers it anyway. A
* limited pass can gray candidates it did not take; they keep their
* buffer position and are simply never counted as members. */
if (!scr_cyc_is_buffered(h) || (scr_gen_limit == SCR_CYC_OLD && !scr_cyc_take))
h->buf = scr_gray_comp;
if (scr_white.n < SCR_CYC_GRAY_LIST_MAX) scr_cyc_push_gray(&scr_white, obj);
if (SCR_RC(obj) > 0) {
scr_gray_referenced++;
scr_comp_referenced++;
}
if (depth == SCR_CYC_WALK_DEPTH) {
scr_cyc_push_work(&scr_pending, obj);
return;
}
void *holder = scr_gray_holder;
scr_gray_holder = obj;
scr_cyc_type(h)->trace(obj, scr_mg_visit, (void *)(depth + 1));
const ScrCycType *type = scr_cyc_type(h);
SCR_CYC_CHILDREN(obj, type, scr_mg_array, scr_mg_visit, depth + 1);
scr_gray_holder = holder;
}
/* markGray from one (black) candidate, through the queue. */
static void scr_mark_gray_from(void *root) {
scr_mark_gray(root, 0);
while (scr_pending.n) {
void *obj = scr_pending.v[--scr_pending.n];
const ScrCycType *type = scr_cyc_type(scr_cyc_hdr(obj));
scr_gray_holder = obj;
SCR_CYC_CHILDREN(obj, type, scr_mg_array, scr_mg_visit, 1);
}
}
/* scan's restoration: blacken an externally referenced object and
* everything it reaches that is not black yet, giving back the trial
* decrements of the edges it follows. */
static void scr_scan_black(void *obj, uintptr_t depth);
static void scr_sb_visit(void *child, void *ctx) {
if (SCR_CYC_SKIP(child)) return;
@@ -1028,14 +1119,29 @@ static void scr_sb_visit(void *child, void *ctx) {
static void scr_scan_black(void *obj, uintptr_t depth) {
ScrCycHdr *h = scr_cyc_hdr(obj);
scr_cyc_set_color(h, SCR_CYC_BLACK);
/* Drop markGray's component number: a survivor leaves the pass unbuffered
* unless it is a candidate that a limited pass did not take. */
if (!scr_cyc_is_buffered(h)) scr_cyc_clear_buffered(h);
if (scr_cyc_gen(h) < SCR_CYC_OLD) scr_cyc_push_work(&scr_promote, obj);
if (depth == SCR_CYC_WALK_DEPTH) {
scr_cyc_push_work(&scr_restore_pending, obj);
return;
}
scr_cyc_type(h)->trace(obj, scr_sb_visit, (void *)(depth + 1));
const ScrCycType *type = scr_cyc_type(h);
SCR_CYC_CHILDREN(obj, type, scr_sb_array, scr_sb_visit, depth + 1);
}
static void scr_restore_from(void *root) {
scr_scan_black(root, 0);
while (scr_restore_pending.n) {
void *obj = scr_restore_pending.v[--scr_restore_pending.n];
const ScrCycType *type = scr_cyc_type(scr_cyc_hdr(obj));
SCR_CYC_CHILDREN(obj, type, scr_sb_array, scr_sb_visit, 1);
}
}
/* scan, walking: restore a gray object that is externally referenced, else
* whiten it, record it once (a later outside root can still restore it;
* settle filters the list) and walk its children. */
static void scr_scan(void *obj, uintptr_t depth);
static void scr_scan_visit(void *child, void *ctx) {
if (SCR_CYC_SKIP(child)) return;
@@ -1045,22 +1151,17 @@ static void scr_scan(void *obj, uintptr_t depth) {
ScrCycHdr *h = scr_cyc_hdr(obj);
if (scr_cyc_color(h) != SCR_CYC_GRAY) return;
if (SCR_RC(obj) > 0) {
scr_scan_black(obj, 0);
while (scr_restore_pending.n) {
void *next = scr_restore_pending.v[--scr_restore_pending.n];
scr_cyc_type(scr_cyc_hdr(next))->trace(next, scr_sb_visit, NULL);
}
scr_restore_from(obj);
return;
}
scr_cyc_set_color(h, SCR_CYC_WHITE);
/* Record each whitened object once. A later outside root can restore it;
* filter the list only after every scan finishes. No gather trace needed. */
scr_cyc_push_work(&scr_white, obj);
if (depth == SCR_CYC_WALK_DEPTH) {
scr_cyc_push_work(&scr_pending, obj);
return;
}
scr_cyc_type(h)->trace(obj, scr_scan_visit, (void *)(depth + 1));
const ScrCycType *type = scr_cyc_type(h);
SCR_CYC_CHILDREN(obj, type, scr_scan_array, scr_scan_visit, depth + 1);
}
/* ── cross-generation edges ───────────────────────────────────────────── */
@@ -1138,22 +1239,28 @@ static SCR_CYC_COMPACT void scr_xg_release(void *obj) {
* spend their time in the trace and teardown calls they make, so this half is
* built for size; the graph walk above it stays fully optimized. Returns how
* many objects the pass freed. */
static SCR_CYC_COMPACT size_t scr_cyc_settle(unsigned gen_limit) {
static SCR_CYC_COMPACT size_t scr_cyc_settle(unsigned gen_limit, bool doomed) {
size_t dead = 0;
scr_cyc_mature_dead = 0;
for (size_t i = 0; i < scr_white.n; i++) {
scr_cyc_note_used(&scr_white);
/* The list holds the dead set: doomed components, and what scan whitened
* (black once a later outside root restored it), or, when the pass found
* nothing externally referenced, every gray object. `doomed` says that
* every entry is a doomed component's already. */
for (size_t i = 0; i < scr_white.n && !doomed; i++) {
void *obj = scr_white.v[i];
ScrCycHdr *h = scr_cyc_hdr(obj);
if (scr_cyc_color(h) != SCR_CYC_WHITE) continue;
/* A limited pass can reach candidates it did not take: out of their
* buffers before they are freed. */
if (scr_cyc_take && scr_cyc_is_buffered(h)) scr_cyc_on_dead(obj);
scr_cyc_set_color(h, SCR_CYC_DOOMED);
if (gen_limit < SCR_CYC_OLD) scr_cyc_freed(scr_cyc_type(h), gen_limit);
else scr_cyc_mature_dead += scr_cyc_gen(h) == SCR_CYC_MATURE;
unsigned color = scr_cyc_color(h);
if (color == SCR_CYC_BLACK) continue;
if (color != SCR_CYC_DOOMED) {
/* A limited pass can reach candidates it did not take: out of their
* buffers before they are freed. */
if (scr_cyc_take && scr_cyc_is_buffered(h)) scr_cyc_on_dead(obj);
scr_cyc_set_color(h, SCR_CYC_DOOMED);
}
scr_white.v[dead++] = obj;
}
scr_white.n = dead;
if (!doomed) scr_white.n = dead;
/* Pin the cross-generation targets of everything about to be freed, while
* `gen` still holds the values the walk filtered on (promotion below is
@@ -1217,13 +1324,19 @@ static SCR_CYC_COMPACT size_t scr_cyc_settle(unsigned gen_limit) {
* over every page the dead structure used, and the structures built from
* them, and every later walk over those, would miss the cache on most
* objects. In address order the blocks come back in the order the bump
* pointers hand out fresh memory. Teardown order is not observable. */
* pointers hand out fresh memory. Teardown order is not observable. The
* level statistics are counted here, where the teardown reads the type
* anyway; they only steer later passes. */
size_t freed = scr_white.n;
if (scr_white.n >= SCR_CYC_ORDER_MIN)
scr_mem_order_for_free(scr_white.v, scr_white.n, sizeof(ScrCycHdr));
for (size_t i = 0; i < scr_white.n; i++) {
void *obj = scr_white.v[i];
scr_cyc_type(scr_cyc_hdr(obj))->free_fn(obj);
ScrCycHdr *h = scr_cyc_hdr(obj);
const ScrCycType *type = scr_cyc_type(h);
if (gen_limit < SCR_CYC_OLD) scr_cyc_freed(type, gen_limit);
else scr_cyc_mature_dead += scr_cyc_gen(h) == SCR_CYC_MATURE;
type->free_fn(obj);
}
scr_white.n = 0;
@@ -1299,6 +1412,7 @@ static size_t scr_cyc_pass_done(size_t freed) {
scr_cyc_trim(&scr_restore_pending);
scr_cyc_trim(&scr_xgen);
scr_cyc_trim(&scr_xgen_edges);
scr_cyc_trim(&scr_live_ranges);
return freed;
}
@@ -1369,28 +1483,114 @@ static size_t scr_cyc_pass(unsigned gen_limit) {
scr_xgen_edges.n = 0;
scr_white.n = 0;
scr_gray_referenced = 0;
for (size_t i = 0; i < scr_cands.n; i++) scr_mark_gray(scr_cands.v[i], 0);
while (scr_pending.n) {
void *obj = scr_pending.v[--scr_pending.n];
scr_gray_holder = obj;
scr_cyc_type(scr_cyc_hdr(obj))->trace(obj, scr_mg_visit, NULL);
scr_live_ranges.n = 0;
/* Components. Each candidate that no earlier walk reached starts a walk
* of its own, and the objects that walk grays first form its component.
* A component whose objects all end the walk at count zero is garbage:
* a count of zero means every reference to the object was trial-deleted,
* i.e. came from an object grayed so far. An earlier component cannot
* hold one (the walk that grayed it would have grayed the target too),
* so every reference comes from inside the component, and a later walk
* cannot add one: its objects would be referencing this component, and
* those references are still counted. scan would whiten exactly this
* set, so it is doomed at once and scan runs only over the components
* that are externally referenced, if any. A limited pass keeps the single
* whole-pass decision below (its untaken candidates cannot carry the
* component number). */
bool split = scr_cyc_take == NULL;
uint32_t comp = 0;
for (size_t i = 0; i < scr_cands.n; i++) {
void *obj = scr_cands.v[i];
if (scr_cyc_color(scr_cyc_hdr(obj)) != SCR_CYC_BLACK) continue; /* walked already */
size_t start = scr_white.n;
scr_gray_comp = split ? ++comp : 0;
scr_comp_referenced = 0;
scr_mark_gray_from(obj);
if (!split) continue;
size_t end = scr_white.n;
if (scr_comp_referenced == 0 && end < SCR_CYC_GRAY_LIST_MAX) {
for (size_t j = start; j < end; j++) scr_cyc_set_color(scr_cyc_hdr(scr_white.v[j]), SCR_CYC_DOOMED);
} else if (scr_live_ranges.n && (uintptr_t)scr_live_ranges.v[scr_live_ranges.n - 1] == start) {
scr_live_ranges.v[scr_live_ranges.n - 1] = (void *)(uintptr_t)end;
} else {
scr_cyc_push_work(&scr_live_ranges, (void *)(uintptr_t)start);
scr_cyc_push_work(&scr_live_ranges, (void *)(uintptr_t)end);
}
}
if (scr_gray_referenced == 0 && scr_white.n < SCR_CYC_GRAY_LIST_MAX) {
for (size_t i = 0; i < scr_white.n; i++) scr_cyc_set_color(scr_cyc_hdr(scr_white.v[i]), SCR_CYC_WHITE);
return scr_cyc_pass_done(scr_cyc_settle(gen_limit));
scr_cyc_note_used(&scr_white);
/* Nothing externally referenced at all: every gray object is garbage,
* including components that looked referenced only because a later
* component held them. settle dooms the recorded set. */
if (scr_gray_referenced == 0 && scr_white.n < SCR_CYC_GRAY_LIST_MAX)
return scr_cyc_pass_done(scr_cyc_settle(gen_limit, split && scr_live_ranges.n == 0));
/* scan, from the recorded set when markGray recorded every gray object:
* the survivors are exactly what the externally referenced gray objects
* reach, so blackening from each of them in turn leaves the garbage gray
* without walking it again, and settle dooms what is still gray. Only
* the referenced components' entries need looking at; the doomed ones
* are settled already. */
if (scr_white.n < SCR_CYC_GRAY_LIST_MAX) {
/* One sweep over the list, keeping the doomed components' entries and,
* of the referenced components', only those still gray: a survivor
* leaves the list as soon as it is seen black, and an object kept here
* that a later restoration blackens is skipped by settle. */
size_t kept = 0, from = 0;
size_t nranges = split ? scr_live_ranges.n : 2;
for (size_t k = 0; k < nranges; k += 2) {
size_t start = split ? (uintptr_t)scr_live_ranges.v[k] : 0;
size_t end = split ? (uintptr_t)scr_live_ranges.v[k + 1] : scr_white.n;
if (start > from) memmove(scr_white.v + kept, scr_white.v + from, (start - from) * sizeof *scr_white.v);
kept += start - from;
for (size_t j = start; j < end; j++) {
void *obj = scr_white.v[j];
if (scr_cyc_color(scr_cyc_hdr(obj)) != SCR_CYC_GRAY) continue;
if (SCR_RC(obj) > 0) scr_restore_from(obj);
else scr_white.v[kept++] = obj;
}
from = end;
}
if (scr_white.n > from) memmove(scr_white.v + kept, scr_white.v + from, (scr_white.n - from) * sizeof *scr_white.v);
scr_white.n = kept + (scr_white.n - from);
return scr_cyc_pass_done(scr_cyc_settle(gen_limit, false));
}
scr_white.n = 0;
for (size_t i = 0; i < scr_cands.n; i++) scr_scan(scr_cands.v[i], 0);
/* Otherwise scan walks from the candidates. Keep only the doomed
* components' entries; scan appends what it whitens. */
if (!split) {
scr_white.n = 0;
} else if (scr_live_ranges.n) {
size_t kept = 0, from = 0;
for (size_t k = 0; k < scr_live_ranges.n; k += 2) {
size_t start = (uintptr_t)scr_live_ranges.v[k], end = (uintptr_t)scr_live_ranges.v[k + 1];
memmove(scr_white.v + kept, scr_white.v + from, (start - from) * sizeof *scr_white.v);
kept += start - from;
from = end;
}
memmove(scr_white.v + kept, scr_white.v + from, (scr_white.n - from) * sizeof *scr_white.v);
scr_white.n = kept + (scr_white.n - from);
}
/* Externally referenced candidates first: what they restore is never
* whitened only to be restored again. */
for (size_t i = 0; i < scr_cands.n; i++) {
void *obj = scr_cands.v[i];
if (scr_cyc_color(scr_cyc_hdr(obj)) == SCR_CYC_GRAY && SCR_RC(obj) > 0) scr_restore_from(obj);
}
for (size_t i = 0; i < scr_cands.n; i++) {
void *obj = scr_cands.v[i];
if (scr_cyc_color(scr_cyc_hdr(obj)) == SCR_CYC_GRAY) scr_scan(obj, 0);
}
while (scr_pending.n) {
void *obj = scr_pending.v[--scr_pending.n];
/* A later outside root may have restored this deferred white node and
* its descendants already. Never scan a black node's edges again. */
if (scr_cyc_color(scr_cyc_hdr(obj)) == SCR_CYC_WHITE)
scr_cyc_type(scr_cyc_hdr(obj))->trace(obj, scr_scan_visit, NULL);
/* A later outside root may have restored this deferred white object and
* its descendants already. Never scan a black object's edges again. */
ScrCycHdr *h = scr_cyc_hdr(obj);
if (scr_cyc_color(h) != SCR_CYC_WHITE) continue;
const ScrCycType *type = scr_cyc_type(h);
SCR_CYC_CHILDREN(obj, type, scr_scan_array, scr_scan_visit, 1);
}
return scr_cyc_pass_done(scr_cyc_settle(gen_limit));
return scr_cyc_pass_done(scr_cyc_settle(gen_limit, false));
}
#ifdef SCR_CYC_TESTING
@@ -1425,7 +1625,7 @@ void scr_collect_cycles(void) {
void scr_cyc_context_cleanup(void) {
scr_collect_cycles();
ScrVec *vectors[] = {&scr_roots[SCR_CYC_NURSERY], &scr_roots[SCR_CYC_MATURE],
&scr_roots[SCR_CYC_OLD], &scr_cands, &scr_promote, &scr_pending, &scr_restore_pending, &scr_white, &scr_xgen, &scr_xgen_edges,
&scr_roots[SCR_CYC_OLD], &scr_cands, &scr_promote, &scr_pending, &scr_restore_pending, &scr_white, &scr_xgen, &scr_xgen_edges, &scr_live_ranges,
&scr_pub_holes[SCR_CYC_NURSERY], &scr_pub_holes[SCR_CYC_MATURE], &scr_pub_holes[SCR_CYC_OLD]};
for (size_t i = 0; i < sizeof vectors / sizeof vectors[0]; i++) {
free(vectors[i]->v);
+3
View File
@@ -92,6 +92,9 @@ typedef struct ScrCycType {
* SCR_CYC_KIND_EMITTED (llvm/alloc.ts FIRST_EMITTED_CYC_KIND). */
enum {
SCR_CYC_KIND_NONE = 0,
/* Also how the collector recognizes a cycle-capable ScrArr, whose dense
* elements it visits inline (scr_cycle.c SCR_CYC_CHILDREN): reserved for
* scr_array.c's descriptor. */
SCR_CYC_KIND_ARRAY,
SCR_CYC_KIND_PROMISE,
SCR_CYC_KIND_BOX,
+101
View File
@@ -0,0 +1,101 @@
// Collector passes whose candidates mix dead structures with live ones: the
// ring under construction when a pass runs, owners that keep an inner node
// of an otherwise dead ring, trees whose child arrays have holes and sparse
// slots, and a live structure too large for a pass to record. Each dead
// structure is reclaimed whole, and everything still reachable survives
// with its contents intact. The sanitized lane's RC audit fails if anything
// below leaks or is freed while reachable.
class Node {
next: Node | null = null;
kids: (Node | undefined)[] = [];
owner: Node | null = null;
value: number;
constructor(value: number) {
this.value = value;
}
}
function ring(size: number, base: number): Node {
const first = new Node(base);
let last = first;
for (let i = 1; i < size; i++) {
const node = new Node(base + i);
last.next = node;
last = node;
}
last.next = first;
return first;
}
function tree(depth: number, owner: Node | null, value: number): Node {
const node = new Node(value);
node.owner = owner;
if (depth > 0) for (let i = 0; i < 3; i++) node.kids.push(tree(depth - 1, node, value * 3 + i));
return node;
}
function total(start: Node): number {
const seen = new Set<Node>();
const stack: Node[] = [start];
let sum = 0;
while (stack.length > 0) {
const node = stack.pop()!;
if (seen.has(node)) continue;
seen.add(node);
sum = (sum + node.value) % 1000003;
if (node.next) stack.push(node.next);
if (node.owner) stack.push(node.owner);
for (const kid of node.kids) if (kid) stack.push(kid);
}
return sum * 1000 + (seen.size % 1000);
}
// Dead rings dropped while the next one is being built; a few are kept.
let rings = 0;
const kept: Node[] = [];
for (let round = 0; round < 3000; round++) {
const r = ring(5 + (round % 7), round);
rings = (rings + r.next!.value) % 1000003;
if (round % 500 === 0) kept.push(r);
}
console.log("rings", rings, kept.map(total).join(" "));
// Rings whose head is dropped while an owner keeps a node in the middle:
// the ring stays reachable through it and must survive whole.
const anchors: Node[] = [];
for (let round = 0; round < 400; round++) {
let mid = ring(30, round * 100);
for (let i = 0; i < 15; i++) mid = mid.next!;
if (round % 4 === 0) anchors.push(mid);
}
let anchored = 0;
for (const anchor of anchors) anchored = (anchored + total(anchor)) % 1000003;
console.log("anchored", anchors.length, anchored);
// Trees with holes and sparse slots in their child arrays.
let trees = 0;
const shaped: Node[] = [];
for (let round = 0; round < 300; round++) {
const t = tree(4, null, round);
if (round % 3 === 0) t.kids.length = 6;
if (round % 5 === 0) t.kids[200] = tree(2, t, round + 7);
if (round % 7 === 0) t.kids[0]!.kids.length = 1;
trees = (trees + total(t)) % 1000003;
if (round % 60 === 0) shaped.push(t.kids[2]!);
}
console.log("trees", trees, shaped.map(total).join(" "));
// A live tree larger than one pass records, built and read while dead
// structures keep dying around it, then dropped.
const big = tree(10, null, 1);
let churn = 0;
for (let round = 0; round < 200; round++) {
const r = ring(50, round);
const branch = big.kids[round % 3]!.kids[(round >> 2) % 3]!;
branch.next = r;
churn = (churn + total(r)) % 1000003;
if (round % 50 === 0) churn = (churn + total(big)) % 1000003;
branch.next = null;
}
console.log("big", churn, total(big));
@@ -0,0 +1,33 @@
'use strict';
// Unhandled rejections are reported after the callback that left them,
// before the next due timer or the next immediate runs (Node's
// runNextTicks between macrotasks), also when several timers expire in
// the same turn.
process.on('unhandledRejection', (reason) => {
console.log('unhandledRejection', reason instanceof Error ? reason.message : JSON.stringify(reason));
});
process.on('uncaughtException', (err, origin) => console.log(origin, err));
const start = Date.now();
setTimeout(() => void Promise.reject({ a: 1 }), 1);
setTimeout(() => void Promise.reject(new TypeError('typed reason')), 2);
setTimeout(() => {
throw 'thrown string';
}, 3);
setTimeout(() => {
console.log('timer 4');
Promise.reject('handled later').catch((e) => console.log('caught', e));
}, 4);
setTimeout(() => console.log('timer 5'), 5);
// Every timer above is due once this returns.
while (Date.now() - start < 20) {}
setTimeout(() => {
setImmediate(() => void Promise.reject('first immediate'));
setImmediate(() => {
console.log('second immediate');
process.nextTick(() => console.log('tick'));
void Promise.reject('second immediate');
});
setImmediate(() => console.log('third immediate'));
}, 30);
File diff suppressed because it is too large Load Diff