mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-03 07:54:40 +08:00
Merge remote-tracking branch 'origin/main' into ni/1-libnode-loader
This commit is contained in:
@@ -370,7 +370,7 @@ export class Client extends DispatcherBase {
|
||||
localAddress: this.__localAddress,
|
||||
};
|
||||
if (channels.beforeConnect.hasSubscribers) {
|
||||
channels.beforeConnect.publish({ connectParams });
|
||||
channels.beforeConnect.publish({ connectParams, connector: this.__connector });
|
||||
}
|
||||
let settled = false;
|
||||
const onConnected = (err: Error | null, socket: Socket | null): void => {
|
||||
@@ -428,7 +428,7 @@ export class Client extends DispatcherBase {
|
||||
if (this.__socket === socket) this.__onSocketClose(socket);
|
||||
});
|
||||
if (channels.connected.hasSubscribers) {
|
||||
channels.connected.publish({ connectParams });
|
||||
channels.connected.publish({ connectParams, connector: this.__connector, socket });
|
||||
}
|
||||
this.emit("connect", this.__url, [this as Dispatcher]);
|
||||
this.__resume(false);
|
||||
@@ -447,7 +447,7 @@ export class Client extends DispatcherBase {
|
||||
): void {
|
||||
this.__connecting = false;
|
||||
if (channels.connectError.hasSubscribers) {
|
||||
channels.connectError.publish({ connectParams, error: err });
|
||||
channels.connectError.publish({ connectParams, connector: this.__connector, error: err });
|
||||
}
|
||||
this.__onError(err);
|
||||
this.emit("connectionError", this.__url, [this as Dispatcher], err);
|
||||
@@ -658,7 +658,7 @@ export class Client extends DispatcherBase {
|
||||
}
|
||||
header += request.headers;
|
||||
if (channels.sendHeaders.hasSubscribers) {
|
||||
channels.sendHeaders.publish({ request: request.diagnosticView(), headers: header });
|
||||
channels.sendHeaders.publish({ request: request.diagnosticView(), headers: header, socket });
|
||||
}
|
||||
if (body === null) {
|
||||
if (contentLength === 0) socket.write(`${header}content-length: 0\r\n\r\n`);
|
||||
|
||||
@@ -244,9 +244,12 @@ export function cycleAppendHelper(): string[] {
|
||||
}
|
||||
|
||||
/** A collector header to install: the object type's constant ScrCycType
|
||||
* descriptor (its trace and teardown; see cycTypeDescriptor). */
|
||||
* descriptor (its trace and teardown; see cycTypeDescriptor). `acyclic`:
|
||||
* the descriptor's trace is scr_cyc_no_children, and `buf` starts out
|
||||
* SCR_CYC_ACYCLIC (scr_cyc_alloc does the same on its path). */
|
||||
export interface CycHeader {
|
||||
type: string;
|
||||
acyclic?: boolean;
|
||||
}
|
||||
|
||||
/** The first collector statistics index of an emitted ScrCycType
|
||||
@@ -391,7 +394,9 @@ function allocLines(
|
||||
` %sa.cgp = getelementptr inbounds i8, ptr %sa.blk, i64 2`,
|
||||
` store i16 0, ptr %sa.cgp ; ScrCycHdr.color, gen`,
|
||||
` %sa.bufp = getelementptr inbounds i8, ptr %sa.blk, i64 ${CYC_BUF}`,
|
||||
` store i32 0, ptr %sa.bufp ; ScrCycHdr.buf`,
|
||||
cyc.acyclic === true
|
||||
? ` store i32 -1, ptr %sa.bufp ; ScrCycHdr.buf = SCR_CYC_ACYCLIC`
|
||||
: ` store i32 0, ptr %sa.bufp ; ScrCycHdr.buf`,
|
||||
` %sa.live = load i64, ptr @scr_cyc_live`,
|
||||
` %sa.live1 = add i64 %sa.live, 1`,
|
||||
` store i64 %sa.live1, ptr @scr_cyc_live`,
|
||||
|
||||
@@ -157,6 +157,44 @@ test("ProgramLifetime hierarchies are never freed and take no reference counts",
|
||||
expect(body(mangleClassRelease("Owner"))).toContain("load i32, ptr %bufp");
|
||||
});
|
||||
|
||||
test("classes whose trace visits nothing start acyclic in a traced hierarchy", () => {
|
||||
const mapper = { kind: "object" as const, className: "Mapper" };
|
||||
const mod: IrModule = {
|
||||
irVersion: 17,
|
||||
sourceFile: loc.file,
|
||||
entry: "main",
|
||||
functions: [{ name: "main", params: [], returnType: VOID, locals: [], body: [], loc }],
|
||||
classes: [
|
||||
{ name: "Mapper", fields: [], methods: [], loc },
|
||||
// A dyn field can reach anything: the hierarchy carries collector headers.
|
||||
{
|
||||
name: "Linked",
|
||||
base: "Mapper",
|
||||
fields: [
|
||||
{ name: "next", type: mapper },
|
||||
{ name: "hook", type: DYN },
|
||||
],
|
||||
methods: [],
|
||||
loc,
|
||||
},
|
||||
{ name: "Simple", base: "Mapper", fields: [{ name: "value", type: F64 }], methods: [], loc },
|
||||
],
|
||||
};
|
||||
const llvm = emitLlvmModule(mod, { pointerBits: 64 });
|
||||
const body = (name: string): string =>
|
||||
new RegExp(`^define internal [a-z]+ @${name}(?:\\.i32)?\\([^]*?^}`, "m").exec(llvm)?.[0] ?? "";
|
||||
const descriptor = (cls: string): string =>
|
||||
llvm.split("\n").find((line) => line.startsWith(`@${mangleClassTrace(cls)}.cyctype = `)) ?? "";
|
||||
expect(descriptor("Simple")).toContain("{ ptr @scr_cyc_no_children, ");
|
||||
expect(body(mangleClassNew("Simple"))).toContain("store i32 -1, ptr %sa.bufp");
|
||||
expect(descriptor("Linked")).toContain(`{ ptr @${mangleClassTrace("Linked")}, `);
|
||||
expect(body(mangleClassNew("Linked"))).toContain("store i32 0, ptr %sa.bufp");
|
||||
// Releases share the hierarchy's possible-root step, which skips the
|
||||
// call for SCR_CYC_ACYCLIC, and a death unbuffers only below -1.
|
||||
expect(body(mangleClassRelease("Simple"))).toContain("icmp slt i32 %buf, 0");
|
||||
expect(body(mangleClassReleaseDirect("Simple"))).toContain("icmp slt i32 %dbuf, -1");
|
||||
});
|
||||
|
||||
test("ProgramLifetime classes pack their fields after a 32-bit header", () => {
|
||||
const node = { kind: "object" as const, className: "Node" };
|
||||
const leaf = { kind: "object" as const, className: "Leaf" };
|
||||
|
||||
@@ -906,6 +906,15 @@ export function emitClassShapes(
|
||||
})),
|
||||
];
|
||||
const refFields = indexedFields.filter((f) => isRefCounted(f.type));
|
||||
const tracedFields = traced
|
||||
? indexedFields.filter((f) => traceAdapter(host, f.type) !== null)
|
||||
: [];
|
||||
// A traced class whose trace visits nothing carries the collector header
|
||||
// only because its hierarchy is traced: it is never on a cycle. Its
|
||||
// descriptor names scr_cyc_no_children and its objects start out
|
||||
// SCR_CYC_ACYCLIC, so releases never buffer them.
|
||||
const acyclic =
|
||||
traced && runtimePrefix === null && !isStreamRooted && tracedFields.length === 0;
|
||||
const bounded =
|
||||
runtimePrefix !== null ||
|
||||
isStreamRooted ||
|
||||
@@ -1089,11 +1098,12 @@ export function emitClassShapes(
|
||||
`entry:`,
|
||||
];
|
||||
const cycType = `@${mangleClassTrace(cls.name)}.cyctype`;
|
||||
if (acyclic) host.declare(`declare void @scr_cyc_no_children(ptr, ptr, ptr)`);
|
||||
if (traced)
|
||||
defs.push(
|
||||
cycTypeDescriptor(
|
||||
cycType,
|
||||
`@${mangleClassTrace(cls.name)}`,
|
||||
acyclic ? "@scr_cyc_no_children" : `@${mangleClassTrace(cls.name)}`,
|
||||
`@${mangleClassGcFree(cls.name)}`,
|
||||
host.nextCycKind?.() ?? 0,
|
||||
),
|
||||
@@ -1113,7 +1123,7 @@ export function emitClassShapes(
|
||||
...emitObjectAlloc(
|
||||
host,
|
||||
sizeOf,
|
||||
traced && !lifetime ? { type: cycType } : null,
|
||||
traced && !lifetime ? { type: cycType, acyclic } : null,
|
||||
storedHeaderBytes(meta),
|
||||
),
|
||||
);
|
||||
@@ -1188,7 +1198,6 @@ export function emitClassShapes(
|
||||
// exactly the complement, then free (the trace/teardown complement
|
||||
// contract in scr_runtime.h).
|
||||
const indexed = indexedFields;
|
||||
const tracedFields = indexed.filter((f) => traceAdapter(host, f.type) !== null);
|
||||
const untracedRefFields = indexed.filter(
|
||||
(f) => isRefCounted(f.type) && traceAdapter(host, f.type) === null,
|
||||
);
|
||||
|
||||
@@ -1117,8 +1117,11 @@ export function emitAsyncAwaitCall(
|
||||
``,
|
||||
];
|
||||
}
|
||||
emitter.declare(`declare void @scr_async_inline_enter(ptr)`);
|
||||
emitter.declare(`declare void @scr_async_inline_exit(ptr)`);
|
||||
const framed = emitter.fiberlessFrames;
|
||||
if (framed) {
|
||||
emitter.declare(`declare void @scr_async_inline_enter(ptr)`);
|
||||
emitter.declare(`declare void @scr_async_inline_exit(ptr)`);
|
||||
}
|
||||
emitter.declare(`declare void @scr_await_hop()`);
|
||||
emitter.declare(`declare ptr @scr_promise_new()`);
|
||||
emitter.declare(`declare void @scr_promise_reject_pending(ptr)`);
|
||||
@@ -1127,11 +1130,12 @@ export function emitAsyncAwaitCall(
|
||||
const out = [
|
||||
head,
|
||||
`entry:`,
|
||||
` %frame = alloca { ptr, ptr }`,
|
||||
` call void @scr_async_inline_enter(ptr %frame)`,
|
||||
...(framed
|
||||
? [` %frame = alloca { ptr, ptr }`, ` call void @scr_async_inline_enter(ptr %frame)`]
|
||||
: []),
|
||||
retTy === "void" ? ` ${bodyCall}` : ` %r = ${bodyCall}`,
|
||||
...emitter.pendingTestLines("%pend"),
|
||||
` call void @scr_async_inline_exit(ptr %frame)`,
|
||||
...(framed ? [` call void @scr_async_inline_exit(ptr %frame)`] : []),
|
||||
// Adopting: the then job's turn comes before the settled promise's
|
||||
// own reaction, whichever way the body completed.
|
||||
...(adopt ? [` call void @scr_await_hop()`] : []),
|
||||
|
||||
@@ -1798,16 +1798,33 @@ export class LlEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
/** Whether a fiberless body running on its caller's stack needs the
|
||||
* frame scr_async_inline_enter/exit keep: it isolates the Error.stack
|
||||
* frame chain, which only stack-observing programs push, and the
|
||||
* AsyncLocalStorage context, which only programs that enter stores
|
||||
* change (RuntimeFeatures asyncContext), as a fresh fiber would. Other
|
||||
* programs run the body with a plain call. The frame also counts the
|
||||
* bodies running on a fiber's stack for the runtime's trap on a fiberless
|
||||
* body that suspends. */
|
||||
get fiberlessFrames(): boolean {
|
||||
return this.stackTraces || this.schedulesInContext;
|
||||
}
|
||||
|
||||
/** The spawn wrapper of a fiberless async function: runs the body on the
|
||||
* caller's stack between scr_async_inline_enter/leave and settles the
|
||||
* fresh promise from its outcome — the fiber trampoline's epilogue,
|
||||
* without the fiber. */
|
||||
* caller's stack (between scr_async_inline_enter/leave when
|
||||
* fiberlessFrames) and settles a fresh promise from its outcome — the
|
||||
* fiber trampoline's epilogue, without the fiber. */
|
||||
emitFiberlessAsyncSpawn(fn: IrFunction, spawnParams: string[]): string[] {
|
||||
const ret = fn.returnType;
|
||||
const retTy = this.llType(ret);
|
||||
const lifted = fn.captures !== undefined;
|
||||
this.declare(`declare void @scr_async_inline_enter(ptr)`);
|
||||
this.declare(`declare ptr @scr_async_inline_leave(ptr)`);
|
||||
const framed = this.fiberlessFrames;
|
||||
if (framed) {
|
||||
this.declare(`declare void @scr_async_inline_enter(ptr)`);
|
||||
this.declare(`declare ptr @scr_async_inline_leave(ptr)`);
|
||||
} else {
|
||||
this.declare(`declare ptr @scr_promise_new()`);
|
||||
}
|
||||
this.declare(`declare void @scr_promise_reject_pending(ptr)`);
|
||||
if (lifted) {
|
||||
this.declare(`declare ptr @scr_closure_retain_v(ptr)`);
|
||||
@@ -1817,15 +1834,17 @@ export class LlEmitter {
|
||||
const out = [
|
||||
`define internal ptr @${mangleAsyncSpawn(fn.name)}(${spawnParams.join(", ")}) ${FN_ATTRS} { ; fiberless ${fn.name}`,
|
||||
`entry:`,
|
||||
` %frame = alloca { ptr, ptr }`,
|
||||
...(framed ? [` %frame = alloca { ptr, ptr }`] : []),
|
||||
// The fiber path holds +1 on a lifted environment for the body's
|
||||
// duration (packed, released by the trampoline); keep that lifetime.
|
||||
...(lifted ? [` %env = call ptr @scr_closure_retain_v(ptr %a0)`] : []),
|
||||
` call void @scr_async_inline_enter(ptr %frame)`,
|
||||
...(framed ? [` call void @scr_async_inline_enter(ptr %frame)`] : []),
|
||||
retTy === "void" ? ` ${bodyCall}` : ` %r = ${bodyCall}`,
|
||||
...(lifted ? [` call void @scr_closure_release(ptr %a0)`] : []),
|
||||
...this.pendingTestLines("%pend"),
|
||||
` %p = call ptr @scr_async_inline_leave(ptr %frame)`,
|
||||
framed
|
||||
? ` %p = call ptr @scr_async_inline_leave(ptr %frame)`
|
||||
: ` %p = call ptr @scr_promise_new()`,
|
||||
` br i1 %pend, label %thrown, label %clean`,
|
||||
`clean:`,
|
||||
...this.asyncFulfillLines(ret, "%p"),
|
||||
@@ -1864,23 +1883,27 @@ export class LlEmitter {
|
||||
}
|
||||
|
||||
/** The inline await entry of a fiberless async function (`sc_ai_*`): its
|
||||
* body on the caller's stack between scr_async_inline_enter/exit, as in
|
||||
* its spawn wrapper, returning the result and leaving a throw pending for
|
||||
* the awaiting coroutine (emitCoroInlineAwait). */
|
||||
* body on the caller's stack (between scr_async_inline_enter/exit when
|
||||
* fiberlessFrames), as in its spawn wrapper, returning the result and
|
||||
* leaving a throw pending for the awaiting coroutine (emitCoroInlineAwait). */
|
||||
emitAsyncInlineAwaitCall(fn: IrFunction): string[] {
|
||||
const retTy = this.llType(fn.returnType);
|
||||
const params = fn.params.map((p, i) => `${this.llType(p.type)} %a${i}`);
|
||||
const args = params.join(", ");
|
||||
this.declare(`declare void @scr_async_inline_enter(ptr)`);
|
||||
this.declare(`declare void @scr_async_inline_exit(ptr)`);
|
||||
const framed = this.fiberlessFrames;
|
||||
if (framed) {
|
||||
this.declare(`declare void @scr_async_inline_enter(ptr)`);
|
||||
this.declare(`declare void @scr_async_inline_exit(ptr)`);
|
||||
}
|
||||
const bodyCall = `call ${retTy} @${mangleFunction(fn.name)}(${args})`;
|
||||
return [
|
||||
`define internal ${retTy} @${mangleAsyncInlineAwaitCall(fn.name)}(${args}) ${FN_ATTRS} { ; inline await ${fn.name}`,
|
||||
`entry:`,
|
||||
` %frame = alloca { ptr, ptr }`,
|
||||
` call void @scr_async_inline_enter(ptr %frame)`,
|
||||
...(framed
|
||||
? [` %frame = alloca { ptr, ptr }`, ` call void @scr_async_inline_enter(ptr %frame)`]
|
||||
: []),
|
||||
retTy === "void" ? ` ${bodyCall}` : ` %r = ${bodyCall}`,
|
||||
` call void @scr_async_inline_exit(ptr %frame)`,
|
||||
...(framed ? [` call void @scr_async_inline_exit(ptr %frame)`] : []),
|
||||
retTy === "void" ? ` ret void` : ` ret ${retTy} %r`,
|
||||
`}`,
|
||||
``,
|
||||
|
||||
@@ -60,6 +60,9 @@ test("publication walkers mark and push inline, with the runtime for the rest",
|
||||
expect(fn).toContain("icmp ult i64 %mrc, 4294967294");
|
||||
expect(fn).toContain("getelementptr inbounds { ptr, i32, i32 }, ptr %mlog, i64 %mlogged");
|
||||
expect(fn).toContain("store i64 -1, ptr %o");
|
||||
// A headered object leaves its candidate buffer only when `buf` is below
|
||||
// -1: SCR_CYC_ACYCLIC (-1) marks an object that is never in one.
|
||||
expect(fn).toContain("icmp sge i32 %mbufw, -1");
|
||||
// Anything else goes through scr_pub_mark.
|
||||
expect(fn).toMatch(/call zeroext i1 @scr_pub_mark\(ptr %ctx, ptr %o, i1 (true|false), i32 4\)/);
|
||||
// A string is a leaf, marked at once; other references go on the stack,
|
||||
|
||||
@@ -434,12 +434,13 @@ export class PublishWalkers {
|
||||
let buf = classId || compact ? String(PUB_LOG_COUNT) : "0";
|
||||
if (cyc) {
|
||||
// ScrCycHdr.buf, the word before the object: its top bit while the
|
||||
// object is a buffered candidate.
|
||||
// object is a buffered candidate, and -1 (SCR_CYC_ACYCLIC) for an
|
||||
// object that never is one.
|
||||
this.host.declare(`declare i32 @scr_cyc_pub_take(ptr)`);
|
||||
lines.push(
|
||||
` %mbufp = getelementptr inbounds i8, ptr %o, i64 -4`,
|
||||
` %mbufw = load i32, ptr %mbufp`,
|
||||
` %mfree = icmp sge i32 %mbufw, 0`,
|
||||
` %mfree = icmp sge i32 %mbufw, -1`,
|
||||
` br i1 %mfree, label %mput, label %mtake`,
|
||||
`mtake:`,
|
||||
` %mix = call i32 @scr_cyc_pub_take(ptr %o)`,
|
||||
|
||||
@@ -1369,7 +1369,7 @@ export function directChildReleaseLines(
|
||||
lines.push(
|
||||
` %${tag}.bufp = getelementptr i8, ptr ${value}, ${S} -${host.cycleBufOffset}`,
|
||||
` %${tag}.buf = load i32, ptr %${tag}.bufp`,
|
||||
` %${tag}.queued = icmp slt i32 %${tag}.buf, 0`,
|
||||
` %${tag}.queued = icmp slt i32 %${tag}.buf, -1 ; buffered, not SCR_CYC_ACYCLIC`,
|
||||
` br i1 %${tag}.queued, label %${tag}.unbuffer, label %${tag}.teardown`,
|
||||
`${tag}.unbuffer:`,
|
||||
` call void @scr_cyc_on_dead(ptr ${value})`,
|
||||
@@ -1595,8 +1595,9 @@ function requestCycleAppend(host: ShapeHost): boolean {
|
||||
|
||||
/** The possible-cycle-root step of a release that left a headered object
|
||||
* alive: scr_cyc_on_release with its early return inlined. Test the sign
|
||||
* of `buf` (the i32 at obj-4; negative for a queued candidate or a tenured
|
||||
* object), and buffer a new candidate: through the module's
|
||||
* of `buf` (the i32 at obj-4; negative for a queued candidate, a young
|
||||
* object, or an object that is never a candidate, SCR_CYC_ACYCLIC), and
|
||||
* buffer a new candidate: through the module's
|
||||
* cycleAppendHelper in optimized builds that support it, through
|
||||
* scr_cyc_on_release otherwise. Every emitted release of a headered object
|
||||
* shares this sequence (scr_runtime.h asserts the offsets); it ends by
|
||||
@@ -1619,16 +1620,16 @@ export function cycleRootLines(host: ShapeHost, done: string): string[] {
|
||||
}
|
||||
|
||||
/** The death of a headered object: scr_cyc_on_dead with its early return
|
||||
* inlined. Only an object still in a candidate buffer
|
||||
* (`buffered` nonzero) has an entry to remove; the sequence continues at
|
||||
* the new block `next`. */
|
||||
* inlined. Only an object still in a candidate buffer (`buf` negative and
|
||||
* not SCR_CYC_ACYCLIC, which is -1) has an entry to remove; the sequence
|
||||
* continues at the new block `next`. */
|
||||
export function cycleDeadLines(host: ShapeHost, next: string): string[] {
|
||||
const S = host.sizeType;
|
||||
host.declare(`declare void @scr_cyc_on_dead(ptr)`);
|
||||
return [
|
||||
` %dbufp = getelementptr i8, ptr %o, ${S} -${host.cycleBufOffset}`,
|
||||
` %dbuf = load i32, ptr %dbufp`,
|
||||
` %dqueued = icmp slt i32 %dbuf, 0`,
|
||||
` %dqueued = icmp slt i32 %dbuf, -1 ; buffered, not SCR_CYC_ACYCLIC`,
|
||||
` br i1 %dqueued, label %unbuffer, label %${next}`,
|
||||
`unbuffer:`,
|
||||
` call void @scr_cyc_on_dead(ptr %o)`,
|
||||
|
||||
@@ -407,9 +407,27 @@ function withTypedReceiver(value: IrExpr, receiver: IrExpr): IrExpr {
|
||||
});
|
||||
}
|
||||
|
||||
/** Re-adds the entries of `set` after the first `from` sorted. */
|
||||
function sortAdded(set: Set<string>, from: number): void {
|
||||
const added = [...set].slice(from).sort();
|
||||
/** Re-adds the classes of `set` after the first `from` in a fixed order:
|
||||
* the program's classes by name, then the classes the runtime implements by
|
||||
* name. The dispatch helpers add each class's branch in front of the
|
||||
* earlier ones, so a runtime class's branch, which answers its methods for
|
||||
* instances of the program's subclasses too, is tested before the
|
||||
* subclasses' own branches. */
|
||||
function sortAdded(lowerer: Lowerer, set: Set<string>, from: number): void {
|
||||
const runtime = (name: string): boolean => {
|
||||
const info = lowerer.classes.get(name);
|
||||
return !!(
|
||||
info &&
|
||||
(info.def.runtime || info.builtinEmitter || info.builtinStream || info.builtinError)
|
||||
);
|
||||
};
|
||||
const added = [...set]
|
||||
.slice(from)
|
||||
.sort(
|
||||
(left, right) =>
|
||||
Number(runtime(left)) - Number(runtime(right)) ||
|
||||
(left < right ? -1 : left > right ? 1 : 0),
|
||||
);
|
||||
for (const name of added) set.delete(name);
|
||||
for (const name of added) set.add(name);
|
||||
}
|
||||
@@ -932,10 +950,10 @@ export class ClassDynamicDispatch {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
// The classes this pass found join the tables by name, not in the order
|
||||
// the scan met them.
|
||||
sortAdded(this.boxed, boxedBefore);
|
||||
sortAdded(this.boxedConstructors, boxedConstructorsBefore);
|
||||
// The classes this pass found join the tables in a fixed order, not in
|
||||
// the order the scan met them.
|
||||
sortAdded(lowerer, this.boxed, boxedBefore);
|
||||
sortAdded(lowerer, this.boxedConstructors, boxedConstructorsBefore);
|
||||
changed = this.caughtDispatch.dynamicPayloads(lowerer, caughtSites, this.boxed) || changed;
|
||||
if (this.boxed.size === 0 && rewrite.size === 0) return changed;
|
||||
const converting = this.convertingHints(lowerer);
|
||||
|
||||
@@ -36,7 +36,8 @@ import {
|
||||
import { identityPreservingWidening } from "./coercions/identity.js";
|
||||
import { scanDivergences } from "./divergences.js";
|
||||
import { applyKeyOrderWitnesses, KeyOrderNotes } from "./key-order-witness.js";
|
||||
import { observeRecordLiteralRebuilds } from "../../ir/traverse.js";
|
||||
import { exprFunctionReference } from "../../ir/function-references.js";
|
||||
import { everyStmtList, observeRecordLiteralRebuilds } from "../../ir/traverse.js";
|
||||
import { checkedClassAssertion } from "./class-assertions.js";
|
||||
import { narrowStoredClassValue, narrowGenericClassValue } from "./class-unions.js";
|
||||
import {
|
||||
@@ -3269,11 +3270,13 @@ export class Lowerer {
|
||||
if (typeof helper === "string") this.sharedHelperNames.add(helper);
|
||||
else this.sharedHelpers.add(helper);
|
||||
}
|
||||
/** Gives each shared helper the location of the entry file's start: the
|
||||
* helpers marked by shareHelper, and those interned by key in the helper
|
||||
* tables. */
|
||||
private settleSharedHelperLocations(): void {
|
||||
const loc: SrcLoc = { file: this.entry.fileName, start: 0, end: 0 };
|
||||
/** Gives each shared helper (those marked by shareHelper, and those
|
||||
* interned by key in the helper tables) one location for all its nodes:
|
||||
* the earliest site in `functions` that uses it, by file and position, so
|
||||
* reports such as --explain-dynamic point at a use whatever order bodies
|
||||
* lowered in. A helper used only by other shared helpers takes their
|
||||
* location; one with no use takes the entry file's start. */
|
||||
private settleSharedHelperLocations(functions: readonly IrFunction[]): void {
|
||||
const interned = new Set<string>([...this.sharedHelperNames, ...this.classMethodValueHelpers]);
|
||||
for (const table of [
|
||||
this.equalityHelpers,
|
||||
@@ -3296,21 +3299,68 @@ export class Lowerer {
|
||||
for (const name of table.values()) interned.add(name);
|
||||
for (const fn of [...this.liftedFns, ...this.implicitFns])
|
||||
if (interned.has(fn.name)) this.sharedHelpers.add(fn);
|
||||
const seen = new Set<object>();
|
||||
const visit = (node: unknown): void => {
|
||||
if (typeof node !== "object" || node === null || seen.has(node)) return;
|
||||
seen.add(node);
|
||||
if (Array.isArray(node)) {
|
||||
for (const item of node) visit(item);
|
||||
return;
|
||||
if (this.sharedHelpers.size === 0) return;
|
||||
const shared = new Map([...this.sharedHelpers].map((fn) => [fn.name, fn]));
|
||||
const earlier = (left: SrcLoc, right: SrcLoc): boolean =>
|
||||
left.file < right.file || (left.file === right.file && left.start < right.start);
|
||||
// The earliest use of each shared helper outside the shared helpers, and
|
||||
// which shared helpers each shared helper uses.
|
||||
const uses = new Map<string, SrcLoc>();
|
||||
const usedBy = new Map<string, Set<string>>();
|
||||
for (const fn of functions) {
|
||||
const inShared = shared.has(fn.name) ? fn.name : undefined;
|
||||
everyStmtList(fn.body, {
|
||||
stmt: () => true,
|
||||
expr: (expr) => {
|
||||
const name = expr.kind === "call" ? expr.callee : exprFunctionReference(expr);
|
||||
if (name === undefined || name === inShared || !shared.has(name)) return true;
|
||||
if (inShared !== undefined) {
|
||||
let users = usedBy.get(name);
|
||||
if (users === undefined) usedBy.set(name, (users = new Set()));
|
||||
users.add(inShared);
|
||||
} else {
|
||||
const current = uses.get(name);
|
||||
if (current === undefined || earlier(expr.loc, current)) uses.set(name, expr.loc);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}
|
||||
// Helpers reached only through other shared helpers take the earliest
|
||||
// location among those, to a fixed point.
|
||||
for (let changed = true; changed;) {
|
||||
changed = false;
|
||||
for (const [name, users] of usedBy) {
|
||||
for (const user of users) {
|
||||
const loc = uses.get(user);
|
||||
const current = uses.get(name);
|
||||
if (loc !== undefined && (current === undefined || earlier(loc, current))) {
|
||||
uses.set(name, loc);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
const record = node as Record<string, unknown>;
|
||||
for (const key of Object.keys(record)) {
|
||||
if (key === "loc") record[key] = loc;
|
||||
else if (key !== "type") visit(record[key]);
|
||||
}
|
||||
};
|
||||
for (const fn of this.sharedHelpers) visit(fn);
|
||||
}
|
||||
const fallback: SrcLoc = { file: this.entry.fileName, start: 0, end: 0 };
|
||||
for (const fn of this.sharedHelpers) {
|
||||
const use = uses.get(fn.name) ?? fallback;
|
||||
const loc: SrcLoc = { file: use.file, start: use.start, end: use.end };
|
||||
const seen = new Set<object>();
|
||||
const visit = (node: unknown): void => {
|
||||
if (typeof node !== "object" || node === null || seen.has(node)) return;
|
||||
seen.add(node);
|
||||
if (Array.isArray(node)) {
|
||||
for (const item of node) visit(item);
|
||||
return;
|
||||
}
|
||||
const record = node as Record<string, unknown>;
|
||||
for (const key of Object.keys(record)) {
|
||||
if (key === "loc") record[key] = loc;
|
||||
else if (key !== "type") visit(record[key]);
|
||||
}
|
||||
};
|
||||
visit(fn);
|
||||
}
|
||||
}
|
||||
/** The first lifted function named `name`, the one
|
||||
* `liftedFns.find((fn) => fn.name === name)` returns. */
|
||||
@@ -7404,7 +7454,7 @@ export class Lowerer {
|
||||
// Lambdas lifted while lowering any of the above (plus synthetic
|
||||
// array-HOF loop functions, which ride the same list), and the
|
||||
// implicit-any instances lowered eagerly at their first call sites.
|
||||
this.settleSharedHelperLocations();
|
||||
this.settleSharedHelperLocations([...functions, ...this.liftedFns, ...this.implicitFns]);
|
||||
functions.push(...this.liftedFns);
|
||||
functions.push(...this.implicitFns);
|
||||
functions.push(...runtimeClassConstructs(this, functions));
|
||||
@@ -8120,7 +8170,13 @@ export class Lowerer {
|
||||
]);
|
||||
observeRecordLiteralRebuilds(null);
|
||||
for (const finalize of this.shapeOrderHelperFinalizers) finalize();
|
||||
this.settleSharedHelperLocations();
|
||||
this.settleSharedHelperLocations([
|
||||
...orderedUnits,
|
||||
...initFunctions,
|
||||
...instanceFunctions,
|
||||
...this.liftedFns,
|
||||
...this.implicitFns,
|
||||
]);
|
||||
// Lifted and implicit functions are created in lowering order; they
|
||||
// are assembled by name.
|
||||
const functions = [
|
||||
|
||||
@@ -4354,7 +4354,9 @@ function canBoxDynCompositeAt(
|
||||
}
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
// Runtime handles (a socket in a diagnostics message) box by
|
||||
// reference wherever the walker meets them, as at the top level.
|
||||
return DYN_HANDLE_KINDS.has(t.kind);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,9 @@ test("awaiting a direct call to a body that never suspends skips the promise", a
|
||||
expect(definition(llvm, "sc_f_middle")).toContain("call double @sc_aw_leaf(double");
|
||||
const entry = definition(llvm, "sc_aw_leaf");
|
||||
expect(entry).toContain("call double @sc_f_leaf(double %a0)");
|
||||
expect(entry).toContain("call void @scr_async_inline_exit(ptr %frame)");
|
||||
// Nothing here enters an AsyncLocalStorage store or reads a stack, so the
|
||||
// body runs with a plain call (fiberlessFrames).
|
||||
expect(entry).not.toContain("@scr_async_inline_enter");
|
||||
// The clean result takes the await's microtask turn and returns directly;
|
||||
// only the throw path builds the rejected promise.
|
||||
const clean = entry.slice(entry.indexOf("clean:"), entry.indexOf("thrown:"));
|
||||
@@ -122,10 +124,10 @@ test("returning a direct call adopts its promise one microtask turn later", asyn
|
||||
// A body that never suspends: the adoption's then-job turn, then the
|
||||
// settled promise's own turn, still without materializing the promise.
|
||||
const entry = definition(llvm, "sc_ad_leaf");
|
||||
const exit = entry.indexOf("call void @scr_async_inline_exit(ptr %frame)");
|
||||
const body = entry.indexOf("call double @sc_f_leaf(double %a0)");
|
||||
const hop = entry.indexOf("call void @scr_await_hop()");
|
||||
expect(exit).toBeGreaterThan(0);
|
||||
expect(hop).toBeGreaterThan(exit);
|
||||
expect(body).toBeGreaterThan(0);
|
||||
expect(hop).toBeGreaterThan(body);
|
||||
expect(hop).toBeLessThan(entry.indexOf("clean:"));
|
||||
const clean = entry.slice(entry.indexOf("clean:"), entry.indexOf("thrown:"));
|
||||
expect(clean).toContain("call void @scr_await_hop()");
|
||||
@@ -190,7 +192,7 @@ test("bodies that suspend only at their own awaits compile to stackless coroutin
|
||||
expect(middle).toContain("call double @sc_ai_leaf(double");
|
||||
const inline = definition(llvm, "sc_ai_leaf");
|
||||
expect(inline).toContain("call double @sc_f_leaf(double %a0)");
|
||||
expect(inline).toContain("call void @scr_async_inline_exit(ptr %frame)");
|
||||
expect(inline).not.toContain("@scr_async_inline_enter");
|
||||
expect(middle).toContain("call zeroext i1 @scr_coro_hop_prepare(ptr");
|
||||
expect(middle).toContain("call void @scr_promise_reject_pending(ptr");
|
||||
expect(middle).toContain("call void @scr_coro_async_finish(ptr");
|
||||
@@ -205,6 +207,58 @@ test("bodies that suspend only at their own awaits compile to stackless coroutin
|
||||
const entry = definition(llvm, "sc_aw_middle");
|
||||
expect(entry).toContain("call ptr @sc_as_middle(double %a0)");
|
||||
expect(entry).toContain("call double @scr_await_f64(ptr %p)");
|
||||
// A body that never suspends stays fiberless.
|
||||
expect(definition(llvm, "sc_as_leaf")).toContain("@scr_async_inline_enter(");
|
||||
// A body that never suspends stays fiberless: its spawn wrapper calls the
|
||||
// body and settles a fresh promise.
|
||||
const spawn = definition(llvm, "sc_as_leaf");
|
||||
expect(spawn).toContain("call double @sc_f_leaf(double %a0)");
|
||||
expect(spawn).toContain("call ptr @scr_promise_new()");
|
||||
expect(spawn).not.toContain("@scr_async_spawn");
|
||||
});
|
||||
|
||||
const framedSources = {
|
||||
// Entering a store makes the frame isolate the AsyncLocalStorage context.
|
||||
context: `
|
||||
import { AsyncLocalStorage } from "node:async_hooks";
|
||||
const store = new AsyncLocalStorage<string>();
|
||||
async function leaf(x: number): Promise<number> {
|
||||
store.enterWith("leaf");
|
||||
return x + 1;
|
||||
}`,
|
||||
// Reading a stack makes the frame isolate the Error.stack frame chain.
|
||||
stack: `
|
||||
async function leaf(x: number): Promise<number> {
|
||||
if (x < 0) console.log(new Error("negative").stack);
|
||||
return x + 1;
|
||||
}`,
|
||||
};
|
||||
|
||||
test("fiberless bodies keep their frame where it isolates a context or a stack", async () => {
|
||||
for (const [name, prefix] of Object.entries(framedSources)) {
|
||||
const llvm = await emit(`${prefix}
|
||||
async function slow(x: number): Promise<number> {
|
||||
await null;
|
||||
return x;
|
||||
}
|
||||
async function coroutine(x: number): Promise<number> {
|
||||
return (await slow(x)) + (await leaf(x));
|
||||
}
|
||||
async function* tick(): AsyncGenerator<number, void, undefined> {
|
||||
yield 0;
|
||||
}
|
||||
async function fibered(x: number): Promise<number> {
|
||||
for await (const t of tick()) x += t;
|
||||
return await leaf(x);
|
||||
}
|
||||
async function main(): Promise<void> {
|
||||
const pending = leaf(1);
|
||||
console.log((await coroutine(2)) + (await fibered(3)) + (await pending));
|
||||
}
|
||||
void main();
|
||||
`);
|
||||
for (const symbol of ["sc_ai_leaf", "sc_aw_leaf", "sc_as_leaf"]) {
|
||||
expect(definition(llvm, symbol), `${name} ${symbol}`).toContain(
|
||||
"call void @scr_async_inline_enter(ptr %frame)",
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6620-weak-keys-net-sockets.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7103-fiberless-plain-calls.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/acyclic-classes-in-traced-hierarchies.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -131,13 +131,6 @@ uint32_t scr_arr_ops_register(ScrArrOps *ops) {
|
||||
|
||||
const ScrArrOps *scr_arr_ops_overflow(const ScrArr *a) { return scr_arr_ext_of(a)->ops; }
|
||||
|
||||
/* Record `ops` as the element ops of `a` (NULL for none). */
|
||||
static void scr_arr_set_ops(ScrArr *a, const ScrArrOps *ops) {
|
||||
uint32_t id = scr_arr_ops_id(ops);
|
||||
a->spare = (uint16_t)((a->spare & ((1u << SCR_ARR_OPS_SHIFT) - 1)) | (id << SCR_ARR_OPS_SHIFT));
|
||||
if (id == SCR_ARR_OPS_OVERFLOW) scr_arr_ext(a)->ops = ops;
|
||||
}
|
||||
|
||||
/* Canonical array indices grow indexed storage and may leave holes. Negative,
|
||||
* fractional, NaN, infinity, and 2^32-1 numeric keys use the ordinary
|
||||
* property vector; only a missing property or a low-level read of a hole
|
||||
@@ -402,9 +395,10 @@ static void scr_arr_sparse_drop_prefix(ScrArr *a, size_t count) {
|
||||
#define SCR_ARR_MIN_GROWTH ((size_t)4)
|
||||
|
||||
/* Inline slots: an array built by scr_arr_new_small / scr_arr_new_ops_small
|
||||
* (empty literals the compiler sees appended to) keeps its first dense
|
||||
* slots and states in its own block, right after the ScrArr, so it costs
|
||||
* one allocation and its first elements share the header's cache lines.
|
||||
* (empty literals the compiler sees appended to), or constructed with a
|
||||
* known element count of up to SCR_ARR_KNOWN_INLINE_MAX, keeps its first
|
||||
* dense slots and states in its own block, right after the ScrArr, so it
|
||||
* costs one allocation and its first elements share the header's cache lines.
|
||||
* `spare` records it. Growth past the inline capacity moves the slots to an
|
||||
* ordinary allocation; the inline room then stays unused until the array is
|
||||
* freed. */
|
||||
@@ -526,6 +520,30 @@ static size_t scr_arr_small_cap(size_t prefix, bool bools) {
|
||||
#endif
|
||||
}
|
||||
|
||||
/* The most slots a construction with a known element count (array
|
||||
* literals, copies, slices and the arrays the runtime builds) keeps inline.
|
||||
* Larger counts, and blocks past the small-object allocator, keep separate
|
||||
* storage: an array that then grows leaves its inline room unused, which
|
||||
* this bound keeps small. */
|
||||
#define SCR_ARR_KNOWN_INLINE_MAX ((size_t)16)
|
||||
|
||||
/* Inline capacity of a construction of `need` elements with `prefix` bytes
|
||||
* of block before the slots, rounded up to fill the block; 0 when its slots
|
||||
* stay separate. */
|
||||
static size_t scr_arr_known_cap(size_t prefix, size_t need, bool bools) {
|
||||
if (need == 0 || need > SCR_ARR_KNOWN_INLINE_MAX) return 0;
|
||||
#if SCR_SMALL_ALLOC
|
||||
size_t unit = bools ? SCR_ARR_BOOL_SLOT_BYTES : SCR_ARR_SLOT_BYTES;
|
||||
size_t bytes = prefix + need * unit;
|
||||
if (bytes > SCR_SA_MAX) return 0;
|
||||
return (scr_sa_class_size(scr_sa_class(bytes)) - prefix) / unit;
|
||||
#else
|
||||
(void)prefix;
|
||||
(void)bools;
|
||||
return need;
|
||||
#endif
|
||||
}
|
||||
|
||||
static void scr_arr_use_inline(ScrArr *a, size_t cap) {
|
||||
a->data = (uint64_t *)((char *)a + SCR_ARR_INLINE_OFFSET);
|
||||
a->cap = (uint32_t)cap;
|
||||
@@ -1180,7 +1198,8 @@ static inline __attribute__((always_inline)) ScrArr *scr_arr_make(ScrElemKind el
|
||||
size_t initial_cap,
|
||||
bool small) {
|
||||
size_t unit = elem == SCR_ELEM_BOOL ? SCR_ARR_BOOL_SLOT_BYTES : SCR_ARR_SLOT_BYTES;
|
||||
size_t inline_cap = small ? scr_arr_small_cap(SCR_ARR_INLINE_OFFSET, elem == SCR_ELEM_BOOL) : 0;
|
||||
size_t inline_cap = small ? scr_arr_small_cap(SCR_ARR_INLINE_OFFSET, elem == SCR_ELEM_BOOL)
|
||||
: scr_arr_known_cap(SCR_ARR_INLINE_OFFSET, initial_cap, elem == SCR_ELEM_BOOL);
|
||||
ScrArr *a = scr_mem_alloc(inline_cap ? SCR_ARR_INLINE_OFFSET + inline_cap * unit : sizeof(ScrArr));
|
||||
if (!a) scr_arr_oom();
|
||||
a->rc = 1;
|
||||
@@ -1310,11 +1329,17 @@ static void scr_arr_gc_free(void *a0) {
|
||||
scr_cyc_free_inline(a);
|
||||
}
|
||||
|
||||
static ScrArr *scr_arr_make_ops(const ScrArrOps *ops, size_t initial_cap, bool small) {
|
||||
/* Inlined into each constructor like scr_arr_make: with `small` and the
|
||||
* count constant, scr_arr_new_ops_small's size class folds to a table
|
||||
* index. */
|
||||
static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops(const ScrArrOps *ops,
|
||||
size_t initial_cap,
|
||||
bool small) {
|
||||
ScrArr *a;
|
||||
bool cyc = ops->trace != NULL;
|
||||
size_t prefix = SCR_ARR_INLINE_OFFSET + (cyc ? sizeof(ScrCycHdr) : 0);
|
||||
size_t inline_cap =
|
||||
small ? scr_arr_small_cap(SCR_ARR_INLINE_OFFSET + (cyc ? sizeof(ScrCycHdr) : 0), false) : 0;
|
||||
small ? scr_arr_small_cap(prefix, false) : scr_arr_known_cap(prefix, initial_cap, false);
|
||||
size_t size = inline_cap ? SCR_ARR_INLINE_OFFSET + inline_cap * SCR_ARR_SLOT_BYTES : sizeof(ScrArr);
|
||||
if (cyc) {
|
||||
static ScrCycType cyc_type = { &scr_arr_trace_v, &scr_arr_gc_free, SCR_CYC_KIND_ARRAY };
|
||||
@@ -1328,12 +1353,19 @@ static ScrArr *scr_arr_make_ops(const ScrArrOps *ops, size_t initial_cap, bool s
|
||||
a->cap = 0;
|
||||
a->elem = SCR_ELEM_REF;
|
||||
a->cyc = cyc;
|
||||
a->spare = 0;
|
||||
/* The ops id is written with the other header fields: reading `spare`
|
||||
* back after scr_arr_use_inline sets its inline flag stalls the
|
||||
* store-to-load forward. */
|
||||
uint32_t id = scr_arr_ops_id(ops);
|
||||
a->spare = (uint16_t)(id << SCR_ARR_OPS_SHIFT);
|
||||
a->filled = 0;
|
||||
a->data = NULL;
|
||||
scr_arr_set_ops(a, ops);
|
||||
if (inline_cap) scr_arr_use_inline(a, inline_cap);
|
||||
else if (initial_cap > 0) scr_arr_grow_dense_to(a, initial_cap, SCR_ARR_GROW_EXACT);
|
||||
/* After the storage: ops past the registry live in the ext, whose
|
||||
* pointer precedes the dense allocation, so inline slots move out first
|
||||
* (scr_arr_ext). */
|
||||
if (id == SCR_ARR_OPS_OVERFLOW) scr_arr_ext(a)->ops = ops;
|
||||
#ifdef SCR_RC_AUDIT
|
||||
scr_live_arrays++;
|
||||
#endif
|
||||
|
||||
@@ -361,7 +361,15 @@ uint32_t scr_cyc_register(ScrCycType *type) {
|
||||
}
|
||||
|
||||
void *scr_cyc_alloc(size_t size, ScrCycType *type) {
|
||||
return scr_cyc_alloc_inline(size, type);
|
||||
void *obj = scr_cyc_alloc_inline(size, type);
|
||||
if (type->trace == scr_cyc_no_children) scr_cyc_hdr(obj)->buf = SCR_CYC_ACYCLIC;
|
||||
return obj;
|
||||
}
|
||||
|
||||
void scr_cyc_no_children(void *obj, ScrTraceVisit visit, void *ctx) {
|
||||
(void)obj;
|
||||
(void)visit;
|
||||
(void)ctx;
|
||||
}
|
||||
|
||||
void scr_cyc_free(void *obj) { scr_cyc_free_inline(obj); }
|
||||
@@ -1175,7 +1183,13 @@ void scr_cyc_on_release(void *obj) {
|
||||
ScrCycHdr *h = scr_cyc_hdr(obj);
|
||||
/* Releasing an existing candidate cannot advance the trigger. A pass
|
||||
* always re-arms after its teardowns, including any candidates they add. */
|
||||
if (scr_cyc_is_buffered(h)) return;
|
||||
if (scr_cyc_release_done(h)) return;
|
||||
/* An object without cycle-capable children whose SCR_CYC_ACYCLIC a pass
|
||||
* or the young-in nursery replaced gets it back. */
|
||||
if (scr_cyc_type(h)->trace == scr_cyc_no_children) {
|
||||
h->buf = SCR_CYC_ACYCLIC;
|
||||
return;
|
||||
}
|
||||
#if SCR_CYC_INLINE_BUFFERING
|
||||
if (scr_cyc_append(h, obj)) return;
|
||||
#endif
|
||||
@@ -1346,14 +1360,17 @@ static void scr_scan_black(void *obj, uintptr_t depth) {
|
||||
ScrCycHdr *h = scr_cyc_hdr(obj);
|
||||
scr_cyc_set_color(h, SCR_CYC_BLACK);
|
||||
/* Drop markGray's component number: a survivor leaves the pass unbuffered
|
||||
* unless it is a candidate that a limited pass did not take. */
|
||||
if (!scr_cyc_is_buffered(h)) scr_cyc_clear_buffered(h);
|
||||
* unless it is a candidate that a limited pass did not take, and one
|
||||
* without cycle-capable children leaves it SCR_CYC_ACYCLIC again. */
|
||||
const ScrCycType *type = scr_cyc_type(h);
|
||||
bool acyclic = type->trace == scr_cyc_no_children;
|
||||
if (!scr_cyc_is_buffered(h)) h->buf = acyclic ? SCR_CYC_ACYCLIC : 0;
|
||||
if (scr_cyc_gen(h) < SCR_CYC_OLD) scr_cyc_push_work(&scr_promote, obj);
|
||||
if (acyclic) return;
|
||||
if (depth == SCR_CYC_WALK_DEPTH) {
|
||||
scr_cyc_push_work(&scr_restore_pending, obj);
|
||||
return;
|
||||
}
|
||||
const ScrCycType *type = scr_cyc_type(h);
|
||||
SCR_CYC_CHILDREN(obj, type, scr_sb_array, scr_sb_visit, depth + 1);
|
||||
}
|
||||
static void scr_restore_from(void *root) {
|
||||
@@ -1881,7 +1898,8 @@ static SCR_CYC_COMPACT size_t scr_cyc_settle(unsigned gen_limit, bool doomed) {
|
||||
void *obj = scr_cands.v[i];
|
||||
ScrCycHdr *h = scr_cyc_hdr(obj);
|
||||
if (scr_cyc_color(h) == SCR_CYC_DOOMED) continue; /* being freed below */
|
||||
if (!scr_cyc_is_buffered(h)) {
|
||||
/* A candidate without cycle-capable children was never on a cycle. */
|
||||
if (!scr_cyc_is_buffered(h) && h->buf != SCR_CYC_ACYCLIC) {
|
||||
/* A survivor promoted into a level its type bypasses moves past
|
||||
* it, as a release would buffer it. */
|
||||
if (scr_cyc_gen(h) == SCR_CYC_MATURE && scr_cyc_bypassed(scr_cyc_type(h), SCR_CYC_MATURE))
|
||||
|
||||
@@ -14065,7 +14065,8 @@ static int scr_weak_key(const ScrDyn *key, void **ptr, unsigned *kind) {
|
||||
if (key->v.handle.tag != SCR_DYNH_ARRAY_BUFFER && key->v.handle.tag != SCR_DYNH_SHARED_ARRAY_BUFFER && key->v.handle.tag != SCR_DYNH_WEAK_MAP &&
|
||||
key->v.handle.tag != SCR_DYNH_WEAK_SET && key->v.handle.tag != SCR_DYNH_STDIO &&
|
||||
key->v.handle.tag != SCR_DYNH_FETCH_REQUEST && key->v.handle.tag != SCR_DYNH_FETCH_RESPONSE &&
|
||||
key->v.handle.tag != SCR_DYNH_MAP && key->v.handle.tag != SCR_DYNH_SET) return -1;
|
||||
key->v.handle.tag != SCR_DYNH_MAP && key->v.handle.tag != SCR_DYNH_SET &&
|
||||
key->v.handle.tag != SCR_DYNH_NET_SOCKET && key->v.handle.tag != SCR_DYNH_NET_SERVER) return -1;
|
||||
*ptr = key->v.handle.ptr; *kind = 4 + key->v.handle.tag; return 1;
|
||||
case SCR_DYN_TYPED_REF:
|
||||
if (!key->v.typed_ref.observed) return -1;
|
||||
|
||||
@@ -678,6 +678,7 @@ void scr_net_server_release(ScrNetServer *s) {
|
||||
#ifdef SCR_RC_AUDIT
|
||||
scr_net_live--;
|
||||
#endif
|
||||
scr_weak_dispose_inline(s); /* weak collections keyed by this socket or server */
|
||||
free(s);
|
||||
}
|
||||
}
|
||||
@@ -724,6 +725,7 @@ void scr_net_sock_release(ScrNetSocket *s) {
|
||||
#ifdef SCR_RC_AUDIT
|
||||
scr_net_live--;
|
||||
#endif
|
||||
scr_weak_dispose_inline(s); /* weak collections keyed by this socket or server */
|
||||
free(s);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +141,11 @@ typedef struct ScrCycType {
|
||||
* Emitted allocation paths read it and take the slow path while it is 0. */
|
||||
uint32_t id;
|
||||
} ScrCycType;
|
||||
/* The trace of a type that holds nothing cycle-capable: it visits nothing.
|
||||
* Emitted classes whose fields hold no cycle-capable value but whose
|
||||
* hierarchy is traced name it (llvm/classes.ts); their objects are
|
||||
* allocated with `buf` SCR_CYC_ACYCLIC. */
|
||||
void scr_cyc_no_children(void *obj, ScrTraceVisit visit, void *ctx);
|
||||
/* Runtime families' kinds. Emitted descriptors count from
|
||||
* SCR_CYC_KIND_EMITTED (llvm/alloc.ts FIRST_EMITTED_CYC_KIND). */
|
||||
enum {
|
||||
@@ -169,7 +174,7 @@ enum {
|
||||
* collector color (SCR_CYC_*) and generation (SCR_CYC_NURSERY..SCR_CYC_OLD,
|
||||
* the walk filter), and `buf`: SCR_CYC_BUFFERED_BIT while the object sits in
|
||||
* its generation's candidate buffer, with its position there below it (O(1)
|
||||
* removal when rc hits 0). */
|
||||
* removal when rc hits 0), or SCR_CYC_ACYCLIC. */
|
||||
typedef struct ScrCycHdr {
|
||||
uint16_t id;
|
||||
uint8_t color;
|
||||
@@ -177,13 +182,25 @@ typedef struct ScrCycHdr {
|
||||
uint32_t buf;
|
||||
} ScrCycHdr;
|
||||
#define SCR_CYC_BUFFERED_BIT 0x80000000u
|
||||
/* `buf` of an object whose type's trace is scr_cyc_no_children: it has no
|
||||
* cycle-capable children, so it is never on a cycle and never a candidate.
|
||||
* Such a type still carries the header when it shares a hierarchy with
|
||||
* cycle-capable classes (headers are uniform per hierarchy). The value has
|
||||
* the buffered bit set, so a release that leaves the object alive returns
|
||||
* at the buffered test and makes no call; scr_cyc_is_buffered is false for
|
||||
* it. No buffer position reaches it (scr_cyc_grow caps the buffers well
|
||||
* below 2^31 entries). */
|
||||
#define SCR_CYC_ACYCLIC 0xffffffffu
|
||||
|
||||
/* This layout is an ABI, not an implementation detail. The emitted
|
||||
* allocation stores the id and zeroes the rest; every emitted release of a
|
||||
* allocation stores the id and zeroes the rest (`buf` SCR_CYC_ACYCLIC for a
|
||||
* class whose trace is scr_cyc_no_children); every emitted release of a
|
||||
* headered object (llvm/shapes.ts cycleRootLines, used by the record, class
|
||||
* and inline RC helpers) mirrors scr_cyc_on_release's early return by
|
||||
* testing the sign of `buf` as an i32 at obj-4, and the emitted inline free
|
||||
* reads the generation as an i8 at obj-5. Hence the assertions. */
|
||||
* testing the sign of `buf` as an i32 at obj-4, the emitted death mirrors
|
||||
* scr_cyc_on_dead's by testing `buf` below -1 (cycleDeadLines), and the
|
||||
* emitted inline free reads the generation as an i8 at obj-5. Hence the
|
||||
* assertions. */
|
||||
_Static_assert(sizeof(ScrCycHdr) == 8, "LLVM backend expects an 8-byte cycle header");
|
||||
_Static_assert(offsetof(ScrCycHdr, id) == 0 && sizeof(((ScrCycHdr *)0)->id) == 2,
|
||||
"the emitted allocation stores the registry id as an i16 at the block start");
|
||||
@@ -215,6 +232,11 @@ static inline void scr_cyc_set_color(ScrCycHdr *h, unsigned color) { h->color =
|
||||
static inline unsigned scr_cyc_gen(const ScrCycHdr *h) { return h->gen; }
|
||||
static inline void scr_cyc_set_gen(ScrCycHdr *h, unsigned gen) { h->gen = (uint8_t)gen; }
|
||||
static inline bool scr_cyc_is_buffered(const ScrCycHdr *h) {
|
||||
return (h->buf & SCR_CYC_BUFFERED_BIT) != 0 && h->buf != SCR_CYC_ACYCLIC;
|
||||
}
|
||||
/* A release that leaves the object alive has nothing to buffer: it is a
|
||||
* candidate already, or it is never one (SCR_CYC_ACYCLIC). */
|
||||
static inline bool scr_cyc_release_done(const ScrCycHdr *h) {
|
||||
return (h->buf & SCR_CYC_BUFFERED_BIT) != 0;
|
||||
}
|
||||
static inline uint32_t scr_cyc_buf_index(const ScrCycHdr *h) {
|
||||
@@ -608,7 +630,7 @@ static inline bool scr_cyc_append(ScrCycHdr *h, void *obj) {
|
||||
* Most releases of long-lived containers take the first early return. */
|
||||
static inline void scr_cyc_released(void *obj) {
|
||||
ScrCycHdr *h = scr_cyc_hdr(obj);
|
||||
if (scr_cyc_is_buffered(h)) return;
|
||||
if (scr_cyc_release_done(h)) return;
|
||||
#if SCR_CYC_INLINE_BUFFERING
|
||||
if (scr_cyc_append(h, obj)) return;
|
||||
#endif
|
||||
|
||||
@@ -398,6 +398,8 @@ static MockRec *mock_cyc_new(int value) {
|
||||
|
||||
static ScrArrOps mock_ops = { &mock_retain, &mock_release, NULL };
|
||||
static ScrArrOps mock_cyc_ops = { &mock_cyc_retain, &mock_cyc_release, &mock_trace };
|
||||
/* First used after test_ops_overflow fills the registry: its arrays keep the ops in their ext. */
|
||||
static ScrArrOps mock_cyc_ops_late = { &mock_cyc_retain, &mock_cyc_release, &mock_trace };
|
||||
|
||||
static void test_ref_elements(void) {
|
||||
#ifdef SCR_RC_AUDIT
|
||||
@@ -514,6 +516,38 @@ static void test_ref_cycle(void) {
|
||||
#endif
|
||||
}
|
||||
|
||||
static void test_ref_known_count_cycle(ScrArrOps *ops) {
|
||||
#ifdef SCR_RC_AUDIT
|
||||
long arrays0 = scr_arr_live_count();
|
||||
#endif
|
||||
/* A known-count construction of cycle-capable elements keeps its slots
|
||||
* after the collector header: the collector traces them, and both the
|
||||
* collected array and one released by its count free the one block. */
|
||||
ScrArr *arr = scr_arr_new_ops(ops, 2);
|
||||
bool in_block = arr->data == (uint64_t *)((char *)arr + ((sizeof(ScrArr) + 7) & ~(size_t)7));
|
||||
/* Ops past the registry (once test_ops_overflow has filled it) live in
|
||||
* the ext, which moves the slots to their own allocation. */
|
||||
bool overflow = (arr->spare >> SCR_ARR_OPS_SHIFT) == SCR_ARR_OPS_OVERFLOW;
|
||||
check(arr->cap >= 2 && in_block == !overflow && scr_arr_ops(arr) == ops,
|
||||
"known-count cycle-capable construction is inline unless its ops need the ext");
|
||||
MockRec *first = mock_cyc_new(1);
|
||||
first->owner = (ScrArr *)scr_arr_retain(arr);
|
||||
scr_arr_push_ref(arr, first);
|
||||
scr_arr_push_ref(arr, mock_cyc_new(2));
|
||||
scr_arr_release(arr);
|
||||
scr_collect_cycles();
|
||||
check(mock_live == 0, "cycle through an inline array collected");
|
||||
ScrArr *plain = scr_arr_new_ops(ops, 3);
|
||||
for (int i = 0; i < 4; i++) scr_arr_push_ref(plain, mock_cyc_new(i));
|
||||
check(scr_arr_len(plain) == 4, "a cycle-capable inline array grows out of its block");
|
||||
scr_arr_release(plain);
|
||||
scr_collect_cycles();
|
||||
check(mock_live == 0, "released inline cycle-capable array frees its elements");
|
||||
#ifdef SCR_RC_AUDIT
|
||||
check(scr_arr_live_count() == arrays0, "known-count cycle: no arrays leaked");
|
||||
#endif
|
||||
}
|
||||
|
||||
static void test_ref_truncate_cycle(void) {
|
||||
#ifdef SCR_RC_AUDIT
|
||||
long arrays0 = scr_arr_live_count();
|
||||
@@ -1543,9 +1577,33 @@ static void test_inline_slots(void) {
|
||||
long strings0 = scr_str_live_count();
|
||||
long arrays0 = scr_arr_live_count();
|
||||
#endif
|
||||
ScrArr *plain = scr_arr_new(SCR_ELEM_F64, 4);
|
||||
check(!inline_slots(plain), "plain constructions keep separate storage");
|
||||
ScrArr *plain = scr_arr_new(SCR_ELEM_F64, 0);
|
||||
check(!inline_slots(plain) && plain->data == NULL, "empty plain constructions allocate no slots");
|
||||
scr_arr_release(plain);
|
||||
/* A construction with a known count up to 16 slots keeps them in its own
|
||||
* block; growth past them moves them out like any inline array. */
|
||||
ScrArr *known = scr_arr_new(SCR_ELEM_F64, 4);
|
||||
check(inline_slots(known) && known->cap >= 4 && known->len == 0 && !scr_arr_has(known, 0) &&
|
||||
scr_arr_states_end(known) == (uint8_t *)(known->data + known->cap) + known->cap,
|
||||
"a known-count construction places its slots in the array block");
|
||||
size_t known_cap = known->cap;
|
||||
for (size_t i = 0; i <= known_cap; i++) scr_arr_push_f64(known, (double)i);
|
||||
check(!inline_slots(known) && scr_arr_len(known) == known_cap + 1 && scr_arr_get_f64(known, 0) == 0 &&
|
||||
scr_arr_get_f64(known, (double)known_cap) == (double)known_cap,
|
||||
"growth past a known-count construction moves the slots out");
|
||||
scr_arr_release(known);
|
||||
ScrArr *sixteen = scr_arr_new(SCR_ELEM_STR, 16);
|
||||
for (int i = 0; i < 16; i++) scr_arr_push_ref(sixteen, scr_str_new("s", 1));
|
||||
check(inline_slots(sixteen) && scr_arr_len(sixteen) == 16, "sixteen known slots stay inline");
|
||||
scr_arr_release(sixteen);
|
||||
ScrArr *large = scr_arr_new(SCR_ELEM_F64, 17);
|
||||
check(!inline_slots(large) && large->cap >= 17, "larger known counts keep separate storage");
|
||||
scr_arr_release(large);
|
||||
ScrArr *known_flags = scr_arr_new(SCR_ELEM_BOOL, 5);
|
||||
scr_arr_push_bool(known_flags, true);
|
||||
check(inline_slots(known_flags) && known_flags->cap >= 5 && scr_arr_get_bool(known_flags, 0),
|
||||
"known-count boolean constructions are inline too");
|
||||
scr_arr_release(known_flags);
|
||||
ScrArr *a = scr_arr_new_small(SCR_ELEM_F64);
|
||||
check(inline_slots(a) && a->cap >= 4 && a->len == 0 &&
|
||||
scr_arr_states_end(a) == (uint8_t *)(a->data + a->cap) + a->cap,
|
||||
@@ -2205,7 +2263,9 @@ int main(int argc, char **argv) {
|
||||
test_nested_rc();
|
||||
test_index_of_includes();
|
||||
test_ref_elements();
|
||||
test_ref_known_count_cycle(&mock_cyc_ops);
|
||||
test_ops_overflow();
|
||||
test_ref_known_count_cycle(&mock_cyc_ops_late);
|
||||
test_ext_through_storage_changes();
|
||||
test_ref_cycle();
|
||||
test_ref_truncate_cycle();
|
||||
|
||||
@@ -1434,6 +1434,38 @@ static void check_death_brings_full_pass(void) {
|
||||
free(keep);
|
||||
}
|
||||
|
||||
/* An object whose type's trace is scr_cyc_no_children starts out
|
||||
* SCR_CYC_ACYCLIC and is never buffered. A pass that reaches it from a dead
|
||||
* cycle frees it with the cycle when the cycle held its last reference, and
|
||||
* otherwise leaves it SCR_CYC_ACYCLIC with its outside count. */
|
||||
static void check_acyclic_objects(void) {
|
||||
static ScrCycType leaf_type = { &scr_cyc_no_children, &node_free, 0 };
|
||||
size_t before = freed;
|
||||
Node *leaf = scr_cyc_alloc(sizeof(*leaf), &leaf_type);
|
||||
ScrCycHdr *h = scr_cyc_hdr(leaf);
|
||||
check(h->buf == SCR_CYC_ACYCLIC && !scr_cyc_is_buffered(h), "acyclic object did not start acyclic");
|
||||
leaf->rc = 2; /* an outside owner and a temporary reference */
|
||||
release_live(leaf);
|
||||
check(h->buf == SCR_CYC_ACYCLIC, "release buffered an acyclic object");
|
||||
|
||||
Node *first = make_ring(2);
|
||||
first->other = leaf;
|
||||
leaf->rc++;
|
||||
release_live(first);
|
||||
scr_cyc_pass_at(SCR_CYC_NURSERY);
|
||||
check(freed == before + 2, "nursery pass did not free the ring");
|
||||
check(leaf->rc == 1 && h->buf == SCR_CYC_ACYCLIC, "acyclic survivor lost its count or its mark");
|
||||
|
||||
Node *second = make_ring(2);
|
||||
second->next->other = leaf;
|
||||
leaf->rc++;
|
||||
release_live(leaf); /* the outside owner lets go: only the ring holds it */
|
||||
check(h->buf == SCR_CYC_ACYCLIC, "release buffered an acyclic object");
|
||||
release_live(second);
|
||||
scr_collect_cycles();
|
||||
check(freed == before + 5, "acyclic object held only by a dead ring was not freed with it");
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
check_inline_buffering();
|
||||
check_old_garbage_reclaimed_while_growing();
|
||||
@@ -1467,6 +1499,7 @@ int main(void) {
|
||||
check_limited_pass();
|
||||
check_large_old_buffer_with_nursery_candidates();
|
||||
check_unjudged_backlog_follows_growth();
|
||||
check_acyclic_objects();
|
||||
printf("cycle collection checks passed: threshold=%zu growth_cap=%zu\n",
|
||||
configured_nursery_threshold(), configured_growth_cap());
|
||||
return 0;
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// Sockets and servers key WeakMaps and WeakSets by identity: the socket a
|
||||
// connect callback sees, the one a 'connection' listener sees, and the
|
||||
// same socket read back out of a diagnostics_channel message are one key.
|
||||
// Entries of closed sockets stop answering once the socket is gone.
|
||||
import * as net from "node:net";
|
||||
import type { Server, Socket } from "node:net";
|
||||
import { channel } from "node:diagnostics_channel";
|
||||
|
||||
interface Conn {
|
||||
label: string;
|
||||
bytes: number;
|
||||
}
|
||||
|
||||
const connections = new WeakMap<Socket, Conn>();
|
||||
const seen = new WeakSet<Socket>();
|
||||
const servers = new WeakMap<Server, string>();
|
||||
const opened = channel("example:socket:opened");
|
||||
|
||||
opened.subscribe((message) => {
|
||||
const { socket, label } = message as { socket: Socket; label: string };
|
||||
console.log(`[diag] ${label} known ${connections.has(socket)}, seen ${seen.has(socket)}`);
|
||||
const conn = connections.get(socket);
|
||||
if (conn !== undefined) conn.label = `${conn.label}+diag`;
|
||||
});
|
||||
|
||||
// Each dial waits for both ends to close, so the two sides log in order.
|
||||
let serverClosed: () => void = () => {};
|
||||
|
||||
const server = net.createServer((socket) => {
|
||||
seen.add(socket);
|
||||
connections.set(socket, { label: "server side", bytes: 0 });
|
||||
socket.on("data", (chunk) => {
|
||||
const conn = connections.get(socket);
|
||||
if (conn !== undefined) conn.bytes += chunk.length;
|
||||
socket.end(`echo ${chunk.toString()}`);
|
||||
});
|
||||
socket.on("close", () => {
|
||||
const conn = connections.get(socket);
|
||||
console.log("server socket closed", conn?.label, conn?.bytes);
|
||||
console.log("delete", connections.delete(socket), connections.delete(socket));
|
||||
serverClosed();
|
||||
});
|
||||
});
|
||||
servers.set(server, "echo server");
|
||||
|
||||
function dial(port: number, label: string): Promise<void> {
|
||||
const bothClosed = new Promise<void>((resolve) => {
|
||||
serverClosed = resolve;
|
||||
});
|
||||
const clientClosed = new Promise<void>((resolve) => {
|
||||
const client: Socket = net.connect(port, "127.0.0.1", () => {
|
||||
connections.set(client, { label, bytes: 0 });
|
||||
console.log(label, "connected; has", connections.has(client), "seen", seen.has(client));
|
||||
opened.publish({ socket: client, label });
|
||||
client.write(label);
|
||||
});
|
||||
let reply = "";
|
||||
client.on("data", (chunk) => {
|
||||
reply += chunk.toString();
|
||||
});
|
||||
client.on("close", () => {
|
||||
console.log(label, "reply", JSON.stringify(reply), "entry", connections.get(client)?.label);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
return Promise.all([clientClosed, bothClosed]).then(() => undefined);
|
||||
}
|
||||
|
||||
server.listen(0, "127.0.0.1", async () => {
|
||||
console.log("server known", servers.has(server), servers.get(server));
|
||||
const address = server.address();
|
||||
const port = typeof address === "object" && address !== null ? address.port : 0;
|
||||
await dial(port, "first");
|
||||
await dial(port, "second");
|
||||
try {
|
||||
connections.set(1 as unknown as Socket, { label: "number", bytes: 0 });
|
||||
} catch (err) {
|
||||
console.log((err as Error).name, (err as Error).message);
|
||||
}
|
||||
server.close(() => console.log("closed; server still known", servers.has(server)));
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
// Never-suspending async functions in a program that enters no
|
||||
// AsyncLocalStorage store and reads no stacks: awaited from stackless and
|
||||
// fiber bodies, called without an await, nested, throwing, and rejecting
|
||||
// without a handler, against concurrent microtasks.
|
||||
const log: string[] = [];
|
||||
|
||||
async function leaf(n: number): Promise<number> {
|
||||
log.push(`leaf ${n}`);
|
||||
return n + 1;
|
||||
}
|
||||
async function nestedLeaf(n: number): Promise<number> {
|
||||
const inner = leaf(n * 10);
|
||||
void inner.then((v) => log.push(`nestedLeaf inner ${v}`));
|
||||
log.push(`nestedLeaf ${n}`);
|
||||
return n;
|
||||
}
|
||||
async function thrower(n: number): Promise<number> {
|
||||
log.push(`thrower ${n}`);
|
||||
if (n > 0) throw new TypeError(`bad ${n}`);
|
||||
return n;
|
||||
}
|
||||
function ticker(name: string, turns: number): Promise<void> {
|
||||
let p = Promise.resolve();
|
||||
for (let i = 0; i < turns; i++) p = p.then(() => void log.push(`${name} tick ${i}`));
|
||||
return p;
|
||||
}
|
||||
|
||||
async function stackless(): Promise<void> {
|
||||
const a = await leaf(1);
|
||||
const b = await leaf(a);
|
||||
try {
|
||||
await thrower(b);
|
||||
} catch (e) {
|
||||
log.push(`stackless caught ${(e as Error).name} ${(e as Error).message}`);
|
||||
}
|
||||
log.push(`stackless ${a + b}`);
|
||||
}
|
||||
async function* source(): AsyncGenerator<number, void, undefined> {
|
||||
yield 1;
|
||||
yield 2;
|
||||
}
|
||||
async function fibered(): Promise<void> {
|
||||
let s = 0;
|
||||
for await (const v of source()) s += await leaf(v);
|
||||
try {
|
||||
s += await thrower(5);
|
||||
} catch (e) {
|
||||
log.push(`fibered caught ${(e as Error).message}`);
|
||||
}
|
||||
log.push(`fibered ${s}`);
|
||||
}
|
||||
async function spawned(): Promise<void> {
|
||||
const p = leaf(7);
|
||||
const q = thrower(0);
|
||||
log.push("spawned before await");
|
||||
log.push(`spawned ${(await p) + (await q)}`);
|
||||
}
|
||||
|
||||
process.on("unhandledRejection", (reason) => console.log(`unhandled ${(reason as Error).message}`));
|
||||
async function main(): Promise<void> {
|
||||
void thrower(9);
|
||||
await Promise.all([ticker("t", 6), stackless(), fibered(), spawned(), nestedLeaf(3)]);
|
||||
for (const line of log) console.log(line);
|
||||
}
|
||||
void main();
|
||||
@@ -0,0 +1,117 @@
|
||||
import { publish } from "@scriptc/threads";
|
||||
|
||||
// Classes whose own fields hold nothing cycle-capable, in a hierarchy where
|
||||
// other classes form cycles: their instances are held by dead cycles, by
|
||||
// live cycles and by plain locals while collections run, and are published
|
||||
// with the contexts that hold them.
|
||||
|
||||
abstract class Mapper {
|
||||
abstract map(n: number): number;
|
||||
}
|
||||
|
||||
class Offset extends Mapper {
|
||||
by: number;
|
||||
constructor(by: number) {
|
||||
super();
|
||||
this.by = by;
|
||||
}
|
||||
map(n: number): number {
|
||||
return n + this.by;
|
||||
}
|
||||
}
|
||||
|
||||
class Scale extends Offset {
|
||||
readonly factor: number;
|
||||
constructor(by: number, factor: number) {
|
||||
super(by);
|
||||
this.factor = factor;
|
||||
}
|
||||
map(n: number): number {
|
||||
return super.map(n) * this.factor;
|
||||
}
|
||||
}
|
||||
|
||||
class Context {
|
||||
readonly steps: Mapper[] = [];
|
||||
mapper: Mapper | undefined = undefined;
|
||||
total = 0;
|
||||
}
|
||||
|
||||
class ContextMapper extends Mapper {
|
||||
readonly context: Context;
|
||||
constructor(context: Context) {
|
||||
super();
|
||||
this.context = context;
|
||||
}
|
||||
map(n: number): number {
|
||||
let value = n;
|
||||
for (const step of this.context.steps) value = step.map(value);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
class Chain extends Mapper {
|
||||
readonly first: Mapper;
|
||||
next: Mapper | undefined = undefined;
|
||||
constructor(first: Mapper) {
|
||||
super();
|
||||
this.first = first;
|
||||
}
|
||||
map(n: number): number {
|
||||
const value = this.first.map(n);
|
||||
return this.next === undefined ? value : this.next.map(value);
|
||||
}
|
||||
}
|
||||
|
||||
function makeContext(i: number, shared: Mapper): Context {
|
||||
const context = new Context();
|
||||
context.steps.push(new Offset(i % 7), shared, new Scale(1, 2));
|
||||
context.mapper = new ContextMapper(context);
|
||||
context.total = context.mapper.map(i);
|
||||
return context;
|
||||
}
|
||||
|
||||
const shared = new Scale(3, 1);
|
||||
const kept: Context[] = [];
|
||||
let sum = 0;
|
||||
for (let i = 0; i < 20000; i++) {
|
||||
const context = makeContext(i, shared);
|
||||
sum += context.total;
|
||||
if (i % 1000 === 0) kept.push(context);
|
||||
}
|
||||
|
||||
// A chain that loops back to itself, holding mappers that are also held
|
||||
// outside it.
|
||||
const outside: Mapper[] = [];
|
||||
let loops = 0;
|
||||
for (let i = 0; i < 5000; i++) {
|
||||
const offset = new Offset(i);
|
||||
const head = new Chain(offset);
|
||||
const tail = new Chain(new Scale(i, 3));
|
||||
head.next = tail;
|
||||
tail.next = undefined;
|
||||
loops += head.map(1);
|
||||
tail.next = head;
|
||||
if (i % 500 === 0) outside.push(offset);
|
||||
}
|
||||
|
||||
let keptTotal = 0;
|
||||
for (const context of kept) keptTotal += context.mapper!.map(1);
|
||||
let outsideTotal = 0;
|
||||
for (const mapper of outside) outsideTotal += mapper.map(2);
|
||||
|
||||
console.log(sum, loops, kept.length, keptTotal, outside.length, outsideTotal, shared.map(4));
|
||||
|
||||
publish(kept);
|
||||
let publishedTotal = 0;
|
||||
for (const context of kept) publishedTotal += context.mapper!.map(3);
|
||||
console.log("published", publishedTotal);
|
||||
try {
|
||||
shared.by = 5;
|
||||
console.log("write: ok");
|
||||
} catch (e) {
|
||||
console.log(`write: ${(e as Error).name}: ${(e as Error).message}`);
|
||||
}
|
||||
const fresh = makeContext(7, new Offset(1));
|
||||
fresh.steps.push(shared);
|
||||
console.log("after", fresh.mapper!.map(2), outside[3]!.map(1));
|
||||
+34
-1
@@ -81,6 +81,34 @@ channel("undici:request:trailers").subscribe((message) => {
|
||||
if (t !== undefined) console.log(` [diag] ${t.label} done after ${t.dispatched} dispatch`);
|
||||
});
|
||||
|
||||
// A transport's connection registry: sockets the wrapped connector dialed,
|
||||
// keyed weakly, and the client diagnostics messages that carry them.
|
||||
const dialed = new WeakMap<Socket, number>();
|
||||
const clientDiag = { beforeConnect: 0, connector: 0, connected: 0, sendHeaders: 0, known: 0, unknown: 0 };
|
||||
const noteSocket = (socket: Socket): void => {
|
||||
const n = dialed.get(socket);
|
||||
if (n === undefined) clientDiag.unknown++;
|
||||
else clientDiag.known++;
|
||||
};
|
||||
channel("undici:client:beforeConnect").subscribe((message) => {
|
||||
const { connector } = message as { connector: unknown };
|
||||
clientDiag.beforeConnect++;
|
||||
if (typeof connector === "function") clientDiag.connector++;
|
||||
});
|
||||
channel("undici:client:connected").subscribe((message) => {
|
||||
const { socket, connectParams } = message as {
|
||||
socket: Socket;
|
||||
connectParams: { protocol: string; hostname: string };
|
||||
};
|
||||
clientDiag.connected++;
|
||||
if (connectParams.protocol === "http:" && connectParams.hostname === "127.0.0.1") noteSocket(socket);
|
||||
});
|
||||
channel("undici:client:sendHeaders").subscribe((message) => {
|
||||
const { socket, headers } = message as { socket: Socket; headers: string };
|
||||
clientDiag.sendHeaders++;
|
||||
if (headers.length > 0) noteSocket(socket);
|
||||
});
|
||||
|
||||
/** A transport-style Agent: one Pool per origin built by the factory, a
|
||||
* wrapped connector, and a Client interceptor that marks dispatches. */
|
||||
class TracingAgent extends Agent {
|
||||
@@ -102,7 +130,9 @@ class TracingAgent extends Agent {
|
||||
pipelining: 1,
|
||||
connect(options, callback) {
|
||||
opened++;
|
||||
return connect(options, callback);
|
||||
const socket = connect(options, callback);
|
||||
dialed.set(socket, opened);
|
||||
return socket;
|
||||
},
|
||||
interceptors: {
|
||||
Client: [
|
||||
@@ -198,6 +228,9 @@ server.listen(0, "127.0.0.1", async () => {
|
||||
await r.body.text();
|
||||
}
|
||||
console.log("connections opened:", agent.connections);
|
||||
console.log(
|
||||
`client diagnostics: ${clientDiag.beforeConnect} before connect (${clientDiag.connector} with a connector), ${clientDiag.connected} connected, ${clientDiag.sendHeaders} send headers; ${clientDiag.known} carry a dialed socket, ${clientDiag.unknown} another`,
|
||||
);
|
||||
console.log("stats:", agent.statsFor(origin));
|
||||
|
||||
console.log("== Pool with a connection limit");
|
||||
|
||||
+34
-1
@@ -115,6 +115,34 @@ channel("undici:request:trailers").subscribe((message) => {
|
||||
if (t !== undefined) console.log(` [diag] ${t.label} done after ${t.dispatched} dispatch`);
|
||||
});
|
||||
|
||||
// A transport's connection registry: sockets the wrapped connector dialed,
|
||||
// keyed weakly, and the client diagnostics messages that carry them.
|
||||
const dialed = new WeakMap<Socket, number>();
|
||||
const clientDiag = { beforeConnect: 0, connector: 0, connected: 0, sendHeaders: 0, known: 0, unknown: 0 };
|
||||
const noteSocket = (socket: Socket): void => {
|
||||
const n = dialed.get(socket);
|
||||
if (n === undefined) clientDiag.unknown++;
|
||||
else clientDiag.known++;
|
||||
};
|
||||
channel("undici:client:beforeConnect").subscribe((message) => {
|
||||
const { connector } = message as { connector: unknown };
|
||||
clientDiag.beforeConnect++;
|
||||
if (typeof connector === "function") clientDiag.connector++;
|
||||
});
|
||||
channel("undici:client:connected").subscribe((message) => {
|
||||
const { socket, connectParams } = message as {
|
||||
socket: Socket;
|
||||
connectParams: { protocol: string; hostname: string };
|
||||
};
|
||||
clientDiag.connected++;
|
||||
if (connectParams.protocol === "http:" && connectParams.hostname === "127.0.0.1") noteSocket(socket);
|
||||
});
|
||||
channel("undici:client:sendHeaders").subscribe((message) => {
|
||||
const { socket, headers } = message as { socket: Socket; headers: string };
|
||||
clientDiag.sendHeaders++;
|
||||
if (headers.length > 0) noteSocket(socket);
|
||||
});
|
||||
|
||||
/** A transport-style Agent: one Pool per origin built by the factory, a
|
||||
* wrapped connector, and a Client interceptor that marks dispatches. */
|
||||
class TracingAgent extends Agent {
|
||||
@@ -136,7 +164,9 @@ class TracingAgent extends Agent {
|
||||
pipelining: 1,
|
||||
connect(options, callback) {
|
||||
opened++;
|
||||
return connect(options, callback);
|
||||
const socket = connect(options, callback);
|
||||
dialed.set(socket, opened);
|
||||
return socket;
|
||||
},
|
||||
interceptors: {
|
||||
Client: [
|
||||
@@ -232,6 +262,9 @@ server.listen(0, "127.0.0.1", async () => {
|
||||
await r.body.text();
|
||||
}
|
||||
console.log("connections opened:", agent.connections);
|
||||
console.log(
|
||||
`client diagnostics: ${clientDiag.beforeConnect} before connect (${clientDiag.connector} with a connector), ${clientDiag.connected} connected, ${clientDiag.sendHeaders} send headers; ${clientDiag.known} carry a dialed socket, ${clientDiag.unknown} another`,
|
||||
);
|
||||
console.log("stats:", agent.statsFor(origin));
|
||||
|
||||
console.log("== Pool with a connection limit");
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
dynamic tier (tier assignment, report-only; --tier=auto)
|
||||
4 functions already operate on dynamic values (1 statement)
|
||||
readName tests/coverage-fixtures/tier-demotions.ts:15:1: %dyn.error.stringConstructor at tests/coverage-fixtures/tier-demotions.ts:17:10
|
||||
%dyn.error.stringConstructor.0 tests/coverage-fixtures/tier-demotions.ts:17:10: dynCall at tests/coverage-fixtures/tier-demotions.ts:17:10
|
||||
%dyn.class.property.0 tests/coverage-fixtures/tier-demotions.ts:17:17: %dyn.class.readPropertiesOrEmpty at tests/coverage-fixtures/tier-demotions.ts:17:17
|
||||
%dyn.error.stringConstructor.0 tests/coverage-fixtures/tier-demotions.ts:17:10: dynCall at tests/coverage-fixtures/tier-demotions.ts:17:10
|
||||
%method.value:%%Error.toString tests/coverage-fixtures/tier-demotions.ts:17:10: dyn.this at tests/coverage-fixtures/tier-demotions.ts:17:10
|
||||
|
||||
2 demotion requests (refusals and fences the dynamic tier would replace)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3400, "san": 4500 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3350, "san": 3910 }
|
||||
Reference in New Issue
Block a user