mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
An async function called inside LifetimeRegion.run now keeps its region after every await: its fiber records the region that was current at spawn, and the event loop makes that region current while it resumes the fiber. A region is not freed (or attempted) while a fiber that carries it exists, so a region released while its async work is pending keeps taking that work's objects and is freed once the work ends. Before, objects created after an await went to the default region, which is never freed, and kept the request's region allocated for good, so per-request regions in async servers grew without bound. Freed region chunks now go to a per-thread cache (at most 8 MiB resident, the first 256 KiB of each kept cleared, fresh pages past it, no huge-page chunks) instead of being unmapped, so a region per request or per batch no longer maps and faults in new memory every time. Adds the requests.ts region benchmark (concurrent async handlers), corpus 5023 and its region-stats checks, and a chunk-reuse runtime unit check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
103 lines
3.0 KiB
TypeScript
103 lines
3.0 KiB
TypeScript
// Per-file regions next to a large live heap: a checker-like graph of ordinary objects with
|
|
// parent links (cycle-collector candidates that stay alive) and, for each file, an emit that
|
|
// builds ProgramLifetime nodes and ordinary objects they own (some cyclic) in a region that is
|
|
// released right after.
|
|
//
|
|
// emit <checker-types> <files> <nodes-per-file> [plain]
|
|
//
|
|
// `plain` never constructs a region.
|
|
import type { ProgramLifetime } from "@scriptc/types";
|
|
import { LifetimeRegion } from "@scriptc/experimental";
|
|
|
|
class Type {
|
|
readonly id: number;
|
|
parent: Type | undefined;
|
|
readonly members: Type[] = [];
|
|
constructor(id: number, parent: Type | undefined) {
|
|
this.id = id;
|
|
this.parent = parent;
|
|
parent?.members.push(this);
|
|
}
|
|
}
|
|
|
|
class Scope {
|
|
readonly parent: Scope | undefined;
|
|
readonly children: Scope[] = [];
|
|
readonly names = new Map<string, OutNode>();
|
|
constructor(parent: Scope | undefined) {
|
|
this.parent = parent;
|
|
parent?.children.push(this);
|
|
}
|
|
}
|
|
|
|
class OutNode implements ProgramLifetime {
|
|
readonly kind: number;
|
|
readonly kids: OutNode[] = [];
|
|
scope: Scope | undefined = undefined;
|
|
text: string;
|
|
constructor(kind: number, text: string) {
|
|
this.kind = kind;
|
|
this.text = text;
|
|
}
|
|
}
|
|
|
|
const typeCount = Number(process.argv[2] ?? "400000");
|
|
const files = Number(process.argv[3] ?? "200");
|
|
const nodesPerFile = Number(process.argv[4] ?? "2000");
|
|
const plain = process.argv[5] === "plain";
|
|
|
|
// The checker: one tree of types, each holding its parent and its members, so a walk
|
|
// from any of them reaches all of them.
|
|
const roots: Type[] = [];
|
|
let id = 0;
|
|
let level: Type[] = [new Type(id++, undefined)];
|
|
while (id < typeCount) {
|
|
const next: Type[] = [];
|
|
for (const t of level) for (let k = 0; k < 4 && id < typeCount; k++) next.push(new Type(id++, t));
|
|
for (const t of next) roots.push(t);
|
|
level = next;
|
|
}
|
|
|
|
function lookUp(t: Type): number {
|
|
let depth = 0;
|
|
for (let p: Type | undefined = t; p !== undefined; p = p.parent) depth++;
|
|
return depth;
|
|
}
|
|
|
|
function emit(file: number): number {
|
|
const top = new Scope(undefined);
|
|
const root = new OutNode(0, `file${file}`);
|
|
let scope = top;
|
|
let sum = 0;
|
|
const seen: Type[] = [];
|
|
for (let i = 0; i < nodesPerFile; i++) {
|
|
const node = new OutNode(i % 7, `n${i}`);
|
|
root.kids.push(node);
|
|
if (i % 50 === 0) scope = new Scope(scope);
|
|
node.scope = scope;
|
|
scope.names.set(node.text, node);
|
|
// Reads of the checker: temporaries that leave its types buffered.
|
|
const t = roots[(file * 31 + i) % roots.length]!.members[i % 4];
|
|
if (t !== undefined) {
|
|
sum += lookUp(t);
|
|
seen.push(t);
|
|
}
|
|
}
|
|
return sum + root.kids.length + seen.length;
|
|
}
|
|
|
|
const t0 = performance.now();
|
|
let total = 0;
|
|
for (let f = 0; f < files; f++) {
|
|
if (plain) {
|
|
total += emit(f);
|
|
continue;
|
|
}
|
|
const region = new LifetimeRegion();
|
|
total += region.run(() => emit(f));
|
|
region.release();
|
|
}
|
|
const ms = performance.now() - t0;
|
|
console.log(`emitted ${files} files, checksum ${total}`);
|
|
console.log(`emit ${ms.toFixed(1)} ms per-file ${(ms / files).toFixed(2)} ms`);
|