1use std::time::Instant;
29
30use rquickjs::class::Trace;
31use rquickjs::function::Opt;
32use rquickjs::{Class, Ctx, Exception, Object, Value};
33
34static PROCESS_START: std::sync::LazyLock<Instant> = std::sync::LazyLock::new(Instant::now);
38static TIME_ORIGIN: std::sync::LazyLock<f64> = std::sync::LazyLock::new(|| {
39 let _ = *PROCESS_START;
41 std::time::SystemTime::now()
42 .duration_since(std::time::UNIX_EPOCH)
43 .map_or(0.0, |d| d.as_secs_f64() * 1000.0)
44});
45
46#[must_use]
49pub fn monotonic_base() -> Instant {
50 *PROCESS_START
51}
52
53#[must_use]
55pub fn now_ms() -> f64 {
56 PROCESS_START.elapsed().as_secs_f64() * 1000.0
57}
58
59#[must_use]
62pub fn time_origin_ms() -> f64 {
63 *TIME_ORIGIN
64}
65
66const MARK: &str = "mark";
67const MEASURE: &str = "measure";
68
69fn mark_options<'js>(ctx: &Ctx<'js>, options: Option<&Value<'js>>) -> rquickjs::Result<(f64, Value<'js>)> {
74 let mut start_time = now_ms();
75 let mut detail = Value::new_null(ctx.clone());
76 let Some(options) = options.and_then(rquickjs::Value::as_object) else {
77 return Ok((start_time, detail));
78 };
79 if let Some(given) = options
80 .get::<_, Value<'js>>("startTime")
81 .ok()
82 .filter(|v| !v.is_undefined())
83 {
84 let Some(n) = given.as_number() else {
85 return Err(Exception::throw_type(ctx, "startTime must be a number"));
86 };
87 if n < 0.0 {
88 return Err(Exception::throw_type(ctx, "startTime cannot be negative"));
89 }
90 start_time = n;
91 }
92 if let Some(given) = options.get::<_, Value<'js>>("detail").ok().filter(|v| !v.is_undefined()) {
93 detail = given;
94 }
95 Ok((start_time, detail))
96}
97
98#[derive(Trace, Clone)]
103#[rquickjs::class(rename = "PerformanceEntry")]
104pub struct PerformanceEntryJs {
105 #[qjs(skip_trace)]
106 name: String,
107 #[qjs(skip_trace)]
108 entry_type: String,
109 #[qjs(skip_trace)]
110 start_time: f64,
111 #[qjs(skip_trace)]
112 duration: f64,
113}
114
115#[allow(unsafe_code)]
116unsafe impl rquickjs::JsLifetime<'_> for PerformanceEntryJs {
117 type Changed<'to> = PerformanceEntryJs;
118}
119
120#[rquickjs::methods(rename_all = "camelCase")]
121impl PerformanceEntryJs {
122 #[qjs(constructor)]
128 fn constructor(ctx: Ctx<'_>) -> rquickjs::Result<Self> {
129 Err(Exception::throw_type(&ctx, "Illegal constructor"))
130 }
131
132 #[qjs(get)]
133 fn name(&self) -> String {
134 self.name.clone()
135 }
136
137 #[qjs(get)]
138 fn entry_type(&self) -> String {
139 self.entry_type.clone()
140 }
141
142 #[qjs(get)]
143 fn start_time(&self) -> f64 {
144 self.start_time
145 }
146
147 #[qjs(get)]
148 fn duration(&self) -> f64 {
149 self.duration
150 }
151
152 #[qjs(rename = "toJSON")]
153 fn to_json<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Object<'js>> {
154 let o = Object::new(ctx)?;
155 o.set("name", self.name.clone())?;
156 o.set("entryType", self.entry_type.clone())?;
157 o.set("startTime", self.start_time)?;
158 o.set("duration", self.duration)?;
159 Ok(o)
160 }
161}
162
163#[derive(Trace)]
166#[rquickjs::class(rename = "PerformanceMark")]
167pub struct PerformanceMarkJs<'js> {
168 #[qjs(skip_trace)]
169 name: String,
170 #[qjs(skip_trace)]
171 start_time: f64,
172 detail: Value<'js>,
173}
174
175#[allow(unsafe_code)]
176unsafe impl<'js> rquickjs::JsLifetime<'js> for PerformanceMarkJs<'js> {
177 type Changed<'to> = PerformanceMarkJs<'to>;
178}
179
180#[rquickjs::methods(rename_all = "camelCase")]
181impl<'js> PerformanceMarkJs<'js> {
182 #[qjs(constructor)]
186 fn constructor(ctx: Ctx<'js>, name: String, options: Opt<Value<'js>>) -> rquickjs::Result<Self> {
187 let (start_time, detail) = mark_options(&ctx, options.0.as_ref())?;
188 Ok(Self {
189 name,
190 start_time,
191 detail,
192 })
193 }
194
195 #[qjs(get)]
196 fn name(&self) -> String {
197 self.name.clone()
198 }
199
200 #[qjs(get)]
201 fn entry_type(&self) -> &'static str {
202 MARK
203 }
204
205 #[qjs(get)]
206 fn start_time(&self) -> f64 {
207 self.start_time
208 }
209
210 #[qjs(get)]
212 fn duration(&self) -> f64 {
213 0.0
214 }
215
216 #[qjs(get)]
217 fn detail(&self) -> Value<'js> {
218 self.detail.clone()
219 }
220
221 #[qjs(rename = "toJSON")]
226 fn to_json(&self, ctx: Ctx<'js>) -> rquickjs::Result<Object<'js>> {
227 let o = Object::new(ctx)?;
228 o.set("name", self.name.clone())?;
229 o.set("entryType", MARK)?;
230 o.set("startTime", self.start_time)?;
231 o.set("duration", 0.0)?;
232 o.set("detail", self.detail.clone())?;
233 Ok(o)
234 }
235}
236
237#[derive(Trace)]
240#[rquickjs::class(rename = "PerformanceMeasure")]
241pub struct PerformanceMeasureJs<'js> {
242 #[qjs(skip_trace)]
243 name: String,
244 #[qjs(skip_trace)]
245 start_time: f64,
246 #[qjs(skip_trace)]
247 duration: f64,
248 detail: Value<'js>,
249}
250
251#[allow(unsafe_code)]
252unsafe impl<'js> rquickjs::JsLifetime<'js> for PerformanceMeasureJs<'js> {
253 type Changed<'to> = PerformanceMeasureJs<'to>;
254}
255
256#[rquickjs::methods(rename_all = "camelCase")]
257impl<'js> PerformanceMeasureJs<'js> {
258 #[qjs(constructor)]
261 fn constructor(ctx: Ctx<'js>) -> rquickjs::Result<Self> {
262 Err(Exception::throw_type(&ctx, "Illegal constructor"))
263 }
264
265 #[qjs(get)]
266 fn name(&self) -> String {
267 self.name.clone()
268 }
269
270 #[qjs(get)]
271 fn entry_type(&self) -> &'static str {
272 MEASURE
273 }
274
275 #[qjs(get)]
276 fn start_time(&self) -> f64 {
277 self.start_time
278 }
279
280 #[qjs(get)]
281 fn duration(&self) -> f64 {
282 self.duration
283 }
284
285 #[qjs(get)]
286 fn detail(&self) -> Value<'js> {
287 self.detail.clone()
288 }
289
290 #[qjs(rename = "toJSON")]
291 fn to_json(&self, ctx: Ctx<'js>) -> rquickjs::Result<Object<'js>> {
292 let o = Object::new(ctx)?;
293 o.set("name", self.name.clone())?;
294 o.set("entryType", MEASURE)?;
295 o.set("startTime", self.start_time)?;
296 o.set("duration", self.duration)?;
297 o.set("detail", self.detail.clone())?;
298 Ok(o)
299 }
300}
301
302#[derive(Trace)]
309struct Buffered<'js> {
310 #[qjs(skip_trace)]
311 name: String,
312 #[qjs(skip_trace)]
313 is_mark: bool,
314 #[qjs(skip_trace)]
315 start_time: f64,
316 value: Value<'js>,
317}
318
319#[derive(Trace)]
321#[rquickjs::class(rename = "Performance")]
322pub struct PerformanceJs<'js> {
323 entries: Vec<Buffered<'js>>,
324}
325
326#[allow(unsafe_code)]
327unsafe impl<'js> rquickjs::JsLifetime<'js> for PerformanceJs<'js> {
328 type Changed<'to> = PerformanceJs<'to>;
329}
330
331impl Default for PerformanceJs<'_> {
332 fn default() -> Self {
333 Self::new()
334 }
335}
336
337impl<'js> PerformanceJs<'js> {
338 #[must_use]
339 pub fn new() -> Self {
340 Self { entries: Vec::new() }
341 }
342
343 fn resolve_mark(&self, ctx: &Ctx<'js>, name: &str) -> rquickjs::Result<f64> {
346 self
347 .entries
348 .iter()
349 .rev()
350 .find(|e| e.is_mark && e.name == name)
351 .map(|e| e.start_time)
352 .ok_or_else(|| Exception::throw_syntax(ctx, &format!("the mark {name:?} does not exist")))
353 }
354
355 fn resolve_timestamp(&self, ctx: &Ctx<'js>, value: &Value<'js>) -> rquickjs::Result<f64> {
358 if let Some(name) = value.as_string() {
359 return self.resolve_mark(ctx, &name.to_string()?);
360 }
361 let Some(n) = value.as_number() else {
362 return Err(Exception::throw_type(
363 ctx,
364 "a performance timestamp must be a mark name or a number",
365 ));
366 };
367 if n < 0.0 {
368 return Err(Exception::throw_type(ctx, "a performance timestamp cannot be negative"));
369 }
370 Ok(n)
371 }
372}
373
374#[rquickjs::methods(rename_all = "camelCase")]
375impl<'js> PerformanceJs<'js> {
376 #[qjs(constructor)]
377 fn constructor(ctx: Ctx<'js>) -> rquickjs::Result<Self> {
378 Err(Exception::throw_type(&ctx, "Illegal constructor"))
379 }
380
381 #[qjs(get)]
383 fn time_origin(&self) -> f64 {
384 time_origin_ms()
385 }
386
387 fn now(&self) -> f64 {
389 now_ms()
390 }
391
392 #[qjs(rename = "toJSON")]
393 fn to_json(&self, ctx: Ctx<'js>) -> rquickjs::Result<Object<'js>> {
394 let o = Object::new(ctx)?;
395 o.set("timeOrigin", time_origin_ms())?;
396 Ok(o)
397 }
398
399 fn mark(&mut self, ctx: Ctx<'js>, name: String, options: Opt<Value<'js>>) -> rquickjs::Result<Value<'js>> {
401 let (start_time, detail) = mark_options(&ctx, options.0.as_ref())?;
402
403 let entry = Class::instance(
404 ctx.clone(),
405 PerformanceMarkJs {
406 name: name.clone(),
407 start_time,
408 detail,
409 },
410 )?;
411 let value = entry.into_value();
412 self.entries.push(Buffered {
413 name,
414 is_mark: true,
415 start_time,
416 value: value.clone(),
417 });
418 Ok(value)
419 }
420
421 fn measure(
428 &mut self,
429 ctx: Ctx<'js>,
430 name: String,
431 start_or_options: Opt<Value<'js>>,
432 end_mark: Opt<Value<'js>>,
433 ) -> rquickjs::Result<Value<'js>> {
434 let arg = start_or_options.0.filter(|v| !v.is_undefined());
435 let end_arg = end_mark.0.filter(|v| !v.is_undefined());
436 let options = arg.as_ref().and_then(|v| v.as_object()).filter(|o| !o.is_array());
437
438 let mut detail = Value::new_null(ctx.clone());
439 let (start_time, duration) = if let Some(options) = options {
440 let get = |key: &str| -> Option<Value<'js>> {
441 options.get::<_, Value<'js>>(key).ok().filter(|v| !v.is_undefined())
442 };
443 let (start, end, dur) = (get("start"), get("end"), get("duration"));
444
445 if end_arg.is_some() {
448 return Err(Exception::throw_type(
449 &ctx,
450 "measure() takes an endMark or a PerformanceMeasureOptions object, not both",
451 ));
452 }
453 if start.is_none() && end.is_none() && dur.is_none() {
454 return Err(Exception::throw_type(
455 &ctx,
456 "PerformanceMeasureOptions must set at least one of start, end or duration",
457 ));
458 }
459 if start.is_some() && end.is_some() && dur.is_some() {
461 return Err(Exception::throw_type(
462 &ctx,
463 "PerformanceMeasureOptions cannot set all of start, end and duration",
464 ));
465 }
466 if let Some(given) = get("detail") {
467 detail = given;
468 }
469
470 let dur = dur.map(|d| self.resolve_timestamp(&ctx, &d)).transpose()?;
471 let start = start.map(|s| self.resolve_timestamp(&ctx, &s)).transpose()?;
472 let end = end.map(|e| self.resolve_timestamp(&ctx, &e)).transpose()?;
473
474 match (start, end, dur) {
475 (Some(s), Some(e), _) => (s, e - s),
476 (Some(s), None, Some(d)) => (s, d),
477 (None, Some(e), Some(d)) => (e - d, d),
478 (Some(s), None, None) => (s, now_ms() - s),
479 (None, Some(e), None) => (0.0, e),
480 (None, None, Some(d)) => (now_ms() - d, d),
481 (None, None, None) => unreachable!("the all-absent case is refused above"),
482 }
483 } else {
484 let start = match &arg {
485 Some(v) => self.resolve_timestamp(&ctx, v)?,
486 None => 0.0,
487 };
488 let end = match &end_arg {
489 Some(v) => self.resolve_timestamp(&ctx, v)?,
490 None => now_ms(),
491 };
492 (start, end - start)
493 };
494
495 let entry = Class::instance(
496 ctx.clone(),
497 PerformanceMeasureJs {
498 name: name.clone(),
499 start_time,
500 duration,
501 detail,
502 },
503 )?;
504 let value = entry.into_value();
505 self.entries.push(Buffered {
506 name,
507 is_mark: false,
508 start_time,
509 value: value.clone(),
510 });
511 Ok(value)
512 }
513
514 fn clear_marks(&mut self, name: Opt<String>) {
516 match name.0 {
517 Some(name) => self.entries.retain(|e| !(e.is_mark && e.name == name)),
518 None => self.entries.retain(|e| !e.is_mark),
519 }
520 }
521
522 fn clear_measures(&mut self, name: Opt<String>) {
524 match name.0 {
525 Some(name) => self.entries.retain(|e| !(!e.is_mark && e.name == name)),
526 None => self.entries.retain(|e| e.is_mark),
527 }
528 }
529
530 fn get_entries(&self) -> Vec<Value<'js>> {
537 let mut out: Vec<&Buffered<'js>> = self.entries.iter().collect();
538 out.sort_by(|a, b| a.start_time.total_cmp(&b.start_time));
539 out.into_iter().map(|e| e.value.clone()).collect()
540 }
541
542 fn get_entries_by_name(&self, name: String, entry_type: Opt<String>) -> Vec<Value<'js>> {
543 let wanted = entry_type.0;
544 let mut out: Vec<&Buffered<'js>> = self
545 .entries
546 .iter()
547 .filter(|e| e.name == name)
548 .filter(|e| match wanted.as_deref() {
549 Some(t) => t == if e.is_mark { MARK } else { MEASURE },
550 None => true,
551 })
552 .collect();
553 out.sort_by(|a, b| a.start_time.total_cmp(&b.start_time));
554 out.into_iter().map(|e| e.value.clone()).collect()
555 }
556
557 fn get_entries_by_type(&self, entry_type: String) -> Vec<Value<'js>> {
558 let want_mark = entry_type == MARK;
559 if !want_mark && entry_type != MEASURE {
560 return Vec::new();
561 }
562 let mut out: Vec<&Buffered<'js>> = self.entries.iter().filter(|e| e.is_mark == want_mark).collect();
563 out.sort_by(|a, b| a.start_time.total_cmp(&b.start_time));
564 out.into_iter().map(|e| e.value.clone()).collect()
565 }
566}
567
568pub fn init(ctx: &Ctx<'_>) -> rquickjs::Result<()> {
574 let globals = ctx.globals();
575 Class::<PerformanceEntryJs>::define(&globals)?;
576 Class::<PerformanceMarkJs>::define(&globals)?;
577 Class::<PerformanceMeasureJs>::define(&globals)?;
578 Class::<PerformanceJs>::define(&globals)?;
579 chain_entry_prototypes(ctx)?;
580
581 let performance = Class::instance(ctx.clone(), PerformanceJs::new())?;
582 globals.set("performance", performance)?;
583 Ok(())
584}
585
586fn chain_entry_prototypes(ctx: &Ctx<'_>) -> rquickjs::Result<()> {
591 let Some(entry_proto) = Class::<PerformanceEntryJs>::prototype(ctx)? else {
592 return Ok(());
593 };
594 if let Some(mark_proto) = Class::<PerformanceMarkJs>::prototype(ctx)? {
595 mark_proto.set_prototype(Some(&entry_proto))?;
596 }
597 if let Some(measure_proto) = Class::<PerformanceMeasureJs>::prototype(ctx)? {
598 measure_proto.set_prototype(Some(&entry_proto))?;
599 }
600 Ok(())
601}