mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-24 06:14:01 +08:00
1222 lines
46 KiB
C
1222 lines
46 KiB
C
#include "../src/scr_runtime.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
typedef struct Node Node;
|
|
struct Node {
|
|
size_t rc;
|
|
Node *next;
|
|
Node *other;
|
|
Node *untraced;
|
|
};
|
|
|
|
static size_t freed;
|
|
static size_t traced; /* trace calls: the collector's walk, one per object visit */
|
|
|
|
static size_t configured_nursery_threshold(void) {
|
|
const char *env = getenv("SCR_CYCLE_THRESHOLD");
|
|
long value = env ? strtol(env, NULL, 10) : 0;
|
|
return value > 0 ? (size_t)value : 256;
|
|
}
|
|
|
|
/* How many times its size after the last full pass the heap may grow before
|
|
* the next once passes stop finding garbage: SCR_CYCLE_GROWTH_CAP rounded
|
|
* down to a power of two from 1 to 16, and 16 when unset. */
|
|
static size_t configured_growth_cap(void) {
|
|
const char *env = getenv("SCR_CYCLE_GROWTH_CAP");
|
|
long value = env ? strtol(env, NULL, 10) : 0;
|
|
if (value <= 0) return 16;
|
|
size_t cap = 1;
|
|
while (cap < 16 && (long)(cap * 2) <= value) cap *= 2;
|
|
return cap;
|
|
}
|
|
|
|
static void check(bool condition, const char *message) {
|
|
if (condition) return;
|
|
fprintf(stderr, "cycle test failed: %s\n", message);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
_Noreturn void scr_trap(const char *msg) {
|
|
fputs(msg, stderr);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
static void node_trace(void *obj, ScrTraceVisit visit, void *ctx) {
|
|
traced++;
|
|
visit(((Node *)obj)->next, ctx);
|
|
visit(((Node *)obj)->other, ctx);
|
|
}
|
|
|
|
static void node_free(void *obj) {
|
|
Node *untraced = ((Node *)obj)->untraced;
|
|
if (untraced) {
|
|
untraced->rc--;
|
|
scr_cyc_on_release(untraced);
|
|
}
|
|
freed++;
|
|
scr_cyc_free(obj);
|
|
}
|
|
|
|
static Node *make_ring_of(size_t count, ScrCycType *type) {
|
|
Node **nodes = calloc(count, sizeof(*nodes));
|
|
if (!nodes) scr_trap("cycle test: out of memory\n");
|
|
for (size_t i = 0; i < count; i++) {
|
|
nodes[i] = scr_cyc_alloc(sizeof(*nodes[i]), type);
|
|
nodes[i]->rc = 1; /* the ring edge that will point at this node */
|
|
}
|
|
for (size_t i = 0; i < count; i++)
|
|
nodes[i]->next = nodes[(i + 1) % count];
|
|
|
|
Node *root = nodes[0];
|
|
root->rc++; /* external owner */
|
|
free(nodes);
|
|
return root;
|
|
}
|
|
|
|
static Node *make_ring(size_t count) {
|
|
static ScrCycType cyc_type = { &node_trace, &node_free, 0 };
|
|
return make_ring_of(count, &cyc_type);
|
|
}
|
|
|
|
static void release_live(Node *node) {
|
|
node->rc--;
|
|
scr_cyc_on_release(node);
|
|
}
|
|
|
|
static void check_sparse_mature_backlog(size_t roots) {
|
|
enum { NODES_PER_RING = 32 };
|
|
size_t scheduled_passes = configured_nursery_threshold();
|
|
Node *ring_roots[2];
|
|
size_t before = freed;
|
|
|
|
for (size_t i = 0; i < roots; i++) {
|
|
ring_roots[i] = make_ring(NODES_PER_RING);
|
|
ring_roots[i]->rc++; /* temporary release makes it a candidate */
|
|
release_live(ring_roots[i]);
|
|
}
|
|
|
|
/* Promote and drain every candidate while the rings are externally live.
|
|
* This also resets scheduled age, independent of release-triggered passes. */
|
|
scr_collect_cycles();
|
|
check(freed == before, "setup full pass freed a live ring");
|
|
|
|
for (size_t i = 0; i < roots; i++) release_live(ring_roots[i]);
|
|
for (size_t i = 1; i < scheduled_passes; i++)
|
|
scr_cyc_collect_scheduled();
|
|
check(freed == before, "mature rings collected before configured boundary");
|
|
|
|
scr_cyc_collect_scheduled();
|
|
|
|
check(freed - before == roots * NODES_PER_RING,
|
|
"configured boundary did not collect the mature rings exactly");
|
|
}
|
|
|
|
static void check_age_reset_when_last_mature_root_dies(void) {
|
|
size_t partial_age = configured_nursery_threshold() / 2;
|
|
size_t scheduled_passes = configured_nursery_threshold();
|
|
size_t before = freed;
|
|
Node *ring;
|
|
Node *leaf;
|
|
|
|
ring = make_ring(32);
|
|
ring->rc++;
|
|
release_live(ring);
|
|
static ScrCycType cyc_type = { &node_trace, &node_free, 0 };
|
|
leaf = scr_cyc_alloc(sizeof(*leaf), &cyc_type);
|
|
leaf->rc = 1;
|
|
leaf->rc++; /* temporary release makes it a nursery candidate */
|
|
release_live(leaf);
|
|
|
|
/* Both objects are now mature, live, unbuffered, and scheduled age is zero. */
|
|
scr_collect_cycles();
|
|
check(freed == before, "setup full pass freed a live age-reset object");
|
|
|
|
leaf->rc++;
|
|
release_live(leaf); /* buffer the mature leaf while it remains externally live */
|
|
for (size_t i = 0; i < partial_age; i++) scr_cyc_collect_scheduled();
|
|
|
|
leaf->rc--;
|
|
scr_cyc_on_dead(leaf); /* removing the last mature root resets its age */
|
|
node_free(leaf);
|
|
check(freed == before + 1, "directly dead mature root was not freed");
|
|
|
|
/* The waiting ring starts a fresh age after the last prior root disappeared. */
|
|
release_live(ring);
|
|
for (size_t i = 1; i < scheduled_passes; i++)
|
|
scr_cyc_collect_scheduled();
|
|
check(freed == before + 1,
|
|
"age reset ring collected before configured boundary");
|
|
scr_cyc_collect_scheduled();
|
|
check(freed == before + 33,
|
|
"age reset ring was not collected at configured boundary");
|
|
}
|
|
|
|
static void check_dead_backlog_rearms_release_trigger(void) {
|
|
enum { BACKLOG = 32 };
|
|
size_t threshold = configured_nursery_threshold();
|
|
Node *leaves[BACKLOG];
|
|
|
|
for (size_t i = 0; i < BACKLOG; i++) {
|
|
static ScrCycType cyc_type = { &node_trace, &node_free, 0 };
|
|
leaves[i] = scr_cyc_alloc(sizeof(*leaves[i]), &cyc_type);
|
|
leaves[i]->rc = 2; /* external owner plus a temporary reference */
|
|
release_live(leaves[i]);
|
|
}
|
|
scr_collect_cycles(); /* promote the live leaves and drain their roots */
|
|
|
|
for (size_t i = 0; i < BACKLOG; i++) {
|
|
leaves[i]->rc++;
|
|
release_live(leaves[i]); /* build a mature candidate backlog */
|
|
}
|
|
if (threshold > 1)
|
|
scr_cyc_collect_scheduled(); /* re-arm over the entire backlog */
|
|
|
|
for (size_t i = 0; i < BACKLOG; i++) {
|
|
leaves[i]->rc--;
|
|
scr_cyc_on_dead(leaves[i]);
|
|
node_free(leaves[i]);
|
|
}
|
|
|
|
size_t before = freed;
|
|
for (size_t i = 0; i < threshold; i++)
|
|
release_live(make_ring(1));
|
|
check(freed - before == threshold,
|
|
"directly dead backlog delayed the next release-triggered pass");
|
|
}
|
|
|
|
static Node *make_leaf(void) {
|
|
static ScrCycType cyc_type = { &node_trace, &node_free, 0 };
|
|
Node *leaf = scr_cyc_alloc(sizeof(*leaf), &cyc_type);
|
|
leaf->rc = 1;
|
|
return leaf;
|
|
}
|
|
|
|
/* A temporary reference taken and dropped, as when a program reads it. */
|
|
static void read_live(Node *node) {
|
|
node->rc++;
|
|
release_live(node);
|
|
}
|
|
|
|
/* A promoted ring whose last outside owner has just been dropped. */
|
|
static void make_dead_mature_ring(size_t count) {
|
|
Node *ring = make_ring(count);
|
|
ring->rc++;
|
|
release_live(ring);
|
|
scr_collect_cycles();
|
|
release_live(ring); /* only the ring edge remains */
|
|
}
|
|
|
|
/* Dead mature rings reclaimed by one scheduled full pass that frees more
|
|
* than it walks live, which restores the backlog trigger's base fraction. */
|
|
static void collect_dead_mature_rings(void) {
|
|
enum { RING = 32, RINGS = 64 };
|
|
Node *rings[RINGS];
|
|
size_t before = freed;
|
|
for (size_t i = 0; i < RINGS; i++) {
|
|
rings[i] = make_ring(RING);
|
|
rings[i]->rc++;
|
|
release_live(rings[i]);
|
|
}
|
|
scr_collect_cycles();
|
|
for (size_t i = 0; i < RINGS; i++) release_live(rings[i]);
|
|
for (size_t i = 0; i < configured_nursery_threshold(); i++)
|
|
scr_cyc_collect_scheduled();
|
|
check(freed == before + RINGS * RING, "scheduled pass missed dead mature rings");
|
|
}
|
|
|
|
static void check_unproductive_backlog_backs_off(void) {
|
|
enum { LIVE = 8192, RING = 32 };
|
|
static Node *leaves[LIVE];
|
|
for (size_t i = 0; i < LIVE; i++) {
|
|
leaves[i] = make_leaf();
|
|
read_live(leaves[i]);
|
|
}
|
|
scr_collect_cycles(); /* promote; the explicit sweep never adapts the threshold */
|
|
collect_dead_mature_rings();
|
|
|
|
/* Reading a quarter of the live heap reaches the backlog trigger, whose
|
|
* pass finds the dead ring among live candidates but little else. */
|
|
size_t before = freed;
|
|
make_dead_mature_ring(RING);
|
|
for (size_t i = 0; i < 3000; i++) read_live(leaves[i]);
|
|
check(freed == before + RING, "backlog trigger missed a dead mature ring");
|
|
|
|
/* That unproductive pass doubled the threshold: the same reads wait. */
|
|
before = freed;
|
|
make_dead_mature_ring(RING);
|
|
for (size_t i = 0; i < 3500; i++) read_live(leaves[i]);
|
|
check(freed == before, "unproductive backlog pass did not back off");
|
|
for (size_t i = 3500; i < 5000; i++) read_live(leaves[i]);
|
|
check(freed == before + RING, "backed-off backlog trigger never collected");
|
|
|
|
/* A productive scheduled pass restores the original fraction. */
|
|
collect_dead_mature_rings();
|
|
before = freed;
|
|
make_dead_mature_ring(RING);
|
|
for (size_t i = 0; i < 3000; i++) read_live(leaves[i]);
|
|
check(freed == before + RING, "productive pass did not restore the backlog trigger");
|
|
|
|
for (size_t i = 0; i < LIVE; i++) {
|
|
leaves[i]->rc--;
|
|
scr_cyc_on_dead(leaves[i]);
|
|
node_free(leaves[i]);
|
|
}
|
|
}
|
|
|
|
/* Survive two passes: nursery to mature, then mature to old. */
|
|
static void age_to_old(Node *node) {
|
|
for (int i = 0; i < 2; i++) {
|
|
read_live(node);
|
|
scr_collect_cycles();
|
|
}
|
|
check(scr_cyc_gen(scr_cyc_hdr(node)) == SCR_CYC_OLD, "survivor did not reach the old generation");
|
|
}
|
|
|
|
/* Fresh live leaves, each released once so the release path keeps running
|
|
* its passes, until `target` objects have been freed or `limit` leaves have
|
|
* been made. Returns how many it made. */
|
|
static size_t grow_until(Node **keep, size_t limit, size_t target) {
|
|
size_t n = 0;
|
|
while (freed < target && n < limit) {
|
|
keep[n] = make_leaf();
|
|
read_live(keep[n]);
|
|
n++;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
static void drop_leaves(Node **keep, size_t n) {
|
|
for (size_t i = 0; i < n; i++) {
|
|
keep[i]->rc--;
|
|
scr_cyc_on_dead(keep[i]);
|
|
node_free(keep[i]);
|
|
}
|
|
}
|
|
|
|
/* A dead cycle that has aged into the old generation is reachable only by a
|
|
* full pass, which must reclaim it before the heap has grown a quarter past
|
|
* its size after the last full pass while no pass has yet asked for a wider
|
|
* fraction. Runs first, so the live heap is below the growth floor and that
|
|
* bound is known. */
|
|
static void check_old_garbage_reclaimed_while_growing(void) {
|
|
enum { RING = 64, FLOOR = 4096, LIMIT = 4 * FLOOR };
|
|
static Node *keep[LIMIT];
|
|
size_t before = freed;
|
|
Node *ring = make_ring(RING);
|
|
age_to_old(ring);
|
|
release_live(ring); /* last outside owner gone: a dead old cycle */
|
|
size_t grown = grow_until(keep, LIMIT, before + RING);
|
|
check(freed == before + RING, "dead old cycle was never reclaimed while the heap grew");
|
|
check(grown <= FLOOR + FLOOR / 4, "dead old cycle outlived the growth bound");
|
|
drop_leaves(keep, grown);
|
|
scr_collect_cycles();
|
|
}
|
|
|
|
/* Dead rings that survived at least one pass, each holding a traced edge
|
|
* into an old ring: whichever level reclaims them must pay those edges off,
|
|
* and must do so without an explicit sweep. */
|
|
static void check_older_rings_into_old(void) {
|
|
enum { OLD_RING = 2048, RING = 8, RINGS = 1024, LIMIT = 4 * (OLD_RING + RING * RINGS) };
|
|
static Node *keep[LIMIT];
|
|
Node *old = make_ring(OLD_RING);
|
|
age_to_old(old);
|
|
size_t before = freed;
|
|
Node *rings[RINGS];
|
|
for (size_t i = 0; i < RINGS; i++) {
|
|
rings[i] = make_ring(RING);
|
|
rings[i]->other = old;
|
|
old->rc++;
|
|
read_live(rings[i]);
|
|
}
|
|
scr_cyc_collect_scheduled(); /* every ring survives at least one pass */
|
|
for (size_t i = 0; i < RINGS; i++) {
|
|
check(scr_cyc_gen(scr_cyc_hdr(rings[i])) != SCR_CYC_NURSERY, "ring was not promoted");
|
|
release_live(rings[i]);
|
|
}
|
|
size_t grown = grow_until(keep, LIMIT, before + RINGS * RING);
|
|
check(freed == before + RINGS * RING, "dead older rings were never reclaimed");
|
|
check(old->rc == 2, "edges into the old generation were not paid off");
|
|
drop_leaves(keep, grown);
|
|
release_live(old);
|
|
scr_collect_cycles();
|
|
}
|
|
|
|
/* A dead cycle spanning the mature and old generations is spared by mature
|
|
* passes; promotion and re-buffering must still hand it to a full pass. */
|
|
static void check_cycle_spanning_mature_and_old(void) {
|
|
enum { LIMIT = 4 * 4096 };
|
|
static Node *keep[LIMIT];
|
|
Node *old = make_ring(4);
|
|
age_to_old(old);
|
|
Node *young = make_ring(4);
|
|
young->other = old;
|
|
old->rc++;
|
|
old->other = young;
|
|
young->rc++;
|
|
read_live(young);
|
|
scr_cyc_collect_scheduled(); /* young survives a pass and is promoted */
|
|
size_t before = freed;
|
|
release_live(old);
|
|
release_live(young); /* only the cycle's own edges remain */
|
|
size_t grown = grow_until(keep, LIMIT, before + 8);
|
|
check(freed == before + 8, "cycle spanning generations was never reclaimed");
|
|
drop_leaves(keep, grown);
|
|
scr_collect_cycles();
|
|
}
|
|
|
|
/* Grow a chain by `count` nodes, each new head owning the previous one and
|
|
* released once, as reading the newest entry of a retained list would. Every
|
|
* buffered head reaches the whole chain, so each full pass walks all of it. */
|
|
static Node *grow_chain(Node *head, size_t count) {
|
|
for (size_t i = 0; i < count; i++) {
|
|
Node *node = make_leaf();
|
|
node->next = head; /* takes over the caller's reference */
|
|
head = node;
|
|
read_live(head);
|
|
}
|
|
return head;
|
|
}
|
|
|
|
/* Full passes over a growing retained structure that find nothing widen the
|
|
* growth fraction, so the structure is not re-walked at every quarter of its
|
|
* growth. How far the fraction may widen, and how a productive pass narrows
|
|
* it, are pinned on a heap at the growth floor by
|
|
* check_growth_cap_bounds_floating_garbage: on a structure this size the
|
|
* default cap lets a dead cycle wait through millions of allocations. Leaves
|
|
* the fraction widened, so it runs after the tests that assume the base
|
|
* schedule. */
|
|
static void check_growth_schedule_follows_yield(void) {
|
|
enum { START = 4096, GROW = 64 * START };
|
|
size_t before = freed;
|
|
Node *head = grow_chain(make_leaf(), START);
|
|
scr_collect_cycles(); /* a baseline the bounds below are measured from */
|
|
size_t walked_before = traced;
|
|
head = grow_chain(head, GROW);
|
|
check(freed == before, "growing a live chain freed part of it");
|
|
/* Counting every level, walking the chain at every quarter or half of its
|
|
* growth costs eight to ten times its final length; the widened schedule
|
|
* keeps it near five. */
|
|
check(traced - walked_before <= 7 * (size_t)GROW,
|
|
"a growing retained structure was re-walked at every fraction of growth");
|
|
|
|
/* Close the chain into one dead cycle: the collector reclaims it whole. */
|
|
Node *tail = head;
|
|
while (tail->next) tail = tail->next;
|
|
tail->other = head;
|
|
head->rc++;
|
|
size_t length = scr_cyc_live; /* nothing else is live between the tests */
|
|
before = freed;
|
|
release_live(head);
|
|
scr_collect_cycles();
|
|
check(freed == before + length, "the closed chain was not reclaimed whole");
|
|
}
|
|
|
|
/* Grow one dead cycle until a full pass reclaims it, and return the live
|
|
* heap at the release that ran that pass. The cycle's only old member is
|
|
* also its only candidate; the young nodes appended to it are never
|
|
* buffered, so only a full pass walks or frees them, and that pass reclaims
|
|
* one old object against all of the growth, plus a dead old ring of
|
|
* `old_garbage` objects when that is nonzero. Dead one-node rings keep the
|
|
* release path running restricted passes meanwhile. The explicit sweeps in
|
|
* age_to_old leave a live heap below the growth floor, so the full pass is
|
|
* due once the heap grows past the floor by the old level's fraction. With
|
|
* `mixed`, the dead ring is mature instead, held in a cycle with one old
|
|
* node: a dead structure only a full pass can see. */
|
|
static size_t grow_dead_cycle_until_full_pass(size_t old_garbage, bool mixed) {
|
|
enum { FLOOR = 4096 };
|
|
size_t limit = FLOOR * 18 + 4 * configured_nursery_threshold();
|
|
Node *garbage = NULL;
|
|
if (old_garbage && !mixed) {
|
|
garbage = make_ring(old_garbage);
|
|
age_to_old(garbage);
|
|
}
|
|
Node *old = make_leaf();
|
|
age_to_old(old);
|
|
if (old_garbage && mixed) {
|
|
Node *tie = make_leaf();
|
|
age_to_old(tie);
|
|
garbage = make_ring(old_garbage);
|
|
read_live(garbage); /* a release-triggered pass may promote it at once */
|
|
if (scr_cyc_gen(scr_cyc_hdr(garbage)) == SCR_CYC_NURSERY) scr_collect_cycles();
|
|
check(scr_cyc_gen(scr_cyc_hdr(garbage)) == SCR_CYC_MATURE, "survivor did not reach the mature generation");
|
|
tie->other = garbage;
|
|
garbage->rc++;
|
|
garbage->other = tie;
|
|
tie->rc++;
|
|
release_live(tie); /* now held only by the ring */
|
|
}
|
|
check(scr_cyc_live < FLOOR, "live heap did not start below the growth floor");
|
|
if (garbage) release_live(garbage); /* last outside owner gone */
|
|
Node *first = make_leaf(); /* owned by old->other */
|
|
first->next = old;
|
|
old->rc++;
|
|
old->other = first;
|
|
release_live(old); /* external owner gone: a dead cycle with one old root */
|
|
size_t old_before = scr_cyc_old_freed;
|
|
for (size_t appended = 1; appended < limit; appended++) {
|
|
Node *node = make_leaf(); /* takes over old's reference to the newest */
|
|
node->next = old->other;
|
|
old->other = node;
|
|
Node *ring = make_ring(1);
|
|
size_t live = scr_cyc_live;
|
|
release_live(ring);
|
|
if (scr_cyc_old_freed != old_before) {
|
|
/* A mature pass that walks the mixed ring first promotes it. */
|
|
size_t old_dead = scr_cyc_old_freed - old_before;
|
|
check((mixed ? old_dead >= 2 && old_dead <= old_garbage + 2 : old_dead == old_garbage + 1)
|
|
&& scr_cyc_live < FLOOR / 2,
|
|
"the full pass did not reclaim the dead old garbage whole");
|
|
return live;
|
|
}
|
|
}
|
|
check(false, "a dead cycle through the old generation outlived the growth cap");
|
|
return 0;
|
|
}
|
|
|
|
/* Passes that keep finding nothing widen the old level's fraction up to the
|
|
* configured cap and no further, two doublings per pass once two passes in a
|
|
* row have asked: at the cap a dead cycle goes exactly when the heap has
|
|
* grown the cap past the floor, not one doubling earlier (a narrower cap)
|
|
* and not later (a wider or missing one). A pass that finds old garbage
|
|
* narrows the fraction at once, and a single pass that finds nothing after
|
|
* it does not widen it again. From where the ring below narrows it (two
|
|
* doublings under any cap of 4 or more, the base fraction under 1 and 2),
|
|
* the second pass in a row that finds nothing reaches the cap; widening a
|
|
* doubling per pass would not. Runs last: it leaves the fraction narrowed
|
|
* but not at the base. */
|
|
static void check_growth_cap_bounds_floating_garbage(void) {
|
|
enum { FLOOR = 4096 };
|
|
size_t cap = configured_growth_cap();
|
|
/* Passes wait for the release trigger, and dead rings float until the
|
|
* next restricted pass, so allow two nursery's worth. */
|
|
size_t slack = 2 * configured_nursery_threshold() + 2;
|
|
size_t narrowed = FLOOR + FLOOR * cap / 2 + slack;
|
|
size_t due = FLOOR + FLOOR * cap;
|
|
/* The previous test left the fraction at the cap. Old garbage narrows it
|
|
* (see below); the pass after that is the first to find nothing and must
|
|
* not widen it; the second in a row widens it to the cap. */
|
|
grow_dead_cycle_until_full_pass(FLOOR / 2 - 64, false);
|
|
size_t live = grow_dead_cycle_until_full_pass(0, false);
|
|
check(live <= narrowed, "a pass that found old garbage did not narrow the growth fraction");
|
|
live = grow_dead_cycle_until_full_pass(0, false);
|
|
check(live <= narrowed, "a single pass that found nothing widened the growth fraction");
|
|
live = grow_dead_cycle_until_full_pass(0, false);
|
|
check(live > due, "two passes in a row that found nothing did not widen to the cap");
|
|
check(live <= due + slack, "the growth fraction widened past the cap");
|
|
live = grow_dead_cycle_until_full_pass(0, false);
|
|
check(live > due && live <= due + slack, "the growth fraction left the cap");
|
|
|
|
/* At any cap, old garbage of an eighth of the floor or more is a yield
|
|
* that asks for at least one doubling less (this ring asks for two below
|
|
* a cap of 2 or more), and narrowing does not wait for a second pass. */
|
|
live = grow_dead_cycle_until_full_pass(FLOOR / 2 - 64, false);
|
|
check(live > due && live <= due + slack, "a capped pass came early or late");
|
|
live = grow_dead_cycle_until_full_pass(0, false);
|
|
check(live <= narrowed, "a pass that found old garbage did not narrow the growth fraction");
|
|
}
|
|
|
|
/* A full pass's yield counts the mature members of its dead set: a dead
|
|
* structure of mature and old objects is invisible to mature passes, so the
|
|
* full pass that frees it found garbage only it could find, and narrows the
|
|
* old level's fraction as old garbage does. Runs after the test above. */
|
|
static void check_mixed_dead_structure_narrows(void) {
|
|
enum { FLOOR = 4096 };
|
|
size_t cap = configured_growth_cap();
|
|
size_t slack = 2 * configured_nursery_threshold() + 2;
|
|
size_t narrowed = FLOOR + FLOOR * cap / 2 + slack;
|
|
size_t due = FLOOR + FLOOR * cap;
|
|
size_t live = 0;
|
|
for (int i = 0; i < 4; i++) live = grow_dead_cycle_until_full_pass(0, false);
|
|
check(live > due && live <= due + slack, "passes that found nothing did not settle at the cap");
|
|
grow_dead_cycle_until_full_pass(FLOOR / 2 - 64, true);
|
|
live = grow_dead_cycle_until_full_pass(0, false);
|
|
check(live <= narrowed, "a pass that freed a dead mature and old structure did not narrow the growth fraction");
|
|
}
|
|
|
|
static void check_deep_ring(void) {
|
|
enum { DEPTH = 100000 };
|
|
size_t before = freed;
|
|
Node *root = make_ring(DEPTH);
|
|
root->rc++;
|
|
release_live(root);
|
|
scr_collect_cycles();
|
|
check(freed == before, "deep live ring was reclaimed");
|
|
Node *node = root;
|
|
for (size_t i = 0; i < DEPTH; i++) {
|
|
check(node->rc == (i == 0 ? 2u : 1u), "deep ring count not restored");
|
|
node = node->next;
|
|
}
|
|
release_live(root);
|
|
scr_collect_cycles();
|
|
check(freed == before + DEPTH, "deep dead ring not reclaimed exactly");
|
|
}
|
|
|
|
static void check_shared_edges_and_outside_owner(void) {
|
|
size_t before = freed;
|
|
Node *root = make_ring(3);
|
|
Node *shared = root->next;
|
|
root->other = shared;
|
|
shared->rc++; /* duplicate edge */
|
|
shared->rc++; /* independent external owner */
|
|
release_live(root);
|
|
scr_collect_cycles();
|
|
check(freed == before, "shared subgraph lost its outside owner");
|
|
check(root->rc == 1 && shared->rc == 3 && shared->next->rc == 1,
|
|
"duplicate edges were not restored once each");
|
|
/* Several candidates overlap the same graph. */
|
|
root->rc++;
|
|
release_live(root);
|
|
release_live(shared);
|
|
scr_collect_cycles();
|
|
check(freed == before + 3, "overlapping candidates were not reclaimed once");
|
|
}
|
|
|
|
static void check_repeated_buffered_release(void) {
|
|
size_t before = freed;
|
|
Node *root = make_ring(3);
|
|
root->rc++;
|
|
release_live(root);
|
|
scr_collect_cycles();
|
|
for (size_t i = 0; i < 1000; i++) {
|
|
root->rc++;
|
|
release_live(root);
|
|
}
|
|
check(freed == before && root->rc == 2,
|
|
"repeated buffered releases changed live references");
|
|
release_live(root);
|
|
for (size_t i = 0; i < configured_nursery_threshold(); i++)
|
|
scr_cyc_collect_scheduled();
|
|
check(freed == before + 3, "buffered live-to-dead transition lost its root");
|
|
}
|
|
|
|
static void check_disconnected_survivors(void) {
|
|
size_t before = freed;
|
|
Node *first = make_ring(2);
|
|
Node *second = make_ring(3);
|
|
Node *dead = make_ring(4);
|
|
scr_collect_cycles(); /* arm the trigger before buffering separate graphs */
|
|
first->rc++;
|
|
release_live(first);
|
|
second->rc++;
|
|
release_live(second);
|
|
release_live(dead);
|
|
scr_collect_cycles();
|
|
check(freed == before + 4, "mixed live/dead components were misclassified");
|
|
check(first->rc == 2 && first->next->rc == 1 &&
|
|
second->rc == 2 && second->next->rc == 1,
|
|
"a later outside root replayed earlier restored edges");
|
|
release_live(first);
|
|
release_live(second);
|
|
scr_collect_cycles();
|
|
check(freed == before + 9, "disconnected survivors were not later reclaimed");
|
|
}
|
|
|
|
static void check_cross_generation_edges(void) {
|
|
size_t before = freed;
|
|
Node *older = make_ring(2);
|
|
older->rc++;
|
|
release_live(older);
|
|
scr_collect_cycles();
|
|
Node *younger = make_ring(2);
|
|
younger->other = older; /* transfer the older external owner */
|
|
release_live(younger);
|
|
scr_cyc_collect_scheduled();
|
|
check(freed >= before + 2, "nursery cycle was not reclaimed");
|
|
scr_collect_cycles();
|
|
check(freed == before + 4, "cross-generation edge was not paid off");
|
|
|
|
before = freed;
|
|
older = make_ring(2);
|
|
older->rc++;
|
|
release_live(older);
|
|
scr_collect_cycles();
|
|
younger = make_ring(2);
|
|
older->other = younger; /* transfer the younger external owner */
|
|
younger->other = older;
|
|
older->rc++;
|
|
/* The nursery candidate is kept alive by an edge the restricted pass
|
|
* cannot subtract. It must remain discoverable for the later full pass. */
|
|
younger->rc++;
|
|
release_live(younger);
|
|
scr_cyc_collect_scheduled();
|
|
check(freed == before, "cross-generation live cycle was reclaimed");
|
|
release_live(older);
|
|
scr_collect_cycles();
|
|
check(freed == before + 4, "cross-generation cycle lost its candidate");
|
|
}
|
|
|
|
static void check_deep_cross_generation_chain(void) {
|
|
enum { DEPTH = 100000 };
|
|
size_t before = freed;
|
|
Node *chain = NULL;
|
|
for (size_t i = 0; i < DEPTH; i++) {
|
|
static ScrCycType cyc_type = { &node_trace, &node_free, 0 };
|
|
Node *node = scr_cyc_alloc(sizeof(*node), &cyc_type);
|
|
node->rc = 1;
|
|
node->next = chain;
|
|
chain = node;
|
|
}
|
|
chain->rc++;
|
|
release_live(chain);
|
|
scr_collect_cycles(); /* promote the externally live chain */
|
|
check(freed == before, "promotion freed an owned deep chain");
|
|
Node *young = make_ring(2);
|
|
young->other = chain; /* the last external owner moves into the nursery */
|
|
release_live(young);
|
|
scr_cyc_collect_scheduled();
|
|
scr_collect_cycles();
|
|
check(freed == before + DEPTH + 2,
|
|
"cross-generation destruction leaked a deep chain");
|
|
}
|
|
|
|
static void check_teardown_rebuffers_and_immortals(void) {
|
|
size_t before = freed;
|
|
static Node immortal = { .rc = SIZE_MAX };
|
|
Node *first = make_ring(2);
|
|
Node *second = make_ring(2);
|
|
first->other = &immortal;
|
|
first->untraced = second; /* a plain-RC owner released by the teardown */
|
|
release_live(first);
|
|
scr_collect_cycles();
|
|
check(freed == before + 4, "teardown candidate did not reach the fixpoint");
|
|
check(immortal.rc == SIZE_MAX, "immortal edge was trial-deleted");
|
|
}
|
|
|
|
static void check_deferred_white_restoration(void) {
|
|
enum { DEPTH = 4096 };
|
|
size_t before = freed;
|
|
Node *root = make_ring(DEPTH);
|
|
Node *outside = root;
|
|
for (size_t i = 0; i < DEPTH / 2; i++) outside = outside->next;
|
|
outside->rc++;
|
|
release_live(root);
|
|
scr_collect_cycles();
|
|
check(freed == before, "deferred white nodes lost an outside owner");
|
|
Node *node = root;
|
|
for (size_t i = 0; i < DEPTH; i++) {
|
|
check(node->rc == (node == outside ? 2u : 1u),
|
|
"deferred white restoration changed edge counts");
|
|
node = node->next;
|
|
}
|
|
release_live(outside);
|
|
scr_collect_cycles();
|
|
check(freed == before + DEPTH, "restored deferred nodes leaked");
|
|
}
|
|
|
|
/* Nursery bypass (scr_cycle.c): candidates of an emitted type (kind != 0)
|
|
* that keep surviving their nursery passes are promoted on release instead
|
|
* of walked; dead cycles of that type are still found by older passes; and
|
|
* once the type's objects die in nursery cycles again, the next probe window
|
|
* finds that and it is walked again. Window, ratio and probe interval mirror
|
|
* SCR_CYC_BYPASS_*. */
|
|
/* Tests only (SCR_CYC_TESTING): one pass at exactly `gen`. */
|
|
size_t scr_cyc_pass_at(unsigned gen);
|
|
static size_t bypass_traced; /* trace calls on bypass-kind nodes */
|
|
static size_t bypass_freed; /* bypass-kind nodes freed */
|
|
static void bypass_trace(void *obj, ScrTraceVisit visit, void *ctx) {
|
|
bypass_traced++;
|
|
node_trace(obj, visit, ctx);
|
|
}
|
|
static void bypass_free(void *obj) {
|
|
bypass_freed++;
|
|
node_free(obj);
|
|
}
|
|
static ScrCycType bypass_type = { &bypass_trace, &bypass_free, 7 };
|
|
|
|
static Node *make_bypass_node(void) {
|
|
Node *node = scr_cyc_alloc(sizeof(*node), &bypass_type);
|
|
node->rc = 1;
|
|
return node;
|
|
}
|
|
|
|
/* Run release-triggered passes, as a program does by releasing: a pass's
|
|
* level then follows growth and backlogs only, never the scheduled age. */
|
|
static void run_release_passes(void) {
|
|
for (size_t i = 0; i <= configured_nursery_threshold(); i++)
|
|
release_live(make_ring(1));
|
|
}
|
|
|
|
/* Trace calls spent on PROBE fresh, externally owned bypass-kind
|
|
* candidates. A full pass first resets the growth baselines, so the probe
|
|
* never trips a mature or full pass: only nursery passes walk. */
|
|
static size_t probe_bypass_walks(Node **owned, size_t probe) {
|
|
scr_collect_cycles();
|
|
size_t before = bypass_traced;
|
|
for (size_t i = 0; i < probe; i++) {
|
|
owned[i] = make_bypass_node();
|
|
read_live(owned[i]);
|
|
}
|
|
run_release_passes();
|
|
return bypass_traced - before;
|
|
}
|
|
|
|
static void check_nursery_bypass(void) {
|
|
enum { WINDOW = 8192, INTERVAL = 16, LIVE = 3 * WINDOW, PROBE = 4096, RINGS = 32 };
|
|
Node **live = calloc(LIVE + 3 * PROBE, sizeof(*live));
|
|
if (!live) scr_trap("cycle test: out of memory\n");
|
|
size_t nlive = 0;
|
|
|
|
/* A walked candidate costs two trace calls (markGray and the restoring
|
|
* scan). Every survivor of the first window is walked; afterwards none is
|
|
* until a probe window opens. */
|
|
for (; nlive < LIVE; nlive++) {
|
|
live[nlive] = make_bypass_node();
|
|
read_live(live[nlive]);
|
|
}
|
|
size_t walks = probe_bypass_walks(live + nlive, PROBE);
|
|
nlive += PROBE;
|
|
check(walks < PROBE / 2, "a type whose candidates always survive was not bypassed");
|
|
|
|
/* Dead rings of the bypassed type wait in the mature buffer for an older
|
|
* pass, which finds them. */
|
|
size_t before = bypass_freed;
|
|
for (size_t r = 0; r < RINGS; r++) {
|
|
Node *a = make_bypass_node(), *b = make_bypass_node();
|
|
a->next = b;
|
|
b->next = a;
|
|
a->rc++; /* the outside owner */
|
|
release_live(a);
|
|
}
|
|
run_release_passes();
|
|
check(bypass_freed - before < 2 * RINGS, "bypassed dead rings were all walked by the nursery");
|
|
scr_collect_cycles();
|
|
check(bypass_freed - before == 2 * RINGS, "an older pass missed dead rings of a bypassed type");
|
|
|
|
/* Now the type's objects die in nursery cycles. Once INTERVAL windows'
|
|
* worth of candidates went past the nursery, a probe window walks them
|
|
* there again, and its yield turns the bypass off. A nursery pass after
|
|
* each release walks the probe window's candidates at their own level:
|
|
* the promoted rings pile up in older buffers, and an older pass the
|
|
* growth schedule runs there would otherwise sweep probed candidates too
|
|
* and leave the nursery's window unfinished. */
|
|
for (size_t r = 0; r < (size_t)WINDOW * (INTERVAL + 2); r++) {
|
|
Node *a = make_bypass_node(), *b = make_bypass_node();
|
|
a->next = b;
|
|
b->next = a;
|
|
a->rc++;
|
|
release_live(a);
|
|
scr_cyc_pass_at(SCR_CYC_NURSERY);
|
|
}
|
|
scr_collect_cycles();
|
|
walks = probe_bypass_walks(live + nlive, PROBE);
|
|
nlive += PROBE;
|
|
check(walks >= PROBE, "a type whose objects die in nursery cycles stayed bypassed");
|
|
|
|
for (size_t i = 0; i < nlive; i++) {
|
|
live[i]->rc = 0;
|
|
scr_cyc_on_dead(live[i]);
|
|
node_free(live[i]);
|
|
}
|
|
free(live);
|
|
scr_collect_cycles();
|
|
}
|
|
|
|
/* The mature level's bypass: a type whose mature candidates keep surviving
|
|
* goes straight to the old generation, its dead cycles are found by full
|
|
* passes, and once its objects die at the mature level again it is judged
|
|
* there again. Passes run at exact levels through the test entry point. */
|
|
static size_t mature_kind_freed;
|
|
static void mature_kind_free(void *obj) {
|
|
mature_kind_freed++;
|
|
node_free(obj);
|
|
}
|
|
static ScrCycType mature_kind_type = { &node_trace, &mature_kind_free, 8 };
|
|
|
|
static Node *make_mature_kind_node(void) {
|
|
Node *node = scr_cyc_alloc(sizeof(*node), &mature_kind_type);
|
|
node->rc = 1;
|
|
return node;
|
|
}
|
|
|
|
/* How many of PROBE fresh, externally owned candidates a release buffers
|
|
* straight into the old generation. */
|
|
static size_t probe_old_on_release(Node **owned, size_t probe) {
|
|
scr_collect_cycles(); /* reset the growth baselines: no pass runs below */
|
|
size_t old = 0;
|
|
for (size_t i = 0; i < probe; i++) {
|
|
owned[i] = make_mature_kind_node();
|
|
read_live(owned[i]);
|
|
old += scr_cyc_gen(scr_cyc_hdr(owned[i])) == SCR_CYC_OLD;
|
|
}
|
|
return old;
|
|
}
|
|
|
|
static void check_mature_bypass(void) {
|
|
enum { WINDOW = 8192, INTERVAL = 16, LIVE = 3 * WINDOW, PROBE = 4096, BATCH = 1024 };
|
|
Node **live = calloc(LIVE + 2 * PROBE, sizeof(*live));
|
|
Node **batch = calloc(BATCH, sizeof(*batch));
|
|
if (!live || !batch) scr_trap("cycle test: out of memory\n");
|
|
size_t nlive = 0;
|
|
|
|
for (; nlive < LIVE; nlive++) {
|
|
live[nlive] = make_mature_kind_node();
|
|
read_live(live[nlive]);
|
|
}
|
|
/* Whatever passes those releases ran, put every node in the mature
|
|
* generation as a candidate (any generation is sound), and walk them all
|
|
* at that level: none dies. */
|
|
scr_collect_cycles();
|
|
for (size_t i = 0; i < nlive; i++) {
|
|
scr_cyc_set_gen(scr_cyc_hdr(live[i]), SCR_CYC_MATURE);
|
|
read_live(live[i]);
|
|
}
|
|
scr_cyc_pass_at(SCR_CYC_MATURE);
|
|
size_t old = probe_old_on_release(live + nlive, PROBE);
|
|
nlive += PROBE;
|
|
check(old >= PROBE / 2, "a type whose mature candidates always survive kept its mature passes");
|
|
|
|
/* Dead rings of it wait for a full pass, which finds them. */
|
|
size_t before = mature_kind_freed;
|
|
for (size_t r = 0; r < 32; r++) {
|
|
Node *a = make_mature_kind_node(), *b = make_mature_kind_node();
|
|
a->next = b;
|
|
b->next = a;
|
|
a->rc++;
|
|
release_live(a);
|
|
}
|
|
scr_cyc_pass_at(SCR_CYC_NURSERY);
|
|
scr_cyc_pass_at(SCR_CYC_MATURE);
|
|
check(mature_kind_freed - before < 64, "rings past the mature level were all walked by it");
|
|
scr_collect_cycles();
|
|
check(mature_kind_freed - before == 64, "a full pass missed dead rings of a type past the mature level");
|
|
|
|
/* Now its rings survive the nursery and die before the next mature pass.
|
|
* The next probe window at the mature level measures that, and the type
|
|
* is judged there again. */
|
|
for (size_t round = 0; round < (size_t)WINDOW * (INTERVAL + 2) / BATCH; round++) {
|
|
for (size_t r = 0; r < BATCH; r++) {
|
|
Node *a = make_mature_kind_node(), *b = make_mature_kind_node();
|
|
a->next = b;
|
|
b->next = a;
|
|
a->rc++; /* the outside owner */
|
|
read_live(a);
|
|
batch[r] = a;
|
|
}
|
|
scr_cyc_pass_at(SCR_CYC_NURSERY);
|
|
for (size_t r = 0; r < BATCH; r++) release_live(batch[r]);
|
|
scr_cyc_pass_at(SCR_CYC_MATURE);
|
|
}
|
|
scr_collect_cycles();
|
|
old = probe_old_on_release(live + nlive, PROBE);
|
|
nlive += PROBE;
|
|
check(old < PROBE / 4, "a type whose objects die at the mature level stayed past it");
|
|
|
|
for (size_t i = 0; i < nlive; i++) {
|
|
live[i]->rc = 0;
|
|
scr_cyc_on_dead(live[i]);
|
|
node_free(live[i]);
|
|
}
|
|
free(live);
|
|
free(batch);
|
|
scr_collect_cycles();
|
|
}
|
|
|
|
/* A bypassing type whose dead cycles consist of several candidates: every
|
|
* member of each ring here is released, so each is buffered on its own. A
|
|
* sample kept at the nursery while its partners went past would always be
|
|
* held alive by a partner's edge, and the type would keep bypassing while
|
|
* all of its rings died young; a probe window keeps every member at the
|
|
* nursery, so the rings die there and the bypass ends. */
|
|
static size_t paired_traced, paired_freed;
|
|
static void paired_trace(void *obj, ScrTraceVisit visit, void *ctx) {
|
|
paired_traced++;
|
|
node_trace(obj, visit, ctx);
|
|
}
|
|
static void paired_free(void *obj) {
|
|
paired_freed++;
|
|
node_free(obj);
|
|
}
|
|
static ScrCycType paired_type = { &paired_trace, &paired_free, 10 };
|
|
|
|
static void check_probe_sees_candidate_rings(void) {
|
|
/* Both members of a ring must be buffered before a pass walks either: with
|
|
* a threshold of 1 every release collects, and the first member's pass
|
|
* promotes the pair out of the nursery. */
|
|
if (configured_nursery_threshold() < 2) return;
|
|
enum { WINDOW = 8192, INTERVAL = 16, LIVE = 2 * WINDOW, RINGS = WINDOW * (INTERVAL + 2) / 2, PROBE = 1024 };
|
|
Node **live = calloc(LIVE + PROBE, sizeof(*live));
|
|
if (!live) scr_trap("cycle test: out of memory\n");
|
|
size_t nlive = 0;
|
|
for (; nlive < LIVE; nlive++) {
|
|
live[nlive] = scr_cyc_alloc(sizeof(*live[nlive]), &paired_type);
|
|
live[nlive]->rc = 1;
|
|
read_live(live[nlive]);
|
|
scr_cyc_pass_at(SCR_CYC_NURSERY);
|
|
}
|
|
scr_collect_cycles();
|
|
size_t before = paired_freed;
|
|
for (size_t r = 0; r < RINGS; r++) {
|
|
Node *a = scr_cyc_alloc(sizeof(*a), &paired_type);
|
|
Node *b = scr_cyc_alloc(sizeof(*b), &paired_type);
|
|
a->rc = 2; /* b's edge and an owner */
|
|
b->rc = 2; /* a's edge and an owner */
|
|
a->next = b;
|
|
b->next = a;
|
|
release_live(a);
|
|
release_live(b);
|
|
scr_cyc_pass_at(SCR_CYC_NURSERY);
|
|
}
|
|
scr_collect_cycles();
|
|
check(paired_freed - before == 2 * RINGS, "rings of candidates leaked");
|
|
/* Nursery passes walk fresh candidates of the type again (two trace
|
|
* calls each; see probe_bypass_walks). */
|
|
size_t traced = paired_traced;
|
|
for (size_t i = 0; i < PROBE; i++) {
|
|
live[nlive] = scr_cyc_alloc(sizeof(*live[nlive]), &paired_type);
|
|
live[nlive]->rc = 1;
|
|
read_live(live[nlive++]);
|
|
}
|
|
run_release_passes();
|
|
check(paired_traced - traced >= PROBE, "a type whose candidate rings die young stayed past the nursery");
|
|
for (size_t i = 0; i < nlive; i++) {
|
|
live[i]->rc = 0;
|
|
scr_cyc_on_dead(live[i]);
|
|
node_free(live[i]);
|
|
}
|
|
free(live);
|
|
scr_collect_cycles();
|
|
}
|
|
|
|
/* A nursery pass re-buffers its survivors past the mature level when
|
|
* their type bypasses it, even if the type's nursery candidates are
|
|
* walked (here half of them die young, in rings). */
|
|
static ScrCycType young_dying_type = { &node_trace, &node_free, 9 };
|
|
|
|
static void check_rebuffer_past_mature(void) {
|
|
enum { BATCH = 1024, ROUNDS = 24, PROBE = 1024 };
|
|
Node **live = calloc((size_t)BATCH * ROUNDS + PROBE, sizeof(*live));
|
|
if (!live) scr_trap("cycle test: out of memory\n");
|
|
size_t nlive = 0;
|
|
for (size_t round = 0; round < ROUNDS; round++) {
|
|
for (size_t i = 0; i < BATCH; i++) {
|
|
Node *keep = scr_cyc_alloc(sizeof(*keep), &young_dying_type);
|
|
keep->rc = 1;
|
|
read_live(keep);
|
|
live[nlive++] = keep;
|
|
Node *a = scr_cyc_alloc(sizeof(*a), &young_dying_type);
|
|
Node *b = scr_cyc_alloc(sizeof(*b), &young_dying_type);
|
|
a->rc = 2; /* b's edge and the outside owner */
|
|
b->rc = 1;
|
|
a->next = b;
|
|
b->next = a;
|
|
release_live(a); /* dead, buffered in the nursery */
|
|
}
|
|
scr_cyc_pass_at(SCR_CYC_NURSERY); /* frees the rings, re-buffers the rest */
|
|
scr_cyc_pass_at(SCR_CYC_MATURE); /* finds nothing */
|
|
}
|
|
scr_collect_cycles();
|
|
for (size_t i = 0; i < PROBE; i++) {
|
|
Node *keep = scr_cyc_alloc(sizeof(*keep), &young_dying_type);
|
|
keep->rc = 1;
|
|
read_live(keep);
|
|
live[nlive++] = keep;
|
|
}
|
|
scr_cyc_pass_at(SCR_CYC_NURSERY);
|
|
size_t old = 0;
|
|
for (size_t i = nlive - PROBE; i < nlive; i++) old += scr_cyc_gen(scr_cyc_hdr(live[i])) == SCR_CYC_OLD;
|
|
check(old >= PROBE / 2, "nursery survivors were re-buffered into a level their type bypasses");
|
|
for (size_t i = 0; i < nlive; i++) {
|
|
live[i]->rc = 0;
|
|
scr_cyc_on_dead(live[i]);
|
|
node_free(live[i]);
|
|
}
|
|
free(live);
|
|
scr_collect_cycles();
|
|
}
|
|
|
|
/* Dead rings with one candidate each: fewer candidates than the nursery
|
|
* threshold, but each candidate's release runs a pass once the live
|
|
* cycle-headered count has grown by SCR_CYC_NURSERY_ALLOCATIONS (4096 in
|
|
* scr_cycle.c) since the last one, so the dead nodes left uncollected stay
|
|
* within that much plus the ring in hand. */
|
|
static void check_allocation_growth_runs_passes(void) {
|
|
enum { RINGS = 80, NODES_PER_RING = 64, ALLOCATION_WINDOW = 4096 };
|
|
scr_collect_cycles();
|
|
size_t before = freed;
|
|
for (size_t i = 0; i < RINGS; i++) release_live(make_ring(NODES_PER_RING));
|
|
size_t dropped = (size_t)RINGS * NODES_PER_RING;
|
|
check(freed > before, "allocation growth ran no pass");
|
|
check(before + dropped - freed <= ALLOCATION_WINDOW + NODES_PER_RING,
|
|
"dead rings outgrew the allocation window");
|
|
scr_collect_cycles();
|
|
check(freed - before == dropped, "dead rings survived a full collection");
|
|
}
|
|
|
|
/* Publication takes its objects out of the candidate buffers while it
|
|
* walks them (scr_cyc_pub_take) and closes the slots when it ends
|
|
* (scr_cyc_pub_fill); a refused publish puts its objects back first
|
|
* (scr_cyc_pub_restore). The rings here are dropped before the publish, so
|
|
* only their buffer entries keep them collectable. Rings taken and made
|
|
* immortal are never walked again; rings put back are collected, like the
|
|
* rings that were never taken. */
|
|
static size_t immortal_traced;
|
|
|
|
static void published_ring_trace(void *obj, ScrTraceVisit visit, void *ctx) {
|
|
if (((Node *)obj)->rc == SIZE_MAX) immortal_traced++;
|
|
node_trace(obj, visit, ctx);
|
|
}
|
|
|
|
static void check_publication_takes_candidates(void) {
|
|
enum { RINGS = 40, NODES_PER_RING = 8 };
|
|
static ScrCycType cyc_type = { &published_ring_trace, &node_free, 0 };
|
|
Node *roots[RINGS];
|
|
uint32_t taken[RINGS];
|
|
size_t before = freed;
|
|
for (size_t i = 0; i < RINGS; i++) {
|
|
roots[i] = make_ring_of(NODES_PER_RING, &cyc_type);
|
|
roots[i]->rc++;
|
|
release_live(roots[i]); /* buffered, still externally owned */
|
|
}
|
|
for (size_t i = 0; i < RINGS; i++)
|
|
if (i % 3 != 0) release_live(roots[i]); /* garbage now */
|
|
/* Low thresholds run passes while the rings are built, which can leave a
|
|
* published root unbuffered: publication takes only buffered objects. */
|
|
bool buffered[RINGS];
|
|
for (size_t i = 0; i < RINGS; i++) {
|
|
buffered[i] = i % 3 != 2 && scr_cyc_is_buffered(scr_cyc_hdr(roots[i]));
|
|
if (buffered[i]) taken[i] = scr_cyc_pub_take(roots[i]);
|
|
}
|
|
for (size_t i = 0; i < RINGS; i++) {
|
|
if (i % 3 == 0) { /* published: every node becomes immortal */
|
|
Node *node = roots[i];
|
|
do {
|
|
Node *next = node->next;
|
|
node->rc = SIZE_MAX;
|
|
node = next;
|
|
} while (node != roots[i]);
|
|
} else if (i % 3 == 1 && buffered[i]) {
|
|
scr_cyc_pub_restore(roots[i], taken[i]); /* refused */
|
|
}
|
|
}
|
|
scr_cyc_pub_fill();
|
|
scr_collect_cycles();
|
|
size_t dropped = 0;
|
|
for (size_t i = 0; i < RINGS; i++) dropped += i % 3 != 0;
|
|
check(freed - before == dropped * NODES_PER_RING,
|
|
"publication left the dropped rings uncollected, or collected a published one");
|
|
check(immortal_traced == 0, "a pass walked a published ring");
|
|
}
|
|
|
|
/* scr_cyc_collect_set: a full-depth pass over the candidates in a set. The
|
|
* others keep their slots for ordinary passes, unless the walk from a taken
|
|
* one reaches them, in which case a dead one is freed and leaves its
|
|
* buffer. */
|
|
static void *taken_slots[8];
|
|
static ScrCycSet taken = {taken_slots, 7};
|
|
static void take(void *obj) {
|
|
size_t j = scr_cyc_set_slot(obj, taken.mask);
|
|
while (taken.slots[j]) j = (j + 1) & taken.mask;
|
|
taken.slots[j] = obj;
|
|
}
|
|
|
|
static void check_limited_pass(void) {
|
|
enum { RING = 16 };
|
|
/* The setup buffers four candidates that must stay buffered until the
|
|
* limited pass; lower thresholds run scheduled passes in between. */
|
|
if (configured_nursery_threshold() < 8) return;
|
|
scr_collect_cycles();
|
|
size_t before = freed;
|
|
/* Candidates the filter refuses: a dead ring it must leave, and a ring
|
|
* whose last outside reference is from a dead ring it takes. */
|
|
Node *older = make_ring(RING);
|
|
release_live(older);
|
|
Node *reached = make_ring(RING);
|
|
Node *fresh = make_ring(RING);
|
|
fresh->other = reached;
|
|
reached->rc++;
|
|
release_live(reached); /* its owner's */
|
|
release_live(fresh);
|
|
/* A live candidate it takes. */
|
|
Node *live = make_ring(RING);
|
|
live->rc++;
|
|
release_live(live);
|
|
take(fresh);
|
|
take(live);
|
|
check(scr_cyc_collect_set(&taken, SCR_CYC_NURSERY) == 2 * RING,
|
|
"limited pass did not free the taken ring and what it reached");
|
|
check(freed - before == 2 * RING, "limited pass freed a live or refused candidate");
|
|
check(scr_cyc_collect_set(&taken, SCR_CYC_OLD) == 0, "a second limited pass freed something");
|
|
memset(taken_slots, 0, sizeof taken_slots);
|
|
scr_collect_cycles();
|
|
check(freed - before == 3 * RING, "the refused dead ring was not left for the next pass");
|
|
release_live(live);
|
|
scr_collect_cycles();
|
|
check(freed - before == 4 * RING, "the live ring was not collected once dead");
|
|
}
|
|
|
|
/* A full pass whose fullest candidate buffer is the old generation's (more
|
|
* entries than a pass keeps between passes), with nursery candidates
|
|
* beside it: every dead ring in either buffer is reclaimed, the live rings
|
|
* keep their counts, and a second round collects through the buffers the
|
|
* first one left behind. */
|
|
static void check_large_old_buffer_with_nursery_candidates(void) {
|
|
enum { OLD = 70000, YOUNG = 300, RING = 2 };
|
|
static Node *old[OLD];
|
|
Node *young[YOUNG];
|
|
for (int round = 0; round < 2; round++) {
|
|
for (size_t i = 0; i < OLD; i++) {
|
|
old[i] = make_ring(RING);
|
|
read_live(old[i]);
|
|
}
|
|
scr_collect_cycles();
|
|
for (size_t i = 0; i < OLD; i++) read_live(old[i]);
|
|
scr_collect_cycles();
|
|
check(scr_cyc_gen(scr_cyc_hdr(old[OLD - 1])) == SCR_CYC_OLD, "rings did not reach the old generation");
|
|
size_t before = freed;
|
|
for (size_t i = 0; i < OLD; i++)
|
|
if (i % 2) release_live(old[i]); /* dead: only the ring edge remains */
|
|
else read_live(old[i]); /* live candidate */
|
|
for (size_t i = 0; i < YOUNG; i++) {
|
|
young[i] = make_ring(RING);
|
|
if (i % 3) release_live(young[i]);
|
|
else read_live(young[i]);
|
|
}
|
|
scr_collect_cycles();
|
|
size_t young_dead = YOUNG - (YOUNG + 2) / 3;
|
|
check(freed - before == (OLD / 2 + young_dead) * RING, "a dead ring was not reclaimed");
|
|
for (size_t i = 0; i < OLD; i += 2) check(old[i]->rc == 2, "a live old ring lost a count");
|
|
for (size_t i = 0; i < YOUNG; i += 3) check(young[i]->rc == 2, "a live young ring lost a count");
|
|
before = freed;
|
|
for (size_t i = 0; i < OLD; i += 2) release_live(old[i]);
|
|
for (size_t i = 0; i < YOUNG; i += 3) release_live(young[i]);
|
|
scr_collect_cycles();
|
|
check(freed - before == (OLD / 2 + (YOUNG + 2) / 3) * RING, "the remaining rings were not reclaimed");
|
|
}
|
|
}
|
|
|
|
int main(void) {
|
|
check_old_garbage_reclaimed_while_growing();
|
|
check_older_rings_into_old();
|
|
check_cycle_spanning_mature_and_old();
|
|
check_unproductive_backlog_backs_off();
|
|
check_sparse_mature_backlog(1);
|
|
check_sparse_mature_backlog(2);
|
|
check_age_reset_when_last_mature_root_dies();
|
|
check_dead_backlog_rearms_release_trigger();
|
|
check_deep_ring();
|
|
check_shared_edges_and_outside_owner();
|
|
check_repeated_buffered_release();
|
|
check_disconnected_survivors();
|
|
check_deferred_white_restoration();
|
|
check_cross_generation_edges();
|
|
check_deep_cross_generation_chain();
|
|
check_teardown_rebuffers_and_immortals();
|
|
check_growth_schedule_follows_yield();
|
|
check_growth_cap_bounds_floating_garbage();
|
|
check_mixed_dead_structure_narrows();
|
|
check_nursery_bypass();
|
|
check_mature_bypass();
|
|
check_probe_sees_candidate_rings();
|
|
check_rebuffer_past_mature();
|
|
check_allocation_growth_runs_passes();
|
|
check_publication_takes_candidates();
|
|
check_limited_pass();
|
|
check_large_old_buffer_with_nursery_candidates();
|
|
printf("cycle collection checks passed: threshold=%zu growth_cap=%zu\n",
|
|
configured_nursery_threshold(), configured_growth_cap());
|
|
return 0;
|
|
}
|