mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-05 08:13:06 +08:00
Compile event-loop metrics, predicate-narrowed catch bindings and other wrapper-reachable gaps (#1433)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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"] },
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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 (`{
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -100,6 +100,7 @@ async function main(): Promise<number> {
|
||||
nodeTest: false,
|
||||
tls: false,
|
||||
cipher: false,
|
||||
perfLoop: false,
|
||||
tlsCa: false,
|
||||
},
|
||||
options.optimization ?? "release",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6390-catch-binding-predicate.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6391-nullish-function-default.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6392-optional-chain-protocol-calls.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6393-function-types-any-params.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6394-in-key-error-subclasses.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6395-generic-object-entries.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6396-generic-any-instantiation.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6397-event-loop-metrics.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6468-generic-unknown-record-entries.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/node-types/event-loop-metrics.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
Vendored
+6
@@ -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"],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
Vendored
+1
@@ -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
@@ -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
@@ -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);
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user