From 0a2a436b7056a4efc2e8dc0ec36a8acd2259679f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 22 May 2026 11:56:51 +0200 Subject: [PATCH] test(parity): expand node:perf_hooks coverage + input validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to #1321 (the perf_hooks implementation). Adds 13 granular node-suite cases plus the runtime behavior they require: - mark/measure instanceof PerformanceEntry / PerformanceMark / PerformanceMeasure - getEntries() unfiltered; getEntriesByName / getEntriesByType no-match - measure({ start, duration, detail }) detail handling - observer: observe 'measure', takeRecords(), and multiple entryTypes in one delivery - eventLoopUtilization() first-call shape - input validation matching Node: performance.mark() / clearMarks() throw a TypeError on a Symbol name; mark() throws on a non-numeric startTime - getEntries* and observer lists now return entries ordered by startTime (Node returns the timeline sorted, stable on ties) 31/33 pass. Two deferrals, recorded in test-parity/known_failures.json: observer/entry-types (typeof obs.observe — native-class member reads lower as calls, #1320) and global/performance-identity (globalThis.performance identity, #1327). --- crates/perry-runtime/src/perf_hooks.rs | 43 +++++++++++++++++-- test-parity/known_failures.json | 6 +++ .../perf_hooks/entries/by-name-no-match.ts | 5 +++ .../perf_hooks/entries/get-entries-all.ts | 8 ++++ .../errors/clear-marks-symbol-throws.ts | 9 ++++ .../errors/mark-invalid-start-time.ts | 9 ++++ .../perf_hooks/errors/mark-symbol-throws.ts | 9 ++++ .../perf_hooks/eventlooputil/first-call.ts | 7 +++ .../perf_hooks/global/performance-identity.ts | 5 +++ .../node-suite/perf_hooks/mark/instanceof.ts | 5 +++ .../perf_hooks/measure/detail-option.ts | 8 ++++ .../perf_hooks/measure/instanceof.ts | 7 +++ .../observer/multiple-entry-types.ts | 15 +++++++ .../perf_hooks/observer/observe-measures.ts | 13 ++++++ .../perf_hooks/observer/take-records.ts | 9 ++++ 15 files changed, 154 insertions(+), 4 deletions(-) create mode 100644 test-parity/node-suite/perf_hooks/entries/by-name-no-match.ts create mode 100644 test-parity/node-suite/perf_hooks/entries/get-entries-all.ts create mode 100644 test-parity/node-suite/perf_hooks/errors/clear-marks-symbol-throws.ts create mode 100644 test-parity/node-suite/perf_hooks/errors/mark-invalid-start-time.ts create mode 100644 test-parity/node-suite/perf_hooks/errors/mark-symbol-throws.ts create mode 100644 test-parity/node-suite/perf_hooks/eventlooputil/first-call.ts create mode 100644 test-parity/node-suite/perf_hooks/global/performance-identity.ts create mode 100644 test-parity/node-suite/perf_hooks/mark/instanceof.ts create mode 100644 test-parity/node-suite/perf_hooks/measure/detail-option.ts create mode 100644 test-parity/node-suite/perf_hooks/measure/instanceof.ts create mode 100644 test-parity/node-suite/perf_hooks/observer/multiple-entry-types.ts create mode 100644 test-parity/node-suite/perf_hooks/observer/observe-measures.ts create mode 100644 test-parity/node-suite/perf_hooks/observer/take-records.ts diff --git a/crates/perry-runtime/src/perf_hooks.rs b/crates/perry-runtime/src/perf_hooks.rs index 1da00ba2e..8ed505950 100644 --- a/crates/perry-runtime/src/perf_hooks.rs +++ b/crates/perry-runtime/src/perf_hooks.rs @@ -95,6 +95,15 @@ fn num_of(v: JSValue) -> Option { } } +/// Throw a `TypeError` with `msg` (catchable by user `try/catch` as a +/// TypeError, matching Node's input-validation errors). Never returns. +fn throw_type_error(msg: &str) -> ! { + let msg_str = crate::string::js_string_from_bytes(msg.as_ptr(), msg.len() as u32); + let err_ptr = crate::error::js_typeerror_new(msg_str); + let err_value = JSValue::pointer(err_ptr as *const u8).bits(); + crate::exception::js_throw(f64::from_bits(err_value)) +} + /// Build a NaN-boxed string value from a Rust `&str`. fn str_value(s: &str) -> JSValue { let ptr = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); @@ -204,12 +213,21 @@ fn as_object_ptr(v: f64) -> Option<*const crate::object::ObjectHeader> { #[no_mangle] pub extern "C" fn js_perf_mark(name_val: f64, options_val: f64) -> f64 { unsafe { + // A Symbol name cannot be coerced to a string (Node throws TypeError). + if crate::symbol::js_is_symbol(name_val) != 0 { + throw_type_error("Cannot convert a Symbol value to a string"); + } let name = coerce_to_string(name_val); let mut start_time = perf_now(); let mut detail_bits = JSValue::null().bits(); if let Some(opts) = as_object_ptr(options_val) { - if let Some(st) = option_number(opts, "startTime") { - start_time = st; + // startTime, when present, must be a finite number (Node: + // ERR_INVALID_ARG_TYPE → a TypeError). + if option_present(opts, "startTime") { + match option_number(opts, "startTime") { + Some(st) => start_time = st, + None => throw_type_error("The \"startTime\" option must be of type number"), + } } detail_bits = option_detail_bits(opts); } @@ -305,8 +323,18 @@ unsafe fn finish_measure(name: String, start_time: f64, duration: f64, detail_bi } // ── getEntries / getEntriesByType / getEntriesByName ───────────────────────── +/// Order entries by startTime ascending, stable on ties (matches the order +/// Node returns from `getEntries*` and observer lists). +fn sort_entries_by_start_time(entries: &mut [PerfEntry]) { + entries.sort_by(|a, b| { + a.start_time + .partial_cmp(&b.start_time) + .unwrap_or(std::cmp::Ordering::Equal) + }); +} + unsafe fn entries_to_array(filter: impl Fn(&PerfEntry) -> bool) -> f64 { - let snapshot: Vec = PERF_ENTRIES.with(|store| { + let mut snapshot: Vec = PERF_ENTRIES.with(|store| { store .borrow() .iter() @@ -314,6 +342,8 @@ unsafe fn entries_to_array(filter: impl Fn(&PerfEntry) -> bool) -> f64 { .cloned() .collect() }); + // Node returns timeline entries ordered by startTime (stable on ties). + sort_entries_by_start_time(&mut snapshot); let mut arr = crate::array::js_array_alloc(snapshot.len() as u32); for e in &snapshot { let obj = entry_to_object(e); @@ -368,6 +398,10 @@ pub extern "C" fn js_perf_get_entries_by_name(name_val: f64, type_val: f64) -> f // `clearMarks()` / `clearMarks(undefined)` clear all marks; `clearMarks(name)` // clears only same-named marks (Node parity). Return `undefined`. unsafe fn clear_entries(entry_type: u8, name_val: f64) -> f64 { + // A Symbol name cannot be coerced to a string (Node throws TypeError). + if crate::symbol::js_is_symbol(name_val) != 0 { + throw_type_error("Cannot convert a Symbol value to a string"); + } let name = if JSValue::from_bits(name_val.to_bits()).is_undefined() { None } else { @@ -666,8 +700,9 @@ pub extern "C" fn js_perf_observer_flush_all( /// Build an array from the in-flight observer `list` entries (for the /// `perf_observer_list` namespace methods). pub unsafe fn current_list_to_array(filter: impl Fn(&PerfEntry) -> bool) -> f64 { - let snapshot: Vec = + let mut snapshot: Vec = CURRENT_LIST.with(|c| c.borrow().iter().filter(|e| filter(e)).cloned().collect()); + sort_entries_by_start_time(&mut snapshot); let mut arr = crate::array::js_array_alloc(snapshot.len() as u32); for e in &snapshot { let obj = entry_to_object(e); diff --git a/test-parity/known_failures.json b/test-parity/known_failures.json index f225c78f9..538b60767 100644 --- a/test-parity/known_failures.json +++ b/test-parity/known_failures.json @@ -14,6 +14,12 @@ "category": "bug-open", "reason": "node:perf_hooks granular parity: typeof obs.observe/disconnect/takeRecords reports the wrong type because member reads on a native-class instance lower as a method call (not a PropertyGet). The call forms work (observe-marks PASSES). Flips to PASS when #1320 lands." }, + "node-suite/perf_hooks/global/performance-identity": { + "issue": "1327", + "added": "2026-05-22", + "category": "bug-open", + "reason": "node:perf_hooks granular parity: globalThis.performance !== the node:perf_hooks named import. The import resolves to a fresh namespace object per read and the global is a separate path; neither is a cached singleton. Flips to PASS when #1327 lands." + }, "node-suite/console/dir/depth-options": { "issue": "1199", "added": "2026-05-20", diff --git a/test-parity/node-suite/perf_hooks/entries/by-name-no-match.ts b/test-parity/node-suite/perf_hooks/entries/by-name-no-match.ts new file mode 100644 index 000000000..dd887f5ab --- /dev/null +++ b/test-parity/node-suite/perf_hooks/entries/by-name-no-match.ts @@ -0,0 +1,5 @@ +import { performance } from "node:perf_hooks"; +// getEntriesByName / getEntriesByType return an empty array when nothing matches. +performance.mark("a"); +console.log("no name:", performance.getEntriesByName("nope").length); +console.log("no type:", performance.getEntriesByType("measure").length); diff --git a/test-parity/node-suite/perf_hooks/entries/get-entries-all.ts b/test-parity/node-suite/perf_hooks/entries/get-entries-all.ts new file mode 100644 index 000000000..357d84b1b --- /dev/null +++ b/test-parity/node-suite/perf_hooks/entries/get-entries-all.ts @@ -0,0 +1,8 @@ +import { performance } from "node:perf_hooks"; +// getEntries() returns the whole timeline (marks + measures), unfiltered. +performance.mark("a", { startTime: 0 }); +performance.mark("b", { startTime: 5 }); +performance.measure("ab", "a", "b"); +const all = performance.getEntries(); +console.log("total:", all.length); +console.log("types:", all.map((e) => e.entryType).join(",")); diff --git a/test-parity/node-suite/perf_hooks/errors/clear-marks-symbol-throws.ts b/test-parity/node-suite/perf_hooks/errors/clear-marks-symbol-throws.ts new file mode 100644 index 000000000..1b62c4dca --- /dev/null +++ b/test-parity/node-suite/perf_hooks/errors/clear-marks-symbol-throws.ts @@ -0,0 +1,9 @@ +import { performance } from "node:perf_hooks"; +// performance.clearMarks(Symbol()) must throw — the name argument cannot be a +// Symbol. (Node throws a TypeError.) +try { + performance.clearMarks(Symbol("f") as any); + console.log("threw: false"); +} catch (e) { + console.log("threw:", e instanceof TypeError); +} diff --git a/test-parity/node-suite/perf_hooks/errors/mark-invalid-start-time.ts b/test-parity/node-suite/perf_hooks/errors/mark-invalid-start-time.ts new file mode 100644 index 000000000..488c8e42a --- /dev/null +++ b/test-parity/node-suite/perf_hooks/errors/mark-invalid-start-time.ts @@ -0,0 +1,9 @@ +import { performance } from "node:perf_hooks"; +// mark(name, { startTime }) must reject a non-numeric startTime with a +// TypeError (Node: code ERR_INVALID_ARG_TYPE). +try { + performance.mark("a", { startTime: "x" as any }); + console.log("threw: false"); +} catch (e) { + console.log("threw:", e instanceof TypeError); +} diff --git a/test-parity/node-suite/perf_hooks/errors/mark-symbol-throws.ts b/test-parity/node-suite/perf_hooks/errors/mark-symbol-throws.ts new file mode 100644 index 000000000..78b43d0bb --- /dev/null +++ b/test-parity/node-suite/perf_hooks/errors/mark-symbol-throws.ts @@ -0,0 +1,9 @@ +import { performance } from "node:perf_hooks"; +// performance.mark(Symbol()) must throw — a Symbol cannot be coerced to a +// string. (Node throws a TypeError.) +try { + performance.mark(Symbol("s") as any); + console.log("threw: false"); +} catch (e) { + console.log("threw:", e instanceof TypeError); +} diff --git a/test-parity/node-suite/perf_hooks/eventlooputil/first-call.ts b/test-parity/node-suite/perf_hooks/eventlooputil/first-call.ts new file mode 100644 index 000000000..74ab5d3fa --- /dev/null +++ b/test-parity/node-suite/perf_hooks/eventlooputil/first-call.ts @@ -0,0 +1,7 @@ +import { performance } from "node:perf_hooks"; +// The first (no-arg) eventLoopUtilization() reading is a self-consistent +// triple: all numbers, utilization in [0, 1]. +const elu = performance.eventLoopUtilization(); +console.log("idle:", typeof elu.idle); +console.log("active:", typeof elu.active); +console.log("utilization in range:", elu.utilization >= 0 && elu.utilization <= 1); diff --git a/test-parity/node-suite/perf_hooks/global/performance-identity.ts b/test-parity/node-suite/perf_hooks/global/performance-identity.ts new file mode 100644 index 000000000..cf43cf8c0 --- /dev/null +++ b/test-parity/node-suite/perf_hooks/global/performance-identity.ts @@ -0,0 +1,5 @@ +import { performance } from "node:perf_hooks"; +// The named import and the global are the same object (Node guarantee: +// globalThis.performance === require("perf_hooks").performance). +console.log("same object:", (globalThis as any).performance === performance); +console.log("global now is fn:", typeof (globalThis as any).performance.now); diff --git a/test-parity/node-suite/perf_hooks/mark/instanceof.ts b/test-parity/node-suite/perf_hooks/mark/instanceof.ts new file mode 100644 index 000000000..b18ffaab4 --- /dev/null +++ b/test-parity/node-suite/perf_hooks/mark/instanceof.ts @@ -0,0 +1,5 @@ +import { performance, PerformanceEntry, PerformanceMark } from "node:perf_hooks"; +// A mark is both a PerformanceEntry and a PerformanceMark. +const m = performance.mark("x"); +console.log("PerformanceEntry:", m instanceof PerformanceEntry); +console.log("PerformanceMark:", m instanceof PerformanceMark); diff --git a/test-parity/node-suite/perf_hooks/measure/detail-option.ts b/test-parity/node-suite/perf_hooks/measure/detail-option.ts new file mode 100644 index 000000000..443758d79 --- /dev/null +++ b/test-parity/node-suite/perf_hooks/measure/detail-option.ts @@ -0,0 +1,8 @@ +import { performance } from "node:perf_hooks"; +// measure(name, { start, duration, detail }) attaches detail and honors the +// numeric start/duration directly. +const m = performance.measure("md", { start: 0, duration: 3, detail: { k: 1 } }); +console.log("name:", m.name); +console.log("startTime:", m.startTime); +console.log("duration:", m.duration); +console.log("detail:", m.detail); diff --git a/test-parity/node-suite/perf_hooks/measure/instanceof.ts b/test-parity/node-suite/perf_hooks/measure/instanceof.ts new file mode 100644 index 000000000..bcbf60a13 --- /dev/null +++ b/test-parity/node-suite/perf_hooks/measure/instanceof.ts @@ -0,0 +1,7 @@ +import { performance, PerformanceEntry, PerformanceMeasure } from "node:perf_hooks"; +// A measure is both a PerformanceEntry and a PerformanceMeasure. +performance.mark("a", { startTime: 0 }); +performance.mark("b", { startTime: 5 }); +const m = performance.measure("ab", "a", "b"); +console.log("PerformanceEntry:", m instanceof PerformanceEntry); +console.log("PerformanceMeasure:", m instanceof PerformanceMeasure); diff --git a/test-parity/node-suite/perf_hooks/observer/multiple-entry-types.ts b/test-parity/node-suite/perf_hooks/observer/multiple-entry-types.ts new file mode 100644 index 000000000..b2ebb440d --- /dev/null +++ b/test-parity/node-suite/perf_hooks/observer/multiple-entry-types.ts @@ -0,0 +1,15 @@ +import { performance, PerformanceObserver } from "node:perf_hooks"; +// observe({ entryTypes: ["mark", "measure"] }) buffers both kinds; entries +// created in one task are delivered together. +await new Promise((resolve) => { + const obs = new PerformanceObserver((list) => { + console.log("marks:", list.getEntriesByType("mark").length); + console.log("measures:", list.getEntriesByType("measure").length); + obs.disconnect(); + resolve(); + }); + obs.observe({ entryTypes: ["mark", "measure"] }); + performance.mark("a", { startTime: 0 }); + performance.mark("b", { startTime: 4 }); + performance.measure("ab", "a", "b"); +}); diff --git a/test-parity/node-suite/perf_hooks/observer/observe-measures.ts b/test-parity/node-suite/perf_hooks/observer/observe-measures.ts new file mode 100644 index 000000000..ab8095800 --- /dev/null +++ b/test-parity/node-suite/perf_hooks/observer/observe-measures.ts @@ -0,0 +1,13 @@ +import { performance, PerformanceObserver } from "node:perf_hooks"; +// An observer subscribed to 'measure' receives only measure entries. +await new Promise((resolve) => { + const obs = new PerformanceObserver((list) => { + console.log("observed:", list.getEntriesByType("measure").map((e) => e.name).join(",")); + obs.disconnect(); + resolve(); + }); + obs.observe({ entryTypes: ["measure"] }); + performance.mark("x", { startTime: 0 }); + performance.mark("y", { startTime: 9 }); + performance.measure("xy", "x", "y"); +}); diff --git a/test-parity/node-suite/perf_hooks/observer/take-records.ts b/test-parity/node-suite/perf_hooks/observer/take-records.ts new file mode 100644 index 000000000..92a9bcd33 --- /dev/null +++ b/test-parity/node-suite/perf_hooks/observer/take-records.ts @@ -0,0 +1,9 @@ +import { performance, PerformanceObserver } from "node:perf_hooks"; +// takeRecords() synchronously drains the observer's buffered entries. +const obs = new PerformanceObserver(() => {}); +obs.observe({ entryTypes: ["mark"] }); +performance.mark("tr"); +const recs = obs.takeRecords(); +console.log("count:", recs.length); +console.log("name:", recs.length ? recs[0].name : "-"); +obs.disconnect();