diff --git a/packages/compiler/src/frontend/lowering/builtin-calls.ts b/packages/compiler/src/frontend/lowering/builtin-calls.ts index 7e1852600..7d3a5d41a 100644 --- a/packages/compiler/src/frontend/lowering/builtin-calls.ts +++ b/packages/compiler/src/frontend/lowering/builtin-calls.ts @@ -1,4 +1,5 @@ import { lowerReadlineCursorCall } from "./builtins/readline.js"; +import { lowerMonitorEventLoopDelayCall } from "./builtins/performance.js"; import { lowerEnvironmentData, lowerReceiveMessageOnPort, @@ -69,6 +70,9 @@ export function lowerBuiltinCall( return lowerAddAbortListener(lowerer, expr, loc); } if (bi.module === "events" && bi.member === "once") return lowerEventsOnce(lowerer, expr, loc); + if (bi.module === "perf_hooks" && bi.member === "monitorEventLoopDelay") { + return lowerMonitorEventLoopDelayCall(lowerer, expr); + } if (bi.module === "events") { const helper = lowerEventsEmitterHelper(lowerer, expr, bi.member, loc); if (helper) return helper; diff --git a/packages/compiler/src/frontend/lowering/builtins/performance.ts b/packages/compiler/src/frontend/lowering/builtins/performance.ts index 19b585e37..7aac3b80c 100644 --- a/packages/compiler/src/frontend/lowering/builtins/performance.ts +++ b/packages/compiler/src/frontend/lowering/builtins/performance.ts @@ -18,6 +18,7 @@ import { } from "../../../ir/ir.js"; import { PERF_ENTRY_FIELDS, PERF_ENTRY_MARKER, perfEntryJsonShape } from "../../type-mapper.js"; import { builtinImportOf } from "./module-bindings.js"; +import { lowerOptionalArgument } from "../optional-arguments.js"; import { checkedIterableSpread } from "../checked-iterable-spread.js"; /** True for any spelling of node:perf_hooks' `performance` object — the @@ -663,21 +664,11 @@ export function lowerHistogramMethodCall( ); } -/** `monitorEventLoopDelay(options?)` from node:perf_hooks (a named import - * or a namespace member): a new sampling histogram handle. The resolution - * must be a literal-free number expression or an object literal option. */ -export function lowerMonitorEventLoopDelayCall( - lowerer: Lowerer, - expr: ts.CallExpression, -): IrExpr | null { - const callee = expr.expression; - const bi = ts.isIdentifier(callee) - ? builtinImportOf(lowerer, callee) - : ts.isPropertyAccessExpression(callee) && !callee.questionDotToken - ? lowerer.builtinMemberOf(callee) - : null; - if (bi === null || bi.module !== "perf_hooks" || bi.member !== "monitorEventLoopDelay") - return null; +/** `monitorEventLoopDelay(options?)` from node:perf_hooks, in any import + * spelling (`lowerBuiltinCall`): a new sampling histogram handle. The + * options are an inline object literal; the runtime validates the + * resolution as Node does (an integer of at least 1). */ +export function lowerMonitorEventLoopDelayCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const loc = locOf(expr); const args = expr.arguments.nodes; let resolution: IrExpr = numLit(10, loc); @@ -698,8 +689,9 @@ export function lowerMonitorEventLoopDelayCall( ); } for (const property of options.properties.nodes) { + const shorthand = ts.isShorthandPropertyAssignment(property); if ( - !ts.isPropertyAssignment(property) || + (!shorthand && !ts.isPropertyAssignment(property)) || !ts.isIdentifier(property.name) || property.name.text !== "resolution" ) { @@ -709,7 +701,13 @@ export function lowerMonitorEventLoopDelayCall( "resolution is the one option", ); } - resolution = lowerer.lowerExprExpecting(property.initializer, F64); + // An undefined resolution takes the default, as Node's destructuring. + resolution = lowerOptionalArgument( + lowerer, + shorthand ? property.name : property.initializer, + F64, + numLit(10, loc), + ); } } return { kind: "libCall", fn: "perf.eldNew", args: [resolution], type: F64, loc }; diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index ea3f7dd50..0eff69710 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -1144,6 +1144,24 @@ function lowerPrivateIn( } /** The string keys every record inherits from Object.prototype. */ +/** Keys built-in errors carry outside their class fields: Node's system + * error properties, and AggregateError's, SuppressedError's and + * AssertionError's own members. */ +const BUILTIN_ERROR_SLOT_KEYS: ReadonlySet = new Set([ + "errno", + "syscall", + "path", + "dest", + "info", + "errors", + "error", + "suppressed", + "actual", + "expected", + "operator", + "generatedMessage", +]); + const OBJECT_PROTOTYPE_KEYS: ReadonlySet = new Set([ "constructor", "__defineGetter__", @@ -1418,6 +1436,9 @@ export function lowerInExpression( loc, }; if (key === "message" || key === "name") return always(); + // Every Error has its own `stack`, and Error.prototype's `toString` + // and `constructor` are among Object.prototype's keys. + if (key === "stack" || OBJECT_PROTOTYPE_KEYS.has(key)) return always(); // A member the static class declares is always present. const declares = (c: ClassInfo): boolean => c.fields.has(key) || @@ -1426,12 +1447,12 @@ export function lowerInExpression( lowerer.findMethodOn(c, `set:${key}`) !== null; const staticInfo = lowerer.classes.get(recv.type.className); if (staticInfo && declares(staticInfo)) return always(); - // Any other key: a descendant class that declares it (instanceof), + // Any other key: a descendant class that declares it (instanceof), or // an own property set at runtime (a throw site's extra fields, - // Object.assign, writes through a dynamic view), or an - // Object.prototype member. `stack` stays fenced: Node has one, a - // compiled error has none. - if (key !== "stack") { + // Object.assign, writes through a dynamic view). Keys built-in errors + // keep outside their class fields depend on the runtime error and + // stay fenced. + if (!BUILTIN_ERROR_SLOT_KEYS.has(key)) { const stable = recv.kind === "varRef" || recv.kind === "caughtNarrow"; const slot = stable ? null : lowerer.declareHiddenLocal("%inRecv", recv.type); const ref = (): IrExpr => (slot ? varRef(slot.id, recv.type, loc) : recv); diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 9a9aaa1a8..6862d2d52 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -151,7 +151,6 @@ import { fencePerfEntryKeyWalk, isPerfEntryType, lowerHistogramMethodCall, - lowerMonitorEventLoopDelayCall, lowerPerfHooksCall, perfEntryTag, } from "./builtins/performance.js"; @@ -254,16 +253,6 @@ const ZLIB_STREAM_METHODS: ReadonlySet = new Set(["flush", "params", "cl export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const loc = locOf(expr); - // node:perf_hooks monitorEventLoopDelay (named import or namespace member). - if ( - (ts.isIdentifier(expr.expression) && expr.expression.text === "monitorEventLoopDelay") || - (ts.isPropertyAccessExpression(expr.expression) && - expr.expression.name.text === "monitorEventLoopDelay") - ) { - const histogram = lowerMonitorEventLoopDelayCall(lowerer, expr); - if (histogram) return histogram; - } - // Object's checker return type is `any`. Claim known object inputs // before generic checked-dynamic dispatch tries to load the constructor. if ( diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 18954047f..14bf8760d 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -366,6 +366,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "perf.entries", "perf.clearMarks", "perf.clearMeasures", + // monitorEventLoopDelay's resolution: Node's ERR_OUT_OF_RANGE. + "perf.eldNew", // Non-TTY stdin: Node's stdin has no setRawMode member, so the call is // the exact catchable TypeError. "process.stdinSetRawMode", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index ab0452670..eef85ae07 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -365,7 +365,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /** perf_hooks event-loop metrics (scr_perf_loop.c): loopStart is the * loop's start in performance.now() ms (0 before it), idleTime the idle * ms since. eldNew(resolution ms) creates a monitorEventLoopDelay - * histogram handle; enable/disable answer whether the state changed; + * histogram handle (MAY THROW Node's resolution RangeError); enable/disable answer whether the state changed; * eldPercentile expects Node's validated range; eldStat(which) reads * 0 min, 1 max, 2 mean, 3 stddev, 4 count, 5 exceeds. */ "perf.loopStart": { argTypes: [], result: F64 }, diff --git a/packages/compiler/src/library/ir-policy.ts b/packages/compiler/src/library/ir-policy.ts index 2d1bc85d4..0c343fa98 100644 --- a/packages/compiler/src/library/ir-policy.ts +++ b/packages/compiler/src/library/ir-policy.ts @@ -54,6 +54,11 @@ const LIB_MODE_REFUSED_PREFIXES: readonly [string, string][] = [ ["webcrypto.", "WebCrypto (crypto.subtle)"], ["fs.watch", "fs.watch"], ["watcher.", "fs.watch"], + // The event-loop metrics sample the loop and its timers (scr_perf_loop.c), + // which library links exclude. + ["perf.loopStart", "performance.eventLoopUtilization"], + ["perf.idleTime", "performance.eventLoopUtilization"], + ["perf.eld", "perf_hooks.monitorEventLoopDelay"], ["test.", "the node:test surface"], ["readable.", "the node:stream surface"], ["writable.", "the node:stream surface"], @@ -158,6 +163,7 @@ export function moduleLibAsyncSurface(mod: IrModule): { surface: string; loc: Sr [features.nodeTest, "the node:test surface"], [features.dynAsync, "the checked-dynamic async surface"], [features.dc, "the diagnostics_channel surface"], + [features.perfLoop, "the perf_hooks event-loop metrics"], ]; for (const [on, surface] of coarse) { if (on) return { surface, loc: entryLoc }; diff --git a/packages/runtime/src/scr_perf_loop.c b/packages/runtime/src/scr_perf_loop.c index 29d98ea29..68d3bf10f 100644 --- a/packages/runtime/src/scr_perf_loop.c +++ b/packages/runtime/src/scr_perf_loop.c @@ -21,7 +21,9 @@ #include #include +#include #include +#include #include #ifdef _WIN32 #include @@ -212,7 +214,27 @@ static void scr_eld_cleanup(void) { scr_elds_len = 0; } +/* Node's validateInteger(resolution, 'options.resolution', 1): a catchable + * RangeError (ERR_OUT_OF_RANGE) for a non-integer or a value out of range, + * with the value as util.inspect renders it ("-0" included). */ +static bool scr_eld_resolution_ok(double resolution) { + bool integer = isfinite(resolution) && trunc(resolution) == resolution; + if (integer && resolution >= 1 && resolution <= 9007199254740991.0) return true; + char received[48]; + if (resolution == 0 && signbit(resolution)) + memcpy(received, "-0", 3); + else + scr_num_received(resolution, received); + char msg[192]; + int len = snprintf(msg, sizeof msg, + "The value of \"options.resolution\" is out of range. It must be %s. Received %s", + integer ? ">= 1 && <= 9007199254740991" : "an integer", received); + scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE"); + return false; +} + double scr_eld_new(double resolution) { + if (!scr_eld_resolution_ok(resolution)) return 0; if (scr_elds_len == 0 && scr_elds == NULL) scr_atexit(scr_eld_cleanup); ScrEld *h = calloc(1, sizeof(ScrEld)); ScrEld **grown = realloc(scr_elds, (scr_elds_len + 1) * sizeof(ScrEld *)); @@ -223,7 +245,7 @@ double scr_eld_new(double resolution) { } scr_elds = grown; h->min_value = INT64_MAX; - h->resolution = resolution >= 1 ? resolution : 10; + h->resolution = resolution; h->tick = scr_closure_new((void *)scr_eld_tick, 0); scr_elds[scr_elds_len++] = h; return (double)scr_elds_len; diff --git a/tests/corpus/6394-in-key-error-subclasses.ts b/tests/corpus/6394-in-key-error-subclasses.ts index cdb28a5a3..5ade6bcd1 100644 --- a/tests/corpus/6394-in-key-error-subclasses.ts +++ b/tests/corpus/6394-in-key-error-subclasses.ts @@ -23,3 +23,9 @@ function retryable(err: Error): boolean { } console.log(retryable(new Error("x")), retryable(new HttpError("a", 500)), retryable(new Timeout("t", 504))); console.log(describe(new Timeout("t", 504))); +// Keys every Error has: its own stack, and Error.prototype's and +// Object.prototype's members. +function inherent(err: Error): boolean[] { + return ["stack" in err, "toString" in err, "constructor" in err, "hasOwnProperty" in err]; +} +console.log(inherent(new Error("s")), inherent(new HttpError("h", 400)), inherent(new TypeError("t"))); diff --git a/tests/corpus/6397-event-loop-metrics.ts b/tests/corpus/6397-event-loop-metrics.ts index e1c3829ac..8477dcc48 100644 --- a/tests/corpus/6397-event-loop-metrics.ts +++ b/tests/corpus/6397-event-loop-metrics.ts @@ -1,6 +1,8 @@ // Event-loop monitoring under @types/node: monitorEventLoopDelay's histogram, // eventLoopUtilization deltas, and an unref'd interval typed NodeJS.Timeout. import { monitorEventLoopDelay, performance } from "node:perf_hooks"; +import * as perfHooks from "node:perf_hooks"; +import { monitorEventLoopDelay as eld } from "node:perf_hooks"; const histogram = monitorEventLoopDelay({ resolution: 10 }); console.log("enable", histogram.enable(), histogram.enable()); @@ -47,3 +49,22 @@ const interval = setInterval(() => { }, 50); interval.unref(); console.log("hasRef", interval.hasRef()); + +// Other import spellings, and Node's resolution validation. +const aliased = eld(); +const namespaced = perfHooks.monitorEventLoopDelay({ resolution: 20 }); +console.log("spellings", aliased.enable(), namespaced.enable(), aliased.disable(), namespaced.disable()); +for (const resolution of [0, -0, -1, 1.5, NaN, Infinity, 1e16, 1]) { + try { + eld({ resolution }); + console.log("created", resolution); + } catch (e) { + console.log((e as Error).name, (e as { code?: string }).code, (e as Error).message); + } +} +// An undefined resolution takes Node's default. +function withResolution(resolution?: number): boolean { + const h = monitorEventLoopDelay({ resolution }); + return h.enable() && h.disable(); +} +console.log("defaults", withResolution(), withResolution(20), eld({ resolution: undefined }).enable()); diff --git a/tests/harness/library-asyncfree.test.ts b/tests/harness/library-asyncfree.test.ts index 0adbef7bc..7c4d029cc 100644 --- a/tests/harness/library-asyncfree.test.ts +++ b/tests/harness/library-asyncfree.test.ts @@ -73,6 +73,21 @@ describe("async_free detection over the IR", () => { ); }); + test("the event-loop metrics refuse", async () => { + expect( + await surfaceOf( + "eld", + `import { monitorEventLoopDelay } from "node:perf_hooks";\nconst h = monitorEventLoopDelay();\nconsole.log(h.enable());\n`, + ), + ).toContain("monitorEventLoopDelay"); + expect( + await surfaceOf( + "elu", + `import { performance } from "node:perf_hooks";\nconsole.log(performance.eventLoopUtilization().idle >= 0);\n`, + ), + ).toContain("eventLoopUtilization"); + }); + test("child_process refuses even in its synchronous spelling", async () => { expect( await surfaceOf( diff --git a/tests/harness/library-profile.test.ts b/tests/harness/library-profile.test.ts index eb1bcc4cd..2b28a6cd0 100644 --- a/tests/harness/library-profile.test.ts +++ b/tests/harness/library-profile.test.ts @@ -569,10 +569,13 @@ describe("library profile fences", () => { expect(processIds).toContain("node-builtin.process.isTTY"); expect(r.profile.fences[1]!.surfaces.every((s) => s.detector !== undefined)).toBe(true); // The user-timing timeline reads the same clock (mark() and measure() - // default to performance.now()), so it joins the perf_hooks family. + // default to performance.now()), and so do the event-loop metrics, so + // they join the perf_hooks family. expect(r.profile.fences[2]!.surfaces.map((s) => s.id)).toEqual([ + "node-builtin.perf_hooks.monitorEventLoopDelay", "node-builtin.perf_hooks.performance.clearMarks", "node-builtin.perf_hooks.performance.clearMeasures", + "node-builtin.perf_hooks.performance.eventLoopUtilization", "node-builtin.perf_hooks.performance.getEntries", "node-builtin.perf_hooks.performance.mark", "node-builtin.perf_hooks.performance.measure",