Compile event-loop metrics, predicate-narrowed catch bindings and other wrapper-reachable gaps (#1433)

This commit is contained in:
Chris Tate
2026-10-20 02:16:25 -05:00
committed by GitHub
69 changed files with 1613 additions and 88 deletions
@@ -1 +1,2 @@
- **Performance entries:** `performance.mark()`, `measure()`, and the `getEntries` methods return entries with Node's fields, `toJSON()`, inspection, and identity. `Object.keys`, `for...in`, object spread, and `hasOwnProperty` on an entry are unsupported, and an entry stored in an `unknown` slot prints as a plain object.
- **Event-loop metrics:** `performance.eventLoopUtilization()` counts the main thread's CPU time since the event loop started as active time, so blocking system calls count as idle where Node counts them as active. `monitorEventLoopDelay()` supports `enable()`, `disable()`, `reset()`, `percentile()`, and the `min`, `max`, `mean`, `stddev`, `count`, and `exceeds` readings; the BigInt readings, `percentiles`, and non-literal option objects are unsupported.
+20 -20
View File
@@ -539,9 +539,9 @@
"apiEntries": 131,
"static": {
"supported": 0,
"partial": 19,
"partial": 25,
"refused": 0,
"not-implemented": 112,
"not-implemented": 106,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -54688,9 +54688,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -54803,9 +54803,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -55286,9 +55286,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -55309,9 +55309,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -55677,9 +55677,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -55815,9 +55815,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -7,8 +7,8 @@
"tierItemCount": 3456,
"tiers": {
"static": {
"audit-partial": 1163,
"verify-gap": 2165,
"audit-partial": 1169,
"verify-gap": 2159,
"replace-refusal": 41,
"classify": 2,
"implement": 85
@@ -54259,12 +54259,15 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:perf_hooks",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:perf_hooks.histogram.percentile",
"tests": [
"tests/corpus/6397-event-loop-metrics.ts",
"tests/fixtures/node-types/event-loop-metrics.ts"
]
}
}
},
@@ -54384,12 +54387,15 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:perf_hooks",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:perf_hooks.histogram.reset",
"tests": [
"tests/corpus/6397-event-loop-metrics.ts",
"tests/fixtures/node-types/event-loop-metrics.ts"
]
}
}
},
@@ -54909,12 +54915,15 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:perf_hooks",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:perf_hooks.histogram.disable",
"tests": [
"tests/corpus/6397-event-loop-metrics.ts",
"tests/fixtures/node-types/event-loop-metrics.ts"
]
}
}
},
@@ -54934,12 +54943,15 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:perf_hooks",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:perf_hooks.histogram.enable",
"tests": [
"tests/corpus/6397-event-loop-metrics.ts",
"tests/fixtures/node-types/event-loop-metrics.ts"
]
}
}
},
@@ -55334,12 +55346,15 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:perf_hooks",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "surface-manifest:node-builtin.perf_hooks.monitorEventLoopDelay",
"tests": [
"tests/corpus/6397-event-loop-metrics.ts",
"tests/fixtures/node-types/event-loop-metrics.ts"
]
}
}
},
@@ -55494,12 +55509,15 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:perf_hooks",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:perf_hooks.performance.eventLoopUtilization",
"tests": [
"tests/corpus/6397-event-loop-metrics.ts",
"tests/fixtures/node-types/event-loop-metrics.ts"
]
}
}
},
@@ -566,9 +566,9 @@
"apiEntries": 131,
"static": {
"supported": 0,
"partial": 19,
"partial": 25,
"refused": 0,
"not-implemented": 112,
"not-implemented": 106,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -75524,7 +75524,9 @@
"tests/corpus/5212-perf-marks-measures.ts",
"tests/corpus/5213-perf-timing-errors.ts",
"tests/corpus/5214-perf-entry-inspect.ts",
"tests/corpus/5215-perf-timing-validation.js"
"tests/corpus/5215-perf-timing-validation.js",
"tests/corpus/6397-event-loop-metrics.ts",
"tests/fixtures/node-types/event-loop-metrics.ts"
]
},
"anchorSource": "chapter"
@@ -77565,8 +77567,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:perf_hooks"
"status": "partial",
"evidence": "compiler-feature:perf_hooks.histogram.percentile",
"tests": [
"tests/corpus/6397-event-loop-metrics.ts",
"tests/fixtures/node-types/event-loop-metrics.ts"
]
},
"anchorSource": "exact"
},
@@ -77680,8 +77686,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:perf_hooks"
"status": "partial",
"evidence": "compiler-feature:perf_hooks.histogram.reset",
"tests": [
"tests/corpus/6397-event-loop-metrics.ts",
"tests/fixtures/node-types/event-loop-metrics.ts"
]
},
"anchorSource": "exact"
},
@@ -78163,8 +78173,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:perf_hooks"
"status": "partial",
"evidence": "compiler-feature:perf_hooks.histogram.disable",
"tests": [
"tests/corpus/6397-event-loop-metrics.ts",
"tests/fixtures/node-types/event-loop-metrics.ts"
]
},
"anchorSource": "exact"
},
@@ -78186,8 +78200,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:perf_hooks"
"status": "partial",
"evidence": "compiler-feature:perf_hooks.histogram.enable",
"tests": [
"tests/corpus/6397-event-loop-metrics.ts",
"tests/fixtures/node-types/event-loop-metrics.ts"
]
},
"anchorSource": "exact"
},
@@ -78554,8 +78572,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:perf_hooks"
"status": "partial",
"evidence": "surface-manifest:node-builtin.perf_hooks.monitorEventLoopDelay",
"tests": [
"tests/corpus/6397-event-loop-metrics.ts",
"tests/fixtures/node-types/event-loop-metrics.ts"
]
},
"anchorSource": "exact"
},
@@ -78704,8 +78726,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:perf_hooks"
"status": "partial",
"evidence": "compiler-feature:perf_hooks.performance.eventLoopUtilization",
"tests": [
"tests/corpus/6397-event-loop-metrics.ts",
"tests/fixtures/node-types/event-loop-metrics.ts"
]
},
"anchorSource": "exact"
},
@@ -132,6 +132,8 @@
"node-builtin.perf_hooks.performance.getEntries": ["tests/corpus/5212-perf-marks-measures.ts", "tests/corpus/5214-perf-entry-inspect.ts", "tests/corpus/5215-perf-timing-validation.js"],
"node-builtin.perf_hooks.performance.clearMarks": ["tests/corpus/5212-perf-marks-measures.ts", "tests/corpus/5213-perf-timing-errors.ts", "tests/corpus/5215-perf-timing-validation.js"],
"node-builtin.perf_hooks.performance.clearMeasures": ["tests/corpus/5212-perf-marks-measures.ts"],
"node-builtin.perf_hooks.performance.eventLoopUtilization": ["tests/corpus/6397-event-loop-metrics.ts", "tests/fixtures/node-types/event-loop-metrics.ts"],
"node-builtin.perf_hooks.monitorEventLoopDelay": ["tests/corpus/6397-event-loop-metrics.ts", "tests/fixtures/node-types/event-loop-metrics.ts"],
"node-builtin.process": ["tests/corpus/990-process-basics.ts", "tests/corpus/2314-process-introspection.ts"],
"node-builtin.process.hrtime": ["tests/corpus/process-hrtime.js"],
"node-builtin.process.memoryUsage": ["tests/corpus/5210-process-memory-usage.ts"],
@@ -232,6 +234,8 @@
{ "chapter": "util", "symbols": ["util.parseEnv"], "status": "partial", "evidence": ["tests/corpus/node24-env-parse.ts", "tests/corpus/node24-env-parse-errors.ts", "tests/corpus/node24-env-imports.cjs", "tests/fixtures/node-types/env-files.ts", "tests/harness/project-config.test.ts"] },
{ "chapter": "process", "symbols": ["process.memoryUsage.rss"], "status": "partial", "evidence": ["tests/corpus/5210-process-memory-usage.ts"] },
{ "chapter": "perf_hooks", "symbols": ["performance.now"], "status": "partial", "evidence": ["tests/corpus/2427-perf-hooks-now.ts", "tests/corpus/2446-performance-global.ts"] },
{ "chapter": "perf_hooks", "symbols": ["performance.eventLoopUtilization"], "status": "partial", "evidence": ["tests/corpus/6397-event-loop-metrics.ts", "tests/fixtures/node-types/event-loop-metrics.ts"] },
{ "chapter": "perf_hooks", "symbols": ["histogram.enable", "histogram.disable", "histogram.percentile", "histogram.reset"], "status": "partial", "evidence": ["tests/corpus/6397-event-loop-metrics.ts", "tests/fixtures/node-types/event-loop-metrics.ts"] },
{ "chapter": "perf_hooks", "symbols": ["performance.mark", "performance.measure", "performance.getEntries", "performance.getEntriesByName", "performance.getEntriesByType", "performance.clearMarks", "performance.clearMeasures"], "status": "partial", "evidence": ["tests/corpus/5212-perf-marks-measures.ts", "tests/corpus/5213-perf-timing-errors.ts", "tests/corpus/5214-perf-entry-inspect.ts", "tests/corpus/5215-perf-timing-validation.js"] },
{ "chapter": "perf_hooks", "symbols": ["PerformanceEntry", "PerformanceMark", "PerformanceMeasure"], "status": "partial", "evidence": ["tests/corpus/5212-perf-marks-measures.ts", "tests/corpus/5214-perf-entry-inspect.ts"] },
{ "chapter": "perf_hooks", "parent": "Class: `PerformanceEntry`", "symbols": ["duration", "entryType", "name", "startTime"], "status": "partial", "evidence": ["tests/corpus/5212-perf-marks-measures.ts", "tests/corpus/5214-perf-entry-inspect.ts"] },
+29
View File
@@ -1166,8 +1166,37 @@ declare module "node:perf_hooks" {
getEntriesByType(type: EntryType): PerformanceEntry[];
clearMarks(name?: string): void;
clearMeasures(name?: string): void;
eventLoopUtilization(
utilization1?: EventLoopUtilization,
utilization2?: EventLoopUtilization,
): EventLoopUtilization;
}
export const performance: Performance;
/* Event-loop metrics: utilization (idle/active ms since the loop started)
* and monitorEventLoopDelay's sampling histogram (nanoseconds). */
export interface EventLoopUtilization {
idle: number;
active: number;
utilization: number;
}
export interface EventLoopMonitorOptions {
resolution?: number | undefined;
}
export interface Histogram {
readonly count: number;
readonly exceeds: number;
readonly max: number;
readonly mean: number;
readonly min: number;
readonly stddev: number;
percentile(percentile: number): number;
reset(): void;
}
export interface IntervalHistogram extends Histogram {
enable(): boolean;
disable(): boolean;
}
export function monitorEventLoopDelay(options?: EventLoopMonitorOptions): IntervalHistogram;
}
declare module "perf_hooks" {
export * from "node:perf_hooks";
@@ -40,5 +40,6 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
tls: features.tls,
tlsCa: features.tlsCa,
cipher: features.cipher,
perfLoop: features.perfLoop,
};
}
@@ -52,6 +52,7 @@ export async function compilePackedLibrary(
nodeTest: false,
tls: false,
cipher: false,
perfLoop: false,
tlsCa: false,
};
const selection = await loadRuntimePack({
@@ -2285,6 +2285,7 @@ export class LlEmitter {
if (runsLoop) {
this.declare(`declare zeroext i1 @scr_loop_run(ptr)`);
if (this.mod.commonJsEntry === true) this.declare(`declare void @scr_loop_commonjs_entry()`);
if (this.readsLoopMetrics()) this.declare(`declare void @scr_perf_loop_mark()`);
this.declare(`declare zeroext i1 @scr_report_unhandled_rejections()`);
this.declare(`declare void @scr_discard_unhandled_rejections()`);
}
@@ -2843,6 +2844,8 @@ export class LlEmitter {
...(runsLoop
? [
...(this.mod.commonJsEntry === true ? [` call void @scr_loop_commonjs_entry()`] : []),
// performance.eventLoopUtilization measures from here on.
...(this.readsLoopMetrics() ? [` call void @scr_perf_loop_mark()`] : []),
` %loop_rejection = call zeroext i1 @scr_loop_run(ptr ${asyncEntry ? "%top" : "null"})`,
` %lexc = call zeroext i1 @scr_exc_pending()`,
` br i1 %lexc, label %luncaught, label %lok`,
@@ -7858,6 +7861,20 @@ export class LlEmitter {
/** Moves need plain slots that nothing else reads behind the binding's
* back: no debugger view, no coroutine frame, no borrowed (+0) return. */
/** Whether the program reads event-loop utilization (perf.loopStart /
* perf.idleTime): main then marks the loop's start before running it. */
private loopMetricsRead: boolean | undefined;
private readsLoopMetrics(): boolean {
if (this.loopMetricsRead !== undefined) return this.loopMetricsRead;
const visitor = {
expr: (e: IrExpr): boolean =>
!(e.kind === "libCall" && (e.fn === "perf.loopStart" || e.fn === "perf.idleTime")),
stmt: (): boolean => true,
};
this.loopMetricsRead = !this.mod.functions.every((fn) => everyStmtList(fn.body, visitor));
return this.loopMetricsRead;
}
private lastUseMovesAllowed(): boolean {
return (
this.debug === null &&
@@ -273,6 +273,14 @@ export const LIB_FN_SYMS: Record<string, string> = {
"perf.entryDetail": "scr_perf_entry_detail",
"perf.entryKeep": "scr_perf_entry_keep",
"perf.entryRef": "scr_perf_entry_ref",
"perf.loopStart": "scr_perf_loop_start",
"perf.idleTime": "scr_perf_idle_time",
"perf.eldNew": "scr_eld_new",
"perf.eldEnable": "scr_eld_enable",
"perf.eldDisable": "scr_eld_disable",
"perf.eldReset": "scr_eld_reset",
"perf.eldPercentile": "scr_eld_percentile",
"perf.eldStat": "scr_eld_stat",
"process.memoryUsageTake": "scr_process_memory_usage_take",
"process.memoryUsageField": "scr_process_memory_usage_field",
"process.rss": "scr_process_rss",
@@ -50,6 +50,8 @@ export interface NativeLinkFeatures {
/** crypto.createCipheriv/createDecipheriv: scr_cipher.c plus the mbedTLS
* archive. */
cipher: boolean;
/** perf_hooks event-loop metrics: scr_perf_loop.c. */
perfLoop: boolean;
tlsCa: boolean;
}
@@ -199,6 +199,8 @@ export interface CcOptions {
/** crypto.createCipheriv/createDecipheriv: compiles scr_cipher.c and links
* the mbedTLS archive (shared with tls). */
cipher?: boolean;
/** perf_hooks event-loop metrics: compiles scr_perf_loop.c. */
perfLoop?: boolean;
/** Internal compiler hook: called only after a strict native artifact hit
* or a successful stable build has installed `outPath`. The executable
* frontend cache uses it to publish its stamp after native dependencies
@@ -510,6 +510,8 @@ export async function compileCInternal(
// The performance timeline (every perf_hooks user-timing call selects
// dynAsync, whose unit carries the timeline's buffer warning).
...(opts.dynAsync ? [rt(join(rtDir, "scr_perf.c"))] : []),
// perf_hooks event-loop metrics, only for programs that read them.
...(opts.perfLoop ? [rt(join(rtDir, "scr_perf_loop.c"))] : []),
// Blob and File (every blob.* call selects dynAsync, whose unit boxes
// their reads as dyn promises).
...(opts.dynAsync ? [rt(join(rtDir, "scr_blob.c"))] : []),
@@ -119,6 +119,7 @@ const features = {
tls: false,
tlsCa: false,
cipher: false,
perfLoop: false,
};
test.each([
@@ -56,6 +56,7 @@ const BASE: NativeLinkFeatures = {
tls: false,
tlsCa: false,
cipher: false,
perfLoop: false,
};
async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) {
@@ -61,6 +61,7 @@ export async function warmNativeCaches(
nodeTest: false,
tls: false,
cipher: false,
perfLoop: false,
tlsCa: false,
},
});
@@ -57,6 +57,7 @@ const native: EarlyExecutableNativeFeatures = {
tls: false,
tlsCa: false,
cipher: false,
perfLoop: false,
};
async function fixture(): Promise<{
@@ -79,6 +79,7 @@ export interface EarlyExecutableNativeFeatures {
nodeTest: boolean;
tls: boolean;
cipher: boolean;
perfLoop: boolean;
tlsCa: boolean;
}
@@ -193,6 +194,7 @@ const BOOLEAN_NATIVE_KEYS = [
"tls",
"tlsCa",
"cipher",
"perfLoop",
] as const satisfies readonly (keyof EarlyExecutableNativeFeatures)[];
function validNativeFeatures(value: unknown): value is EarlyExecutableNativeFeatures {
@@ -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;
@@ -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
@@ -134,6 +135,12 @@ export function lowerPerfHooksCall(
loc,
};
}
if (
access.name.text === "eventLoopUtilization" &&
isPerfHooksPerformanceExpr(lowerer, access.expression)
) {
return lowerEventLoopUtilization(lowerer, expr);
}
const timeline = PERF_TIMELINE_CALLS.get(access.name.text);
if (timeline && isPerfHooksPerformanceExpr(lowerer, access.expression)) {
return lowerPerfTimelineCall(lowerer, expr, access.name.text, timeline, loc);
@@ -142,7 +149,7 @@ export function lowerPerfHooksCall(
lowerer.noLowering(
`perf_hooks performance.${access.name.text}`,
expr,
"performance.now() (and its .bind(performance) function value), mark, measure, the getEntries family, clearMarks and clearMeasures are the lowered surface",
"performance.now() (and its .bind(performance) function value), mark, measure, the getEntries family, clearMarks, clearMeasures and eventLoopUtilization are the lowered surface",
);
}
return null;
@@ -513,3 +520,305 @@ export function fencePerfEntryKeyWalk(
`${what} over performance entries (Node keeps their fields as prototype getters, so own-key walks see none; read the fields or call toJSON())`,
);
}
/* ── event-loop metrics: eventLoopUtilization and monitorEventLoopDelay ── */
/** The stdlib IntervalHistogram (monitorEventLoopDelay's result), by the
* receiver's checker type. Its value is the runtime histogram handle. */
function isIntervalHistogram(lowerer: Lowerer, node: ts.Expression): boolean {
const symbol = lowerer.typeOf(node).getSymbol();
return symbol?.name === "IntervalHistogram" && lowerer.isStdlibSymbol(symbol);
}
/** IntervalHistogram's numeric members, as perf.eldStat selectors. */
const HISTOGRAM_STATS: ReadonlyMap<string, number> = new Map([
["min", 0],
["max", 1],
["mean", 2],
["stddev", 3],
["count", 4],
["exceeds", 5],
]);
/** `h.min`, `h.max`, … on an IntervalHistogram: the runtime's current
* statistic (Node's hdr_histogram values; nanoseconds). Null for every
* other read. */
export function lowerHistogramProperty(
lowerer: Lowerer,
expr: ts.PropertyAccessExpression,
): IrExpr | null {
const which = HISTOGRAM_STATS.get(expr.name.text);
if (
which === undefined ||
expr.questionDotToken ||
!isIntervalHistogram(lowerer, expr.expression)
)
return null;
const loc = locOf(expr);
return {
kind: "libCall",
fn: "perf.eldStat",
args: [lowerer.lowerExprExpecting(expr.expression, F64), numLit(which, loc)],
type: F64,
loc,
};
}
/** enable(), disable(), reset() and percentile(p) on an IntervalHistogram.
* Other members (the BigInt twins, the percentiles Map) fence by name. */
export function lowerHistogramMethodCall(
lowerer: Lowerer,
expr: ts.CallExpression,
access: ts.PropertyAccessExpression,
): IrExpr | null {
if (access.questionDotToken || !isIntervalHistogram(lowerer, access.expression)) return null;
const loc = locOf(expr);
const name = access.name.text;
const handle = (): IrExpr => lowerer.lowerExprExpecting(access.expression, F64);
const args = expr.arguments.nodes;
if ((name === "enable" || name === "disable" || name === "reset") && args.length === 0) {
const fn =
name === "enable"
? "perf.eldEnable"
: name === "disable"
? "perf.eldDisable"
: "perf.eldReset";
return { kind: "libCall", fn, args: [handle()], type: name === "reset" ? VOID : BOOL, loc };
}
if (name === "percentile" && args.length === 1 && !ts.isSpreadElement(args[0]!)) {
// Node validates first: NaN, p <= 0 and p > 100 throw ERR_OUT_OF_RANGE.
const h = lowerer.declareHiddenLocal("%histogram", F64);
const p = lowerer.declareHiddenLocal("%percentile", F64);
const pRef = (): IrExpr => varRef(p.id, F64, loc);
const cmp = (op: "!==" | "<=" | ">", left: IrExpr, right: IrExpr): IrExpr => ({
kind: "bin",
op,
left,
right,
type: BOOL,
loc,
});
const or = (left: IrExpr, right: IrExpr): IrExpr => ({
kind: "logical",
op: "||",
left,
right,
type: BOOL,
loc,
});
const outOfRange = or(
or(cmp("!==", pRef(), pRef()), cmp("<=", pRef(), numLit(0, loc))),
cmp(">", pRef(), numLit(100, loc)),
);
const message: IrExpr = {
kind: "strConcat",
left: {
kind: "strLit",
value: 'The value of "percentile" is out of range. It must be > 0 && <= 100. Received ',
type: STRING,
loc,
},
right: { kind: "toString", operand: pRef(), type: STRING, loc },
type: STRING,
loc,
};
const thrown: IrExpr = {
kind: "libCall",
fn: "error.nodeThrow",
args: [
numLit(2, loc),
{ kind: "strLit", value: "ERR_OUT_OF_RANGE", type: STRING, loc },
message,
],
type: F64,
loc,
};
return {
kind: "seqExpr",
stmts: [
{ kind: "varDecl", localId: h.id, init: handle(), loc },
{ kind: "varDecl", localId: p.id, init: lowerer.lowerExprExpecting(args[0]!, F64), loc },
],
result: {
kind: "ternary",
cond: outOfRange,
then: thrown,
else_: {
kind: "libCall",
fn: "perf.eldPercentile",
args: [varRef(h.id, F64, loc), pRef()],
type: F64,
loc,
},
type: F64,
loc,
},
type: F64,
loc,
};
}
lowerer.noLowering(
`IntervalHistogram.${name}`,
expr,
"enable(), disable(), reset(), percentile(p) and the min, max, mean, stddev, count and exceeds readings are the lowered surface",
);
}
/** `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);
if (args.length > 1 || args.some(ts.isSpreadElement)) {
lowerer.noLowering(
"this monitorEventLoopDelay call form",
expr,
"pass at most one options object",
);
}
if (args.length === 1) {
const options = args[0]!;
if (!ts.isObjectLiteralExpression(options)) {
lowerer.noLowering(
"monitorEventLoopDelay with a non-literal options object",
options,
"spell the options inline: monitorEventLoopDelay({ resolution: 20 })",
);
}
for (const property of options.properties.nodes) {
const shorthand = ts.isShorthandPropertyAssignment(property);
if (
(!shorthand && !ts.isPropertyAssignment(property)) ||
!ts.isIdentifier(property.name) ||
property.name.text !== "resolution"
) {
lowerer.noLowering(
"monitorEventLoopDelay options other than resolution",
property,
"resolution is the one option",
);
}
// 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 };
}
/** performance.eventLoopUtilization(u1?, u2?) — Node's
* lib/internal/perf/event_loop_utilization.js over the runtime's loop
* start and idle time, one lifted helper per arity. */
function lowerEventLoopUtilization(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
const loc = locOf(expr);
const args = expr.arguments.nodes;
const resultT = lowerer.mapTypeOf(lowerer.typeOf(expr));
if (resultT?.kind !== "record" || args.length > 2 || args.some(ts.isSpreadElement)) {
lowerer.noLowering("this performance.eventLoopUtilization call form", expr);
}
const helper = eluHelperOf(lowerer, args.length, resultT);
return {
kind: "call",
callee: helper,
args: args.map((arg) => lowerer.lowerExprExpecting(arg, resultT)),
type: resultT,
loc,
};
}
function eluHelperOf(lowerer: Lowerer, arity: number, recT: IrType & { kind: "record" }): string {
const name = `%perf.elu.${arity}`;
if (lowerer.liftedFns.some((f) => f.name === name)) return name;
const loc: SrcLoc = { file: "<builtin>", start: 0, end: 0 };
const f64Local = (id: string) => ({ id, name: id, type: F64, mutable: false });
const params = Array.from({ length: arity }, (_, i) => ({
localId: `u${i + 1}.0`,
name: `u${i + 1}`,
type: recT as IrType,
}));
const locals = [
...params.map((p) => ({ id: p.localId, name: p.name, type: recT as IrType, mutable: false })),
f64Local("ls.0"),
f64Local("idle.0"),
f64Local("active.0"),
];
const ref = (id: string): IrExpr => varRef(id, F64, loc);
const field = (param: string, f: string): IrExpr => ({
kind: "recordGet",
obj: varRef(param, recT, loc),
shapeId: recT.shapeId,
field: f,
type: F64,
loc,
});
const bin = (op: "+" | "-" | "/" | "<=", left: IrExpr, right: IrExpr): IrExpr => ({
kind: "bin",
op,
left,
right,
type: op === "<=" ? BOOL : F64,
loc,
});
const record = (idle: IrExpr, active: IrExpr, utilization: IrExpr): IrExpr => ({
kind: "recordLit",
fields: [
{ name: "idle", value: idle },
{ name: "active", value: active },
{ name: "utilization", value: utilization },
],
type: recT,
loc,
});
const result = (idle: IrExpr, active: IrExpr): IrStmt => ({
kind: "return",
value: record(idle, active, bin("/", active, bin("+", idle, active))),
loc,
});
const decl = (id: string, init: IrExpr): IrStmt => ({ kind: "varDecl", localId: id, init, loc });
const body: IrStmt[] = [
decl("ls.0", { kind: "libCall", fn: "perf.loopStart", args: [], type: F64, loc }),
{
kind: "if",
cond: bin("<=", ref("ls.0"), numLit(0, loc)),
then: [
{ kind: "return", value: record(numLit(0, loc), numLit(0, loc), numLit(0, loc)), loc },
],
else_: null,
loc,
},
];
if (arity === 2) {
body.push(
decl("idle.0", bin("-", field("u1.0", "idle"), field("u2.0", "idle"))),
decl("active.0", bin("-", field("u1.0", "active"), field("u2.0", "active"))),
result(ref("idle.0"), ref("active.0")),
);
} else {
body.push(
decl("idle.0", { kind: "libCall", fn: "perf.idleTime", args: [], type: F64, loc }),
decl(
"active.0",
bin(
"-",
bin("-", { kind: "libCall", fn: "perf.now", args: [], type: F64, loc }, ref("ls.0")),
ref("idle.0"),
),
),
arity === 0
? result(ref("idle.0"), ref("active.0"))
: result(
bin("-", ref("idle.0"), field("u1.0", "idle")),
bin("-", ref("active.0"), field("u1.0", "active")),
),
);
}
lowerer.liftedFns.push({ name, params, returnType: recT, locals, body, loc });
return name;
}
@@ -443,7 +443,12 @@ export function paramShapes(
declParam,
lowerer.runtimeOptionalBindingType(
declParam.name,
lowerer.mapTypeOf(tsType) ?? lowerer.irTypeOf(declParam.name),
lowerer.mapTypeOf(tsType) ??
// A TypeScript instantiation at `any` (a type argument inferred
// from an untyped value) takes the checked-dynamic slot.
((tsType.flags & ts.TypeFlags.Any) !== 0 && !isJsSourceFile(declParam.getSourceFile())
? DYN
: lowerer.irTypeOf(declParam.name)),
),
lowerer.shapes,
lowerer.unions,
@@ -188,7 +188,8 @@ function protocolSignature(
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
): ProtocolSignature | null {
const recvTs = lowerer.typeOf(access.expression);
// A guarded optional step's receiver still carries its nullish arms.
const recvTs = lowerer.checker.getNonNullableType(lowerer.typeOf(access.expression));
const property = lowerer.checker.getPropertyOfType(recvTs, access.name.text);
if (!property) return null;
const declarations = lowerer.checker.declarationsOf(property);
@@ -240,10 +241,11 @@ export function lowerProtocolMethodCall(
access: ts.PropertyAccessExpression,
recv: IrExpr,
): IrExpr | null {
// An optional chain that already guarded this step (`s?.m(x)`, the chain
// re-dispatching with the narrowed receiver) calls like a plain one.
if (
recv.type.kind !== "dyn" ||
call.questionDotToken ||
access.questionDotToken ||
lowerer.chainBlocked(call, access) ||
call.arguments.nodes.some(ts.isSpreadElement)
)
return null;
@@ -302,7 +302,13 @@ export function genericCallInstance(
const retTs = lowerer.checker.getReturnTypeOfSignature(rsig);
const local = localClassResultInstance(lowerer, expr, info, rsig, params, retTs);
if (local) return local;
const mappedReturn = lowerer.mapTypeOf(retTs);
// A result instantiated at `any` (a type argument inferred from an
// untyped value, such as an untyped forwarding call) is checked-dynamic.
const mappedReturn =
lowerer.mapTypeOf(retTs) ??
((retTs.flags & ts.TypeFlags.Any) !== 0 && !isJsSourceFile(info.decl.getSourceFile())
? DYN
: null);
if (!mappedReturn) {
fenceGenericSignatureResult(lowerer, expr, retTs);
lowerer.badType(expr, retTs);
@@ -691,7 +697,11 @@ function unifySignatureBindings(
tsBindings.set(sym, lowerer.typeParamTsResolver(inst) ?? inst);
}
if (!bindings.has(sym)) {
const mapped = lowerer.mapTypeOf(inst);
const mapped =
lowerer.mapTypeOf(inst) ??
((inst.flags & ts.TypeFlags.Any) !== 0 && !isJsSourceFile(info.decl.getSourceFile())
? DYN
: null);
// A visitor's inferred T can be void. It still binds the
// return convention of `(node) => T`; discarding it leaves
// the callback's return type spuriously uninstantiated.
@@ -143,6 +143,7 @@ import {
import {
fencePerfEntryKeyWalk,
isPerfEntryType,
lowerHistogramMethodCall,
lowerPerfHooksCall,
perfEntryTag,
} from "./builtins/performance.js";
@@ -241,7 +242,6 @@ const ZLIB_STREAM_METHODS: ReadonlySet<string> = new Set(["flush", "params", "cl
export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
const loc = locOf(expr);
// Object's checker return type is `any`. Claim known object inputs
// before generic checked-dynamic dispatch tries to load the constructor.
if (
@@ -1379,6 +1379,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
// .bind(performance) function value over the runtime's
// process-start-anchored monotonic clock.
lowerPerfHooksCall(lowerer, expr, expr.expression) ??
lowerHistogramMethodCall(lowerer, expr, expr.expression) ??
// The composed crypto pattern (randomBytes(n).toString(enc))
// — its receiver is a Buffer-typed CALL no other lowering claims.
lowerer.lowerCryptoComposedCall(expr, expr.expression) ??
@@ -3289,7 +3290,13 @@ function lowerDynReceiverMethodCall(
// array — scalar narrowings bridge through maybeNarrow's dynCheck and
// take the ordinary typed paths, but there is no static home for an
// any-elemented array). Typed receivers keep their own lowerings.
const recvTs = lowerer.typeOf(access.expression);
// An optional step the chain already guarded (`s?.m(x)`) has a receiver
// proven non-nullish.
const rawRecvTs = lowerer.typeOf(access.expression);
const recvTs =
access.questionDotToken !== undefined && lowerer.chainHandled.has(access)
? lowerer.checker.getNonNullableType(rawRecvTs)
: rawRecvTs;
if (lowerer.classImplementedProtocol(recvTs)) {
const property = lowerer.checker.getPropertyOfType(recvTs, access.name.text);
if (
@@ -1,3 +1,5 @@
import { lowerHistogramProperty } from "./builtins/performance.js";
import { cleanFuncAdaptable } from "./coercions/function-adapters.js";
import { staticFunctionNameRead } from "./static-function-names.js";
import { argumentsOwner, typedArgumentsArity, typedArgumentsCandidate } from "./typed-arguments.js";
import { dateJsonType, isDateJsonSource } from "./builtins/dates.js";
@@ -1948,6 +1950,11 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
const staticName = staticFunctionNameRead(lowerer, expr);
if (staticName !== null) return { kind: "strLit", value: staticName, type: STRING, loc };
}
// An IntervalHistogram's readings (min, max, mean, stddev, count, exceeds).
{
const reading = lowerHistogramProperty(lowerer, expr);
if (reading) return reading;
}
// `.length` of a forwarded fixed tuple (a typed `arguments`, a tuple
// rest parameter, an alias of either): the signature's arity.
if (expr.name.text === "length" && !expr.questionDotToken) {
@@ -4715,6 +4722,22 @@ function lowerNullishPair(
);
}
}
// An optional callback defaulted to a function of another signature
// (`o.tags ?? (() => ({}))`, fewer parameters or a narrower result): the
// result is the callback's own function type, and the default adapts into
// it (JavaScript ignores surplus arguments). The checker's merged union
// signature would lose the callback's result shape.
if (present?.kind === "func" && present.rest !== true) {
const raw = lowerer.lowerExpr(expr.right);
if (
raw.type.kind === "func" &&
(typeEquals(raw.type, present) || cleanFuncAdaptable(lowerer, raw.type, present))
) {
const right = lowerer.coerceInto(expr.right, raw, present);
lowerer.nullishFuncResults.add(expr);
return { kind: "nullish", left, right, type: present, loc };
}
}
lowerer.unsupported(
"SC1090",
expr,
@@ -12688,6 +12711,19 @@ export function caughtRead(
if (narrowed?.kind === "dyn") {
return { kind: "caughtToDyn", value: ref, type: DYN, loc };
}
// A binding a user type predicate (or an assertion) narrowed to a
// structural type (`isErrorLike(e)` → `{ message: string; … }`, an
// intersection like `TypeError & { code: unknown }`): the thrown value is
// still whatever was thrown, so it crosses as a checked-dynamic value and
// each member read validates its own result, as JavaScript reads it.
const checked = lowerer.typeOf(node);
if (
narrowed?.kind === "record" ||
narrowed?.kind === "union" ||
(narrowed === null && (checked.flags & (ts.TypeFlags.Object | ts.TypeFlags.Intersection)) !== 0)
) {
return { kind: "caughtToDyn", value: ref, type: DYN, loc };
}
lowerer.unsupported("SC1063", node);
}
@@ -13765,6 +13801,24 @@ const OBJECT_PROTOTYPE_KEYS: ReadonlySet<string> = new Set([
* idiom over multiple shapes), index-signature keys, class instances,
* and dyn/unknown stay fenced. Keys are literal strings — a computed key
* over a shape would need the runtime key table. */
/** 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<string> = new Set([
"errno",
"syscall",
"path",
"dest",
"info",
"errors",
"error",
"suppressed",
"actual",
"expected",
"operator",
"generatedMessage",
]);
function lowerInExpression(lowerer: Lowerer, expr: ts.BinaryExpression, loc: SrcLoc): IrExpr {
if (
lowerer.isStdlibGlobal(expr.right, "process") &&
@@ -14000,6 +14054,50 @@ function lowerInExpression(lowerer: Lowerer, expr: ts.BinaryExpression, loc: Src
) {
return { kind: "boolLit", value: true, type: BOOL, loc };
}
// 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)) &&
(recv.kind === "varRef" || recv.kind === "caughtNarrow")
) {
return { kind: "boolLit", value: true, type: BOOL, loc };
}
// Any other key: a member a program class declares. The receiver's
// runtime class is its static class or a descendant; the key is
// present exactly when that class (or an ancestor) declares it. Keys
// built-in errors keep outside their class fields depend on the
// runtime error and stay fenced, as do keys no program class declares.
const declares = (c: ClassInfo): boolean =>
c.fields.has(key) ||
lowerer.findMethodOn(c, key) !== null ||
lowerer.findMethodOn(c, `get:${key}`) !== null ||
lowerer.findMethodOn(c, `set:${key}`) !== null;
if (
(recv.kind === "varRef" || recv.kind === "caughtNarrow") &&
!BUILTIN_ERROR_SLOT_KEYS.has(key) &&
[...lowerer.classes.values()].some((c) => c.builtinError !== true && declares(c))
) {
const staticName = recv.type.className;
const staticInfo = lowerer.classes.get(staticName);
if (staticInfo && declares(staticInfo))
return { kind: "boolLit", value: true, type: BOOL, loc };
const tests: IrExpr[] = [];
for (const c of lowerer.classes.values()) {
const name = c.def.name;
if (name === staticName || !lowerer.isSubclassOf(name, staticName) || !declares(c))
continue;
// A declaring ancestor below the static class already covers it.
if (c.base && c.base.def.name !== staticName && declares(c.base)) continue;
tests.push({ kind: "instanceOf", value: recv, className: name, type: BOOL, loc });
}
return tests.reduce<IrExpr>(
(acc, test) =>
acc.kind === "boolLit"
? test
: { kind: "logical", op: "||", left: acc, right: test, type: BOOL, loc },
{ kind: "boolLit", value: false, type: BOOL, loc },
);
}
lowerer.unsupported(
"SC1090",
expr,
@@ -4887,7 +4887,8 @@ export function lowerVarDecl(
if (
!decl.type &&
init.type.kind === "func" &&
lowerer.typedArgumentsLambdas.has(stripTypeWrappers(decl.initializer))
(lowerer.typedArgumentsLambdas.has(stripTypeWrappers(decl.initializer)) ||
lowerer.nullishFuncResults.has(stripTypeWrappers(decl.initializer)))
)
type = init.type;
// A TypeScript lambda whose closure returns the runtime result (a missing
@@ -1917,6 +1917,9 @@ export class Lowerer {
/** Const bindings holding a forwarded fixed tuple (`const args =
* arguments as any` in a typed-arguments function) → the tuple type. */
readonly tupleAliasSymbols = new Map<ts.Symbol, IrType>();
/** `??` expressions whose result is the optional callback's own function
* type (an adapted function default); their bindings take that type. */
readonly nullishFuncResults = new Set<ts.Node>();
/** The static names of the current generic instance's name-keyed
* function parameters (static-function-names.ts). */
staticFnNames: Map<ts.Symbol, string | null> | null = null;
@@ -2397,6 +2397,27 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [
name: "perf_hooks.performance.clearMeasures",
fns: ["perf.clearMeasures"],
},
{
id: "node-builtin.perf_hooks.performance.eventLoopUtilization",
kind: "node-builtin",
name: "perf_hooks.performance.eventLoopUtilization",
fns: ["perf.loopStart", "perf.idleTime"],
note: "active time is the main thread's CPU time since the loop started; the one- and two-argument difference forms are supported",
},
{
id: "node-builtin.perf_hooks.monitorEventLoopDelay",
kind: "node-builtin",
name: "perf_hooks.monitorEventLoopDelay",
fns: [
"perf.eldNew",
"perf.eldEnable",
"perf.eldDisable",
"perf.eldReset",
"perf.eldPercentile",
"perf.eldStat",
],
note: "an unref'd sampling interval records the delay histogram; enable, disable, reset, percentile and the min, max, mean, stddev, count and exceeds readings are supported",
},
// ── the process global's ambient reads and authority calls
// (lowerProcessProperty/lowerProcessMethodCall — process is a
// provenance-checked global here, not an importable module).
+101 -4
View File
@@ -1556,7 +1556,8 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
// parameter (variadic higher-order wrappers): the checker keeps both
// conditionals symbolic inside the body, and their `infer` positions
// match a call signature, which matchInferReference cannot decide.
const viaSignature = mapBoundSignatureAlias(type, ctx);
const viaSignature =
mapBoundSignatureAlias(type, ctx) ?? mapBoundKeyofNeverConditional(type, ctx);
if (viaSignature !== null) {
contextResolutions++;
return viaSignature;
@@ -3809,6 +3810,27 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
return { kind: "record", shapeId: perfEntryShape(ctx.shapes, ctx.unions) };
}
// readline's getCursorPos() record, in Node's key order ({ cols, rows }).
// perf_hooks' EventLoopUtilization (performance.eventLoopUtilization's
// result): a plain data record in Node's key order.
if (isStdlibInterface("EventLoopUtilization")) {
return {
kind: "record",
shapeId: ctx.shapes.intern(
[
{ name: "active", type: F64 },
{ name: "idle", type: F64 },
{ name: "utilization", type: F64 },
],
false,
undefined,
["idle", "active", "utilization"],
),
};
}
// monitorEventLoopDelay's IntervalHistogram: a numeric handle into the
// runtime's histogram table, like a timer handle. Its members lower over
// the handle (builtins/performance.ts).
if (isStdlibInterface("IntervalHistogram")) return F64;
if (isStdlibInterface("CursorPos")) {
return {
kind: "record",
@@ -4348,6 +4370,22 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null
ts.isParameter(decl) &&
(decl.questionToken !== undefined || decl.initializer !== undefined);
let pt = mapType(checker.getTypeOfSymbol(p), ctx);
// A parameter a TypeScript program types `any` takes whatever the
// caller passes: the checked-dynamic slot, as an `any`-typed binding
// already is (dynFallbackType). Declaration files keep their paths.
// Only function declarations and spelled function types: lambdas and
// methods in object literals keep their existing checked-dynamic story.
if (
!pt &&
!rest &&
decl !== undefined &&
ts.isParameter(decl) &&
decl.parent !== undefined &&
(ts.isFunctionDeclaration(decl.parent) || ts.isFunctionTypeNode(decl.parent)) &&
!decl.getSourceFile().isDeclarationFile &&
(checker.getTypeOfSymbol(p).flags & ts.TypeFlags.Any) !== 0
)
pt = DYN;
if (
decl &&
ts.isParameter(decl) &&
@@ -5101,11 +5139,15 @@ function mapGenericIndexedAccess(type: ts.Type, ctx: TypeMapperCtx): IrType | nu
const obj = type.getObjectType();
if (obj === undefined || !(obj.flags & ts.TypeFlags.TypeParameter)) return null;
const bound = resolveTypeParam(obj);
const idx = type.getIndexType();
if (idx === undefined) return null;
// `T[keyof T]` over a T bound to an unknown-valued record view
// (`Record<string, unknown>` maps to the checked-dynamic value, see
// mapRecordType): every member reads as unknown.
if (bound?.kind === "dyn" && idx.isIndexType() && idx.getTarget() === obj) return DYN;
if (!bound || bound.kind !== "record") return null;
const shape = shapes.get(bound.shapeId);
if (!shape || shape.tuple) return null;
const idx = type.getIndexType();
if (idx === undefined) return null;
let covered: IrType[] | null = null;
if (idx.isStringLiteralType()) {
const f = shape.fields.find((x) => x.name === idx.value);
@@ -5122,7 +5164,62 @@ function mapGenericIndexedAccess(type: ts.Type, ctx: TypeMapperCtx): IrType | nu
}
if (!covered || covered.length === 0) return null;
const first = covered[0]!;
return covered.every((t) => typeEquals(t, first)) ? first : null;
if (covered.every((t) => typeEquals(t, first))) return first;
// Only the `keyof T` index itself covers every field at once; a key
// parameter bound to string keeps its literal-key resolution below.
if (!(idx.isIndexType() && idx.getTarget() === obj)) return null;
// Fields of different scalar types (`Object.entries` over a bound record
// reads `T[keyof T]`): any one of them, the union of their types.
const arms: IrType[] = [];
for (const t of covered) {
const parts = t.kind === "union" ? (ctx.unions.get(t.unionId)?.arms ?? null) : [t];
if (!parts) return null;
for (const part of parts) {
if (
part.kind !== "f64" &&
part.kind !== "string" &&
part.kind !== "bool" &&
part.kind !== "undefinedT" &&
part.kind !== "nullT"
)
return null;
if (!arms.some((a) => typeEquals(a, part))) arms.push(part);
}
}
if (arms.length === 1) return arms[0]!;
arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
return { kind: "union", unionId: ctx.unions.intern(arms) };
}
/** `keyof T extends never ? A : B` over a bound record T (the lib's
* Object.entries element spelling): decided by whether the binding has any
* key. */
function mapBoundKeyofNeverConditional(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
const { resolveTypeParam, shapes } = ctx;
if (!resolveTypeParam || !type.isConditionalType()) return null;
const check = type.getCheckType();
const ext = type.getExtendsType();
if (!check || !ext || !(ext.flags & ts.TypeFlags.Never) || !check.isIndexType()) return null;
const target = check.getTarget();
if (!target || !(target.flags & ts.TypeFlags.TypeParameter)) return null;
const bound = resolveTypeParam(target);
// A T bound to the checked-dynamic value (`Record<string, unknown>`, an
// unknown-valued record view) has no static key set to decide the
// branch, but both branches of the lib's `keyof T extends never ?
// unknown : T[keyof T]` read as unknown there: one answer either way.
if (bound?.kind === "dyn") {
const whenEmpty = type.getTrueType();
const otherwise = type.getFalseType();
const a = whenEmpty ? mapType(whenEmpty, ctx) : null;
const b = otherwise ? mapType(otherwise, ctx) : null;
return a && b && typeEquals(a, b) ? a : null;
}
if (bound?.kind !== "record") return null;
const shape = shapes.get(bound.shapeId);
if (!shape || shape.tuple) return null;
const hasKeys = shape.fields.length > 0 || shape.indexValue !== undefined;
const branch = hasKeys ? type.getFalseType() : type.getTrueType();
return branch ? mapType(branch, ctx) : null;
}
/** A mapped type over a type parameter of the instance being lowered (`{
+1
View File
@@ -554,6 +554,7 @@ async function compileExecutableNative(
tls: features.tls,
tlsCa: features.tlsCa,
cipher: features.cipher,
perfLoop: features.perfLoop,
...(onArtifactReady === undefined ? {} : { onArtifactReady }),
...(ffi === null
? {}
@@ -363,6 +363,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = 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",
@@ -361,6 +361,20 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"perf.entryDetail": { argTypes: [F64], result: DYN },
"perf.entryKeep": { argTypes: [DYN], result: VOID },
"perf.entryRef": { argTypes: [F64], result: DYN },
/** 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 (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 },
"perf.idleTime": { argTypes: [], result: F64 },
"perf.eldNew": { argTypes: [F64], result: F64 },
"perf.eldEnable": { argTypes: [F64], result: BOOL },
"perf.eldDisable": { argTypes: [F64], result: BOOL },
"perf.eldReset": { argTypes: [F64], result: VOID },
"perf.eldPercentile": { argTypes: [F64, F64], result: F64 },
"perf.eldStat": { argTypes: [F64, F64], result: F64 },
/** process.memoryUsage(): memoryUsageTake samples the resident set and
* the runtime heap once (MAY THROW Node's uv_resident_set_memory error)
* and memoryUsageField(k) reads that sample (0 rss, 1 heapTotal, 2
@@ -49,6 +49,9 @@ export interface RuntimeFeatures {
tls: boolean;
tlsCa: boolean;
cipher: boolean;
/** perf_hooks event-loop metrics (eventLoopUtilization,
* monitorEventLoopDelay): scr_perf_loop.c. */
perfLoop: boolean;
}
const DYN_ASYNC_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
@@ -407,6 +410,7 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run
tls: false,
tlsCa: false,
cipher: false,
perfLoop: false,
};
const keepGoing = (): boolean => stopAt === undefined || !features[stopAt];
let tables: TypeTables | undefined;
@@ -502,6 +506,8 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run
}
if (fn.startsWith("dgram.") || fn.startsWith("dns.")) features.dgram = true;
if (fn.startsWith("tlsca.")) features.tlsCa = true;
if (fn.startsWith("perf.loop") || fn === "perf.idleTime" || fn.startsWith("perf.eld"))
features.perfLoop = true;
// scr_cipher.c over the mbedTLS archive; DEP0182 reports through the
// process-warning unit, and unknown output encodings render through
// the inspect unit.
@@ -53,6 +53,11 @@ const LIB_MODE_REFUSED_PREFIXES: readonly [string, string][] = [
["crypto.cipherNew", "crypto.createCipheriv and createDecipheriv"],
["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"],
@@ -157,6 +162,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 };
+1
View File
@@ -100,6 +100,7 @@ async function main(): Promise<number> {
nodeTest: false,
tls: false,
cipher: false,
perfLoop: false,
tlsCa: false,
},
options.optimization ?? "release",
+1
View File
@@ -155,6 +155,7 @@ function validFeatures(value: unknown): value is NativeLinkFeatures {
"tls",
"tlsCa",
"cipher",
"perfLoop",
].every((name) => typeof features[name] === "boolean");
}
@@ -104,6 +104,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")(
tls: false,
tlsCa: false,
cipher: false,
perfLoop: false,
nativeFetch: false,
netEffective: false,
httpEffective: false,
+14
View File
@@ -1847,6 +1847,13 @@
"status": "static",
"note": "recognized module (bare and node:-prefixed specifiers)"
},
{
"id": "node-builtin.perf_hooks.monitorEventLoopDelay",
"kind": "node-builtin",
"name": "perf_hooks.monitorEventLoopDelay",
"status": "static",
"note": "an unref'd sampling interval records the delay histogram; enable, disable, reset, percentile and the min, max, mean, stddev, count and exceeds readings are supported"
},
{
"id": "node-builtin.perf_hooks.performance.clearMarks",
"kind": "node-builtin",
@@ -1859,6 +1866,13 @@
"name": "perf_hooks.performance.clearMeasures",
"status": "static"
},
{
"id": "node-builtin.perf_hooks.performance.eventLoopUtilization",
"kind": "node-builtin",
"name": "perf_hooks.performance.eventLoopUtilization",
"status": "static",
"note": "active time is the main thread's CPU time since the loop started; the one- and two-argument difference forms are supported"
},
{
"id": "node-builtin.perf_hooks.performance.getEntries",
"kind": "node-builtin",
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6390-catch-binding-predicate.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6391-nullish-function-default.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6392-optional-chain-protocol-calls.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6393-function-types-any-params.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6394-in-key-error-subclasses.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6395-generic-object-entries.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6396-generic-any-instantiation.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6397-event-loop-metrics.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6468-generic-unknown-record-entries.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/fixtures/node-types/event-loop-metrics.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/fixtures/node-types/timeout-type.ts"
],
"diags": []
}
@@ -62,6 +62,8 @@ const OPTIONAL = [
["scr_async_dyn.c", any("dynAsync", "dynInvoke", "dc", "fileHandle", "nativeFetch")],
// The performance timeline emits its buffer warning through scr_async_dyn.c.
["scr_perf.c", "dynAsync"],
// perf_hooks event-loop metrics, only for programs that read them.
["scr_perf_loop.c", "perfLoop"],
// Blob and File box their reads as dyn promises through scr_async_dyn.c.
["scr_blob.c", "dynAsync"],
["scr_zlib.c", "zlib"],
+15
View File
@@ -169,6 +169,21 @@ double scr_process_uptime(void);
/* perf_hooks performance.now(): fractional ms since process start (the
* uptime anchor — Node's timeOrigin for a compiled program). */
double scr_perf_now(void);
/* perf_hooks event-loop metrics (scr_perf_loop.c). loop_mark records the
* loop's start (the emitted main calls it before the loop runs, only in
* programs that read the metric); loop_start answers it (0 before) and
* idle_time the idle ms since. The eld_* family is monitorEventLoopDelay's
* IntervalHistogram over a handle; eld_percentile expects 0 < p <= 100; eld_stat which: 0 min, 1 max, 2 mean, 3 stddev,
* 4 count, 5 exceeds. */
void scr_perf_loop_mark(void);
double scr_perf_loop_start(void);
double scr_perf_idle_time(void);
double scr_eld_new(double resolution);
bool scr_eld_enable(double handle);
bool scr_eld_disable(double handle);
void scr_eld_reset(double handle);
double scr_eld_percentile(double handle, double percentile);
double scr_eld_stat(double handle, double which);
/* perf_hooks user timing (scr_perf.c): mark, measure, the getEntries
* family and the clears take the call's dyn argument vector (borrowed)
* and apply Node's validation and errors. mark and measure answer 1 and
+327
View File
@@ -0,0 +1,327 @@
/* node:perf_hooks event-loop metrics: performance.eventLoopUtilization()
* and monitorEventLoopDelay()'s IntervalHistogram.
*
* Event-loop utilization. Node measures idle time as the time its loop
* spends blocked in the poll phase. A compiled program's main thread is
* busy exactly while it runs on the CPU (or blocks in a synchronous call),
* so the thread's CPU time stands in for Node's active time: idle is the
* wall time since the loop started minus that CPU time. The loop start is
* marked by the emitted main just before the loop runs, and only in
* programs that ask for the metric; before it, utilization is all zeros,
* like Node during the main module's synchronous evaluation.
*
* Event-loop delay. Each histogram samples on an unref'd interval at its
* resolution and records the time between samples in nanoseconds, like
* Node's ELDHistogram (Histogram::RecordDelta). Values land in an
* HdrHistogram-shaped counts array (lowest 1, three significant figures:
* 2048 sub-buckets per power of two), so min, max, mean, stddev and
* percentiles answer the bucket values Node's hdr_histogram answers. */
#include "scr_runtime.h"
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifdef _WIN32
#include <windows.h>
#endif
/* ── event-loop utilization ─────────────────────────────────────────── */
static SCR_TL double scr_perf_loop_start_ms = 0; /* performance.now() at loop start */
static SCR_TL double scr_perf_loop_cpu0_ms = 0; /* thread CPU time at loop start */
/* The calling thread's CPU time in milliseconds; 0 where unavailable. */
static double scr_perf_thread_cpu_ms(void) {
#if defined(_WIN32)
FILETIME created, exited, kernel, user;
if (!GetThreadTimes(GetCurrentThread(), &created, &exited, &kernel, &user)) return 0;
ULARGE_INTEGER k, u;
k.LowPart = kernel.dwLowDateTime;
k.HighPart = kernel.dwHighDateTime;
u.LowPart = user.dwLowDateTime;
u.HighPart = user.dwHighDateTime;
return (double)(k.QuadPart + u.QuadPart) / 1e4; /* 100ns units */
#elif defined(CLOCK_THREAD_CPUTIME_ID) && !defined(__wasi__)
struct timespec ts;
if (clock_gettime(CLOCK_THREAD_CPUTIME_ID, &ts) != 0) return 0;
return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1e6;
#else
return 0;
#endif
}
void scr_perf_loop_mark(void) {
if (scr_perf_loop_start_ms > 0) return;
scr_perf_loop_cpu0_ms = scr_perf_thread_cpu_ms();
double now = scr_perf_now();
/* Node's loopStart is positive once set; a mark at the very first
* microsecond still counts as started. */
scr_perf_loop_start_ms = now > 0 ? now : 1e-6;
}
double scr_perf_loop_start(void) { return scr_perf_loop_start_ms; }
double scr_perf_idle_time(void) {
if (scr_perf_loop_start_ms <= 0) return 0;
double wall = scr_perf_now() - scr_perf_loop_start_ms;
double busy = scr_perf_thread_cpu_ms() - scr_perf_loop_cpu0_ms;
double idle = wall - busy;
if (idle < 0) idle = 0;
if (idle > wall) idle = wall;
return idle;
}
/* ── HdrHistogram-shaped counts (lowest 1, 3 significant figures) ───── */
#define SCR_HDR_SUB_BUCKET_HALF_MAG 10
#define SCR_HDR_SUB_BUCKET_HALF (1 << SCR_HDR_SUB_BUCKET_HALF_MAG) /* 1024 */
#define SCR_HDR_SUB_BUCKET_COUNT (SCR_HDR_SUB_BUCKET_HALF * 2) /* 2048 */
#define SCR_HDR_SUB_BUCKET_MASK ((int64_t)SCR_HDR_SUB_BUCKET_COUNT - 1)
typedef struct {
int64_t *counts; /* owned, grown on demand */
size_t counts_len;
int64_t total;
int64_t min_value; /* INT64_MAX while empty */
int64_t max_value; /* 0 while empty */
double count; /* Node's count_ (successful records) */
uint64_t prev_ns; /* RecordDelta's previous sample; 0 = none */
double resolution; /* ms */
double timer; /* the sampling interval's handle while enabled, else 0 */
ScrClosure *tick; /* owned: the sampling callback, identifies this histogram */
bool enabled;
} ScrEld;
static int scr_hdr_bucket_index(int64_t value) {
int pow2ceiling = 64 - __builtin_clzll((uint64_t)(value | SCR_HDR_SUB_BUCKET_MASK));
return pow2ceiling - (SCR_HDR_SUB_BUCKET_HALF_MAG + 1);
}
static size_t scr_hdr_counts_index(int64_t value) {
int bucket = scr_hdr_bucket_index(value);
int64_t sub = value >> bucket;
return (size_t)(((int64_t)(bucket + 1) << SCR_HDR_SUB_BUCKET_HALF_MAG) + (sub - SCR_HDR_SUB_BUCKET_HALF));
}
static int64_t scr_hdr_value_at_index(size_t index) {
int bucket = (int)(index >> SCR_HDR_SUB_BUCKET_HALF_MAG) - 1;
int64_t sub = (int64_t)(index & (SCR_HDR_SUB_BUCKET_HALF - 1)) + SCR_HDR_SUB_BUCKET_HALF;
if (bucket < 0) {
sub -= SCR_HDR_SUB_BUCKET_HALF;
bucket = 0;
}
return sub << bucket;
}
static int64_t scr_hdr_range_size(int64_t value) {
int bucket = scr_hdr_bucket_index(value);
int64_t sub = value >> bucket;
int adjusted = sub >= SCR_HDR_SUB_BUCKET_COUNT ? bucket + 1 : bucket;
return (int64_t)1 << adjusted;
}
static int64_t scr_hdr_lowest_equivalent(int64_t value) {
int bucket = scr_hdr_bucket_index(value);
int64_t sub = value >> bucket;
return sub << bucket;
}
static int64_t scr_hdr_highest_equivalent(int64_t value) {
return scr_hdr_lowest_equivalent(value) + scr_hdr_range_size(value) - 1;
}
static int64_t scr_hdr_median_equivalent(int64_t value) {
return scr_hdr_lowest_equivalent(value) + (scr_hdr_range_size(value) >> 1);
}
/* Newton's square root: keeps this unit free of a libm reference. */
static double scr_eld_sqrt(double x) {
if (!(x > 0)) return x == 0 ? 0 : NAN;
double r = x >= 1 ? x : 1;
for (int i = 0; i < 64; i++) {
double next = 0.5 * (r + x / r);
if (next == r) break;
r = next;
}
return r;
}
static void scr_eld_record(ScrEld *h, int64_t value) {
if (value < 0) return;
size_t index = scr_hdr_counts_index(value);
if (index >= h->counts_len) {
size_t len = h->counts_len ? h->counts_len : SCR_HDR_SUB_BUCKET_COUNT;
while (len <= index) len *= 2;
int64_t *grown = realloc(h->counts, len * sizeof(int64_t));
if (grown == NULL) return;
for (size_t i = h->counts_len; i < len; i++) grown[i] = 0;
h->counts = grown;
h->counts_len = len;
}
h->counts[index]++;
h->total++;
if (value != 0 && value < h->min_value) h->min_value = value;
if (value > h->max_value) h->max_value = value;
h->count += 1;
}
static void scr_eld_clear(ScrEld *h) {
for (size_t i = 0; i < h->counts_len; i++) h->counts[i] = 0;
h->total = 0;
h->min_value = INT64_MAX;
h->max_value = 0;
h->count = 0;
h->prev_ns = 0;
}
/* ── monitorEventLoopDelay ──────────────────────────────────────────── */
static SCR_TL ScrEld **scr_elds;
static SCR_TL size_t scr_elds_len;
static ScrEld *scr_eld_of(double handle) {
if (!(handle >= 1) || handle > (double)scr_elds_len) return NULL;
return scr_elds[(size_t)handle - 1];
}
static void scr_eld_tick(ScrClosure *self) {
for (size_t i = 0; i < scr_elds_len; i++) {
ScrEld *h = scr_elds[i];
if (h->tick != self) continue;
uint64_t now = scr_hrtime_ns();
if (h->prev_ns > 0 && now > h->prev_ns) scr_eld_record(h, (int64_t)(now - h->prev_ns));
h->prev_ns = now;
return;
}
}
/* Histograms live for the thread's lifetime (Node frees them with their
* JS object); release them when the script ends so their callbacks and
* counts do not outlive it. */
static void scr_eld_cleanup(void) {
for (size_t i = 0; i < scr_elds_len; i++) {
ScrEld *h = scr_elds[i];
scr_closure_release(h->tick);
free(h->counts);
free(h);
}
free(scr_elds);
scr_elds = NULL;
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 *));
if (h == NULL || grown == NULL) {
free(h);
if (grown != NULL) scr_elds = grown;
return 0;
}
scr_elds = grown;
h->min_value = INT64_MAX;
h->resolution = resolution;
h->tick = scr_closure_new((void *)scr_eld_tick, 0);
scr_elds[scr_elds_len++] = h;
return (double)scr_elds_len;
}
bool scr_eld_enable(double handle) {
ScrEld *h = scr_eld_of(handle);
if (h == NULL || h->enabled) return false;
h->enabled = true;
h->timer = scr_set_interval(scr_closure_retain(h->tick), h->resolution);
scr_timer_unref(h->timer);
return true;
}
bool scr_eld_disable(double handle) {
ScrEld *h = scr_eld_of(handle);
if (h == NULL || !h->enabled) return false;
h->enabled = false;
scr_clear_interval(h->timer);
h->timer = 0;
return true;
}
void scr_eld_reset(double handle) {
ScrEld *h = scr_eld_of(handle);
if (h != NULL) scr_eld_clear(h);
}
/* hdr_value_at_percentile; the compiler checked Node's 0 < p <= 100 range. */
double scr_eld_percentile(double handle, double percentile) {
ScrEld *h = scr_eld_of(handle);
if (h == NULL || h->total == 0) return 0;
double requested = percentile < 100.0 ? percentile : 100.0;
int64_t wanted = (int64_t)((requested / 100.0) * (double)h->total + 0.5);
if (wanted < 1) wanted = 1;
int64_t seen = 0;
for (size_t i = 0; i < h->counts_len; i++) {
seen += h->counts[i];
if (seen >= wanted) return (double)scr_hdr_highest_equivalent(scr_hdr_value_at_index(i));
}
return 0;
}
/* which: 0 min, 1 max, 2 mean, 3 stddev, 4 count, 5 exceeds. */
double scr_eld_stat(double handle, double which) {
ScrEld *h = scr_eld_of(handle);
int w = (int)which;
if (h == NULL) return w == 0 ? (double)INT64_MAX : w >= 2 && w <= 3 ? NAN : 0;
switch (w) {
case 0:
if (h->counts_len > 0 && h->counts[0] > 0) return 0;
return h->min_value == INT64_MAX ? (double)INT64_MAX
: (double)scr_hdr_lowest_equivalent(h->min_value);
case 1:
return h->max_value == 0 ? 0 : (double)scr_hdr_highest_equivalent(h->max_value);
case 2:
case 3: {
if (h->total == 0) return NAN;
double sum = 0;
for (size_t i = 0; i < h->counts_len; i++) {
if (h->counts[i] == 0) continue;
sum += (double)h->counts[i] * (double)scr_hdr_median_equivalent(scr_hdr_value_at_index(i));
}
double mean = sum / (double)h->total;
if (w == 2) return mean;
double geo = 0;
for (size_t i = 0; i < h->counts_len; i++) {
if (h->counts[i] == 0) continue;
double dev = (double)scr_hdr_median_equivalent(scr_hdr_value_at_index(i)) - mean;
geo += dev * dev * (double)h->counts[i];
}
return scr_eld_sqrt(geo / (double)h->total);
}
case 4:
return h->count;
default:
return 0;
}
}
@@ -0,0 +1,42 @@
// A catch binding narrowed by a user type predicate reads its fields.
interface ErrorLike {
message: string;
name: string;
code?: string;
}
function isErrorLike(value: unknown): value is ErrorLike {
return typeof value === "object" && value !== null && "message" in value && "name" in value;
}
interface Coded {
code: string;
}
function isCoded(value: unknown): value is Coded {
return typeof value === "object" && value !== null && typeof (value as Coded).code === "string";
}
function report(fn: () => void): void {
try {
fn();
} catch (err) {
if (isErrorLike(err)) {
console.log("error", { message: err ? err.message : "none", name: err.name, code: err.code ?? "-" });
if (err.message.includes("disk")) console.log("disk problem");
} else {
console.log("not error-like", String(err));
}
if (isCoded(err) && err.code === "ENOSPC") console.log("coded", err.code);
}
}
report(() => {
throw new Error("disk full");
});
report(() => {
const e = new Error("no space") as Error & { code?: string };
e.code = "ENOSPC";
throw e;
});
report(() => {
throw "plain";
});
report(() => {
throw { message: "obj", name: "Custom", code: "ENOSPC" };
});
@@ -0,0 +1,13 @@
// `??` with a function fallback whose signature differs from the optional
// callback's (fewer parameters, narrower result).
interface Opts {
tags?: (a: number, b: number) => Record<string, unknown>;
label?: (n: number) => string;
}
function run(o: Opts, x: number, y: number): string {
const tags = o.tags ?? (() => ({}));
const label = o.label ?? ((n: number) => `#${n}`);
return `${label(x)} ${JSON.stringify(tags(x, y))}`;
}
console.log(run({}, 1, 2));
console.log(run({ tags: (a, b) => ({ a, b }), label: (n) => `n${n}` }, 3, 4));
@@ -0,0 +1,47 @@
// Optional calls on an interface-typed method chain (a do-nothing tracer).
interface Span {
addTags(tags: Record<string, unknown>): Span;
}
interface Scope {
active(): Span | null;
}
interface Tracer {
scope(): Scope;
}
class NoopSpan implements Span {
addTags(tags: Record<string, unknown>): Span {
console.log("tags", JSON.stringify(tags));
return this;
}
}
class NoopScope implements Scope {
span: Span | null;
constructor(span: Span | null) {
this.span = span;
}
active(): Span | null {
return this.span;
}
}
class NoopTracer implements Tracer {
s: Scope;
constructor(s: Scope) {
this.s = s;
}
scope(): Scope {
return this.s;
}
}
const quiet: Tracer = new NoopTracer(new NoopScope(null));
const loud: Tracer = new NoopTracer(new NoopScope(new NoopSpan()));
function tagActiveSpan(tracer: Tracer, tags: Record<string, unknown>): void {
tracer.scope().active()?.addTags(tags);
}
tagActiveSpan(quiet, { a: 1 });
tagActiveSpan(loud, { b: 2 });
// Narrowed locals and non-null assertions reach the same protocol calls.
const scope2: Scope = new NoopScope(new NoopSpan());
const current = scope2.active();
current?.addTags({ c: 3 });
scope2.active()!.addTags({ d: 4 });
@@ -0,0 +1,8 @@
// Function values whose parameters are typed any (optional or not).
function build(limit?: any): string {
return `limit=${limit ?? "none"}`;
}
const fns: Array<(limit?: any) => string> = [build];
console.log(fns[0]!(), fns[0]!(5));
const take: (v: any) => number = (v) => (typeof v === "number" ? v : -1);
console.log(take(3), take("x"));
@@ -0,0 +1,31 @@
// `'key' in err` on Error receivers, including subclasses that declare it.
class HttpError extends Error {
status: number;
constructor(message: string, status: number) {
super(message);
this.status = status;
}
}
function describe(err: Error): string {
if ("status" in err) return `status ${(err as HttpError).status}`;
if ("code" in err) return "has code";
return `plain ${err.message}`;
}
console.log(describe(new Error("x")));
console.log(describe(new HttpError("nf", 404)));
class Timeout extends HttpError {
get retryable(): boolean {
return true;
}
}
function retryable(err: Error): boolean {
return "retryable" in err;
}
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")));
@@ -0,0 +1,7 @@
// Object.entries over a generic record parameter.
function maskSecrets<T extends Record<string, unknown>>(tags: T): Record<string, unknown> {
return Object.fromEntries(
Object.entries(tags).map(([k, v]) => [k, k.includes("secret") ? "[redacted]" : v]),
);
}
console.log(JSON.stringify(maskSecrets({ user: "ada", secretToken: "x", n: 1 })));
@@ -0,0 +1,18 @@
// A generic method instantiated at `any` from an untyped forwarding call.
type AnyFn = (...args: any) => any;
class Tracer {
trace<T>(name: string, fn: () => T): T {
console.log("span", name);
return fn();
}
}
const tracer = new Tracer();
function traced<F extends AnyFn>(name: string, fn: F): F {
return function (this: unknown) {
const args = arguments;
const result = tracer.trace(name, () => fn.apply(this, args as any));
return result;
} as F;
}
const add = traced("add", (a: number, b: number) => a + b);
console.log(add(1, 2));
+70
View File
@@ -0,0 +1,70 @@
// 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());
const first = performance.eventLoopUtilization();
console.log("before loop", first.idle, first.active, first.utilization);
let last = performance.eventLoopUtilization();
let ticks = 0;
// The unref'd interval keeps nothing alive; this timer holds the loop open
// until the third sample.
const keepAlive = setTimeout(() => console.log("never"), 10_000);
const interval = setInterval(() => {
const now = performance.eventLoopUtilization();
const delta = performance.eventLoopUtilization(now, last);
last = now;
ticks++;
const p50 = histogram.percentile(50);
const p99 = histogram.percentile(99);
console.log(
"tick",
ticks,
delta.utilization >= 0 && delta.utilization <= 1,
delta.idle >= 0,
p50 > 0,
p99 >= p50,
histogram.max >= p99,
histogram.min <= p50,
histogram.count > 0,
);
histogram.reset();
console.log("after reset", histogram.count, histogram.max, histogram.min, Number.isNaN(histogram.mean));
if (ticks === 3) {
clearInterval(interval);
clearTimeout(keepAlive);
console.log("disable", histogram.disable(), histogram.disable());
const since = performance.eventLoopUtilization(first);
console.log("since", since.utilization > 0 && since.utilization <= 1, since.idle > 0);
try {
histogram.percentile(0);
} catch (e) {
console.log((e as Error).message, (e as { code?: string }).code);
}
}
}, 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());
@@ -0,0 +1,33 @@
// Inside a generic body, `Object.entries(tags)` over `T extends
// Record<string, unknown>` reads `[string, T[keyof T]]` elements. An
// instance whose T is the unknown-valued record view reads every member
// as unknown; a record instance reads the union of its member types.
function upper(text: string): string {
return text.toUpperCase();
}
function scrub(text: string): string {
return text.replace(/secret/g, "***");
}
function describe<T extends Record<string, unknown>>(tags: T): string[] {
return Object.entries(tags).map(
([key, value]) => `${key}=${typeof value === "string" ? upper(value) : String(value)}`,
);
}
function redact<T extends Record<string, unknown>>(tags: T): T {
return Object.fromEntries(
Object.entries(tags).map(([key, value]) => [key, typeof value === "string" ? scrub(value) : value]),
) as T;
}
function flatten(value: unknown): Record<string, unknown> {
return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : { value };
}
console.log(describe(flatten({ b: 1, a: "x", c: true })));
console.log(describe(flatten(5)));
console.log(describe({ name: "n", size: 3 }));
const cleaned = redact(flatten({ token: "secret", count: 2 }));
console.log(cleaned, Object.keys(cleaned));
+1
View File
@@ -21,6 +21,7 @@ What the fixture pins (see tests/harness/project-config.test.ts):
(process.uptime, Buffer.from, setInterval) reports the SC2020-family
fence naming @types/node, instead of typechecking its way into a broken
binary — and never a raw "Cannot find name" error.
- `event-loop-metrics.ts` and `timeout-type.ts` — performance.eventLoopUtilization, monitorEventLoopDelay's IntervalHistogram and NodeJS.Timeout as declared types lower under @types/node exactly as under the fallback declarations.
- `source-import/` — an imported TypeScript source may use the `RequestInfo` global supplied by TypeScript's default DOM lib while scriptc adopts the project's @types/node declarations.
Projects WITHOUT @types/node (every other fixture and the whole corpus) keep
+49
View File
@@ -0,0 +1,49 @@
// 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";
const histogram = monitorEventLoopDelay({ resolution: 10 });
console.log("enable", histogram.enable(), histogram.enable());
const first = performance.eventLoopUtilization();
console.log("before loop", first.idle, first.active, first.utilization);
let last = performance.eventLoopUtilization();
let ticks = 0;
// The unref'd interval keeps nothing alive; this timer holds the loop open
// until the third sample.
const keepAlive = setTimeout(() => console.log("never"), 10_000);
const interval: NodeJS.Timeout = setInterval(() => {
const now = performance.eventLoopUtilization();
const delta = performance.eventLoopUtilization(now, last);
last = now;
ticks++;
const p50 = histogram.percentile(50);
const p99 = histogram.percentile(99);
console.log(
"tick",
ticks,
delta.utilization >= 0 && delta.utilization <= 1,
delta.idle >= 0,
p50 > 0,
p99 >= p50,
histogram.max >= p99,
histogram.min <= p50,
histogram.count > 0,
);
histogram.reset();
console.log("after reset", histogram.count, histogram.max, histogram.min, Number.isNaN(histogram.mean));
if (ticks === 3) {
clearInterval(interval);
clearTimeout(keepAlive);
console.log("disable", histogram.disable(), histogram.disable());
const since = performance.eventLoopUtilization(first);
console.log("since", since.utilization > 0 && since.utilization <= 1, since.idle > 0);
try {
histogram.percentile(0);
} catch (e) {
console.log((e as Error).message, (e as { code?: string }).code);
}
}
}, 50);
interval.unref();
console.log("hasRef", interval.hasRef());
+30
View File
@@ -0,0 +1,30 @@
// NodeJS.Timeout as a declared type under @types/node: fields, clearTimeout,
// ref/unref/hasRef, refresh.
class Poller {
private timer: NodeJS.Timeout | undefined;
private count = 0;
start(): void {
this.timer = setInterval(() => {
this.count++;
console.log("poll", this.count);
if (this.count === 3) this.stop();
}, 5);
}
stop(): void {
if (this.timer) {
clearInterval(this.timer);
this.timer = undefined;
console.log("stopped");
}
}
}
new Poller().start();
const t: NodeJS.Timeout = setTimeout(() => console.log("never"), 1000);
console.log(t.hasRef());
t.unref();
console.log(t.hasRef());
t.ref();
console.log(t.hasRef());
clearTimeout(t);
const timers: NodeJS.Timeout[] = [setTimeout(() => console.log("a"), 30), setTimeout(() => console.log("b"), 31)];
console.log(timers.length);
+15
View File
@@ -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(
+4 -1
View File
@@ -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",
+26
View File
@@ -52,6 +52,32 @@ test("node-types: filesystem capacity supports typed number and BigInt results u
expect(comparableStderr(native.stderr)).toBe(node.stderr);
});
for (const [name, title] of [
[
"event-loop-metrics",
"eventLoopUtilization and monitorEventLoopDelay follow @types/node's types",
],
["timeout-type", "NodeJS.Timeout fields, ref/unref/hasRef and clearInterval under @types/node"],
] as const) {
test(`node-types: ${title}`, async () => {
const outDir = outDirFor(name);
const entry = join(nodeTypesDir, `${name}.ts`);
const result = await compile(entry, {
outPath: join(outDir, process.platform === "win32" ? `${name}.exe` : name),
outDir,
sanitize,
});
expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true);
if (!result.ok) return;
const [native, node] = await Promise.all([
execFileAsync(result.binaryPath),
execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]),
]);
expect(native.stdout).toBe(node.stdout);
expect(comparableStderr(native.stderr)).toBe(node.stderr);
});
}
test("node-types: http header arrays and server.address() follow @types/node's types", async () => {
const outDir = outDirFor("http-headers-address");
const entry = join(nodeTypesDir, "http-headers-address.ts");
@@ -42,6 +42,7 @@ const features: NativeLinkFeatures = {
tls: false,
tlsCa: false,
cipher: false,
perfLoop: false,
};
test("native toolchain configuration, staging and helper validation are static", async () => {
@@ -43,6 +43,7 @@ const base: NativeLinkFeatures = {
tls: false,
tlsCa: false,
cipher: false,
perfLoop: false,
};
test("runtime manifest validation and selection are entirely static", async () => {