Skip to main content

nodejs/stdlib/
perf_hooks.rs

1//! Node `perf_hooks` module.
2//!
3//! Exposes the `performance` object. `performance.now()` returns REAL monotonic
4//! milliseconds elapsed since a process-start reference captured lazily in a
5//! `OnceLock` (`std::time::Instant` — a true monotonic clock, never faked or
6//! fuzzed). `timeOrigin` is the wall-clock time (Unix ms) at that same reference
7//! point, so `timeOrigin + now()` approximates `Date.now()` as Node guarantees.
8//!
9//! `mark`/`measure`/`getEntriesByName`/`getEntriesByType`/`clearMarks` are
10//! implemented against a small in-process entry buffer guarded by a `Mutex`. This
11//! is best-effort: entries accumulate for the life of the process and the buffer
12//! is not bounded (Node's PerformanceObserver / buffered-entry eviction is not
13//! modeled), but marks and measures created and queried within a run behave
14//! correctly.
15//!
16//! `performance` is surfaced as a `Builtin("performance")` namespace value, so
17//! `performance.now()` dispatches through `call_method` → `call_builtin_function`
18//! ("performance.now") → this module's `call`, and `performance.timeOrigin`
19//! reads through `namespace_property` → this module's `constant`. The parent wires
20//! BOTH the `perf_hooks` and `performance` namespaces to `call`/`constant` (see
21//! the wiring note in the accompanying report).
22
23use crate::host::{with_host, JsObj};
24use fusevm::Value;
25use indexmap::IndexMap;
26use std::cell::RefCell;
27use std::sync::{Mutex, OnceLock};
28use std::time::Instant;
29
30/// Methods available on both the `perf_hooks` module and its `performance`
31/// object. (`timeOrigin` is a data property, served by `constant`.)
32pub const METHODS: &[&str] = &[
33    "now",
34    "mark",
35    "measure",
36    "getEntriesByName",
37    "getEntriesByType",
38    "getEntries",
39    "clearMarks",
40    "clearMeasures",
41    "createHistogram",
42    "eventLoopUtilization",
43    "monitorEventLoopDelay",
44    "timerify",
45    // Internal hook the `timerify` wrapper calls to deliver its 'function' entry
46    // (hidden `@@` name — not a user-facing method, only reachable by the wrapper).
47    "@@timerify_record",
48];
49
50/// Methods dispatched on an `@@native = "Histogram"` object (from
51/// `createHistogram()` / `monitorEventLoopDelay()`; reported to the parent for
52/// `instance_has_method` / `instance_call` wiring).
53pub const HISTOGRAM_METHODS: &[&str] = &[
54    "record",
55    "recordDelta",
56    "reset",
57    "percentile",
58    "add",
59    "enable",
60    "disable",
61];
62
63/// Methods dispatched on an `@@native = "PerformanceObserver"` object.
64pub const PERFORMANCE_OBSERVER_METHODS: &[&str] = &["observe", "disconnect", "takeRecords"];
65
66/// Methods dispatched on an `@@native = "PerformanceObserverEntryList"` object.
67pub const OBSERVER_ENTRY_LIST_METHODS: &[&str] =
68    &["getEntries", "getEntriesByName", "getEntriesByType"];
69
70/// Node's sentinel `min` for an empty histogram (`i64::MAX`).
71const EMPTY_HISTOGRAM_MIN: f64 = 9_223_372_036_854_775_807.0;
72
73thread_local! {
74    /// Live `PerformanceObserver` objects that should be notified when a mark or
75    /// measure is recorded (only ever touched on the thread that owns them).
76    static OBSERVERS: RefCell<Vec<Value>> = const { RefCell::new(Vec::new()) };
77}
78
79/// The process-start reference: a monotonic `Instant` paired with the Unix-epoch
80/// milliseconds at the same moment. Captured once, lazily.
81struct Origin {
82    instant: Instant,
83    unix_ms: f64,
84}
85
86fn origin() -> &'static Origin {
87    static ORIGIN: OnceLock<Origin> = OnceLock::new();
88    ORIGIN.get_or_init(|| Origin {
89        instant: Instant::now(),
90        unix_ms: std::time::SystemTime::now()
91            .duration_since(std::time::UNIX_EPOCH)
92            .map(|d| d.as_secs_f64() * 1000.0)
93            .unwrap_or(0.0),
94    })
95}
96
97/// Real monotonic milliseconds since the process-start reference.
98fn now_ms() -> f64 {
99    origin().instant.elapsed().as_secs_f64() * 1000.0
100}
101
102/// A recorded performance entry (`PerformanceEntry` shape).
103#[derive(Clone)]
104struct Entry {
105    name: String,
106    entry_type: &'static str,
107    start_time: f64,
108    duration: f64,
109}
110
111/// The in-process entry buffer (marks + measures), in insertion order.
112fn entries() -> &'static Mutex<Vec<Entry>> {
113    static ENTRIES: OnceLock<Mutex<Vec<Entry>>> = OnceLock::new();
114    ENTRIES.get_or_init(|| Mutex::new(Vec::new()))
115}
116
117/// Non-function properties of `perf_hooks` / `performance`.
118///
119/// `perf_hooks.performance` → the `performance` namespace. `performance.timeOrigin`
120/// → the fixed Unix-ms origin. `perf_hooks.constants` → a (currently empty) map.
121pub fn constant(name: &str) -> Option<Value> {
122    match name {
123        "performance" => Some(with_host(|h| h.alloc(JsObj::Builtin("performance".into())))),
124        "timeOrigin" => Some(Value::Float(origin().unix_ms)),
125        "constants" => Some(with_host(|h| h.new_object(IndexMap::new()))),
126        // Constructor names, exposed as values so `require('perf_hooks').X`
127        // resolves and `typeof X === 'function'` holds. Only `PerformanceObserver`
128        // is meaningfully instantiable here (see `construct`); the others exist for
129        // name/`instanceof` resolution. Parent wires `PerformanceObserver`
130        // construction into `construct`.
131        "Performance"
132        | "PerformanceEntry"
133        | "PerformanceMark"
134        | "PerformanceMeasure"
135        | "PerformanceObserver"
136        | "PerformanceObserverEntryList"
137        | "PerformanceResourceTiming" => Some(with_host(|h| h.alloc(JsObj::Builtin(name.into())))),
138        _ => None,
139    }
140}
141
142pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
143    Some(match method {
144        "now" => Ok(Value::Float(now_ms())),
145        "mark" => Ok(mark(args)),
146        "measure" => Ok(measure(args)),
147        "getEntries" => Ok(entries_to_array(|_| true)),
148        "getEntriesByName" => {
149            let name = super::arg_str(args, 0);
150            // Optional second arg filters by entryType (an explicit `undefined`
151            // means "no filter", matching Node).
152            let ty = match args.get(1) {
153                Some(v) if !matches!(v, Value::Undef) => Some(super::arg_str(args, 1)),
154                _ => None,
155            };
156            Ok(entries_to_array(|e| {
157                e.name == name && ty.as_deref().map(|t| t == e.entry_type).unwrap_or(true)
158            }))
159        }
160        "getEntriesByType" => {
161            let ty = super::arg_str(args, 0);
162            Ok(entries_to_array(|e| e.entry_type == ty))
163        }
164        "clearMarks" => Ok(clear("mark", args)),
165        "clearMeasures" => Ok(clear("measure", args)),
166        // A real histogram over recorded values (see `histogram_instance_call`).
167        "createHistogram" => Ok(new_histogram()),
168        "eventLoopUtilization" => Ok(event_loop_utilization(args)),
169        // A histogram-shaped monitor. LIMITATION: node-js has no background event-
170        // loop-delay sampler, so this histogram accumulates no samples on its own
171        // (it stays empty until values are `record`ed manually). `enable`/`disable`
172        // are no-ops. Honest empty data, never a fabricated delay distribution.
173        "monitorEventLoopDelay" => Ok(new_histogram()),
174        "timerify" => timerify(args),
175        "@@timerify_record" => Ok(timerify_record(args)),
176        _ => return None,
177    })
178}
179
180// ── timerify ──────────────────────────────────────────────────────────────────
181// `performance.timerify(fn)` wraps `fn` so each call records a 'function'
182// PerformanceEntry (name = `fn.name`, duration = call time). The wrapper is a REAL
183// JS closure compiled + invoked here (the same re-entrant factory technique
184// `util.promisify` uses), closing over the original function and a native record
185// hook (`Builtin("performance.@@timerify_record")`). Node delivers 'function'
186// entries to subscribed PerformanceObservers only — they are NOT retained on the
187// global timeline (`performance.getEntriesByType('function')` is empty in v26) — so
188// the hook notifies observers without buffering the entry.
189
190/// Compile a single JS expression and run it on the current host, returning its
191/// completion value (re-entrant-safe; mirrors `util`'s `run_completion`).
192fn run_completion(src: &str) -> Result<Value, String> {
193    let prog = crate::compile_completion(src)?;
194    let chunk = crate::load_merged(prog);
195    crate::host::run_chunk_on(chunk)
196}
197
198const TIMERIFY_SRC: &str = "(function(original, record){\n\
199  var perf = require('perf_hooks').performance;\n\
200  return function(){\n\
201    var start = perf.now();\n\
202    try {\n\
203      return original.apply(this, arguments);\n\
204    } finally {\n\
205      record(original.name || '', start, perf.now());\n\
206    }\n\
207  };\n\
208})";
209
210/// `performance.timerify(fn[, options])` → a wrapped `fn` that records a 'function'
211/// `PerformanceEntry` (its call duration) on every invocation.
212fn timerify(args: &[Value]) -> Result<Value, String> {
213    let orig = args.first().cloned().unwrap_or(Value::Undef);
214    if !with_host(|h| crate::host::is_callable(h, &orig)) {
215        return Err(
216            "TypeError [ERR_INVALID_ARG_TYPE]: The \"fn\" argument must be of type function".into(),
217        );
218    }
219    let factory = run_completion(TIMERIFY_SRC)?;
220    let record = with_host(|h| h.alloc(JsObj::Builtin("performance.@@timerify_record".into())));
221    crate::host::invoke(&factory, vec![orig, record], None)
222}
223
224/// Native hook invoked by the `timerify` wrapper `(name, startTime, endTime)`:
225/// deliver a 'function' entry (call duration) to subscribed observers. Not stored
226/// on the global timeline — matching Node v26, where function entries reach
227/// observers only.
228fn timerify_record(args: &[Value]) -> Value {
229    let name = super::arg_str(args, 0);
230    let start = super::arg_num(args, 1);
231    let end = super::arg_num(args, 2);
232    let e = Entry {
233        name,
234        entry_type: "function",
235        start_time: start,
236        duration: (end - start).max(0.0),
237    };
238    notify_observers(&e);
239    Value::Undef
240}
241
242/// `new PerformanceObserver(callback)` — build an observer holding its callback,
243/// its subscribed entry types, and a pending-entries buffer. Reported to the
244/// parent for `construct` wiring.
245pub fn construct(name: &str, args: &[Value]) -> Result<Value, String> {
246    match name {
247        "PerformanceObserver" => {
248            let cb = args.first().cloned().unwrap_or(Value::Undef);
249            Ok(with_host(|h| {
250                let types = h.new_array(Vec::new());
251                let buffer = h.new_array(Vec::new());
252                let mut m = IndexMap::new();
253                m.insert("@@native".into(), h.new_str("PerformanceObserver"));
254                m.insert("@@cb".into(), cb);
255                m.insert("@@types".into(), types);
256                m.insert("@@buffer".into(), buffer);
257                h.new_object(m)
258            }))
259        }
260        _ => Err(crate::host::type_error(&format!(
261            "perf_hooks.{name} is not a constructor"
262        ))),
263    }
264}
265
266/// `performance.mark(name)`: record a mark entry at the current time and return a
267/// `PerformanceEntry` for it.
268fn mark(args: &[Value]) -> Value {
269    let name = super::arg_str(args, 0);
270    let start = now_ms();
271    let e = Entry {
272        name,
273        entry_type: "mark",
274        start_time: start,
275        duration: 0.0,
276    };
277    if let Ok(mut buf) = entries().lock() {
278        buf.push(e.clone());
279    }
280    notify_observers(&e);
281    entry_object(&e)
282}
283
284/// `performance.measure(name, startMark, endMark)`: record a measure spanning two
285/// previously recorded marks (missing marks default to `0`/now), returning its
286/// `PerformanceEntry`.
287fn measure(args: &[Value]) -> Value {
288    let name = super::arg_str(args, 0);
289    let start_mark = args.get(1).map(|_| super::arg_str(args, 1));
290    let end_mark = args.get(2).map(|_| super::arg_str(args, 2));
291    let mark_time = |m: &Option<String>, default: f64| -> f64 {
292        match m {
293            Some(n) => entries()
294                .lock()
295                .ok()
296                .and_then(|b| {
297                    b.iter()
298                        .rev()
299                        .find(|e| e.entry_type == "mark" && &e.name == n)
300                        .map(|e| e.start_time)
301                })
302                .unwrap_or(default),
303            None => default,
304        }
305    };
306    let start = mark_time(&start_mark, 0.0);
307    let end = mark_time(&end_mark, now_ms());
308    let e = Entry {
309        name,
310        entry_type: "measure",
311        start_time: start,
312        duration: (end - start).max(0.0),
313    };
314    if let Ok(mut buf) = entries().lock() {
315        buf.push(e.clone());
316    }
317    notify_observers(&e);
318    entry_object(&e)
319}
320
321/// `clearMarks([name])` / `clearMeasures([name])`: drop entries of the given kind
322/// (all, or only those named `name` when a name is supplied). Returns undefined.
323fn clear(kind: &'static str, args: &[Value]) -> Value {
324    let name = args.first().map(|_| super::arg_str(args, 0));
325    if let Ok(mut buf) = entries().lock() {
326        buf.retain(|e| {
327            if e.entry_type != kind {
328                return true;
329            }
330            match &name {
331                Some(n) => &e.name != n,
332                None => false,
333            }
334        });
335    }
336    Value::Undef
337}
338
339/// Build a JS array of `PerformanceEntry` objects for the buffered entries
340/// matching `pred`.
341fn entries_to_array(pred: impl Fn(&Entry) -> bool) -> Value {
342    let matched: Vec<Entry> = entries()
343        .lock()
344        .map(|b| b.iter().filter(|e| pred(e)).cloned().collect())
345        .unwrap_or_default();
346    with_host(|h| {
347        let items: Vec<Value> = matched.iter().map(|e| entry_object_h(h, e)).collect();
348        h.new_array(items)
349    })
350}
351
352/// Allocate a `PerformanceEntry`-shaped object.
353fn entry_object(e: &Entry) -> Value {
354    with_host(|h| entry_object_h(h, e))
355}
356
357fn entry_object_h(h: &mut crate::host::JsHost, e: &Entry) -> Value {
358    let mut m = IndexMap::new();
359    m.insert("name".into(), h.new_str(e.name.clone()));
360    m.insert("entryType".into(), h.new_str(e.entry_type));
361    m.insert("startTime".into(), Value::Float(e.start_time));
362    m.insert("duration".into(), Value::Float(e.duration));
363    h.new_object(m)
364}
365
366// ── histogram (createHistogram / monitorEventLoopDelay) ───────────────────────
367
368/// A fresh, empty histogram object. Recorded values accumulate in the hidden
369/// `@@vals` array; the `count`/`min`/`max`/`mean`/`stddev`/`exceeds` data
370/// properties are kept in sync on every `record`, so a plain property read
371/// (`h.min`) returns the right value without a getter.
372fn new_histogram() -> Value {
373    with_host(|h| {
374        let vals = h.new_array(Vec::new());
375        let mut m = IndexMap::new();
376        m.insert("@@native".into(), h.new_str("Histogram"));
377        m.insert("@@vals".into(), vals);
378        m.insert("count".into(), Value::Float(0.0));
379        m.insert("min".into(), Value::Float(EMPTY_HISTOGRAM_MIN));
380        m.insert("max".into(), Value::Float(0.0));
381        m.insert("mean".into(), Value::Float(f64::NAN));
382        m.insert("stddev".into(), Value::Float(f64::NAN));
383        m.insert("exceeds".into(), Value::Float(0.0));
384        h.new_object(m)
385    })
386}
387
388/// Dispatch a method on a `Histogram` instance (`@@native = "Histogram"`).
389pub fn histogram_instance_call(
390    recv: &Value,
391    method: &str,
392    args: &[Value],
393) -> Result<Value, String> {
394    match method {
395        "record" => {
396            let n = super::arg_num(args, 0);
397            push_value(recv, n);
398            update_stats(recv);
399            Ok(Value::Undef)
400        }
401        // Record the elapsed time (ms) since the previous `recordDelta` (or since
402        // the histogram was created, for the first call).
403        "recordDelta" => {
404            let now = now_ms();
405            let last = read_hidden_num(recv, "@@last").unwrap_or(now);
406            set_hidden_num(recv, "@@last", now);
407            if read_hidden_num(recv, "@@last_seen").is_some() {
408                push_value(recv, now - last);
409                update_stats(recv);
410            }
411            set_hidden_num(recv, "@@last_seen", 1.0);
412            Ok(Value::Undef)
413        }
414        "reset" => {
415            with_host(|h| {
416                if let Some(vals) = hidden(recv, "@@vals") {
417                    if let Some(JsObj::Array(items)) = h.get_mut(&vals) {
418                        items.clear();
419                    }
420                }
421            });
422            update_stats(recv);
423            Ok(Value::Undef)
424        }
425        "percentile" => {
426            let p = super::arg_num(args, 0);
427            Ok(Value::Float(percentile(recv, p)))
428        }
429        "add" => {
430            // Merge another histogram's recorded values into this one.
431            if let Some(other) = args.first() {
432                for v in histogram_values(other) {
433                    push_value(recv, v);
434                }
435                update_stats(recv);
436            }
437            Ok(Value::Undef)
438        }
439        // Interval-form controls: no background sampler to toggle (see the
440        // `monitorEventLoopDelay` note). Accepted for compatibility.
441        "enable" | "disable" => Ok(Value::Bool(true)),
442        _ => Err(crate::host::type_error(&format!(
443            "{method} is not a function"
444        ))),
445    }
446}
447
448/// Push a recorded value onto the histogram's `@@vals` array.
449fn push_value(recv: &Value, n: f64) {
450    with_host(|h| {
451        let v = Value::Float(n);
452        if let Some(vals) = match h.get(recv) {
453            Some(JsObj::Object(p)) => p.get("@@vals").cloned(),
454            _ => None,
455        } {
456            if let Some(JsObj::Array(items)) = h.get_mut(&vals) {
457                items.push(v);
458            }
459        }
460    });
461}
462
463/// The recorded values of any histogram object, as `f64`s.
464fn histogram_values(recv: &Value) -> Vec<f64> {
465    with_host(|h| match h.get(recv) {
466        Some(JsObj::Object(p)) => match p.get("@@vals").and_then(|a| h.get(a)) {
467            Some(JsObj::Array(items)) => items.iter().map(|v| h.to_number(v)).collect(),
468            _ => Vec::new(),
469        },
470        _ => Vec::new(),
471    })
472}
473
474/// Recompute `count`/`min`/`max`/`mean`/`stddev` from `@@vals` and write them back.
475fn update_stats(recv: &Value) {
476    let vals = histogram_values(recv);
477    let (count, min, max, mean, stddev) = if vals.is_empty() {
478        (0.0, EMPTY_HISTOGRAM_MIN, 0.0, f64::NAN, f64::NAN)
479    } else {
480        let n = vals.len() as f64;
481        let sum: f64 = vals.iter().sum();
482        let mean = sum / n;
483        let var = vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n;
484        let min = vals.iter().cloned().fold(f64::INFINITY, f64::min);
485        let max = vals.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
486        (n, min, max, mean, var.sqrt())
487    };
488    with_host(|h| {
489        if let Some(JsObj::Object(p)) = h.get_mut(recv) {
490            p.insert("count".into(), Value::Float(count));
491            p.insert("min".into(), Value::Float(min));
492            p.insert("max".into(), Value::Float(max));
493            p.insert("mean".into(), Value::Float(mean));
494            p.insert("stddev".into(), Value::Float(stddev));
495        }
496    });
497}
498
499/// Nearest-rank percentile of the recorded values (empty → 0), matching Node's
500/// integer-valued percentile results for small samples.
501fn percentile(recv: &Value, p: f64) -> f64 {
502    let mut vals = histogram_values(recv);
503    if vals.is_empty() {
504        return 0.0;
505    }
506    vals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
507    let n = vals.len();
508    let rank = (p / 100.0 * n as f64).ceil() as usize;
509    let idx = rank.clamp(1, n) - 1;
510    vals[idx]
511}
512
513/// A hidden own property of `recv`, if present.
514fn hidden(recv: &Value, key: &str) -> Option<Value> {
515    with_host(|h| match h.get(recv) {
516        Some(JsObj::Object(p)) => p.get(key).cloned(),
517        _ => None,
518    })
519}
520
521fn read_hidden_num(recv: &Value, key: &str) -> Option<f64> {
522    hidden(recv, key).map(|v| with_host(|h| h.to_number(&v)))
523}
524
525fn set_hidden_num(recv: &Value, key: &str, n: f64) {
526    with_host(|h| {
527        if let Some(JsObj::Object(p)) = h.get_mut(recv) {
528            p.insert(key.to_string(), Value::Float(n));
529        }
530    });
531}
532
533// ── eventLoopUtilization ──────────────────────────────────────────────────────
534
535/// `performance.eventLoopUtilization([util1[, util2]])` → `{ idle, active,
536/// utilization }`.
537///
538/// LIMITATION (documented, not faked): node-js does not separately instrument the
539/// event loop's idle vs active time. The honest best-effort is `active` = total
540/// milliseconds elapsed since process start (real uptime) and `idle` = 0, so
541/// `utilization` = 1. When a prior result is passed, the numbers are the delta
542/// between it and now (Node's diff form).
543fn event_loop_utilization(args: &[Value]) -> Value {
544    let active_now = now_ms();
545    let (prev_idle, prev_active) = match args.first() {
546        Some(prev) => (
547            hidden_num(prev, "idle").unwrap_or(0.0),
548            hidden_num(prev, "active").unwrap_or(0.0),
549        ),
550        None => (0.0, 0.0),
551    };
552    let idle = 0.0 - prev_idle;
553    let active = active_now - prev_active;
554    let denom = idle + active;
555    let utilization = if denom > 0.0 { active / denom } else { 0.0 };
556    with_host(|h| {
557        let mut m = IndexMap::new();
558        m.insert("idle".into(), Value::Float(idle));
559        m.insert("active".into(), Value::Float(active));
560        m.insert("utilization".into(), Value::Float(utilization));
561        h.new_object(m)
562    })
563}
564
565fn hidden_num(recv: &Value, key: &str) -> Option<f64> {
566    with_host(|h| match h.get(recv) {
567        Some(JsObj::Object(p)) => p.get(key).map(|v| h.to_number(v)),
568        _ => None,
569    })
570}
571
572// ── PerformanceObserver ───────────────────────────────────────────────────────
573
574/// Dispatch a method on a `PerformanceObserver` instance.
575pub fn observer_instance_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
576    match method {
577        // `observe({ entryTypes: [...] } | { type: '...' })`: record the subscribed
578        // types and register so future marks/measures notify this observer.
579        "observe" => {
580            let opts = args.first().cloned().unwrap_or(Value::Undef);
581            let types = observe_types(&opts);
582            with_host(|h| {
583                let items: Vec<Value> = types.iter().map(|t| h.new_str(t.clone())).collect();
584                let arr = h.new_array(items);
585                if let Some(JsObj::Object(p)) = h.get_mut(recv) {
586                    p.insert("@@types".into(), arr);
587                }
588            });
589            OBSERVERS.with(|o| {
590                let mut list = o.borrow_mut();
591                if !list.iter().any(|v| same_ref(v, recv)) {
592                    list.push(recv.clone());
593                }
594            });
595            Ok(Value::Undef)
596        }
597        "disconnect" => {
598            OBSERVERS.with(|o| o.borrow_mut().retain(|v| !same_ref(v, recv)));
599            with_host(|h| {
600                if let Some(buf) = match h.get(recv) {
601                    Some(JsObj::Object(p)) => p.get("@@buffer").cloned(),
602                    _ => None,
603                } {
604                    if let Some(JsObj::Array(items)) = h.get_mut(&buf) {
605                        items.clear();
606                    }
607                }
608            });
609            Ok(Value::Undef)
610        }
611        // Drain and return the observer's buffered entries.
612        "takeRecords" => {
613            let taken: Vec<Value> = with_host(|h| match h.get(recv) {
614                Some(JsObj::Object(p)) => match p.get("@@buffer").and_then(|a| h.get(a)) {
615                    Some(JsObj::Array(items)) => items.clone(),
616                    _ => Vec::new(),
617                },
618                _ => Vec::new(),
619            });
620            with_host(|h| {
621                if let Some(buf) = match h.get(recv) {
622                    Some(JsObj::Object(p)) => p.get("@@buffer").cloned(),
623                    _ => None,
624                } {
625                    if let Some(JsObj::Array(items)) = h.get_mut(&buf) {
626                        items.clear();
627                    }
628                }
629            });
630            Ok(with_host(|h| h.new_array(taken)))
631        }
632        _ => Err(crate::host::type_error(&format!(
633            "{method} is not a function"
634        ))),
635    }
636}
637
638/// The entry types an `observe(options)` call subscribes to (`entryTypes` array
639/// or a single `type`).
640fn observe_types(opts: &Value) -> Vec<String> {
641    with_host(|h| match h.get(opts) {
642        Some(JsObj::Object(p)) => {
643            if let Some(JsObj::Array(items)) = p.get("entryTypes").and_then(|a| h.get(a)) {
644                items.iter().map(|v| h.str_of(v)).collect()
645            } else if let Some(t) = p.get("type") {
646                vec![h.str_of(t)]
647            } else {
648                Vec::new()
649            }
650        }
651        _ => Vec::new(),
652    })
653}
654
655/// Deliver a just-recorded entry to every subscribed observer.
656///
657/// DEVIATION (documented): Node batches entries and delivers them to the observer
658/// callback asynchronously on a microtask. node-js delivers SYNCHRONOUSLY, one
659/// entry per notification, right after the mark/measure is recorded. The callback
660/// receives `(entryList, observer)` exactly as Node's does.
661fn notify_observers(e: &Entry) {
662    let observers: Vec<Value> = OBSERVERS.with(|o| o.borrow().clone());
663    if observers.is_empty() {
664        return;
665    }
666    for obs in observers {
667        let types: Vec<String> = with_host(|h| match h.get(&obs) {
668            Some(JsObj::Object(p)) => match p.get("@@types").and_then(|a| h.get(a)) {
669                Some(JsObj::Array(items)) => items.iter().map(|v| h.str_of(v)).collect(),
670                _ => Vec::new(),
671            },
672            _ => Vec::new(),
673        });
674        if !types.iter().any(|t| t == e.entry_type) {
675            continue;
676        }
677        // Buffer the entry on the observer, then invoke its callback with a
678        // single-entry list.
679        let entry = entry_object(e);
680        with_host(|h| {
681            if let Some(buf) = match h.get(&obs) {
682                Some(JsObj::Object(p)) => p.get("@@buffer").cloned(),
683                _ => None,
684            } {
685                if let Some(JsObj::Array(items)) = h.get_mut(&buf) {
686                    items.push(entry);
687                }
688            }
689        });
690        let cb = with_host(|h| match h.get(&obs) {
691            Some(JsObj::Object(p)) => p.get("@@cb").cloned(),
692            _ => None,
693        });
694        let Some(cb) = cb else { continue };
695        let list = entry_list_object(vec![entry_object(e)]);
696        let _ = crate::host::invoke(&cb, vec![list, obs.clone()], None);
697    }
698}
699
700/// Build a `PerformanceObserverEntryList` wrapping `items`.
701fn entry_list_object(items: Vec<Value>) -> Value {
702    with_host(|h| {
703        let arr = h.new_array(items);
704        let mut m = IndexMap::new();
705        m.insert("@@native".into(), h.new_str("PerformanceObserverEntryList"));
706        m.insert("@@entries".into(), arr);
707        h.new_object(m)
708    })
709}
710
711/// Dispatch a method on a `PerformanceObserverEntryList` instance.
712pub fn entry_list_instance_call(
713    recv: &Value,
714    method: &str,
715    args: &[Value],
716) -> Result<Value, String> {
717    let items: Vec<Value> = with_host(|h| match h.get(recv) {
718        Some(JsObj::Object(p)) => match p.get("@@entries").and_then(|a| h.get(a)) {
719            Some(JsObj::Array(v)) => v.clone(),
720            _ => Vec::new(),
721        },
722        _ => Vec::new(),
723    });
724    let prop = |v: &Value, key: &str| {
725        with_host(|h| match h.get(v) {
726            Some(JsObj::Object(p)) => p.get(key).map(|x| h.str_of(x)),
727            _ => None,
728        })
729    };
730    match method {
731        "getEntries" => Ok(with_host(|h| h.new_array(items))),
732        "getEntriesByName" => {
733            let name = super::arg_str(args, 0);
734            let ty = match args.get(1) {
735                Some(v) if !matches!(v, Value::Undef) => Some(super::arg_str(args, 1)),
736                _ => None,
737            };
738            let filtered: Vec<Value> = items
739                .into_iter()
740                .filter(|it| {
741                    prop(it, "name").as_deref() == Some(name.as_str())
742                        && ty
743                            .as_deref()
744                            .map(|t| prop(it, "entryType").as_deref() == Some(t))
745                            .unwrap_or(true)
746                })
747                .collect();
748            Ok(with_host(|h| h.new_array(filtered)))
749        }
750        "getEntriesByType" => {
751            let ty = super::arg_str(args, 0);
752            let filtered: Vec<Value> = items
753                .into_iter()
754                .filter(|it| prop(it, "entryType").as_deref() == Some(ty.as_str()))
755                .collect();
756            Ok(with_host(|h| h.new_array(filtered)))
757        }
758        _ => Err(crate::host::type_error(&format!(
759            "{method} is not a function"
760        ))),
761    }
762}
763
764/// Heap-identity comparison for two reference values.
765fn same_ref(a: &Value, b: &Value) -> bool {
766    matches!((a, b), (Value::Obj(x), Value::Obj(y)) if x == y)
767}