Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion scripts/node_suite_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def normalize(text: str) -> str:
SLOW_MODULES = {
"http", "http2", "https", "net", "dgram", "tls", "cluster", "dns",
"stream", "child_process", "worker_threads", "inspector",
"inspector-promises", "repl", "diagnostics_channel", "timers", "fetch",
"inspector-promises", "repl", "diagnostics_channel", "timers", "perf_hooks", "fetch",
}

tests = []
Expand Down
74 changes: 74 additions & 0 deletions test-parity/node-suite/perf_hooks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# `node:perf_hooks` parity coverage

This suite uses Node.js **v26.5.0** as its exact behavioral oracle. The audit is
based on that tag's `lib/internal/perf/*`, `lib/internal/histogram.js`,
`test/parallel/test-perf-hooks-*.js`, and `test/sequential/test-perf-hooks.js`.
The comparison sources are Deno **v2.9.2** (`ext/node/polyfills/perf_hooks*`,
`ext/node/ops/perf_hooks.rs`, and `tests/unit_node/perf_hooks_test.ts`) and Bun
**v1.3.14** (`src/js/node/perf_hooks.ts`, the internal event-loop-delay module,
and the primary histogram/perf-hooks tests). Node remains authoritative where
those runtimes deliberately expose a smaller or Web-oriented surface.

## Deterministic contract map

The 148 granular fixtures cover:

- module exports, aliases, global identity, public descriptors, prototypes,
tags, constructors, receivers, and exact supported observer types;
- `now()`, `timeOrigin`, `nodeTiming`, `toJSON()`, and ELU
shape/order/arithmetic invariants without snapshotting host timestamps;
- marks, measures, clears, timeline sorting, repeated-name resolution, detail
cloning, validation, and snapshot/reference identity;
- observer subscription modes, buffering, queue draining, disconnect/reobserve,
replacement, multiple observers, callback/list identity, EntryList filtering,
branding, sorting, and explicit `setImmediate` delivery barriers;
- timerify call/construct behavior, `this`, arguments, result/error identity,
wrapper metadata, nesting, async settlement, observer entries, and optional
histograms;
- recordable histogram empty/populated state, Number/BigInt twins, validation,
percentiles, reset, add/isolation, `recordDelta()`, serialization, and illegal
construction;
- event-loop-delay handle identity, inherited histogram surface, option
validation, enable/disable transitions, reset, and cleanup;
- controlled synthetic resource timing accessors, transfer-size cache modes,
serialization, timeline/observer delivery, validation, and cleanup;
- all stable Node GC constants and Node bootstrap milestone relationships.

The module is in the sequential node-suite lane because observer dispatch,
immediate barriers, timerify settlement, and event-loop histogram lifecycle must
not be perturbed by six concurrent parity jobs.

## Isolation and stopping boundary

Every created mark, measure, resource record, observer, or delay histogram is
cleared, disconnected, or disabled by the fixture that owns it. Time values are
reported only through types, ordering, ranges, or controlled synthetic inputs.
No fixture requires an exact timestamp, duration, percentile, delay,
utilization, or scheduler threshold.

The following remain intentionally outside this deterministic lane:

- GC entries/forced GC/finalization;
- exact event-loop delay or utilization measurements and CPU contention;
- real DNS/HTTP/network resource timings;
- workers, inspector/trace integration, bootstrap timing snapshots;
- races, stress, leak, exhaustion, and large-sample percentile tests.

Node 26.5.0 event-loop-delay histograms do **not** expose `ref()`, `unref()`, or
`hasRef()`; their absence is documented rather than inventing an unsupported
contract. Buffer-full event timing and `recordDelta()` magnitude are not used as
pass conditions because they depend on loop scheduling. Resource timing uses
fully synthetic Fetch Timing Info rather than real I/O.

## Measured provenance

The final 148-fixture tree was executed twice with byte-identical output under
Node 26.5.0: **148/148** in both runs. Two sequential Perry classifications with
the release compiler were also identical: **87 pass, 60 output differences, one
compile failure, zero timeouts**. The compile failure is the isolated named
`eventLoopUtilization` export contract; Perry does not currently export it.

The same fixtures produced **101 pass, 33 differences, 14 runtime errors, zero
timeouts** on Deno 2.9.2, and **97 pass, 24 differences, 27 runtime errors, zero
timeouts** on Bun 1.3.14. Those results describe intentional and incomplete
compatibility surfaces; they do not replace Node as the oracle.
22 changes: 13 additions & 9 deletions test-parity/node-suite/perf_hooks/aaa-time-origin/not-lazy.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
const start = Date.now();
import { performance } from "node:perf_hooks";

setTimeout(() => {
const elapsed = Date.now() - start;
const originAge = Date.now() - performance.timeOrigin;

console.log("elapsed observed:", elapsed >= 15);
console.log("origin includes wait:", originAge + 5 >= elapsed);
console.log("now includes wait:", performance.now() + 5 >= elapsed);
}, 25);
const before = performance.now();
await new Promise<void>((resolve) => setImmediate(resolve));
const after = performance.now();
console.log(
"timeOrigin stable:",
performance.timeOrigin === performance.timeOrigin,
);
console.log("now advances:", after >= before);
console.log(
"origin relation:",
performance.timeOrigin + after >= performance.timeOrigin,
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { performance } from "node:perf_hooks";
try {
performance.clearMeasures(Symbol("x") as any);
console.log("no throw");
} catch (error) {
console.log((error as Error).name);
}
9 changes: 9 additions & 0 deletions test-parity/node-suite/perf_hooks/clear/repeated-name.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { performance } from "node:perf_hooks";
performance.clearMarks();
performance.mark("same", { startTime: 1 });
performance.mark("same", { startTime: 2 });
performance.mark("other", { startTime: 3 });
performance.clearMarks("same");
console.log("same:", performance.getEntriesByName("same", "mark").length);
console.log("other:", performance.getEntriesByName("other", "mark").length);
performance.clearMarks();
7 changes: 7 additions & 0 deletions test-parity/node-suite/perf_hooks/constants/all-gc-values.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { constants } from "node:perf_hooks";
console.log(Object.keys(constants).sort().join(","));
console.log(
Object.entries(constants).sort(([a], [b]) => a.localeCompare(b)).map((
[key, value],
) => `${key}=${value}`).join(","),
);
22 changes: 22 additions & 0 deletions test-parity/node-suite/perf_hooks/entries/argument-validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { performance } from "node:perf_hooks";
function outcome(label: string, fn: () => unknown) {
try {
fn();
console.log(label, "no throw");
} catch (error) {
console.log(label, (error as any).name, (error as any).code);
}
}
outcome("byName missing", () => (performance.getEntriesByName as any)());
outcome("byType missing", () => (performance.getEntriesByType as any)());
outcome(
"byName symbol",
() => performance.getEntriesByName(Symbol("x") as any),
);
outcome(
"byType symbol",
() => performance.getEntriesByType(Symbol("x") as any),
);
performance.mark("1", { startTime: 1 });
console.log("name coerced:", performance.getEntriesByName(1 as any).length);
performance.clearMarks();
16 changes: 16 additions & 0 deletions test-parity/node-suite/perf_hooks/entries/snapshot-identity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { performance } from "node:perf_hooks";
performance.clearMarks();
try {
const entry = performance.mark("identity", { startTime: 1 });
const first = performance.getEntriesByType("mark");
const second = performance.getEntriesByType("mark");
first.length = 0;
console.log("fresh arrays:", first !== second);
console.log("same entries:", second[0] === entry);
console.log(
"mutation isolated:",
performance.getEntriesByType("mark").length,
);
} finally {
performance.clearMarks();
}
16 changes: 16 additions & 0 deletions test-parity/node-suite/perf_hooks/entries/timeline-sorting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { performance } from "node:perf_hooks";
performance.clearMarks();
performance.clearMeasures();
try {
performance.mark("late", { startTime: 20 });
performance.mark("early", { startTime: 5 });
performance.measure("span", { start: 5, duration: 3 });
console.log(
performance.getEntries().map((e) =>
`${e.name}:${e.entryType}:${e.startTime}`
).join(","),
);
} finally {
performance.clearMarks();
performance.clearMeasures();
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
import { performance } from "node:perf_hooks";

const start = performance.eventLoopUtilization();

setTimeout(() => {
const end = performance.eventLoopUtilization();
const twoArg = performance.eventLoopUtilization(end, start);
const oneArg = performance.eventLoopUtilization(end);
const twoSpan = twoArg.idle + twoArg.active;
const oneSpan = oneArg.idle + oneArg.active;

console.log("two arg type:", typeof twoArg.utilization);
console.log("two arg wider than one arg:", twoSpan > oneSpan + 5);
console.log("two arg in range:", twoArg.utilization >= 0 && twoArg.utilization <= 1);
}, 35);
const newer = { idle: 10, active: 20, utilization: 2 / 3 };
const older = { idle: 3, active: 5, utilization: 5 / 8 };
const delta = performance.eventLoopUtilization(newer, older);
console.log("idle:", delta.idle);
console.log("active:", delta.active);
console.log("utilization:", delta.utilization === 15 / 22);
23 changes: 23 additions & 0 deletions test-parity/node-suite/perf_hooks/global/illegal-constructors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {
Performance,
PerformanceEntry,
PerformanceMeasure,
PerformanceObserverEntryList,
PerformanceResourceTiming,
} from "node:perf_hooks";
for (
const [name, Ctor] of Object.entries({
Performance,
PerformanceEntry,
PerformanceMeasure,
PerformanceObserverEntryList,
PerformanceResourceTiming,
})
) {
try {
new (Ctor as any)();
console.log(`${name}: no throw`);
} catch (error) {
console.log(`${name}:`, (error as any).name, (error as any).code);
}
}
9 changes: 9 additions & 0 deletions test-parity/node-suite/perf_hooks/histogram/add-isolation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createHistogram } from "node:perf_hooks";
const source = createHistogram();
const target = createHistogram();
source.record(4);
source.record(8);
console.log("add return:", target.add(source) === undefined);
console.log("copied:", target.count, target.min, target.max);
source.record(16);
console.log("isolated:", target.count, target.max);
10 changes: 10 additions & 0 deletions test-parity/node-suite/perf_hooks/histogram/add-validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createHistogram } from "node:perf_hooks";
const h = createHistogram();
for (const value of [undefined, null, 1, "x", {}, h.toJSON()]) {
try {
h.add(value as any);
console.log(typeof value, "no throw");
} catch (error) {
console.log(typeof value, (error as Error).name, (error as any).code);
}
}
10 changes: 10 additions & 0 deletions test-parity/node-suite/perf_hooks/histogram/empty-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createHistogram } from "node:perf_hooks";
const h = createHistogram();
console.log("counts:", h.count, h.countBigInt === 0n);
console.log(
"bounds:",
h.minBigInt === 9223372036854775807n,
h.maxBigInt === 0n,
);
console.log("exceeds:", h.exceeds, h.exceedsBigInt === 0n);
console.log("nan stats:", Number.isNaN(h.mean), Number.isNaN(h.stddev));
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createHistogram } from "node:perf_hooks";
const h = createHistogram();
console.log("constructor:", h.constructor.name);
try {
new (h.constructor as any)();
console.log("no throw");
} catch (error) {
console.log((error as Error).name, (error as any).code);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { monitorEventLoopDelay } from "node:perf_hooks";
const h = monitorEventLoopDelay();
try {
console.log("surface:", typeof h.reset, typeof h.enable, typeof h.disable);
console.log("initial:", h.count, h.countBigInt === 0n);
console.log("reset:", h.reset() === undefined, h.count);
console.log(
"handle methods:",
typeof (h as any).ref,
typeof (h as any).unref,
typeof (h as any).hasRef,
);
} finally {
h.disable();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { monitorEventLoopDelay } from "node:perf_hooks";
const first = monitorEventLoopDelay();
const second = monitorEventLoopDelay();
try {
console.log("distinct:", first !== second);
console.log("constructors:", first.constructor.name, second.constructor.name);
} finally {
first.disable();
second.disable();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { monitorEventLoopDelay } from "node:perf_hooks";
const h = monitorEventLoopDelay();
try {
console.log("enable:", h.enable(), h.enable());
console.log("disable:", h.disable(), h.disable());
console.log("reenable:", h.enable());
} finally {
h.disable();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { monitorEventLoopDelay } from "node:perf_hooks";
function outcome(label: string, options: any) {
let h: any;
try {
h = monitorEventLoopDelay(options);
console.log(label, "ok");
} catch (error) {
console.log(label, (error as Error).name, (error as any).code);
} finally {
h?.disable();
}
}
outcome("default", undefined);
outcome("null", null);
outcome("number", 1);
outcome("zero", { resolution: 0 });
outcome("fraction", { resolution: 1.5 });
outcome("string", { resolution: "10" });
outcome("valid", { resolution: 10 });
16 changes: 16 additions & 0 deletions test-parity/node-suite/perf_hooks/histogram/options-validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { createHistogram } from "node:perf_hooks";
function outcome(label: string, value: any) {
try {
createHistogram(value);
console.log(label, "ok");
} catch (error) {
console.log(label, (error as Error).name, (error as any).code);
}
}
outcome("valid", { lowest: 1, highest: 11, figures: 1 });
outcome("null", null);
outcome("number", 1);
outcome("lowest string", { lowest: "1" });
outcome("highest null", { highest: null });
outcome("figures fraction", { figures: 1.5 });
outcome("figures high", { figures: 6 });
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createHistogram } from "node:perf_hooks";
const h = createHistogram();
h.record(1);
for (const value of [-1, 101, NaN, "50", null] as const) {
try {
h.percentile(value as any);
console.log(String(value), "no throw");
} catch (error) {
console.log(String(value), (error as Error).name, (error as any).code);
}
}
17 changes: 17 additions & 0 deletions test-parity/node-suite/perf_hooks/histogram/percentiles-maps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { createHistogram } from "node:perf_hooks";
const h = createHistogram();
h.record(2);
h.record(3);
const numbers = h.percentiles;
const bigints = h.percentilesBigInt;
console.log("maps:", numbers instanceof Map, bigints instanceof Map);
console.log(
"keys:",
[...numbers.keys()].join(","),
[...bigints.keys()].join(","),
);
console.log(
"value types:",
[...numbers.values()].every((v) => typeof v === "number"),
[...bigints.values()].every((v) => typeof v === "bigint"),
);
15 changes: 15 additions & 0 deletions test-parity/node-suite/perf_hooks/histogram/percentiles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { createHistogram } from "node:perf_hooks";
const h = createHistogram();
for (const n of [1, 2, 4, 8]) h.record(n);
console.log(
"endpoints:",
h.percentile(1) === h.min,
h.percentile(100) === h.max,
);
console.log(
"bigint endpoints:",
h.percentileBigInt(1) === h.minBigInt,
h.percentileBigInt(100) === h.maxBigInt,
);
console.log("ordered:", h.percentile(25) <= h.percentile(75));
console.log("types:", typeof h.percentile(50), typeof h.percentileBigInt(50));
Loading
Loading