1use 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
30pub 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 "@@timerify_record",
48];
49
50pub const HISTOGRAM_METHODS: &[&str] = &[
54 "record",
55 "recordDelta",
56 "reset",
57 "percentile",
58 "add",
59 "enable",
60 "disable",
61];
62
63pub const PERFORMANCE_OBSERVER_METHODS: &[&str] = &["observe", "disconnect", "takeRecords"];
65
66pub const OBSERVER_ENTRY_LIST_METHODS: &[&str] =
68 &["getEntries", "getEntriesByName", "getEntriesByType"];
69
70const EMPTY_HISTOGRAM_MIN: f64 = 9_223_372_036_854_775_807.0;
72
73thread_local! {
74 static OBSERVERS: RefCell<Vec<Value>> = const { RefCell::new(Vec::new()) };
77}
78
79struct 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
97fn now_ms() -> f64 {
99 origin().instant.elapsed().as_secs_f64() * 1000.0
100}
101
102#[derive(Clone)]
104struct Entry {
105 name: String,
106 entry_type: &'static str,
107 start_time: f64,
108 duration: f64,
109}
110
111fn 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
117pub 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 "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 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 "createHistogram" => Ok(new_histogram()),
168 "eventLoopUtilization" => Ok(event_loop_utilization(args)),
169 "monitorEventLoopDelay" => Ok(new_histogram()),
174 "timerify" => timerify(args),
175 "@@timerify_record" => Ok(timerify_record(args)),
176 _ => return None,
177 })
178}
179
180fn 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
210fn 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
224fn 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
242pub 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
266fn 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
284fn 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
321fn 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
339fn 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
352fn 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
366fn 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
388pub 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 "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 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 "enable" | "disable" => Ok(Value::Bool(true)),
442 _ => Err(crate::host::type_error(&format!(
443 "{method} is not a function"
444 ))),
445 }
446}
447
448fn 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
463fn 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
474fn 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
499fn 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
513fn 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
533fn 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
572pub fn observer_instance_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
576 match method {
577 "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 "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
638fn 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
655fn 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 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
700fn 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
711pub 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
764fn same_ref(a: &Value, b: &Value) -> bool {
766 matches!((a, b), (Value::Obj(x), Value::Obj(y)) if x == y)
767}