mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-07 08:33:50 +08:00
Never buffer instances of classes whose trace visits nothing
A class carries the collector header when any class in its hierarchy can be on a cycle, so instances of classes whose own fields hold nothing cycle-capable were buffered as candidates and walked by every level. Such an object has no traced children and is never on a cycle. Their descriptors now name scr_cyc_no_children, and they are allocated with buf set to SCR_CYC_ACYCLIC. That value has the buffered bit set, so a release that leaves the object alive makes no call; scr_cyc_is_buffered is false for it, and an emitted death or publication mark takes the object out of a candidate buffer only when buf is below -1. A pass that walks such an object as a child restores the value when it survives.
This commit is contained in:
@@ -244,9 +244,12 @@ export function cycleAppendHelper(): string[] {
|
||||
}
|
||||
|
||||
/** A collector header to install: the object type's constant ScrCycType
|
||||
* descriptor (its trace and teardown; see cycTypeDescriptor). */
|
||||
* descriptor (its trace and teardown; see cycTypeDescriptor). `acyclic`:
|
||||
* the descriptor's trace is scr_cyc_no_children, and `buf` starts out
|
||||
* SCR_CYC_ACYCLIC (scr_cyc_alloc does the same on its path). */
|
||||
export interface CycHeader {
|
||||
type: string;
|
||||
acyclic?: boolean;
|
||||
}
|
||||
|
||||
/** The first collector statistics index of an emitted ScrCycType
|
||||
@@ -391,7 +394,9 @@ function allocLines(
|
||||
` %sa.cgp = getelementptr inbounds i8, ptr %sa.blk, i64 2`,
|
||||
` store i16 0, ptr %sa.cgp ; ScrCycHdr.color, gen`,
|
||||
` %sa.bufp = getelementptr inbounds i8, ptr %sa.blk, i64 ${CYC_BUF}`,
|
||||
` store i32 0, ptr %sa.bufp ; ScrCycHdr.buf`,
|
||||
cyc.acyclic === true
|
||||
? ` store i32 -1, ptr %sa.bufp ; ScrCycHdr.buf = SCR_CYC_ACYCLIC`
|
||||
: ` store i32 0, ptr %sa.bufp ; ScrCycHdr.buf`,
|
||||
` %sa.live = load i64, ptr @scr_cyc_live`,
|
||||
` %sa.live1 = add i64 %sa.live, 1`,
|
||||
` store i64 %sa.live1, ptr @scr_cyc_live`,
|
||||
|
||||
@@ -157,6 +157,44 @@ test("ProgramLifetime hierarchies are never freed and take no reference counts",
|
||||
expect(body(mangleClassRelease("Owner"))).toContain("load i32, ptr %bufp");
|
||||
});
|
||||
|
||||
test("classes whose trace visits nothing start acyclic in a traced hierarchy", () => {
|
||||
const mapper = { kind: "object" as const, className: "Mapper" };
|
||||
const mod: IrModule = {
|
||||
irVersion: 17,
|
||||
sourceFile: loc.file,
|
||||
entry: "main",
|
||||
functions: [{ name: "main", params: [], returnType: VOID, locals: [], body: [], loc }],
|
||||
classes: [
|
||||
{ name: "Mapper", fields: [], methods: [], loc },
|
||||
// A dyn field can reach anything: the hierarchy carries collector headers.
|
||||
{
|
||||
name: "Linked",
|
||||
base: "Mapper",
|
||||
fields: [
|
||||
{ name: "next", type: mapper },
|
||||
{ name: "hook", type: DYN },
|
||||
],
|
||||
methods: [],
|
||||
loc,
|
||||
},
|
||||
{ name: "Simple", base: "Mapper", fields: [{ name: "value", type: F64 }], methods: [], loc },
|
||||
],
|
||||
};
|
||||
const llvm = emitLlvmModule(mod, { pointerBits: 64 });
|
||||
const body = (name: string): string =>
|
||||
new RegExp(`^define internal [a-z]+ @${name}(?:\\.i32)?\\([^]*?^}`, "m").exec(llvm)?.[0] ?? "";
|
||||
const descriptor = (cls: string): string =>
|
||||
llvm.split("\n").find((line) => line.startsWith(`@${mangleClassTrace(cls)}.cyctype = `)) ?? "";
|
||||
expect(descriptor("Simple")).toContain("{ ptr @scr_cyc_no_children, ");
|
||||
expect(body(mangleClassNew("Simple"))).toContain("store i32 -1, ptr %sa.bufp");
|
||||
expect(descriptor("Linked")).toContain(`{ ptr @${mangleClassTrace("Linked")}, `);
|
||||
expect(body(mangleClassNew("Linked"))).toContain("store i32 0, ptr %sa.bufp");
|
||||
// Releases share the hierarchy's possible-root step, which skips the
|
||||
// call for SCR_CYC_ACYCLIC, and a death unbuffers only below -1.
|
||||
expect(body(mangleClassRelease("Simple"))).toContain("icmp slt i32 %buf, 0");
|
||||
expect(body(mangleClassReleaseDirect("Simple"))).toContain("icmp slt i32 %dbuf, -1");
|
||||
});
|
||||
|
||||
test("ProgramLifetime classes pack their fields after a 32-bit header", () => {
|
||||
const node = { kind: "object" as const, className: "Node" };
|
||||
const leaf = { kind: "object" as const, className: "Leaf" };
|
||||
|
||||
@@ -906,6 +906,15 @@ export function emitClassShapes(
|
||||
})),
|
||||
];
|
||||
const refFields = indexedFields.filter((f) => isRefCounted(f.type));
|
||||
const tracedFields = traced
|
||||
? indexedFields.filter((f) => traceAdapter(host, f.type) !== null)
|
||||
: [];
|
||||
// A traced class whose trace visits nothing carries the collector header
|
||||
// only because its hierarchy is traced: it is never on a cycle. Its
|
||||
// descriptor names scr_cyc_no_children and its objects start out
|
||||
// SCR_CYC_ACYCLIC, so releases never buffer them.
|
||||
const acyclic =
|
||||
traced && runtimePrefix === null && !isStreamRooted && tracedFields.length === 0;
|
||||
const bounded =
|
||||
runtimePrefix !== null ||
|
||||
isStreamRooted ||
|
||||
@@ -1089,11 +1098,12 @@ export function emitClassShapes(
|
||||
`entry:`,
|
||||
];
|
||||
const cycType = `@${mangleClassTrace(cls.name)}.cyctype`;
|
||||
if (acyclic) host.declare(`declare void @scr_cyc_no_children(ptr, ptr, ptr)`);
|
||||
if (traced)
|
||||
defs.push(
|
||||
cycTypeDescriptor(
|
||||
cycType,
|
||||
`@${mangleClassTrace(cls.name)}`,
|
||||
acyclic ? "@scr_cyc_no_children" : `@${mangleClassTrace(cls.name)}`,
|
||||
`@${mangleClassGcFree(cls.name)}`,
|
||||
host.nextCycKind?.() ?? 0,
|
||||
),
|
||||
@@ -1113,7 +1123,7 @@ export function emitClassShapes(
|
||||
...emitObjectAlloc(
|
||||
host,
|
||||
sizeOf,
|
||||
traced && !lifetime ? { type: cycType } : null,
|
||||
traced && !lifetime ? { type: cycType, acyclic } : null,
|
||||
storedHeaderBytes(meta),
|
||||
),
|
||||
);
|
||||
@@ -1188,7 +1198,6 @@ export function emitClassShapes(
|
||||
// exactly the complement, then free (the trace/teardown complement
|
||||
// contract in scr_runtime.h).
|
||||
const indexed = indexedFields;
|
||||
const tracedFields = indexed.filter((f) => traceAdapter(host, f.type) !== null);
|
||||
const untracedRefFields = indexed.filter(
|
||||
(f) => isRefCounted(f.type) && traceAdapter(host, f.type) === null,
|
||||
);
|
||||
|
||||
@@ -60,6 +60,9 @@ test("publication walkers mark and push inline, with the runtime for the rest",
|
||||
expect(fn).toContain("icmp ult i64 %mrc, 4294967294");
|
||||
expect(fn).toContain("getelementptr inbounds { ptr, i32, i32 }, ptr %mlog, i64 %mlogged");
|
||||
expect(fn).toContain("store i64 -1, ptr %o");
|
||||
// A headered object leaves its candidate buffer only when `buf` is below
|
||||
// -1: SCR_CYC_ACYCLIC (-1) marks an object that is never in one.
|
||||
expect(fn).toContain("icmp sge i32 %mbufw, -1");
|
||||
// Anything else goes through scr_pub_mark.
|
||||
expect(fn).toMatch(/call zeroext i1 @scr_pub_mark\(ptr %ctx, ptr %o, i1 (true|false), i32 4\)/);
|
||||
// A string is a leaf, marked at once; other references go on the stack,
|
||||
|
||||
@@ -434,12 +434,13 @@ export class PublishWalkers {
|
||||
let buf = classId || compact ? String(PUB_LOG_COUNT) : "0";
|
||||
if (cyc) {
|
||||
// ScrCycHdr.buf, the word before the object: its top bit while the
|
||||
// object is a buffered candidate.
|
||||
// object is a buffered candidate, and -1 (SCR_CYC_ACYCLIC) for an
|
||||
// object that never is one.
|
||||
this.host.declare(`declare i32 @scr_cyc_pub_take(ptr)`);
|
||||
lines.push(
|
||||
` %mbufp = getelementptr inbounds i8, ptr %o, i64 -4`,
|
||||
` %mbufw = load i32, ptr %mbufp`,
|
||||
` %mfree = icmp sge i32 %mbufw, 0`,
|
||||
` %mfree = icmp sge i32 %mbufw, -1`,
|
||||
` br i1 %mfree, label %mput, label %mtake`,
|
||||
`mtake:`,
|
||||
` %mix = call i32 @scr_cyc_pub_take(ptr %o)`,
|
||||
|
||||
@@ -1369,7 +1369,7 @@ export function directChildReleaseLines(
|
||||
lines.push(
|
||||
` %${tag}.bufp = getelementptr i8, ptr ${value}, ${S} -${host.cycleBufOffset}`,
|
||||
` %${tag}.buf = load i32, ptr %${tag}.bufp`,
|
||||
` %${tag}.queued = icmp slt i32 %${tag}.buf, 0`,
|
||||
` %${tag}.queued = icmp slt i32 %${tag}.buf, -1 ; buffered, not SCR_CYC_ACYCLIC`,
|
||||
` br i1 %${tag}.queued, label %${tag}.unbuffer, label %${tag}.teardown`,
|
||||
`${tag}.unbuffer:`,
|
||||
` call void @scr_cyc_on_dead(ptr ${value})`,
|
||||
@@ -1595,8 +1595,9 @@ function requestCycleAppend(host: ShapeHost): boolean {
|
||||
|
||||
/** The possible-cycle-root step of a release that left a headered object
|
||||
* alive: scr_cyc_on_release with its early return inlined. Test the sign
|
||||
* of `buf` (the i32 at obj-4; negative for a queued candidate or a tenured
|
||||
* object), and buffer a new candidate: through the module's
|
||||
* of `buf` (the i32 at obj-4; negative for a queued candidate, a young
|
||||
* object, or an object that is never a candidate, SCR_CYC_ACYCLIC), and
|
||||
* buffer a new candidate: through the module's
|
||||
* cycleAppendHelper in optimized builds that support it, through
|
||||
* scr_cyc_on_release otherwise. Every emitted release of a headered object
|
||||
* shares this sequence (scr_runtime.h asserts the offsets); it ends by
|
||||
@@ -1619,16 +1620,16 @@ export function cycleRootLines(host: ShapeHost, done: string): string[] {
|
||||
}
|
||||
|
||||
/** The death of a headered object: scr_cyc_on_dead with its early return
|
||||
* inlined. Only an object still in a candidate buffer
|
||||
* (`buffered` nonzero) has an entry to remove; the sequence continues at
|
||||
* the new block `next`. */
|
||||
* inlined. Only an object still in a candidate buffer (`buf` negative and
|
||||
* not SCR_CYC_ACYCLIC, which is -1) has an entry to remove; the sequence
|
||||
* continues at the new block `next`. */
|
||||
export function cycleDeadLines(host: ShapeHost, next: string): string[] {
|
||||
const S = host.sizeType;
|
||||
host.declare(`declare void @scr_cyc_on_dead(ptr)`);
|
||||
return [
|
||||
` %dbufp = getelementptr i8, ptr %o, ${S} -${host.cycleBufOffset}`,
|
||||
` %dbuf = load i32, ptr %dbufp`,
|
||||
` %dqueued = icmp slt i32 %dbuf, 0`,
|
||||
` %dqueued = icmp slt i32 %dbuf, -1 ; buffered, not SCR_CYC_ACYCLIC`,
|
||||
` br i1 %dqueued, label %unbuffer, label %${next}`,
|
||||
`unbuffer:`,
|
||||
` call void @scr_cyc_on_dead(ptr %o)`,
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/acyclic-classes-in-traced-hierarchies.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -361,7 +361,15 @@ uint32_t scr_cyc_register(ScrCycType *type) {
|
||||
}
|
||||
|
||||
void *scr_cyc_alloc(size_t size, ScrCycType *type) {
|
||||
return scr_cyc_alloc_inline(size, type);
|
||||
void *obj = scr_cyc_alloc_inline(size, type);
|
||||
if (type->trace == scr_cyc_no_children) scr_cyc_hdr(obj)->buf = SCR_CYC_ACYCLIC;
|
||||
return obj;
|
||||
}
|
||||
|
||||
void scr_cyc_no_children(void *obj, ScrTraceVisit visit, void *ctx) {
|
||||
(void)obj;
|
||||
(void)visit;
|
||||
(void)ctx;
|
||||
}
|
||||
|
||||
void scr_cyc_free(void *obj) { scr_cyc_free_inline(obj); }
|
||||
@@ -1175,7 +1183,13 @@ void scr_cyc_on_release(void *obj) {
|
||||
ScrCycHdr *h = scr_cyc_hdr(obj);
|
||||
/* Releasing an existing candidate cannot advance the trigger. A pass
|
||||
* always re-arms after its teardowns, including any candidates they add. */
|
||||
if (scr_cyc_is_buffered(h)) return;
|
||||
if (scr_cyc_release_done(h)) return;
|
||||
/* An object without cycle-capable children whose SCR_CYC_ACYCLIC a pass
|
||||
* or the young-in nursery replaced gets it back. */
|
||||
if (scr_cyc_type(h)->trace == scr_cyc_no_children) {
|
||||
h->buf = SCR_CYC_ACYCLIC;
|
||||
return;
|
||||
}
|
||||
#if SCR_CYC_INLINE_BUFFERING
|
||||
if (scr_cyc_append(h, obj)) return;
|
||||
#endif
|
||||
@@ -1346,14 +1360,17 @@ 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);
|
||||
* unless it is a candidate that a limited pass did not take, and one
|
||||
* without cycle-capable children leaves it SCR_CYC_ACYCLIC again. */
|
||||
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);
|
||||
if (acyclic) return;
|
||||
if (depth == SCR_CYC_WALK_DEPTH) {
|
||||
scr_cyc_push_work(&scr_restore_pending, obj);
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
@@ -1881,7 +1898,8 @@ static SCR_CYC_COMPACT size_t scr_cyc_settle(unsigned gen_limit, bool doomed) {
|
||||
void *obj = scr_cands.v[i];
|
||||
ScrCycHdr *h = scr_cyc_hdr(obj);
|
||||
if (scr_cyc_color(h) == SCR_CYC_DOOMED) continue; /* being freed below */
|
||||
if (!scr_cyc_is_buffered(h)) {
|
||||
/* A candidate without cycle-capable children was never on a cycle. */
|
||||
if (!scr_cyc_is_buffered(h) && h->buf != SCR_CYC_ACYCLIC) {
|
||||
/* A survivor promoted into a level its type bypasses moves past
|
||||
* it, as a release would buffer it. */
|
||||
if (scr_cyc_gen(h) == SCR_CYC_MATURE && scr_cyc_bypassed(scr_cyc_type(h), SCR_CYC_MATURE))
|
||||
|
||||
@@ -141,6 +141,11 @@ typedef struct ScrCycType {
|
||||
* Emitted allocation paths read it and take the slow path while it is 0. */
|
||||
uint32_t id;
|
||||
} ScrCycType;
|
||||
/* The trace of a type that holds nothing cycle-capable: it visits nothing.
|
||||
* Emitted classes whose fields hold no cycle-capable value but whose
|
||||
* hierarchy is traced name it (llvm/classes.ts); their objects are
|
||||
* allocated with `buf` SCR_CYC_ACYCLIC. */
|
||||
void scr_cyc_no_children(void *obj, ScrTraceVisit visit, void *ctx);
|
||||
/* Runtime families' kinds. Emitted descriptors count from
|
||||
* SCR_CYC_KIND_EMITTED (llvm/alloc.ts FIRST_EMITTED_CYC_KIND). */
|
||||
enum {
|
||||
@@ -169,7 +174,7 @@ enum {
|
||||
* collector color (SCR_CYC_*) and generation (SCR_CYC_NURSERY..SCR_CYC_OLD,
|
||||
* the walk filter), and `buf`: SCR_CYC_BUFFERED_BIT while the object sits in
|
||||
* its generation's candidate buffer, with its position there below it (O(1)
|
||||
* removal when rc hits 0). */
|
||||
* removal when rc hits 0), or SCR_CYC_ACYCLIC. */
|
||||
typedef struct ScrCycHdr {
|
||||
uint16_t id;
|
||||
uint8_t color;
|
||||
@@ -177,13 +182,25 @@ typedef struct ScrCycHdr {
|
||||
uint32_t buf;
|
||||
} ScrCycHdr;
|
||||
#define SCR_CYC_BUFFERED_BIT 0x80000000u
|
||||
/* `buf` of an object whose type's trace is scr_cyc_no_children: it has no
|
||||
* cycle-capable children, so it is never on a cycle and never a candidate.
|
||||
* Such a type still carries the header when it shares a hierarchy with
|
||||
* cycle-capable classes (headers are uniform per hierarchy). The value has
|
||||
* the buffered bit set, so a release that leaves the object alive returns
|
||||
* at the buffered test and makes no call; scr_cyc_is_buffered is false for
|
||||
* it. No buffer position reaches it (scr_cyc_grow caps the buffers well
|
||||
* below 2^31 entries). */
|
||||
#define SCR_CYC_ACYCLIC 0xffffffffu
|
||||
|
||||
/* This layout is an ABI, not an implementation detail. The emitted
|
||||
* allocation stores the id and zeroes the rest; every emitted release of a
|
||||
* allocation stores the id and zeroes the rest (`buf` SCR_CYC_ACYCLIC for a
|
||||
* class whose trace is scr_cyc_no_children); every emitted release of a
|
||||
* headered object (llvm/shapes.ts cycleRootLines, used by the record, class
|
||||
* and inline RC helpers) mirrors scr_cyc_on_release's early return by
|
||||
* testing the sign of `buf` as an i32 at obj-4, and the emitted inline free
|
||||
* reads the generation as an i8 at obj-5. Hence the assertions. */
|
||||
* testing the sign of `buf` as an i32 at obj-4, the emitted death mirrors
|
||||
* scr_cyc_on_dead's by testing `buf` below -1 (cycleDeadLines), and the
|
||||
* emitted inline free reads the generation as an i8 at obj-5. Hence the
|
||||
* assertions. */
|
||||
_Static_assert(sizeof(ScrCycHdr) == 8, "LLVM backend expects an 8-byte cycle header");
|
||||
_Static_assert(offsetof(ScrCycHdr, id) == 0 && sizeof(((ScrCycHdr *)0)->id) == 2,
|
||||
"the emitted allocation stores the registry id as an i16 at the block start");
|
||||
@@ -215,6 +232,11 @@ static inline void scr_cyc_set_color(ScrCycHdr *h, unsigned color) { h->color =
|
||||
static inline unsigned scr_cyc_gen(const ScrCycHdr *h) { return h->gen; }
|
||||
static inline void scr_cyc_set_gen(ScrCycHdr *h, unsigned gen) { h->gen = (uint8_t)gen; }
|
||||
static inline bool scr_cyc_is_buffered(const ScrCycHdr *h) {
|
||||
return (h->buf & SCR_CYC_BUFFERED_BIT) != 0 && h->buf != SCR_CYC_ACYCLIC;
|
||||
}
|
||||
/* A release that leaves the object alive has nothing to buffer: it is a
|
||||
* candidate already, or it is never one (SCR_CYC_ACYCLIC). */
|
||||
static inline bool scr_cyc_release_done(const ScrCycHdr *h) {
|
||||
return (h->buf & SCR_CYC_BUFFERED_BIT) != 0;
|
||||
}
|
||||
static inline uint32_t scr_cyc_buf_index(const ScrCycHdr *h) {
|
||||
@@ -608,7 +630,7 @@ static inline bool scr_cyc_append(ScrCycHdr *h, void *obj) {
|
||||
* Most releases of long-lived containers take the first early return. */
|
||||
static inline void scr_cyc_released(void *obj) {
|
||||
ScrCycHdr *h = scr_cyc_hdr(obj);
|
||||
if (scr_cyc_is_buffered(h)) return;
|
||||
if (scr_cyc_release_done(h)) return;
|
||||
#if SCR_CYC_INLINE_BUFFERING
|
||||
if (scr_cyc_append(h, obj)) return;
|
||||
#endif
|
||||
|
||||
@@ -1434,6 +1434,38 @@ static void check_death_brings_full_pass(void) {
|
||||
free(keep);
|
||||
}
|
||||
|
||||
/* An object whose type's trace is scr_cyc_no_children starts out
|
||||
* SCR_CYC_ACYCLIC and is never buffered. A pass that reaches it from a dead
|
||||
* cycle frees it with the cycle when the cycle held its last reference, and
|
||||
* otherwise leaves it SCR_CYC_ACYCLIC with its outside count. */
|
||||
static void check_acyclic_objects(void) {
|
||||
static ScrCycType leaf_type = { &scr_cyc_no_children, &node_free, 0 };
|
||||
size_t before = freed;
|
||||
Node *leaf = scr_cyc_alloc(sizeof(*leaf), &leaf_type);
|
||||
ScrCycHdr *h = scr_cyc_hdr(leaf);
|
||||
check(h->buf == SCR_CYC_ACYCLIC && !scr_cyc_is_buffered(h), "acyclic object did not start acyclic");
|
||||
leaf->rc = 2; /* an outside owner and a temporary reference */
|
||||
release_live(leaf);
|
||||
check(h->buf == SCR_CYC_ACYCLIC, "release buffered an acyclic object");
|
||||
|
||||
Node *first = make_ring(2);
|
||||
first->other = leaf;
|
||||
leaf->rc++;
|
||||
release_live(first);
|
||||
scr_cyc_pass_at(SCR_CYC_NURSERY);
|
||||
check(freed == before + 2, "nursery pass did not free the ring");
|
||||
check(leaf->rc == 1 && h->buf == SCR_CYC_ACYCLIC, "acyclic survivor lost its count or its mark");
|
||||
|
||||
Node *second = make_ring(2);
|
||||
second->next->other = leaf;
|
||||
leaf->rc++;
|
||||
release_live(leaf); /* the outside owner lets go: only the ring holds it */
|
||||
check(h->buf == SCR_CYC_ACYCLIC, "release buffered an acyclic object");
|
||||
release_live(second);
|
||||
scr_collect_cycles();
|
||||
check(freed == before + 5, "acyclic object held only by a dead ring was not freed with it");
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
check_inline_buffering();
|
||||
check_old_garbage_reclaimed_while_growing();
|
||||
@@ -1467,6 +1499,7 @@ int main(void) {
|
||||
check_limited_pass();
|
||||
check_large_old_buffer_with_nursery_candidates();
|
||||
check_unjudged_backlog_follows_growth();
|
||||
check_acyclic_objects();
|
||||
printf("cycle collection checks passed: threshold=%zu growth_cap=%zu\n",
|
||||
configured_nursery_threshold(), configured_growth_cap());
|
||||
return 0;
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { publish } from "@scriptc/threads";
|
||||
|
||||
// Classes whose own fields hold nothing cycle-capable, in a hierarchy where
|
||||
// other classes form cycles: their instances are held by dead cycles, by
|
||||
// live cycles and by plain locals while collections run, and are published
|
||||
// with the contexts that hold them.
|
||||
|
||||
abstract class Mapper {
|
||||
abstract map(n: number): number;
|
||||
}
|
||||
|
||||
class Offset extends Mapper {
|
||||
by: number;
|
||||
constructor(by: number) {
|
||||
super();
|
||||
this.by = by;
|
||||
}
|
||||
map(n: number): number {
|
||||
return n + this.by;
|
||||
}
|
||||
}
|
||||
|
||||
class Scale extends Offset {
|
||||
readonly factor: number;
|
||||
constructor(by: number, factor: number) {
|
||||
super(by);
|
||||
this.factor = factor;
|
||||
}
|
||||
map(n: number): number {
|
||||
return super.map(n) * this.factor;
|
||||
}
|
||||
}
|
||||
|
||||
class Context {
|
||||
readonly steps: Mapper[] = [];
|
||||
mapper: Mapper | undefined = undefined;
|
||||
total = 0;
|
||||
}
|
||||
|
||||
class ContextMapper extends Mapper {
|
||||
readonly context: Context;
|
||||
constructor(context: Context) {
|
||||
super();
|
||||
this.context = context;
|
||||
}
|
||||
map(n: number): number {
|
||||
let value = n;
|
||||
for (const step of this.context.steps) value = step.map(value);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
class Chain extends Mapper {
|
||||
readonly first: Mapper;
|
||||
next: Mapper | undefined = undefined;
|
||||
constructor(first: Mapper) {
|
||||
super();
|
||||
this.first = first;
|
||||
}
|
||||
map(n: number): number {
|
||||
const value = this.first.map(n);
|
||||
return this.next === undefined ? value : this.next.map(value);
|
||||
}
|
||||
}
|
||||
|
||||
function makeContext(i: number, shared: Mapper): Context {
|
||||
const context = new Context();
|
||||
context.steps.push(new Offset(i % 7), shared, new Scale(1, 2));
|
||||
context.mapper = new ContextMapper(context);
|
||||
context.total = context.mapper.map(i);
|
||||
return context;
|
||||
}
|
||||
|
||||
const shared = new Scale(3, 1);
|
||||
const kept: Context[] = [];
|
||||
let sum = 0;
|
||||
for (let i = 0; i < 20000; i++) {
|
||||
const context = makeContext(i, shared);
|
||||
sum += context.total;
|
||||
if (i % 1000 === 0) kept.push(context);
|
||||
}
|
||||
|
||||
// A chain that loops back to itself, holding mappers that are also held
|
||||
// outside it.
|
||||
const outside: Mapper[] = [];
|
||||
let loops = 0;
|
||||
for (let i = 0; i < 5000; i++) {
|
||||
const offset = new Offset(i);
|
||||
const head = new Chain(offset);
|
||||
const tail = new Chain(new Scale(i, 3));
|
||||
head.next = tail;
|
||||
tail.next = undefined;
|
||||
loops += head.map(1);
|
||||
tail.next = head;
|
||||
if (i % 500 === 0) outside.push(offset);
|
||||
}
|
||||
|
||||
let keptTotal = 0;
|
||||
for (const context of kept) keptTotal += context.mapper!.map(1);
|
||||
let outsideTotal = 0;
|
||||
for (const mapper of outside) outsideTotal += mapper.map(2);
|
||||
|
||||
console.log(sum, loops, kept.length, keptTotal, outside.length, outsideTotal, shared.map(4));
|
||||
|
||||
publish(kept);
|
||||
let publishedTotal = 0;
|
||||
for (const context of kept) publishedTotal += context.mapper!.map(3);
|
||||
console.log("published", publishedTotal);
|
||||
try {
|
||||
shared.by = 5;
|
||||
console.log("write: ok");
|
||||
} catch (e) {
|
||||
console.log(`write: ${(e as Error).name}: ${(e as Error).message}`);
|
||||
}
|
||||
const fresh = makeContext(7, new Offset(1));
|
||||
fresh.steps.push(shared);
|
||||
console.log("after", fresh.mapper!.map(2), outside[3]!.map(1));
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3350, "san": 3910 }
|
||||
Reference in New Issue
Block a user