mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Keep a candidate's generation when a limited cycle pass reaches it
A limited pass (scr_cyc_collect_set, which freeing a LifetimeRegion runs over the objects the region owned) takes only the candidates in its set. Its walk can still reach other candidates, and one that is referenced from outside is blackened by scan and promoted to the next generation while it keeps its slot in its old generation's buffer. scr_cyc_on_dead finds a candidate's buffer by its generation, so when that object died it removed another buffer's entry and left its own slot pointing at the freed block, which the next pass then read. Such a candidate is no longer promoted: it keeps the generation of the buffer it is in until a pass takes it.
This commit is contained in:
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5025-lifetime-region-shared-candidate.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -1474,7 +1474,10 @@ static void scr_scan_black(void *obj, uintptr_t depth) {
|
||||
const ScrCycType *type = scr_cyc_type(h);
|
||||
bool acyclic = type->trace == scr_cyc_no_children;
|
||||
if (!scr_cyc_is_buffered(h)) h->buf = acyclic ? SCR_CYC_ACYCLIC : 0;
|
||||
if (scr_cyc_gen(h) < SCR_CYC_OLD) scr_cyc_push_work(&scr_promote, obj);
|
||||
/* Such a candidate also keeps its generation: the generation names the
|
||||
* buffer its slot is in (scr_cyc_on_dead), so it is promoted only once a
|
||||
* pass takes it. */
|
||||
if (scr_cyc_gen(h) < SCR_CYC_OLD && !scr_cyc_is_buffered(h)) scr_cyc_push_work(&scr_promote, obj);
|
||||
if (acyclic) return;
|
||||
if (depth == SCR_CYC_WALK_DEPTH) {
|
||||
scr_cyc_push_work(&scr_restore_pending, obj);
|
||||
|
||||
@@ -1314,6 +1314,44 @@ static void check_limited_pass(void) {
|
||||
check(freed - before == 4 * RING, "the live ring was not collected once dead");
|
||||
}
|
||||
|
||||
/* A limited pass that reaches a live candidate it did not take leaves the
|
||||
* candidate in its slot and in its generation, which names the slot's
|
||||
* buffer: when the candidate dies afterwards, it leaves that buffer, and no
|
||||
* later pass meets its freed block. */
|
||||
static void check_limited_pass_keeps_untaken_generation(void) {
|
||||
enum { RING = 16 };
|
||||
if (configured_nursery_threshold() < 8) return;
|
||||
scr_collect_cycles();
|
||||
size_t before = freed;
|
||||
static ScrCycType cyc_type = { &node_trace, &node_free, 0 };
|
||||
Node *shared = scr_cyc_alloc(sizeof(*shared), &cyc_type);
|
||||
shared->rc = 1; /* its owner */
|
||||
/* A dead ring the filter takes points at it. */
|
||||
Node *dead = make_ring(RING);
|
||||
dead->other = shared;
|
||||
shared->rc++;
|
||||
/* It loses a reference and becomes a candidate the filter refuses. */
|
||||
shared->rc++;
|
||||
release_live(shared);
|
||||
release_live(dead);
|
||||
ScrCycHdr *h = scr_cyc_hdr(shared);
|
||||
unsigned gen = scr_cyc_gen(h);
|
||||
check(scr_cyc_is_buffered(h), "the shared node is not a candidate");
|
||||
take(dead);
|
||||
check(scr_cyc_collect_set(&taken, SCR_CYC_NURSERY) == RING,
|
||||
"limited pass did not free the taken ring");
|
||||
check(scr_cyc_is_buffered(h) && scr_cyc_gen(h) == gen,
|
||||
"limited pass moved a candidate it did not take out of its generation");
|
||||
memset(taken_slots, 0, sizeof taken_slots);
|
||||
/* Its owner lets go: it dies directly, out of its buffer. */
|
||||
check(shared->rc == 1, "the shared node kept a reference from the freed ring");
|
||||
shared->rc--;
|
||||
scr_cyc_on_dead(shared);
|
||||
node_free(shared);
|
||||
scr_collect_cycles();
|
||||
check(freed - before == RING + 1, "the passes after the limited one freed something else");
|
||||
}
|
||||
|
||||
/* 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
|
||||
@@ -1593,6 +1631,7 @@ int main(void) {
|
||||
check_allocation_growth_runs_passes();
|
||||
check_publication_takes_candidates();
|
||||
check_limited_pass();
|
||||
check_limited_pass_keeps_untaken_generation();
|
||||
check_large_old_buffer_with_nursery_candidates();
|
||||
check_unjudged_backlog_follows_growth();
|
||||
check_acyclic_objects();
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
// LifetimeRegion (@scriptc/experimental): freeing a region whose objects
|
||||
// own a cycle runs a limited cycle pass over what the region owned. That
|
||||
// pass also reaches a shared object the region pointed at, which is a
|
||||
// candidate of its own (it lost a reference) and stays referenced from
|
||||
// outside. The shared object must stay a valid candidate after the pass:
|
||||
// it dies afterwards, later passes run, and the program prints the same
|
||||
// as under Node.js.
|
||||
import type { ProgramLifetime } from "@scriptc/types";
|
||||
import { LifetimeRegion } from "@scriptc/experimental";
|
||||
|
||||
class Shared {
|
||||
readonly label: string;
|
||||
peers: Shared[] = [];
|
||||
constructor(label: string) {
|
||||
this.label = label;
|
||||
}
|
||||
}
|
||||
|
||||
// Ordinary objects the region's instance owns: a cycle that points back
|
||||
// into the region and at the shared object.
|
||||
class Owned {
|
||||
holder: Holder | undefined = undefined;
|
||||
shared: Shared | undefined = undefined;
|
||||
partner: Owned | undefined = undefined;
|
||||
}
|
||||
|
||||
class Holder implements ProgramLifetime {
|
||||
owned: Owned | undefined = undefined;
|
||||
readonly id: number;
|
||||
constructor(id: number) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
|
||||
// Cyclic garbage, so the collector runs passes of its own.
|
||||
class Ring {
|
||||
next: Ring | undefined = undefined;
|
||||
readonly value: number;
|
||||
constructor(value: number) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
function churn(n: number): number {
|
||||
let total = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const a = new Ring(i);
|
||||
const b = new Ring(i + 1);
|
||||
a.next = b;
|
||||
b.next = a;
|
||||
total += a.value + b.next.value;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
function round(k: number): string {
|
||||
let shared: Shared | undefined = new Shared(`shared ${k}`);
|
||||
// A second reference, dropped below: the shared object becomes a candidate.
|
||||
let keep: Shared[] = [shared];
|
||||
shared.peers.push(new Shared(`peer ${k}`));
|
||||
const region = new LifetimeRegion();
|
||||
region.run(() => {
|
||||
const holder = new Holder(k);
|
||||
const owned = new Owned();
|
||||
const partner = new Owned();
|
||||
owned.partner = partner;
|
||||
partner.partner = owned;
|
||||
owned.holder = holder;
|
||||
owned.shared = shared;
|
||||
holder.owned = owned;
|
||||
});
|
||||
keep = [];
|
||||
region.release();
|
||||
const label = shared.label + " / " + shared.peers[0]!.label + " / " + keep.length;
|
||||
// The shared object dies, and the passes that follow read the candidate buffers.
|
||||
shared = undefined;
|
||||
churn(2000);
|
||||
return label;
|
||||
}
|
||||
|
||||
for (let k = 0; k < 20; k++) {
|
||||
const label = round(k);
|
||||
if (k % 5 === 0) console.log(label);
|
||||
}
|
||||
console.log("churn", churn(10000));
|
||||
Reference in New Issue
Block a user