use crate::js_runtime::state::DomState;
use deno_core::op2;
use deno_core::OpState;
use rand::rngs::StdRng;
use rand::{RngExt, SeedableRng};
use rand_distr::{Distribution, Exp, LogNormal};
use std::time::Instant;
pub struct PerfState {
origin: Instant,
origin_unix_ms: f64,
rng: StdRng,
log_normal: LogNormal<f64>,
spike_exp: Exp<f64>,
last_us: f64,
}
impl PerfState {
pub fn new() -> Self {
Self::with_seed(0xCAFEF00DDEADBEEF)
}
pub fn with_seed(seed: u64) -> Self {
let origin_unix_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs_f64() * 1000.0)
.unwrap_or(0.0);
Self {
origin: Instant::now(),
origin_unix_ms,
rng: StdRng::seed_from_u64(seed),
log_normal: LogNormal::new(2.079_441_541_679_835, 0.4).expect("valid lognormal"),
spike_exp: Exp::new(1.0 / 200.0).expect("valid exp"),
last_us: 0.0,
}
}
pub fn now_ms(&mut self) -> f64 {
let raw_us = self.origin.elapsed().as_nanos() as f64 / 1000.0;
let q = (raw_us / 100.0).floor() * 100.0;
let jitter = self.log_normal.sample(&mut self.rng).clamp(0.0, 35.0);
let spike = if self.rng.random_bool(1.0 / 1024.0) {
self.spike_exp.sample(&mut self.rng).min(1500.0)
} else {
0.0
};
let candidate = q + jitter + spike;
let value = candidate.max(self.last_us);
self.last_us = value;
value / 1000.0
}
}
impl Default for PerfState {
fn default() -> Self {
Self::new()
}
}
#[op2(fast)]
pub fn op_perf_now_humanized(s: &mut OpState) -> f64 {
let s = s.borrow_mut::<PerfState>();
s.now_ms()
}
#[op2(fast)]
pub fn op_perf_time_origin_ms(s: &mut OpState) -> f64 {
let s = s.borrow::<PerfState>();
s.origin_unix_ms
}
#[derive(serde::Serialize)]
pub struct JsResourceTiming {
pub name: String,
pub entry_type: String,
pub start_time: f64,
pub duration: f64,
pub fetch_start: f64,
pub domain_lookup_start: f64,
pub domain_lookup_end: f64,
pub connect_start: f64,
pub connect_end: f64,
pub secure_connection_start: f64,
pub request_start: f64,
pub response_start: f64,
pub response_end: f64,
pub transfer_size: u64,
pub encoded_body_size: u64,
pub decoded_body_size: u64,
}
#[op2]
#[serde]
pub fn op_perf_get_resource_timings(state: &mut OpState) -> Vec<JsResourceTiming> {
let state = state.borrow::<DomState>();
state
.resource_timings
.iter()
.map(|t| JsResourceTiming {
name: "https://example.com/placeholder".to_string(),
entry_type: "resource".to_string(),
start_time: t.request_start_ms,
duration: t.response_end_ms - t.request_start_ms,
fetch_start: t.request_start_ms,
domain_lookup_start: t.dns_start_ms,
domain_lookup_end: t.dns_end_ms,
connect_start: t.connect_start_ms,
connect_end: t.connect_end_ms,
secure_connection_start: t.tls_start_ms,
request_start: t.request_start_ms,
response_start: t.response_start_ms,
response_end: t.response_end_ms,
transfer_size: 0,
encoded_body_size: 0,
decoded_body_size: 0,
})
.collect()
}
deno_core::extension!(
perf_extension,
ops = [
op_perf_now_humanized,
op_perf_get_resource_timings,
op_perf_time_origin_ms,
],
);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn distribution_has_distinct_jitter_values() {
let mut s = PerfState::with_seed(7);
let mut samples: Vec<f64> = (0..500).map(|_| s.now_ms()).collect();
samples.sort_by(|a, b| a.partial_cmp(b).unwrap());
samples.dedup_by(|a, b| (*a - *b).abs() < 1e-9);
assert!(
samples.len() > 10,
"expected >10 distinct values, got {}",
samples.len()
);
}
#[test]
fn jitter_is_bounded_and_non_negative() {
let mut s = PerfState::with_seed(0xDEADBEEF);
for _ in 0..10_000 {
let v = s.log_normal.sample(&mut s.rng).clamp(0.0, 35.0);
assert!((0.0..=35.0).contains(&v));
}
}
#[test]
fn deterministic_across_runs_with_same_seed() {
let mut a = PerfState::with_seed(123);
let mut b = PerfState::with_seed(123);
let ja: Vec<f64> = (0..100).map(|_| a.log_normal.sample(&mut a.rng)).collect();
let jb: Vec<f64> = (0..100).map(|_| b.log_normal.sample(&mut b.rng)).collect();
assert_eq!(ja, jb);
}
#[test]
fn occasional_heavy_tail_spikes() {
let mut s = PerfState::with_seed(0xBEEF);
let mut max_jitter_us = 0.0_f64;
for _ in 0..100_000 {
let j = s.log_normal.sample(&mut s.rng).clamp(0.0, 35.0);
let spike = if s.rng.random_bool(1.0 / 1024.0) {
s.spike_exp.sample(&mut s.rng).min(1500.0)
} else {
0.0
};
max_jitter_us = max_jitter_us.max(j + spike);
}
assert!(
max_jitter_us > 250.0,
"expected at least one spike >250 µs, got max {} µs",
max_jitter_us
);
}
}