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
43 changes: 39 additions & 4 deletions crates/perry-runtime/src/perf_hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,15 @@ fn num_of(v: JSValue) -> Option<f64> {
}
}

/// 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);
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -305,15 +323,27 @@ 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<PerfEntry> = PERF_ENTRIES.with(|store| {
let mut snapshot: Vec<PerfEntry> = PERF_ENTRIES.with(|store| {
store
.borrow()
.iter()
.filter(|e| filter(e))
.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);
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<PerfEntry> =
let mut snapshot: Vec<PerfEntry> =
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);
Expand Down
6 changes: 6 additions & 0 deletions test-parity/known_failures.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions test-parity/node-suite/perf_hooks/entries/by-name-no-match.ts
Original file line number Diff line number Diff line change
@@ -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);
8 changes: 8 additions & 0 deletions test-parity/node-suite/perf_hooks/entries/get-entries-all.ts
Original file line number Diff line number Diff line change
@@ -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(","));
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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);
}
7 changes: 7 additions & 0 deletions test-parity/node-suite/perf_hooks/eventlooputil/first-call.ts
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
@@ -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);
5 changes: 5 additions & 0 deletions test-parity/node-suite/perf_hooks/mark/instanceof.ts
Original file line number Diff line number Diff line change
@@ -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);
8 changes: 8 additions & 0 deletions test-parity/node-suite/perf_hooks/measure/detail-option.ts
Original file line number Diff line number Diff line change
@@ -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);
7 changes: 7 additions & 0 deletions test-parity/node-suite/perf_hooks/measure/instanceof.ts
Original file line number Diff line number Diff line change
@@ -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);
15 changes: 15 additions & 0 deletions test-parity/node-suite/perf_hooks/observer/multiple-entry-types.ts
Original file line number Diff line number Diff line change
@@ -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<void>((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");
});
13 changes: 13 additions & 0 deletions test-parity/node-suite/perf_hooks/observer/observe-measures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { performance, PerformanceObserver } from "node:perf_hooks";
// An observer subscribed to 'measure' receives only measure entries.
await new Promise<void>((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");
});
9 changes: 9 additions & 0 deletions test-parity/node-suite/perf_hooks/observer/take-records.ts
Original file line number Diff line number Diff line change
@@ -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();