Add an experimental young-in nursery for compiled classes and records (#1319)

This commit is contained in:
Chris Tate
2026-10-19 07:32:01 -05:00
committed by GitHub
28 changed files with 1455 additions and 9 deletions
@@ -3,11 +3,13 @@ import { type IrModule } from "../ir/ir.js";
import { moduleRuntimeFeatures } from "../ir/runtime-features.js";
import { hasForeignFfiCallback } from "./ffi-callbacks.js";
import type { NativeLinkFeatures } from "./native-link-info.js";
import { youngNurseryRequested } from "./young-nursery.js";
export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
const features = moduleRuntimeFeatures(mod);
return {
...(mod.workers ? { workers: true } : {}),
...(youngNurseryRequested() && mod.lib === undefined ? { youngNursery: true } : {}),
regex: features.regex,
copying: features.copying,
textDecoderLegacy: features.legacyTextDecoder,
@@ -1,5 +1,5 @@
import { expect, test } from "vitest";
import { F64, STRING, VOID, type IrModule } from "../../ir/ir.js";
import { DYN, F64, STRING, VOID, type IrModule } from "../../ir/ir.js";
import { buildClassGraph, classMembershipIntervals, emitClassMembershipHelper } from "./classes.js";
import { emitLlvmModule } from "./emitter.js";
import {
@@ -316,3 +316,52 @@ test("unconstructed narrowing layouts may omit abstract slots while live classes
});
expect(() => emitLlvmModule(mod)).toThrow("no implementation of run for Dormant");
});
test("the young-in nursery flag adds the nursery entry to traced constructors only", () => {
const linked = { kind: "object" as const, className: "Linked" };
const mod: IrModule = {
irVersion: 16,
sourceFile: loc.file,
entry: "main",
functions: [{ name: "main", params: [], returnType: VOID, locals: [], body: [], loc }],
classes: [
{ name: "Scalar", fields: [{ name: "value", type: F64 }], methods: [], loc },
// A dyn field can reach anything: the class carries a collector header.
{
name: "Linked",
fields: [
{ name: "next", type: linked },
{ name: "hook", type: DYN },
],
methods: [],
loc,
},
],
};
const helper = (llvm: string, name: string): string => {
const start = llvm.indexOf(`define internal ptr @${name}(`);
expect(start).toBeGreaterThanOrEqual(0);
return llvm.slice(start, llvm.indexOf("\n}\n", start));
};
const saved = process.env["SCRIPTC_YOUNG_NURSERY"];
try {
delete process.env["SCRIPTC_YOUNG_NURSERY"];
const off = emitLlvmModule(mod, { pointerBits: 64 });
expect(off).not.toContain("scr_yin");
process.env["SCRIPTC_YOUNG_NURSERY"] = "1";
const on = emitLlvmModule(mod, { pointerBits: 64 });
const linkedNew = helper(on, mangleClassNew("Linked"));
expect(linkedNew).toContain("@scr_yin_skip");
expect(linkedNew).toContain("call void @scr_yin_enter_slow(ptr %o)");
expect(linkedNew).toContain("store i8 3, ptr %ye.genp");
// An acyclic class has no collector header and never enters.
expect(helper(on, mangleClassNew("Scalar"))).not.toContain("scr_yin");
// Targets without the inline allocator call the whole entry.
const narrow = emitLlvmModule(mod, { pointerBits: 32 });
expect(helper(narrow, mangleClassNew("Linked"))).toContain("call void @scr_yin_enter(ptr %o)");
} finally {
if (saved === undefined) delete process.env["SCRIPTC_YOUNG_NURSERY"];
else process.env["SCRIPTC_YOUNG_NURSERY"] = saved;
}
});
@@ -52,6 +52,7 @@ import {
retainBody,
traceAdapter,
type ShapeHost,
youngEntryLines,
} from "./shapes.js";
/** One virtual method slot of a hierarchy: the ROOT-MOST declaring class
@@ -851,6 +852,11 @@ export function emitClassShapes(
);
}
nw.push(...undefFieldInits(host, meta, nullable));
// Young instances: program-owned traced classes. Classes rooted at a
// runtime base (errors, emitters, events, streams) carry runtime state
// and stay with trial deletion, like every runtime family.
if (traced && !lifetime && (!meta.hierarchy || meta.root.def.runtime !== true))
nw.push(...youngEntryLines(host));
nw.push(` ret ptr ${meta.hierarchy ? "%o.vt" : "%o"}`, `}`, ``);
defs.push(...nw);
@@ -11,6 +11,7 @@ import {
RECORD_SOURCES_GATE,
WEAK_KEYS_GATE,
} from "./alloc.js";
import { youngNurseryRequested } from "../young-nursery.js";
import { hasLoneSurrogate, llvmBytes as llBytes, wtf8Bytes } from "../literals.js";
import { InternalCompilerError } from "../../errors.js";
import { createHash } from "node:crypto";
@@ -1463,6 +1464,7 @@ export class LlEmitter {
...(this.rcMode !== undefined ? { rcMode: this.rcMode } : {}),
...(this.rcHelpersNoinline ? { rcHelpersNoinline: true } : {}),
nextCycKind: () => FIRST_EMITTED_CYC_KIND + this.cycKinds++,
...(youngNurseryRequested() && mod.lib === undefined ? { youngNursery: true } : {}),
unionsById: this.unionsById,
declare: (decl) => this.declare(decl),
needOom: () => this.needOom(),
@@ -29,6 +29,7 @@ import {
mangleRecordTrace,
} from "../mangle.js";
import {
CYC_HDR,
cycTypeDescriptor,
emitObjectAlloc,
emitObjectFree,
@@ -58,6 +59,10 @@ import {
import { recordMayKeepSource } from "./record-sources.js";
import { LlvmUnsupportedError } from "./unsupported.js";
/** SCR_CYC_MAX_TYPES (scr_rc.h): the registry's size, and the length of
* the young-in nursery's per-id skip countdown. */
const CYC_MAX_TYPES = 32768;
/** What the tables need from the emitter: the extern-declaration ledger
* and the module-wide cycle/shape indexes. */
export interface ShapeHost {
@@ -119,6 +124,67 @@ export interface ShapeHost {
readonly tracedUnions: Set<string>;
/** The zeroed `%ScrUnion` an inline store reads for a NULL dummy. */
unionNoneRef(): string;
/** The young-in nursery (backend/young-nursery.ts): `new` hands each
* traced program-owned instance to scr_yin_enter. Absent: off. */
readonly youngNursery?: boolean;
}
/** The nursery entry of a new traced instance in `%o`, after its count is
* stored; nothing when the nursery is off. Where the allocator is inlined
* (64-bit, process-global state) the entry is inline too: a type that is
* skipping the nursery (its scr_yin_skip countdown is nonzero) counts down
* and stays out; otherwise, while the nursery has room (n < lim in ScrYin,
* scr_rc.h), list the object at index n and mark its header young
* (generation SCR_CYC_YOUNG, `buf` the buffered bit and the index), and
* call scr_yin_enter_slow when it has none (it collects the nursery
* first). Other targets call scr_yin_enter, which does all of it.
* Labels use the `ye.` prefix; the lines end in an open block. */
export function youngEntryLines(host: ShapeHost): string[] {
if (host.youngNursery !== true) return [];
if (host.inlineAlloc !== true || host.threadLocalAlloc === true) {
host.declare(`declare void @scr_yin_enter(ptr)`);
return [` call void @scr_yin_enter(ptr %o) ; the young-in nursery`];
}
host.declare(`@scr_yin = external global { ptr, i64, i64, i64 }`);
host.declare(`@scr_yin_skip = external global [${CYC_MAX_TYPES} x i32]`);
host.declare(`declare void @scr_yin_enter_slow(ptr)`);
return [
` %ye.idp = getelementptr inbounds i8, ptr %o, i64 -${CYC_HDR} ; the young-in nursery`,
` %ye.id16 = load i16, ptr %ye.idp ; ScrCycHdr.id`,
` %ye.id = zext i16 %ye.id16 to i64`,
` %ye.sp = getelementptr inbounds [${CYC_MAX_TYPES} x i32], ptr @scr_yin_skip, i64 0, i64 %ye.id`,
` %ye.s = load i32, ptr %ye.sp`,
` %ye.skip = icmp ne i32 %ye.s, 0`,
` br i1 %ye.skip, label %ye.skipped, label %ye.enter`,
`ye.skipped: ; a type that does not die young: counted down, stays out`,
` %ye.s1 = sub i32 %ye.s, 1`,
` store i32 %ye.s1, ptr %ye.sp`,
` br label %ye.done`,
`ye.enter:`,
` %ye.v = load ptr, ptr @scr_yin`,
` %ye.np = getelementptr inbounds i8, ptr @scr_yin, i64 8`,
` %ye.n = load i64, ptr %ye.np`,
` %ye.lp = getelementptr inbounds i8, ptr @scr_yin, i64 16`,
` %ye.l = load i64, ptr %ye.lp`,
` %ye.room = icmp ult i64 %ye.n, %ye.l`,
` br i1 %ye.room, label %ye.fast, label %ye.slow`,
`ye.fast:`,
` %ye.slot = getelementptr inbounds ptr, ptr %ye.v, i64 %ye.n`,
` store ptr %o, ptr %ye.slot`,
` %ye.n1 = add i64 %ye.n, 1`,
` store i64 %ye.n1, ptr %ye.np`,
` %ye.genp = getelementptr inbounds i8, ptr %o, i64 -5`,
` store i8 3, ptr %ye.genp ; ScrCycHdr.gen = SCR_CYC_YOUNG`,
` %ye.bufp = getelementptr inbounds i8, ptr %o, i64 -4`,
` %ye.i = trunc i64 %ye.n to i32`,
` %ye.buf = or i32 %ye.i, -2147483648`,
` store i32 %ye.buf, ptr %ye.bufp ; ScrCycHdr.buf = buffered bit | index`,
` br label %ye.done`,
`ye.slow:`,
` call void @scr_yin_enter_slow(ptr %o)`,
` br label %ye.done`,
`ye.done:`,
];
}
/** What class emission asks of lifetime regions (LifetimeRegions in
@@ -1622,6 +1688,7 @@ export function emitRecordShapes(
` store ptr %ovf, ptr %ovfp`,
);
}
if (traced) nw.push(...youngEntryLines(host));
nw.push(` ret ptr %o`, `}`, ``);
defs.push(...nw);
@@ -16,6 +16,9 @@ import type { NativeTargetSpec } from "./targets.js";
export interface NativeLinkFeatures {
workers?: boolean;
/** The young-in nursery (backend/young-nursery.ts): the SCR_YIN
* variant of the collector unit. Absent when off. */
youngNursery?: boolean;
regex: boolean;
copying: boolean;
textDecoderLegacy: boolean;
@@ -46,6 +46,8 @@ export interface CcOptions {
frameworks?: readonly string[];
/** Isolated native Worker contexts; every runtime object must use TLS. */
workers?: boolean;
/** Compile the runtime with SCR_YIN (backend/young-nursery.ts). */
youngNursery?: boolean;
regex?: boolean;
/** The program uses one of the copying/typed-array bridge intrinsics
@@ -445,6 +445,7 @@ export async function compileCInternal(
: [optimization === "dev" ? "-O0" : "-O2"]),
...executableSectionFlags.compile,
...(opts.textDecoderLegacy ? ["-DSCR_TEXT_DECODER_LEGACY"] : []),
...(opts.youngNursery ? ["-DSCR_YIN"] : []),
"-fno-math-errno",
// The runtime object model uses type-punned C: a hierarchy
// upcast is a raw pointer cast, so one object's header (rc, vt) and
@@ -643,6 +644,7 @@ export async function compileCInternal(
: [optimization === "dev" ? "-O0" : "-O2"]),
...executableSectionFlags.compile,
...(opts.textDecoderLegacy ? ["-DSCR_TEXT_DECODER_LEGACY"] : []),
...(opts.youngNursery ? ["-DSCR_YIN"] : []),
"-fno-math-errno",
"-fno-strict-aliasing", // the emitted object model type-puns — see buildArgs
"-Wno-deprecated-declarations",
@@ -80,6 +80,7 @@ export interface RuntimePackManifest {
export interface RuntimeFeatureSet extends NativeLinkFeatures {
workers: boolean;
youngNursery: boolean;
eventTarget: boolean;
nativeFetch: boolean;
netEffective: boolean;
@@ -266,6 +267,7 @@ export function effectiveRuntimeFeatures(
return {
...features,
workers: features.workers === true,
youngNursery: features.youngNursery === true,
eventTarget: features.eventTarget === true,
nativeFetch,
netEffective,
@@ -0,0 +1,9 @@
/** The young-in nursery (phase 1, experimental): compile-time opt-in with
* SCRIPTC_YOUNG_NURSERY=1. Executables then allocate compiled class and
* record instances young (the emitted constructors call scr_yin_enter) and
* link the collector unit built with SCR_YIN, which collects young cycles
* in nursery collections (scr_cycle.c, "young-in nursery"). Unset, nothing
* the compiler emits or links changes. */
export function youngNurseryRequested(env: NodeJS.ProcessEnv = process.env): boolean {
return env["SCRIPTC_YOUNG_NURSERY"] === "1";
}
@@ -49,6 +49,7 @@ export interface EarlyExecutableNativeFeatures {
/** Omitted is the historical release posture. */
optimization?: "dev" | "speed";
workers?: boolean;
youngNursery?: boolean;
regex: boolean;
copying: boolean;
textDecoderLegacy: boolean;
@@ -111,6 +112,8 @@ export interface EarlyExecutableCacheOptions {
windowsSubsystem?: "gui";
/** Omitted unless --detached-exit is set. */
detachedExit?: true;
/** Omitted unless SCRIPTC_YOUNG_NURSERY=1 (backend/young-nursery.ts). */
youngNursery?: true;
npmStatic: readonly string[] | "auto" | null;
/** Raw manifest identity: path and bytes. Native archives remain under the
* stricter native cache's independent dependency validation. */
@@ -200,7 +203,8 @@ function validNativeFeatures(value: unknown): value is EarlyExecutableNativeFeat
(native.optimization === undefined ||
native.optimization === "dev" ||
native.optimization === "speed") &&
(native.workers === undefined || typeof native.workers === "boolean"),
(native.workers === undefined || typeof native.workers === "boolean") &&
(native.youngNursery === undefined || typeof native.youngNursery === "boolean"),
);
}
@@ -221,6 +225,7 @@ function cacheKey(options: EarlyExecutableCacheOptions): string {
...(options.strip ? ["strip"] : []),
...(options.windowsSubsystem === "gui" ? ["windows-subsystem-gui"] : []),
...(options.detachedExit ? ["detached-exit"] : []),
...(options.youngNursery ? ["young-nursery"] : []),
options.npmStatic === null
? "<npm-static-off>"
: options.npmStatic === "auto"
@@ -260,6 +265,7 @@ function routeKey(options: Omit<EarlyExecutableRouteOptions, "nativeEnvironment"
if (options.strip) hash.update("strip\0");
if (options.windowsSubsystem === "gui") hash.update("windows-subsystem-gui\0");
if (options.detachedExit) hash.update("detached-exit\0");
if (options.youngNursery) hash.update("young-nursery\0");
hash
.update(
options.npmStatic === null
+3
View File
@@ -113,6 +113,7 @@ import { provenanceSources } from "./frontend/provenance-registry.js";
import { clearResolveCaches } from "./frontend/resolve.js";
import { loadFfiProfile, type FfiProfile } from "./ffi/ffi-manifest.js";
import { executableLinkFeatures } from "./backend/executable-features.js";
import { youngNurseryRequested } from "./backend/young-nursery.js";
import { FrontendInputTracker, trackedReadFile } from "./frontend/input-tracker.js";
import {
libraryFrontendImplementationFingerprint,
@@ -548,6 +549,7 @@ async function compileExecutableNative(
watch: features.watch,
foreignFfi: features.foreignFfi,
...(features.workers ? { workers: true } : {}),
...(features.youngNursery ? { youngNursery: true } : {}),
nodeTest: features.nodeTest,
tls: features.tls,
tlsCa: features.tlsCa,
@@ -1046,6 +1048,7 @@ async function compileTracked(
...(opts.strip ? { strip: true as const } : {}),
...(opts.windowsSubsystem === "gui" ? { windowsSubsystem: "gui" as const } : {}),
...(opts.detachedExit === true ? { detachedExit: true as const } : {}),
...(youngNurseryRequested() ? { youngNursery: true as const } : {}),
npmStatic: opts.npmStatic ?? null,
ffiProfile:
opts.ffiProfilePath === undefined || ffiProfileBytes === null
+2
View File
@@ -123,6 +123,8 @@ function validFeatures(value: unknown): value is NativeLinkFeatures {
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
const features = value as Record<string, unknown>;
if (features["workers"] !== undefined && typeof features["workers"] !== "boolean") return false;
if (features["youngNursery"] !== undefined && typeof features["youngNursery"] !== "boolean")
return false;
return [
"regex",
"copying",
@@ -73,6 +73,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")(
systemLibraries: [],
features: {
workers: false,
youngNursery: false,
eventTarget: false,
regex: true,
copying: false,
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5981-young-nursery-cycles.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5982-young-nursery-workers.ts"
],
"diags": []
}
@@ -125,6 +125,15 @@ function variantsFor(source) {
defines: ["SCR_TEXT_DECODER_LEGACY"],
});
}
if (source === "scr_cycle.c") {
// The young-in nursery (SCRIPTC_YOUNG_NURSERY=1): the collector with
// nursery collections; programs built without it link the default.
variants.push({
id: "young-nursery",
when: { youngNursery: true },
defines: ["SCR_YIN"],
});
}
return variants;
}
@@ -343,7 +343,7 @@ async function build() {
...(unitHelper === null ? {} : { speed_codegen: unitHelper.identity }),
},
macros: {
executable: ["SCR_TEXT_DECODER_LEGACY"],
executable: ["SCR_TEXT_DECODER_LEGACY", "SCR_YIN"],
excluded: ["SCR_RC_AUDIT", "SCR_ASAN_FIBERS"],
sanitizer: "external-toolchain-required",
},
@@ -27,7 +27,11 @@ export function withLibraryRuntimeFlavors(matrix) {
},
].map((unit) => ({
...unit,
variants: unit.variants.filter((variant) => !variant.defines.includes("SCR_WORKERS")),
// Libraries run neither workers nor the young-in nursery (an
// executable-only experiment).
variants: unit.variants.filter(
(variant) => !variant.defines.includes("SCR_WORKERS") && !variant.defines.includes("SCR_YIN"),
),
}));
const flavors = { ...matrix.flavors };
for (const [flavor, spec] of Object.entries(matrix.flavors)) {
@@ -115,6 +115,15 @@ function variantsFor(source) {
defines: ["SCR_TEXT_DECODER_LEGACY"],
});
}
if (source === "scr_cycle.c") {
// The young-in nursery (SCRIPTC_YOUNG_NURSERY=1): the collector with
// nursery collections; programs built without it link the default.
variants.push({
id: "young-nursery",
when: { youngNursery: true },
defines: ["SCR_YIN"],
});
}
return variants;
}
@@ -440,7 +440,7 @@ async function build() {
...(unitHelper === null ? {} : { speed_codegen: unitHelper.identity }),
},
macros: {
executable: ["SCR_TEXT_DECODER_LEGACY"],
executable: ["SCR_TEXT_DECODER_LEGACY", "SCR_YIN"],
excluded: ["SCR_RC_AUDIT", "SCR_ASAN_FIBERS"],
sanitizer: "external-toolchain-required",
},
@@ -61,6 +61,26 @@ test("library pack modes preserve pure runtime features and exclude ambient even
expect(matrix.runtime_units.some((unit) => unit.source === "scr_library.c")).toBe(false);
});
test("the young-in nursery collector is an executable-only variant of scr_cycle.c", () => {
const ids = (units: { source: string; variants: { id: string }[] }[]) =>
units.find((unit) => unit.source === "scr_cycle.c")!.variants.map((variant) => variant.id);
expect(ids(matrix.runtime_units)).toEqual([
"default",
"default-workers",
"young-nursery",
"young-nursery-workers",
]);
const young = matrix.runtime_units
.find((unit) => unit.source === "scr_cycle.c")
.variants.find((variant) => variant.id === "young-nursery");
expect(young).toMatchObject({
when: { youngNursery: true, workers: false },
defines: ["SCR_YIN"],
});
for (const mode of ["library-release", "library-thread-release"])
expect(ids(matrix.flavors[mode].runtime_units)).toEqual(["default"]);
});
test("mobile packs ship only library modes and their permitted vendor archives", () => {
const mobile = createRuntimePackMatrix({
target: { object_format: "elf" },
+427 -4
View File
@@ -444,6 +444,22 @@ static SCR_TL unsigned scr_gen_limit = SCR_CYC_OLD;
#endif
static size_t scr_cyc_pass(unsigned gen_limit);
#ifdef SCR_YIN
/* The young-in nursery's state (see "young-in nursery" below): the nursery
* itself (scr_rc.h), every live young object at the index its header
* records; a collection's mark worklist, dead set and deferred releases;
* per-object counts of references from other young objects, valid during a
* collection. */
SCR_TL ScrYin scr_yin = {0};
static SCR_TL ScrVec scr_yin_stack;
static SCR_TL ScrVec scr_yin_dead;
static SCR_TL ScrVec scr_yin_xrel;
static SCR_TL uint32_t *scr_yin_counts;
static SCR_TL size_t scr_yin_counts_cap;
static SCR_TL bool scr_yin_busy;
static void scr_yin_leave(ScrCycHdr *h, void *obj);
static size_t scr_yin_collect(void);
#endif
static void scr_cyc_route_bypassed(void);
/* A limited pass (scr_cyc_collect_set) takes only the candidates in this
* set from the buffers up to `scr_cyc_take_gen`; NULL takes them all. */
@@ -809,6 +825,12 @@ static SCR_CYC_SCHEDULING void scr_cyc_collect_due(void) {
* per turn would walk the whole live heap every turn — which is exactly the
* cost the generations exist to avoid. */
void scr_cyc_collect_scheduled(void) {
#ifdef SCR_YIN
/* A collection point also collects the nursery when anything entered it
* since its last collection, so young garbage does not wait for the
* allocation trigger in a program that has gone idle. */
if (scr_yin.n != 0) scr_yin_collect();
#endif
scr_cyc_route_bypassed();
scr_cyc_note_fall();
if (scr_cyc_buffered() == 0) {
@@ -996,6 +1018,13 @@ static SCR_CYC_COMPACT void scr_cyc_route_bypassed(void) {
void scr_cyc_on_dead(void *obj) {
ScrCycHdr *h = scr_cyc_hdr(obj);
if (!scr_cyc_is_buffered(h)) return;
#ifdef SCR_YIN
/* A young object leaves the nursery; nothing else lists it. */
if (scr_cyc_gen(h) == SCR_CYC_YOUNG) {
scr_yin_leave(h, obj);
return;
}
#endif
/* O(1) removal: swap the last entry into the hole. A buffered object's
* generation never changes (promotion runs only on objects the pass has
* already drained from the buffers), so `gen` names the right one. */
@@ -1026,6 +1055,18 @@ static SCR_TL ScrVec scr_pub_holes[SCR_CYC_NGENS];
uint32_t scr_cyc_pub_take(void *obj) {
ScrCycHdr *h = scr_cyc_hdr(obj);
#ifdef SCR_YIN
/* A young object (its header reads as buffered) leaves the nursery,
* unbuffered: young objects never become immortal in the nursery, and so
* never reach another thread from it. The answer UINT32_MAX makes
* scr_pub_mark's recorded position 0 ("not taken"), so a refused publish
* leaves it out of every buffer, as it was. It is reachable from the
* graph being published, so it needs no candidate. */
if (scr_cyc_gen(h) == SCR_CYC_YOUNG) {
scr_yin_leave(h, obj);
return UINT32_MAX;
}
#endif
unsigned gen = scr_cyc_gen(h);
uint32_t index = scr_cyc_buf_index(h);
scr_roots[gen].v[index] = NULL;
@@ -1102,9 +1143,19 @@ void scr_cyc_on_release(void *obj) {
* two must agree on which edges are internal. Nothing to do for NULL
* (unassigned slots), immortal (interned statics have no header at all), or
* a generation this pass does not collect. */
#ifdef SCR_YIN
/* Trial deletion walks a young object as a nursery-level one: structures
* that mix young objects with families that are not young (arrays,
* closures) are found by the candidates of the latter, exactly as before.
* A young survivor stays young (scan_black promotes only below OLD) and a
* young member of the dead set leaves the nursery before its teardown. */
#define SCR_CYC_WALK_GEN(h) (scr_cyc_gen(h) == SCR_CYC_YOUNG ? SCR_CYC_NURSERY : scr_cyc_gen(h))
#else
#define SCR_CYC_WALK_GEN(h) scr_cyc_gen(h)
#endif
#define SCR_CYC_SKIP(child) \
((child) == NULL || SCR_RC(child) == SIZE_MAX \
|| scr_cyc_gen(scr_cyc_hdr(child)) > scr_gen_limit)
|| SCR_CYC_WALK_GEN(scr_cyc_hdr(child)) > scr_gen_limit)
/* 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
@@ -1147,7 +1198,7 @@ static inline __attribute__((always_inline)) void scr_mark_gray(void *obj, uintp
static void scr_mg_visit(void *child, void *ctx) {
if (child == NULL || SCR_RC(child) == SIZE_MAX) return;
ScrCycHdr *h = scr_cyc_hdr(child);
if (scr_cyc_gen(h) > scr_gen_limit) {
if (SCR_CYC_WALK_GEN(h) > scr_gen_limit) {
scr_cyc_push_work(&scr_xgen_edges, scr_gray_holder);
scr_cyc_push_work(&scr_xgen_edges, child);
return;
@@ -1172,7 +1223,12 @@ static inline __attribute__((always_inline)) void scr_mark_gray(void *obj, uintp
* 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))
if (!scr_cyc_is_buffered(h) || (scr_gen_limit == SCR_CYC_OLD && !scr_cyc_take
#ifdef SCR_YIN
/* a young object's `buf` is its nursery index */
&& scr_cyc_gen(h) != SCR_CYC_YOUNG
#endif
))
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) {
@@ -1322,6 +1378,340 @@ static SCR_CYC_COMPACT void scr_xg_release(void *obj) {
scr_rc_destroy(obj, scr_xg_destroy);
}
#ifdef SCR_YIN
/* ── young-in nursery ─────────────────────────────────────────────────── */
/* Programs built with the young-in nursery (SCRIPTC_YOUNG_NURSERY=1, which
* links this unit built with SCR_YIN) allocate compiled class and record
* instances YOUNG: the emitted constructor hands each new object to
* scr_yin_enter, which lists it in the nursery and marks its header
* SCR_CYC_YOUNG with its nursery index in `buf`, under the buffered bit.
* The bit makes every release path treat the object as an existing
* candidate: a release that leaves it alive makes no call, and its death
* calls scr_cyc_on_dead, which takes it out of the nursery
* (scr_yin_leave) before the teardown runs. Young objects are never
* candidates, but trial deletion walks them as nursery-level objects when
* a candidate reaches them (SCR_CYC_WALK_GEN).
*
* Whenever the nursery holds SCR_YOUNG_NURSERY_SIZE live objects (4096 by
* default), at collection points and before every full sweep, the nursery
* is collected on its own, in time proportional to its size and without
* walking anything older:
* count for every young object, the references to it from traced
* fields of other young objects (one trace per young object);
* mark an object whose count is below its reference count is
* referenced from outside the nursery (a stack, a global, an
* older object, a runtime structure, FFI, a fiber or a coroutine
* frame: every one of them holds a counted reference); mark
* from each such object through young edges;
* sweep what is unmarked is referenced only from unmarked young
* objects, which is garbage. Marked objects join trial
* deletion's nursery generation, unbuffered; dead ones are torn
* down, giving up only the edges that leave the dead set (like
* the white set's cross-generation edges, after every teardown).
* Survivors go to trial deletion's nursery rather than past it because the
* structures that survive are mostly held by families that are not young
* (arrays, closures, maps): a dead tree whose nodes are young and whose
* child arrays are not survives the nursery collection through the
* arrays, and the arrays' own candidates reach it there.
* Soundness rests on the two invariants trial deletion already needs: every
* strong reference is counted, and a trace visits exactly the counted
* references to headered children. The counts are recomputed at every
* collection, so no store barrier is needed and none can be missed.
*
* Candidates. A release of a young object never buffers it, so a structure
* whose last outside reference was dropped while its root was young has no
* candidate, and when the structure reaches beyond the nursery (a young
* node in an array that is not young, say), the nursery collection sees
* the outside edge as a root and keeps it. So every outside root a
* collection finds (count below the reference count) becomes a candidate
* as it is promoted. That suffices: every survivor is reachable from such
* a root, so if a survivor was already garbage, so was its root, and trial
* deletion reaches the survivor from the root's candidate; garbage formed
* after the promotion has a candidate from the release that formed it,
* since survivors are no longer young. */
static void scr_cyc_trim(ScrVec *vec);
#define SCR_YIN_DEFAULT_SIZE 4096
#define SCR_CYC_YIN_PROMOTED SCR_CYC_NURSERY
/* Pushes onto the collection's work vectors, inline. */
static inline __attribute__((always_inline)) void scr_yin_push(ScrVec *vec, void *obj) {
if (__builtin_expect(vec->n == vec->cap, 0)) scr_cyc_grow(vec);
vec->v[vec->n++] = obj;
if (vec->n > vec->used) vec->used = vec->n;
}
#define SCR_YIN_MAX_SIZE ((size_t)1 << 30)
static size_t scr_yin_size(void) {
static SCR_TL size_t cached = 0;
if (cached == 0) {
const char *env = scr_getenv("SCR_YOUNG_NURSERY_SIZE");
long v = env ? strtol(env, NULL, 10) : 0;
cached = v > 0 ? ((size_t)v < SCR_YIN_MAX_SIZE ? (size_t)v : SCR_YIN_MAX_SIZE)
: SCR_YIN_DEFAULT_SIZE;
}
return cached;
}
/* Collections, survivors and dead objects, printed at exit with
* SCR_YOUNG_NURSERY_STATS=1 (measurement). */
static SCR_TL size_t scr_yin_stat[3];
static void scr_yin_stats_print(void) {
fprintf(stderr, "scriptc young nursery: %zu collections, %zu survivors, %zu dead\n",
scr_yin_stat[0], scr_yin_stat[1], scr_yin_stat[2]);
}
/* Types that do not die in young cycles. Only a young object that dies in
* a nursery collection gains anything there; every other one costs: one
* that dies by reference counting makes a call to leave the nursery (where
* it made none before), one that is published is moved out of it, and one
* that survives a collection is kept by it, and becomes a trial-deletion
* candidate when an outside reference holds it. Structures held by
* families that are not young (tree nodes in child arrays, objects
* captured by closures) survive, and plain data dies by reference counting.
* So each thread keeps, per registry id, how many of the type's young
* objects left the nursery (by a collection or otherwise) and how many of
* them a collection freed, and after SCR_YIN_WINDOW of them a type freed
* for fewer than one in SCR_YIN_RATIO is allocated as before (trial
* deletion's nursery) for its next SCR_YIN_PROBE allocations, then
* enters again for a fresh window: the same bypass-and-probe scheme as the
* levels' bypass above. The countdown
* (scr_yin_skip, an ABI: emitted constructors test and decrement it
* inline) only steers which objects enter the nursery. */
#define SCR_YIN_WINDOW 1024
#define SCR_YIN_RATIO 4
#define SCR_YIN_PROBE (64 * SCR_YIN_WINDOW)
#ifdef SCR_WORKERS
static _Thread_local uint32_t *scr_yin_skip_tl;
static uint32_t *scr_yin_skip_table(void) {
if (!scr_yin_skip_tl) {
scr_yin_skip_tl = calloc(SCR_CYC_MAX_TYPES, sizeof *scr_yin_skip_tl);
if (!scr_yin_skip_tl) scr_cyc_oom();
}
return scr_yin_skip_tl;
}
#else
uint32_t scr_yin_skip[SCR_CYC_MAX_TYPES] = {0};
static uint32_t *scr_yin_skip_table(void) { return scr_yin_skip; }
#endif
typedef struct {
uint32_t met, freed;
} ScrYinType;
/* SCR_YOUNG_NURSERY_ALL=1 keeps every type young (test lanes). */
static bool scr_yin_all(void) {
static SCR_TL int cached = -1;
if (cached < 0) {
const char *env = scr_getenv("SCR_YOUNG_NURSERY_ALL");
cached = env != NULL && env[0] == '1';
}
return cached != 0;
}
static SCR_TL ScrYinType *scr_yin_types;
static __attribute__((noinline, cold)) void scr_yin_decide(uint16_t id, ScrYinType *t) {
if (t->freed * SCR_YIN_RATIO < t->met && !scr_yin_all()) scr_yin_skip_table()[id] = SCR_YIN_PROBE;
t->met = t->freed = 0;
}
static inline __attribute__((always_inline)) void scr_yin_met(uint16_t id, bool dead) {
ScrYinType *t = &scr_yin_types[id];
t->freed += dead;
if (__builtin_expect(++t->met >= SCR_YIN_WINDOW, 0)) scr_yin_decide(id, t);
}
void scr_yin_enter_slow(void *obj);
/* The whole nursery entry, for targets whose constructors call it. */
void scr_yin_enter(void *obj) {
uint32_t *skip = &scr_yin_skip_table()[scr_cyc_hdr(obj)->id];
if (*skip != 0) {
--*skip;
return;
}
scr_yin_enter_slow(obj);
}
/* The slow half of the nursery entry (the emitted constructors inline the
* rest: skip, or append while n < lim). The nursery holds at most
* SCR_YOUNG_NURSERY_SIZE live young objects: the object that would exceed
* that collects the nursery first, then enters. */
void scr_yin_enter_slow(void *obj) {
size_t size = scr_yin_size();
if (!scr_yin_types) {
scr_yin_types = calloc(SCR_CYC_MAX_TYPES, sizeof *scr_yin_types);
if (!scr_yin_types) scr_cyc_oom();
}
if (scr_yin.n >= size) {
static SCR_TL bool stats_checked = false;
if (!stats_checked) {
stats_checked = true;
const char *env = scr_getenv("SCR_YOUNG_NURSERY_STATS");
if (env && env[0] == '1') atexit(scr_yin_stats_print);
}
scr_yin_collect();
}
if (scr_yin.n == scr_yin.cap) {
/* Positions are 31-bit (ScrCycHdr.buf); the size is capped below. */
size_t cap = scr_yin.cap ? scr_yin.cap * 2 : 1024;
void **grown = realloc(scr_yin.v, cap * sizeof *grown);
if (!grown) scr_cyc_oom();
scr_yin.v = grown;
scr_yin.cap = cap;
}
scr_yin.lim = scr_yin.cap < size ? scr_yin.cap : size;
ScrCycHdr *h = scr_cyc_hdr(obj);
scr_cyc_set_gen(h, SCR_CYC_YOUNG);
scr_cyc_set_buffered(h, (uint32_t)scr_yin.n);
scr_yin.v[scr_yin.n++] = obj;
}
/* O(1) removal: the last young object takes the hole. */
static void scr_yin_leave(ScrCycHdr *h, void *obj) {
size_t i = scr_cyc_buf_index(h);
#ifdef SCR_RC_AUDIT
if (i >= scr_yin.n || scr_yin.v[i] != obj) scr_trap("scriptc: young object outside the nursery\n");
#endif
void *last = scr_yin.v[--scr_yin.n];
scr_yin.v[i] = last;
if (last != obj) scr_cyc_set_buffered(scr_cyc_hdr(last), (uint32_t)i);
scr_cyc_clear_buffered(h);
scr_cyc_set_gen(h, SCR_CYC_NURSERY);
scr_yin_met(h->id, false);
}
static void scr_yin_count_visit(void *child, void *ctx) {
(void)ctx;
if (child == NULL || SCR_RC(child) == SIZE_MAX) return;
ScrCycHdr *h = scr_cyc_hdr(child);
if (scr_cyc_gen(h) == SCR_CYC_YOUNG) scr_yin_counts[scr_cyc_buf_index(h)]++;
}
/* Marked young objects are GRAY (young objects are otherwise BLACK). */
static void scr_yin_mark_visit(void *child, void *ctx) {
(void)ctx;
if (child == NULL || SCR_RC(child) == SIZE_MAX) return;
ScrCycHdr *h = scr_cyc_hdr(child);
if (scr_cyc_gen(h) != SCR_CYC_YOUNG || scr_cyc_color(h) == SCR_CYC_GRAY) return;
scr_cyc_set_color(h, SCR_CYC_GRAY);
scr_yin_push(&scr_yin_stack, child);
}
/* A dead object's edge: young and unmarked is dead too (nothing to give
* up); a marked young object (promoted now or soon) or an older one keeps
* the count until every teardown has run. */
static void scr_yin_dead_visit(void *child, void *ctx) {
(void)ctx;
if (child == NULL || SCR_RC(child) == SIZE_MAX) return;
ScrCycHdr *h = scr_cyc_hdr(child);
if (scr_cyc_gen(h) == SCR_CYC_YOUNG && scr_cyc_color(h) != SCR_CYC_GRAY) return;
scr_yin_push(&scr_yin_xrel, child);
}
/* Fully optimized, unlike trial deletion's per-pass bookkeeping: its loops
* run once per young object. */
static __attribute__((noinline)) size_t scr_yin_collect(void) {
if (scr_yin_busy || scr_collecting) return 0;
size_t n = scr_yin.n;
if (n == 0) return 0;
/* What died outside passes so far counts toward the death rule; what this
* collection frees does not (the reading is refreshed at its end). */
scr_cyc_note_fall();
scr_yin_busy = true;
/* Releases made by teardowns below buffer candidates but must not start
* a trial-deletion pass while the dead set is half torn down, and take
* the call, as during a pass (scr_cyc_window_update). */
scr_collecting = true;
scr_cyc_window_update();
if (n > scr_yin_counts_cap) {
size_t cap = scr_yin_counts_cap ? scr_yin_counts_cap * 2 : 1024;
while (cap < n) cap *= 2;
uint32_t *counts = realloc(scr_yin_counts, cap * sizeof *counts);
if (!counts) scr_cyc_oom();
scr_yin_counts = counts;
scr_yin_counts_cap = cap;
}
memset(scr_yin_counts, 0, n * sizeof *scr_yin_counts);
void **v = scr_yin.v;
for (size_t k = 0; k < n; k++)
scr_cyc_type(scr_cyc_hdr(v[k]))->trace(v[k], scr_yin_count_visit, NULL);
#ifdef SCR_RC_AUDIT
/* Membership and counts: every listed object is young at its index and
* alive, and holds at least the references young objects make to it
* (fewer would mean an uncounted reference). */
for (size_t k = 0; k < n; k++) {
ScrCycHdr *h = scr_cyc_hdr(v[k]);
if (scr_cyc_gen(h) != SCR_CYC_YOUNG || !scr_cyc_is_buffered(h) || scr_cyc_buf_index(h) != k ||
scr_cyc_color(h) != SCR_CYC_BLACK)
scr_trap("scriptc: corrupt young nursery entry\n");
if (SCR_RC(v[k]) == 0 || SCR_RC(v[k]) < scr_yin_counts[k])
scr_trap("scriptc: young object with uncounted references\n");
}
#endif
for (size_t k = 0; k < n; k++) {
void *obj = v[k];
ScrCycHdr *h = scr_cyc_hdr(obj);
if (scr_cyc_color(h) == SCR_CYC_GRAY || SCR_RC(obj) <= scr_yin_counts[k]) continue;
scr_cyc_set_color(h, SCR_CYC_GRAY);
scr_yin_push(&scr_yin_stack, obj);
while (scr_yin_stack.n) {
void *x = scr_yin_stack.v[--scr_yin_stack.n];
scr_cyc_type(scr_cyc_hdr(x))->trace(x, scr_yin_mark_visit, NULL);
}
}
/* Split: promote the marked objects; record the dead ones and their
* edges to survivors and older objects, which are given up after every
* teardown (until then each holds the count it was made with, so no
* teardown's releases can free a target early). Edges between dead
* objects need nothing: both go. No block is freed before every dead
* object has been traced. */
scr_yin_dead.n = 0;
scr_yin_xrel.n = 0;
for (size_t k = 0; k < n; k++) {
void *obj = v[k];
ScrCycHdr *h = scr_cyc_hdr(obj);
if (scr_cyc_color(h) == SCR_CYC_GRAY) {
scr_cyc_set_color(h, SCR_CYC_BLACK);
scr_cyc_set_gen(h, SCR_CYC_YIN_PROMOTED);
scr_cyc_clear_buffered(h);
/* An outside root becomes a candidate (see "candidates" above). */
if (SCR_RC(obj) > scr_yin_counts[k]) scr_cyc_on_release(obj);
scr_yin_met(h->id, false);
} else {
scr_yin_met(h->id, true);
scr_cyc_set_color(h, SCR_CYC_DOOMED);
scr_yin_push(&scr_yin_dead, obj);
scr_cyc_type(h)->trace(obj, scr_yin_dead_visit, NULL);
}
}
scr_yin.n = 0;
scr_yin_stat[0]++;
scr_yin_stat[1] += n - scr_yin_dead.n;
scr_yin_stat[2] += scr_yin_dead.n;
size_t freed = scr_yin_dead.n;
for (size_t i = 0; i < scr_yin_dead.n; i++) {
void *obj = scr_yin_dead.v[i];
scr_cyc_type(scr_cyc_hdr(obj))->free_fn(obj);
}
scr_yin_dead.n = 0;
scr_xg_freed = 0;
for (size_t i = 0; i < scr_yin_xrel.n; i++) scr_xg_release(scr_yin_xrel.v[i]);
freed += scr_xg_freed;
scr_yin_xrel.n = 0;
scr_cyc_trim(&scr_yin_stack);
scr_cyc_trim(&scr_yin_dead);
scr_cyc_trim(&scr_yin_xrel);
scr_cyc_live_seen = scr_cyc_live;
scr_collecting = false;
scr_cyc_window_update();
scr_yin_busy = false;
/* The candidates buffered above count toward trial deletion's trigger,
* which only a release checks; a program whose releases are all of
* young objects would otherwise never reach it. */
if (scr_cyc_buffered() >= scr_cyc_trigger || scr_cyc_live >= scr_cyc_alloc_trigger)
scr_cyc_collect_due();
return freed;
}
#endif
/* Dead sets at least this large are torn down in address order. Smaller
* ones mostly stay in cache, where ordering them costs more than it saves. */
#define SCR_CYC_ORDER_MIN 16384
@@ -1430,6 +1820,9 @@ static SCR_CYC_COMPACT size_t scr_cyc_settle(unsigned gen_limit, bool doomed) {
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;
#ifdef SCR_YIN
if (scr_cyc_gen(h) == SCR_CYC_YOUNG) scr_yin_leave(h, obj);
#endif
type->free_fn(obj);
}
scr_white.n = 0;
@@ -1699,9 +2092,16 @@ void scr_cyc_route_bypassed_at(void) { scr_cyc_route_bypassed(); }
size_t scr_cyc_collect_set(const ScrCycSet *set, unsigned gen) {
if (scr_collecting) return 0;
#ifdef SCR_YIN
/* Young objects are invisible to trial deletion: collect them first, so
* survivors are promoted where the limited pass can walk them. */
size_t young = scr_yin_collect();
#else
size_t young = 0;
#endif
scr_cyc_take = set;
scr_cyc_take_gen = gen;
size_t freed = scr_cyc_pass(SCR_CYC_OLD);
size_t freed = young + scr_cyc_pass(SCR_CYC_OLD);
scr_cyc_take = NULL;
return freed;
}
@@ -1715,6 +2115,11 @@ void scr_collect_cycles(void) {
* can have buffered anything new, so a pass that comes up empty ends
* this; and since each repeat needs a strictly smaller live heap to
* continue, it terminates. */
#ifdef SCR_YIN
/* The nursery first: afterwards no young object is left (a teardown
* never allocates one), so the passes see every object. */
scr_yin_collect();
#endif
while (scr_cyc_pass(SCR_CYC_OLD) && scr_cyc_buffered()) {
}
}
@@ -1733,6 +2138,24 @@ void scr_cyc_context_cleanup(void) {
free(scr_cyc_kinds);
scr_cyc_kinds = NULL;
scr_cyc_kinds_cap = 0;
#ifdef SCR_YIN
ScrVec *young[] = {&scr_yin_stack, &scr_yin_dead, &scr_yin_xrel};
for (size_t i = 0; i < sizeof young / sizeof young[0]; i++) {
free(young[i]->v);
*young[i] = (ScrVec){0};
}
free(scr_yin.v);
scr_yin = (ScrYin){0};
free(scr_yin_types);
scr_yin_types = NULL;
#ifdef SCR_WORKERS
free(scr_yin_skip_tl);
scr_yin_skip_tl = NULL;
#endif
free(scr_yin_counts);
scr_yin_counts = NULL;
scr_yin_counts_cap = 0;
#endif
#if SCR_CYC_INLINE_BUFFERING
scr_cyc_nursery_bypassing = 0;
#endif
+32
View File
@@ -607,4 +607,36 @@ SCR_COLD void scr_throw_published_field(const char *prop, const char *owner);
bool scr_throw_published_array(int op, const struct ScrArr *a, double index, double n);
SCR_COLD void scr_throw_published_collection(bool set);
/* Young-in nursery (scr_cycle.c built with SCR_YIN, linked by programs
* compiled with SCRIPTC_YOUNG_NURSERY=1). A young object's header has
* generation SCR_CYC_YOUNG, above every trial-deletion level, and its
* nursery index in `buf` under SCR_CYC_BUFFERED_BIT, so release paths treat
* it as a buffered candidate (no call on a release that leaves it alive,
* scr_cyc_on_dead at its death). Emitted constructors of compiled classes
* and records call scr_yin_enter on each new object, after storing its
* count; the call may collect the nursery first. */
#define SCR_CYC_YOUNG 3
void scr_yin_enter(void *obj);
/* The inline entry's halves: the per-registry-id countdown of allocations
* that skip the nursery (process-wide in executables without workers,
* which are the ones that inline the entry), and the append when the
* nursery has no room. */
extern uint32_t scr_yin_skip[SCR_CYC_MAX_TYPES];
void scr_yin_enter_slow(void *obj);
/* The nursery: live young objects v[0..n), capacity cap. Its layout is an
* ABI: emitted constructors (llvm/shapes.ts youngEntryLines) append inline
* while n < lim, writing the header themselves, and call scr_yin_enter
* otherwise; lim stays at most the capacity and the collection size. */
typedef struct ScrYin {
void **v;
size_t n;
size_t lim;
size_t cap;
} ScrYin;
extern SCR_TL ScrYin scr_yin;
#if UINTPTR_MAX == UINT64_MAX
_Static_assert(offsetof(ScrYin, n) == 8 && offsetof(ScrYin, lim) == 16,
"llvm/shapes.ts appends to the nursery at these offsets");
#endif
#endif /* SCR_RC_H */
+442
View File
@@ -0,0 +1,442 @@
/* The young-in nursery (scr_cycle.c built with SCR_YIN): young objects
* allocated as emitted constructors allocate them, released as emitted
* release paths release them, and collected by nursery collections, trial
* deletion and the full sweep. Every check must hold for any nursery size
* (SCR_YOUNG_NURSERY_SIZE), including a one-object nursery that collects at
* every allocation. */
#include "../src/scr_runtime.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct Plain {
size_t rc;
} Plain;
typedef struct Obj Obj;
struct Obj {
size_t rc;
Obj *a;
Obj *b;
Plain *plain; /* untraced: released by the teardown, never visited */
};
static size_t freed, plain_freed, disposed;
static void check(bool condition, const char *message) {
if (condition) return;
fprintf(stderr, "young nursery test failed: %s\n", message);
exit(EXIT_FAILURE);
}
_Noreturn void scr_trap(const char *msg) {
fputs(msg, stderr);
exit(EXIT_FAILURE);
}
size_t scr_cyc_pass_at(unsigned gen);
static void plain_release(Plain *p) {
if (p && --p->rc == 0) {
plain_freed++;
free(p);
}
}
static void obj_trace(void *o, ScrTraceVisit visit, void *ctx) {
Obj *x = o;
if (x->a) visit(x->a, ctx);
if (x->b) visit(x->b, ctx);
}
/* The collector's teardown: untraced children only, then the block. */
static void obj_gcfree(void *o) {
plain_release(((Obj *)o)->plain);
freed++;
scr_cyc_free(o);
}
static ScrCycType young_type = {&obj_trace, &obj_gcfree, 0, 0};
static ScrCycType mature_type = {&obj_trace, &obj_gcfree, 0, 0};
/* An emitted constructor: zeroed headered block, rc = 1, then the nursery. */
static Obj *young_new_of(ScrCycType *type) {
Obj *o = scr_cyc_alloc(sizeof(Obj), type);
o->rc = 1;
scr_yin_enter(o);
return o;
}
static Obj *young_new(void) { return young_new_of(&young_type); }
/* A runtime family that is not young (gen 0, trial deletion only). */
static Obj *mature_new(void) {
Obj *o = scr_cyc_alloc(sizeof(Obj), &mature_type);
o->rc = 1;
return o;
}
static void release(Obj *o);
static void destroy(void *p) {
Obj *o = p;
release(o->a);
release(o->b);
plain_release(o->plain);
freed++;
scr_cyc_free(o);
}
/* An emitted release: the buffered-bit tests are what young objects rely on. */
static void release(Obj *o) {
if (!o || o->rc == SIZE_MAX) return;
if (--o->rc == 0) {
scr_cyc_dead(o);
scr_rc_destroy(o, destroy);
return;
}
scr_cyc_released(o);
}
static Obj *retain(Obj *o) {
if (o && o->rc != SIZE_MAX) o->rc++;
return o;
}
/* h->a or h->b = v (v borrowed): unlink, then release the old value. */
static void store(Obj *h, int slot, Obj *v) {
retain(v);
Obj **f = slot == 0 ? &h->a : &h->b;
Obj *old = *f;
*f = v;
release(old);
}
static unsigned gen_of(Obj *o) { return scr_cyc_gen(scr_cyc_hdr(o)); }
static bool buffered(Obj *o) { return scr_cyc_is_buffered(scr_cyc_hdr(o)); }
static void count_dispose(void *obj) {
(void)obj;
disposed++;
}
/* Everything collectable is collected and the live count is back. */
static void settle(size_t live, const char *what) {
scr_collect_cycles();
if (scr_cyc_live != live) {
fprintf(stderr, "live %zu, expected %zu\n", scr_cyc_live, live);
check(false, what);
}
}
/* With a nursery of a few objects, a pair's first object is promoted before
* the pair is linked, and the pair then waits for an older level's pass:
* the exact checks need room for the pair. */
static bool small_nursery(void) {
const char *size = getenv("SCR_YOUNG_NURSERY_SIZE");
return size != NULL && atol(size) < 4;
}
static void check_dead_pair_and_live_ring(void) {
size_t live = scr_cyc_live, before = freed;
Obj *a = young_new(), *b = young_new();
store(a, 0, b);
store(b, 0, a);
release(a);
release(b);
check(freed == before, "a young cycle died by reference counting");
enum { RING = 10 };
Obj *head = young_new(), *last = retain(head);
for (int i = 1; i < RING; i++) {
Obj *n = young_new();
store(last, 0, n);
release(last);
last = n;
}
store(last, 0, head);
release(last);
settle(live + RING, "dead young pair not collected or live ring freed");
check(freed == before + 2, "dead young pair not freed exactly");
check(gen_of(head) != SCR_CYC_YOUNG && !buffered(head), "survivor not promoted unbuffered");
release(head); /* an older release buffers the ring (and may collect it) */
settle(live, "promoted ring not collected after its last reference");
check(freed == before + 2 + RING, "promoted ring not freed exactly");
}
static void check_rc_deaths_leave_the_nursery(void) {
enum { N = 300 };
size_t live = scr_cyc_live, before = freed;
Obj *objs[N];
for (int i = 0; i < N; i++) objs[i] = young_new();
/* Pairs (i, i+1) for even i; every third object dies by RC first. */
for (int i = 0; i + 1 < N; i += 2) {
store(objs[i], 0, objs[i + 1]);
store(objs[i + 1], 0, objs[i]);
}
size_t rc_dead = 0;
for (int i = 0; i < N; i += 6) {
/* break the pair, then drop both: plain RC deaths of young objects */
store(objs[i], 0, NULL);
store(objs[i + 1], 0, NULL);
release(objs[i]);
release(objs[i + 1]);
objs[i] = objs[i + 1] = NULL;
rc_dead += 2;
}
check(freed == before + rc_dead, "young RC deaths not freed at once");
/* Keep every fourth surviving pair alive from outside; drop the rest. */
Obj *kept[N];
size_t nkept = 0, dropped = 0;
for (int i = 0; i < N; i += 2) {
if (!objs[i]) continue;
if ((i / 2) % 4 == 0) kept[nkept++] = objs[i];
else {
release(objs[i]);
dropped += 2;
}
release(objs[i + 1]);
}
settle(live + 2 * nkept, "young pairs not collected after RC deaths");
check(freed == before + rc_dead + dropped, "dead pairs not freed exactly");
for (size_t k = 0; k < nkept; k++) {
check(gen_of(kept[k]) != SCR_CYC_YOUNG, "kept pair not promoted");
release(kept[k]);
}
settle(live, "kept pairs not collected after release");
}
static void check_dead_set_gives_up_outgoing_edges(void) {
size_t live = scr_cyc_live, before = freed, plain_before = plain_freed;
Obj *m = mature_new(); /* held from outside, and by the dead set */
Obj *s = young_new(); /* survivor held from outside, and by the dead set */
Obj *d1 = young_new(), *d2 = young_new();
Plain *p = calloc(1, sizeof *p);
p->rc = 1;
d1->plain = p;
store(d1, 0, d2);
store(d2, 0, d1);
store(d1, 1, m);
store(d2, 1, s);
release(d1);
release(d2);
check(m->rc == 2 && s->rc == 2, "setup counts");
settle(live + 2, "dead set with outgoing edges not collected");
check(freed == before + 2, "only the dead pair is freed");
check(plain_freed == plain_before + 1, "untraced child of the dead set not released");
check(m->rc == 1 && s->rc == 1, "dead set's outgoing edges not given up exactly once");
check(gen_of(s) != SCR_CYC_YOUNG, "survivor not promoted");
release(m);
release(s);
settle(live, "outside objects not freed at their last release");
}
static void check_older_holders_keep_young_alive(void) {
size_t live = scr_cyc_live, before = freed;
Obj *m = mature_new();
Obj *y1 = young_new(), *y2 = young_new();
store(y1, 0, y2);
store(y2, 0, y1);
store(m, 0, y1);
release(y1);
release(y2);
settle(live + 3, "young cycle held by an older object was freed");
check(freed == before, "nothing may be freed while the holder lives");
release(m); /* m dies by RC; its release of y1 buffers the promoted cycle */
settle(live, "cycle not collected after its older holder died");
check(freed == before + 3, "holder and cycle not freed exactly");
}
static void check_trial_deletion_releases_into_the_nursery(void) {
size_t live = scr_cyc_live, before = freed;
Obj *m1 = mature_new(), *m2 = mature_new();
Obj *y = young_new(), *z = young_new();
store(m1, 0, m2);
store(m2, 0, m1);
store(m1, 1, y); /* y's only reference: dies when the white set goes */
store(m2, 1, z); /* z keeps an outside reference */
release(y);
release(m1);
release(m2);
/* A trial-deletion pass alone walks the young objects at the nursery
* level: y joins the white set (and leaves the nursery), z survives. */
scr_cyc_pass_at(SCR_CYC_OLD);
check(freed == before + 3, "white set and its only-held young child not freed");
check(z->rc == 1, "outside-held young child damaged");
check(small_nursery() || gen_of(z) == SCR_CYC_YOUNG, "trial deletion promoted a young survivor");
release(z);
settle(live, "nursery inconsistent after trial deletion freed young objects");
check(freed == before + 4, "last young object not freed");
/* A young cycle held only by a dead older cycle: one pass frees all. */
before = freed;
m1 = mature_new();
m2 = mature_new();
Obj *y1 = young_new(), *y2 = young_new();
store(y1, 0, y2);
store(y2, 0, y1);
store(m1, 0, m2);
store(m2, 0, m1);
store(m1, 1, y1);
release(y1);
release(y2);
release(m1);
release(m2);
scr_cyc_pass_at(SCR_CYC_OLD);
check(freed == before + 4, "mixed dead structure not freed by one pass");
settle(live, "nursery inconsistent after a mixed dead structure");
}
/* A dead structure whose root died young and that reaches beyond the
* nursery: the nursery collection keeps it (an older object holds a young
* member) and must leave a candidate, or nothing short of a full sweep
* would ever reach it. */
static void check_dead_structures_beyond_the_nursery(void) {
size_t live = scr_cyc_live, before = freed;
Obj *r = young_new();
Obj *a = mature_new(); /* a family that is not young: an array, say */
Obj *n = young_new();
r->a = a; /* moved in, never released: a is no candidate */
store(a, 0, n);
store(n, 0, r);
release(n);
release(r); /* the last outside reference, dropped while r is young */
for (int i = 0; i < 1024 && freed == before; i++) scr_cyc_collect_scheduled();
check(freed == before + 3, "dead structure reaching beyond the nursery never collected");
settle(live, "structure beyond the nursery");
}
/* A type whose objects keep surviving stops entering the nursery for a
* while, then is met again. */
static void check_types_that_do_not_die_young(void) {
enum { KEPT = 5000, PROBE = 64 * 1024 };
static ScrCycType kept_type = {&obj_trace, &obj_gcfree, 0, 0};
size_t live = scr_cyc_live;
Obj **kept = malloc(KEPT * sizeof *kept);
for (int i = 0; i < KEPT; i++) kept[i] = young_new_of(&kept_type);
scr_collect_cycles(); /* meets them all: none dies */
Obj *out = young_new_of(&kept_type);
check(gen_of(out) != SCR_CYC_YOUNG && !buffered(out), "surviving type still enters the nursery");
release(out);
bool met_again = false;
for (int i = 0; i < 2 * PROBE && !met_again; i++) {
Obj *probe = young_new_of(&kept_type);
met_again = gen_of(probe) == SCR_CYC_YOUNG;
release(probe);
}
check(met_again, "surviving type never met again");
for (int i = 0; i < KEPT; i++) release(kept[i]);
free(kept);
settle(live, "surviving type left objects behind");
}
static void check_publication_takes_young_objects(void) {
size_t live = scr_cyc_live;
Obj *y1 = young_new(), *y2 = young_new(), *y3 = young_new();
/* scr_pub_mark takes an object whose header reads as buffered, and
* records the answer plus one as its position (0: nothing to restore). */
if (buffered(y1)) {
check((uint32_t)(scr_cyc_pub_take(y1) + 1) == 0 || small_nursery(), "young take recorded");
scr_cyc_pub_fill();
}
check(gen_of(y1) != SCR_CYC_YOUNG && !buffered(y1), "published young object not taken");
y1->rc = SIZE_MAX; /* published: immortal from here on */
/* y3 took y1's nursery slot: its death and y2's must still find theirs. */
release(y3);
release(y2);
settle(live + 1, "nursery inconsistent after a publish took a young object");
/* A refused publish restores nothing for a young object: it stays out of
* the nursery and every buffer, reachable from the refused graph. */
Obj *y4 = young_new();
uint32_t recorded = scr_cyc_pub_take(y4) + 1;
check(recorded == 0, "young take recorded a position");
scr_cyc_pub_fill();
check(gen_of(y4) != SCR_CYC_YOUNG && !buffered(y4), "refused publish left the object young");
release(y4);
settle(live + 1, "refused young object not freed");
}
static void check_deep_young_structures(void) {
enum { DEPTH = 200000 };
static ScrCycType deep_type = {&obj_trace, &obj_gcfree, 0, 0};
size_t live = scr_cyc_live, before = freed;
/* A long live chain (deep marking), and a long dead chain closing a cycle. */
Obj *keep = young_new_of(&deep_type), *at = retain(keep);
for (int i = 1; i < DEPTH; i++) {
Obj *n = young_new_of(&deep_type);
store(at, 0, n);
release(at);
at = n;
}
release(at);
Obj *head = young_new_of(&deep_type);
at = retain(head);
for (int i = 1; i < DEPTH; i++) {
Obj *n = young_new_of(&deep_type);
store(at, 0, n);
release(at);
at = n;
}
store(at, 0, head);
release(at);
release(head);
settle(live + DEPTH, "deep young structures");
check(freed == before + DEPTH, "dead chain not freed exactly");
release(keep); /* an acyclic chain: freed by reference counting */
check(freed == before + 2 * DEPTH, "live chain not freed at its release");
settle(live, "deep young structures left objects behind");
}
static void check_collection_points_and_limited_passes(void) {
size_t live = scr_cyc_live, before = freed;
if (small_nursery()) return;
Obj *a = young_new(), *b = young_new();
store(a, 0, b);
store(b, 0, a);
release(a);
release(b);
scr_cyc_collect_scheduled(); /* a collection point collects the nursery */
check(freed == before + 2, "collection point left young garbage");
a = young_new();
b = young_new();
store(a, 0, b);
store(b, 0, a);
release(a);
release(b);
void *slots[4] = {0};
ScrCycSet none = {slots, 3};
check(scr_cyc_collect_set(&none, SCR_CYC_OLD) >= 2, "limited pass skipped the nursery");
check(freed == before + 4, "limited pass left young garbage");
check(scr_cyc_live == live, "limited pass live count");
}
static void check_weak_observers_see_young_deaths(void) {
size_t live = scr_cyc_live, before = freed;
disposed = 0;
scr_weak_dispose_hook = count_dispose;
Obj *a = young_new(), *b = young_new(), *c = young_new();
store(a, 0, b);
store(b, 0, a);
release(a);
release(b);
release(c); /* RC death */
settle(live, "weak observer case");
scr_weak_dispose_hook = NULL;
check(freed == before + 3 && disposed == 3, "weak dispose hook missed a young death");
}
int main(void) {
check_dead_pair_and_live_ring();
check_rc_deaths_leave_the_nursery();
check_dead_set_gives_up_outgoing_edges();
check_older_holders_keep_young_alive();
check_trial_deletion_releases_into_the_nursery();
check_dead_structures_beyond_the_nursery();
check_types_that_do_not_die_young();
check_publication_takes_young_objects();
check_deep_young_structures();
check_collection_points_and_limited_passes();
check_weak_observers_see_young_deaths();
const char *size = getenv("SCR_YOUNG_NURSERY_SIZE");
printf("young nursery checks passed: size=%s\n", size ? size : "default");
return 0;
}
+80
View File
@@ -0,0 +1,80 @@
import { execFile } from "node:child_process";
import { mkdir } from "node:fs/promises";
import { join } from "node:path";
import { promisify } from "node:util";
import { expect, test } from "vitest";
const execFileAsync = promisify(execFile);
const testDir = import.meta.dirname;
// The young-in nursery variant of the collector (SCR_YIN), with the RC
// audit's membership and count checks, at nursery sizes from one object
// (a collection at every allocation) to the default.
test("young-in nursery collects young cycles at every nursery size", async () => {
const buildDir = join(testDir, "build");
await mkdir(buildDir, { recursive: true });
const bin = join(buildDir, "test_yin");
await execFileAsync("clang", [
"-std=c11",
"-O1",
"-Wall",
"-Wextra",
"-fsanitize=address,undefined",
"-DSCR_CYC_TESTING",
"-DSCR_YIN",
"-DSCR_RC_AUDIT",
"-o",
bin,
join(testDir, "test_yin.c"),
join(testDir, "../src/scr_cycle.c"),
join(testDir, "../src/scr_alloc.c"),
]);
const baseEnv = {
...process.env,
ASAN_OPTIONS: "detect_leaks=0:halt_on_error=1",
UBSAN_OPTIONS: "halt_on_error=1",
};
delete baseEnv.SCR_YOUNG_NURSERY_SIZE;
delete baseEnv.SCR_CYCLE_THRESHOLD;
for (const [size, threshold] of [
[undefined, undefined],
["1", undefined],
["2", undefined],
["7", undefined],
["64", "1"],
["1", "1"],
] as const) {
const env = { ...baseEnv };
if (size !== undefined) env.SCR_YOUNG_NURSERY_SIZE = size;
if (threshold !== undefined) env.SCR_CYCLE_THRESHOLD = threshold;
const run = await execFileAsync(bin, [], { env });
expect(run.stdout).toBe(`young nursery checks passed: size=${size ?? "default"}\n`);
}
});
// Trial deletion is unchanged in the young-nursery build: the whole cycle
// suite passes against it (it allocates no young objects).
test("trial deletion checks pass against the young-nursery collector", async () => {
const buildDir = join(testDir, "build");
await mkdir(buildDir, { recursive: true });
const bin = join(buildDir, "test_cycle_yin");
await execFileAsync("clang", [
"-std=c11",
"-O1",
"-Wall",
"-Wextra",
"-fsanitize=address,undefined",
"-DSCR_CYC_TESTING",
"-DSCR_YIN",
"-o",
bin,
join(testDir, "test_cycle.c"),
join(testDir, "../src/scr_cycle.c"),
join(testDir, "../src/scr_alloc.c"),
]);
const env = { ...process.env, ASAN_OPTIONS: "detect_leaks=0:halt_on_error=1" };
delete env.SCR_CYCLE_THRESHOLD;
delete env.SCR_CYCLE_GROWTH_CAP;
const run = await execFileAsync(bin, [], { env });
expect(run.stdout).toBe("cycle collection checks passed: threshold=256 growth_cap=16\n");
});
+178
View File
@@ -0,0 +1,178 @@
// Young cycles of classes and records, dropped at once or kept from every
// kind of outside holder: globals, older objects, arrays, maps, closures,
// weak-map values, error objects, generator and async frames. Constructors
// that throw after linking `this` into a cycle leave a partly built object
// reachable from its peer. Churn between the steps runs collections (with
// the young-in nursery, nursery collections; with SCR_YOUNG_NURSERY_SIZE=1,
// one per allocation). Whatever is still reachable keeps its contents; the
// sanitized lane's RC audit fails if anything leaks or is freed early.
class Pair {
peer: Pair | null = null;
label: string;
n: number;
constructor(label: string, n: number) {
this.label = label;
this.n = n;
}
}
interface Cell {
value: number;
next: Cell | null;
owner: Pair | null;
}
class Fragile {
other: Fragile | null = null;
stage = 0;
name: string;
constructor(name: string, peer: Fragile | null, fail: boolean) {
this.name = name;
this.other = peer;
if (peer) peer.other = this;
this.stage = 1;
if (fail) throw new Error(`${name} failed at stage ${this.stage}`);
this.stage = 2;
}
}
class Holder extends Error {
held: Pair | null = null;
}
function pair(label: string, n: number): Pair {
const a = new Pair(`${label}.a`, n);
const b = new Pair(`${label}.b`, n + 1);
a.peer = b;
b.peer = a;
return a;
}
function ring(size: number, base: number): Cell {
const first: Cell = { value: base, next: null, owner: null };
let last = first;
for (let i = 1; i < size; i++) {
const cell: Cell = { value: base + i, next: null, owner: null };
last.next = cell;
last = cell;
}
last.next = first;
return first;
}
function ringSum(start: Cell): number {
let total = start.value;
for (let c = start.next; c !== null && c !== start; c = c.next) total += c.value;
return total;
}
function churn(rounds: number): number {
let acc = 0;
for (let i = 0; i < rounds; i++) {
acc = (acc + pair("t", i).peer!.n) % 1000003;
acc = (acc + ringSum(ring(5, i))) % 1000003;
}
return acc;
}
function show(p: Pair | null): string {
return p === null ? "null" : `${p.label}:${p.n}<->${p.peer?.label ?? "none"}`;
}
// Dropped at once, in bulk.
console.log("churn", churn(20000));
// Kept from a global, an older object, an array, a map and a closure.
const kept = pair("global", 1);
const old = new Pair("old", 0);
console.log("churn", churn(5000));
old.peer = pair("from-old", 2);
const list: Pair[] = [];
const byName = new Map<string, Pair>();
for (let i = 0; i < 50; i++) {
list.push(pair(`list${i}`, i));
byName.set(`map${i}`, pair(`map${i}`, i));
}
const captured = pair("closure", 3);
const read = (): string => show(captured.peer);
const cells: Cell[] = [];
for (let i = 0; i < 20; i++) {
const c = ring(4, i * 10);
c.owner = pair(`cell${i}`, i);
cells.push(c);
}
console.log("churn", churn(30000));
console.log(show(kept), show(kept.peer));
console.log(show(old.peer), show(old.peer!.peer));
console.log(show(list[17]!), show(byName.get("map33")!.peer));
console.log(read());
console.log(ringSum(cells[7]!), show(cells[7]!.owner), ringSum(cells[19]!.next!));
// A weak map whose value refers back to its young key keeps the key while
// the key is held; dropping the key leaves the pair to the collector.
const weak = new WeakMap<Pair, Pair>();
const key = pair("key", 7);
weak.set(key, key.peer!);
for (let i = 0; i < 200; i++) {
const k = pair(`weak${i}`, i);
weak.set(k, k.peer!);
}
console.log("churn", churn(20000));
console.log(weak.has(key), show(weak.get(key) ?? null));
// Error objects (runtime-rooted classes) holding young cycles.
const errors: Holder[] = [];
for (let i = 0; i < 10; i++) {
const e = new Holder(`holder ${i}`);
e.held = pair(`err${i}`, i);
errors.push(e);
}
console.log("churn", churn(10000));
console.log(errors[4]!.message, show(errors[4]!.held));
// Constructors that throw after linking into a cycle.
const survivors: Fragile[] = [];
for (let i = 0; i < 100; i++) {
const peer = new Fragile(`peer${i}`, null, false);
try {
new Fragile(`victim${i}`, peer, i % 3 === 0);
} catch (e) {
if (i < 6) console.log((e as Error).message);
}
if (i % 10 === 0) survivors.push(peer);
}
console.log("churn", churn(10000));
for (const s of survivors.slice(0, 4))
console.log(s.name, s.stage, s.other?.name, s.other?.stage, s.other?.other === s);
// Generator frames hold young objects across collections.
function* pairs(count: number): Generator<Pair> {
for (let i = 0; i < count; i++) {
const p = pair(`gen${i}`, i);
churn(500);
yield p;
}
}
let genTotal = 0;
for (const p of pairs(30)) genTotal += p.peer!.n;
console.log("generator", genTotal);
// Async frames hold young objects across awaits.
async function hold(n: number): Promise<string> {
const a = pair(`async${n}`, n);
const cell = ring(3, n);
cell.owner = a;
await null;
churn(3000);
await new Promise<void>((resolve) => setTimeout(resolve, 1));
churn(3000);
return `${show(a)} ${show(cell.owner)} ${ringSum(cell)}`;
}
async function main(): Promise<void> {
const pending: Promise<string>[] = [hold(1), hold(2), hold(3)];
for (const p of pending) console.log(await p);
console.log("churn", churn(10000));
console.log(show(kept), show(list[49]!), read());
}
void main();
@@ -0,0 +1,81 @@
import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
import { publish } from "@scriptc/threads";
// Every thread builds and drops young cycles of its own while it publishes
// graphs of freshly built objects and posts them. Publishing takes young
// objects out of the thread's nursery before they become shared, so the
// receiving thread reads them intact while both threads keep collecting.
// Receivers publish() what they receive (a no-op in scriptc) and read only
// data, so Node.js prints the same.
class Node {
parent: Node | null = null;
next: Node | null = null;
children: Node[] = [];
label: string;
constructor(label: string) {
this.label = label;
}
}
function graph(prefix: string, width: number): Node {
const root = new Node(`${prefix}root`);
let last: Node | null = null;
for (let i = 0; i < width; i++) {
const child = new Node(`${prefix}${i}`);
child.parent = root;
if (last !== null) last.next = child;
last = child;
root.children.push(child);
}
if (last !== null) last.next = root.children[0]!;
return root;
}
function churn(rounds: number): number {
let acc = 0;
for (let i = 0; i < rounds; i++) {
const a = new Node(`a${i}`);
const b = new Node(`b${i}`);
a.next = b;
b.next = a;
acc = (acc + a.next.label.length + b.next.label.length) % 1000003;
}
return acc;
}
function describe(root: Node): string {
let out = root.label;
for (const c of root.children) out += ` ${c.label}^${c.parent?.label}>${c.next?.label}`;
return out;
}
if (isMainThread) {
const replies: string[] = [];
let pending = 3;
for (let w = 0; w < 3; w++) {
const shared = publish(graph(`m${w}.`, 3));
const worker = new Worker(new URL(import.meta.url), { workerData: { id: w, shared } });
worker.on("message", (message: unknown) => {
const received = message as { id: number; graph: Node; churned: number };
publish(received.graph);
replies[w] = `worker ${received.id}: ${describe(received.graph)} churn=${received.churned}`;
});
worker.on("exit", () => {
if (--pending === 0) {
for (const line of replies) console.log(line);
console.log(`main churn ${churn(20000)}`);
}
});
}
console.log(`main churn ${churn(20000)}`);
} else {
const { id, shared } = workerData as { id: number; shared: Node };
publish(shared);
const churned = churn(30000);
const fresh = graph(`w${id}.`, 4);
fresh.children[1]!.children.push(new Node(`w${id}.leaf`));
publish(fresh);
churn(10000);
parentPort!.postMessage({ id, graph: fresh, churned: (churned + describe(shared).length) % 1000003 });
}