mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-24 06:14:01 +08:00
An async function whose body can never park its execution context completes before its call returns, exactly like a fiber that never suspends. The LLVM emitter now proves that from the emitted IR: a body qualifies when neither its own text nor that of any module function it mentions (transitively) contains a suspension entry point (scr_await_*, scr_module_await, scr_promise_await_settled, scr_test_sub, generator yields, async-generator operations, fiber accessors). wasm32, programs embedding the dynamic engine, module-evaluation bodies, dyn results, and programs using node:test subtests (t.test awaits on the current fiber even from sync code) keep fibers. Qualifying spawn wrappers call the body on the caller's stack between scr_async_inline_enter/leave, which save and restore what a fresh fiber isolates (the Error.stack frame chain and the AsyncLocalStorage context), then settle a fresh promise from the outcome through the same fulfill/reject paths the fiber trampoline uses. A park or hop while a fiberless frame is on a fiber's stack traps as an internal error rather than reordering. In async-pipeline, loadUser and authorize become fiberless: one fiber per request instead of five. New corpus programs pin microtask ordering, rejections (handled and unhandled), finally, nested suspending spawns, closures, methods, Error.stack, AsyncLocalStorage, and a fiber-switch stress test. Final benchmarks for the branch (three commits) vs perf/harness, runs=15. macOS arm64 (local): | workload | baseline ms | candidate ms | change | 95% CI | verdict | size | node ms | candidate/node | | --- | ---: | ---: | ---: | --- | --- | ---: | ---: | ---: | | json-records | 279.2 | 280.7 | +0.5% | -1.8% .. +3.0% | neutral | +0.0% | 230.93 | 1.22x | | regex-logs | 305.8 | 306.0 | +0.1% | -0.8% .. +2.0% | neutral | +0.0% | 132.42 | 2.31x | | ast-interp | 235.3 | 235.1 | -0.1% | -0.6% .. +0.4% | neutral | +0.0% | 133.55 | 1.76x | | records-sort | 621.6 | 619.0 | -0.4% | -1.1% .. +0.6% | neutral | +0.0% | 131.56 | 4.71x | | template-render | 202.8 | 201.9 | -0.4% | -1.7% .. +2.6% | neutral | +0.0% | 99.28 | 2.03x | | functional-pipeline | 226.0 | 226.4 | +0.2% | -0.4% .. +0.6% | neutral | +0.0% | 120.24 | 1.88x | | async-pipeline | 267.0 | 21.6 | -91.9% | -92.2% .. -91.4% | faster | +0.0% | 58.58 | 0.37x | | word-graph | 417.6 | 566.8 | +35.7% | -49.6% .. +171.5% | neutral | +0.0% | 944.76 | 0.60x | | alloc-trees | 311.1 | 311.3 | +0.1% | -4.8% .. +2.3% | neutral | +0.0% | 87.37 | 3.56x | | numeric-kernels | 79.0 | 80.5 | +1.9% | -4.1% .. +8.4% | neutral | +0.0% | 94.05 | 0.86x | | validate-errors | 169.9 | 169.4 | -0.3% | -12.0% .. +12.3% | neutral | +0.0% | 517.13 | 0.33x | | csv-numbers | 379.3 | 372.8 | -1.7% | -27.8% .. +12.5% | neutral | +0.0% | 360.69 | 1.03x | | log-summary | 180.7 | 184.0 | +1.8% | -14.4% .. +9.1% | neutral | +0.0% | 181.51 | 1.01x | | inventory-report | 231.7 | 239.0 | +3.2% | -11.1% .. +10.9% | neutral | +0.0% | 245.91 | 0.97x | (word-graph was host noise from concurrent agents; the immediate recheck measured -2.0%, neutral.) Linux x86_64 (sandbox): | workload | baseline ms | candidate ms | change | 95% CI | verdict | size | node ms | candidate/node | | --- | ---: | ---: | ---: | --- | --- | ---: | ---: | ---: | | json-records | 1031.8 | 1019.7 | -1.2% | -5.5% .. +3.2% | neutral | +0.0% | 627.39 | 1.63x | | regex-logs | 534.1 | 533.6 | -0.1% | -5.5% .. +1.9% | neutral | +0.0% | 336.44 | 1.59x | | ast-interp | 538.1 | 536.8 | -0.2% | -1.7% .. +2.3% | neutral | +0.0% | 324.66 | 1.65x | | records-sort | 1798.6 | 1749.9 | -2.7% | -4.8% .. +0.5% | neutral | +0.0% | 326.49 | 5.36x | | template-render | 379.2 | 371.0 | -2.1% | -9.3% .. +1.0% | neutral | +0.0% | 287.51 | 1.29x | | functional-pipeline | 608.5 | 596.7 | -1.9% | -3.9% .. +3.2% | neutral | +0.0% | 390.16 | 1.53x | | async-pipeline | 250.3 | 35.9 | -85.7% | -86.2% .. -85.3% | faster | -0.0% | 123.43 | 0.29x | | word-graph | 462.1 | 461.7 | -0.1% | -3.3% .. +2.4% | neutral | +0.0% | 690.81 | 0.67x | | alloc-trees | 690.4 | 699.1 | +1.3% | -0.9% .. +4.1% | neutral | +0.0% | 174.67 | 4.00x | | numeric-kernels | 176.0 | 172.8 | -1.8% | -3.4% .. +0.6% | neutral | +0.0% | 176.9 | 0.98x | | validate-errors | 304.4 | 301.0 | -1.1% | -4.9% .. +1.6% | neutral | +0.0% | 945.13 | 0.32x | | csv-numbers | 602.8 | 605.0 | +0.4% | -4.7% .. +1.9% | neutral | +0.0% | 696.66 | 0.87x | | log-summary | 332.9 | 332.6 | -0.1% | -4.0% .. +1.4% | neutral | +0.0% | 384.79 | 0.86x | | inventory-report | 601.0 | 583.5 | -2.9% | -6.5% .. +5.6% | neutral | +0.0% | 585.31 | 1.00x |
157 lines
5.1 KiB
TypeScript
157 lines
5.1 KiB
TypeScript
// Await-free async functions complete before their call returns, so the
|
|
// compiler runs them without a fiber. Their promises must still behave
|
|
// exactly like any other settled promise: awaiting one takes the microtask
|
|
// hop, then/catch reactions interleave with queueMicrotask and nextTick in
|
|
// Node's order, rejections surface at the await or reach a catch handler,
|
|
// and a fiberless body may spawn a suspending async function whose
|
|
// continuation resumes later from the event loop.
|
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
|
const log: string[] = [];
|
|
async function syncValue(n: number): Promise<number> {
|
|
log.push("syncValue " + n);
|
|
return n * 2;
|
|
}
|
|
async function syncVoid(label: string): Promise<void> {
|
|
console.log("syncVoid", label);
|
|
}
|
|
async function syncThrow(message: string): Promise<number> {
|
|
console.log("syncThrow", message);
|
|
throw new Error(message);
|
|
}
|
|
async function syncString(s: string): Promise<string> {
|
|
return s + "!";
|
|
}
|
|
async function syncBool(n: number): Promise<boolean> {
|
|
return n > 0;
|
|
}
|
|
interface Rec {
|
|
id: number;
|
|
tags: string[];
|
|
}
|
|
async function syncRecord(id: number): Promise<Rec> {
|
|
return { id, tags: ["t" + id, "u" + id] };
|
|
}
|
|
async function syncError(): Promise<Error> {
|
|
return new Error("made in an await-free body");
|
|
}
|
|
function helper(n: number): number {
|
|
return n + 1;
|
|
}
|
|
async function viaHelper(n: number): Promise<number> {
|
|
return helper(helper(n));
|
|
}
|
|
async function countdown(n: number): Promise<number> {
|
|
if (n === 0) return 0;
|
|
countdown(n - 1).then((v) => console.log("countdown settled", n - 1, v));
|
|
return n;
|
|
}
|
|
async function suspends(label: string): Promise<number> {
|
|
console.log("suspends start", label);
|
|
await null;
|
|
console.log("suspends resumed", label);
|
|
return 7;
|
|
}
|
|
async function fiberlessCallsSuspending(): Promise<number> {
|
|
const p = suspends("inner");
|
|
console.log("fiberless after spawn");
|
|
p.then((v) => console.log("inner settled", v));
|
|
return 1;
|
|
}
|
|
async function chain(label: string, n: number): Promise<number> {
|
|
let total = 0;
|
|
for (let i = 0; i < n; i++) {
|
|
const v = await syncValue(i);
|
|
console.log("chain", label, i, v);
|
|
total += v;
|
|
}
|
|
return total;
|
|
}
|
|
class Service {
|
|
readonly base: number;
|
|
constructor(base: number) {
|
|
this.base = base;
|
|
}
|
|
async get(n: number): Promise<number> {
|
|
return this.base + n;
|
|
}
|
|
async fail(): Promise<string> {
|
|
throw new RangeError("service failure " + this.base);
|
|
}
|
|
}
|
|
|
|
const store = new AsyncLocalStorage<string>();
|
|
async function readStore(): Promise<string> {
|
|
return store.getStore() ?? "none";
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
const a = syncValue(1);
|
|
console.log("after syncValue call:", log.join(","));
|
|
queueMicrotask(() => console.log("microtask A"));
|
|
a.then((v) => console.log("then a", v));
|
|
Promise.resolve(5).then((v) => console.log("resolved then", v));
|
|
process.nextTick(() => console.log("tick"));
|
|
console.log("await a", await a);
|
|
console.log("string", await syncString("x"), "bool", await syncBool(2), await syncBool(-1));
|
|
const record = await syncRecord(3);
|
|
console.log("record", record.id, record.tags.join("|"));
|
|
await syncVoid("v");
|
|
console.log("helper", await viaHelper(1));
|
|
try {
|
|
await syncThrow("bad");
|
|
} catch (e) {
|
|
console.log("caught", (e as Error).message);
|
|
}
|
|
const rejected = syncThrow("later");
|
|
console.log("rejected created");
|
|
rejected.catch((e: unknown) => console.log("catch handler", e instanceof Error ? e.message : e));
|
|
try {
|
|
await rejected;
|
|
} catch (e) {
|
|
console.log("caught again", (e as Error).message);
|
|
}
|
|
const fanout: Promise<number>[] = [syncValue(10), syncValue(11), suspends("all")];
|
|
const all = await Promise.all(fanout);
|
|
console.log("all", all.join(","));
|
|
const mapped = await Promise.all([1, 2, 3].map(syncValue));
|
|
console.log("mapped", mapped.join(","));
|
|
const raced = await Promise.race([suspends("race"), syncValue(20)]);
|
|
console.log("raced", raced);
|
|
const chains: Promise<number>[] = [chain("A", 3), chain("B", 3)];
|
|
const totals = await Promise.all(chains);
|
|
console.log("totals", totals.join(","));
|
|
try {
|
|
try {
|
|
throw new Error("outer");
|
|
} finally {
|
|
syncThrow("in finally").catch((e: unknown) =>
|
|
console.log("finally rejection", e instanceof Error ? e.message : e),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
console.log("outer survived", (e as Error).message);
|
|
}
|
|
console.log("fiberless->suspending", await fiberlessCallsSuspending());
|
|
console.log("countdown", await countdown(3));
|
|
let counter = 0;
|
|
const bump = async (k: number): Promise<number> => {
|
|
counter += k;
|
|
return counter;
|
|
};
|
|
console.log("closure", await bump(2), await bump(3), counter);
|
|
const service = new Service(100);
|
|
console.log("method", await service.get(1));
|
|
try {
|
|
await service.fail();
|
|
} catch (e) {
|
|
console.log("method rejection", e instanceof RangeError, (e as Error).message);
|
|
}
|
|
const error = await syncError();
|
|
console.log("stack", error.message, error.stack!.includes("at syncError"));
|
|
console.log("store", await store.run("scoped", readStore), await readStore());
|
|
}
|
|
|
|
main().then(() => console.log("main done"));
|
|
console.log("sync end");
|