mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-22 05:59:31 +08:00
Add an experimental young-in nursery for compiled classes and records (#1319)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5981-young-nursery-cycles.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -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" },
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
@@ -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 });
|
||||
}
|
||||
Reference in New Issue
Block a user