use std::cell::{Cell, RefCell};
use std::collections::VecDeque;
use std::time::Duration;
use crate::reactive::{EffectId, ScopeId};
pub(crate) const CAP: usize = 200;
#[derive(Clone, Debug)]
#[allow(dead_code)] pub enum TimelineEvent {
EffectRun {
id: EffectId,
scope: Option<ScopeId>,
dur_us: u32,
t_ms: f64,
seq: u64,
},
Handler {
scope: ScopeId,
name: String,
args_summary: String,
dur_us: u32,
t_ms: f64,
seq: u64,
},
}
impl TimelineEvent {
pub fn seq(&self) -> u64 {
match self {
TimelineEvent::EffectRun { seq, .. } => *seq,
TimelineEvent::Handler { seq, .. } => *seq,
}
}
}
thread_local! {
static TIMELINE: RefCell<VecDeque<TimelineEvent>> =
RefCell::new(VecDeque::with_capacity(CAP));
static SEQ: Cell<u64> = const { Cell::new(0) };
}
pub fn push_effect_run(id: EffectId, scope: Option<ScopeId>, dur: Duration) {
let seq = SEQ.with(|c| {
let n = c.get() + 1;
c.set(n);
n
});
let dur_us = dur.as_micros().min(u32::MAX as u128) as u32;
let t_ms = now_ms();
push(TimelineEvent::EffectRun {
id,
scope,
dur_us,
t_ms,
seq,
});
}
pub fn push_handler(scope: ScopeId, name: &str, args_summary: String, dur: Duration) {
let seq = SEQ.with(|c| {
let n = c.get() + 1;
c.set(n);
n
});
let dur_us = dur.as_micros().min(u32::MAX as u128) as u32;
let t_ms = now_ms();
push(TimelineEvent::Handler {
scope,
name: name.to_string(),
args_summary,
dur_us,
t_ms,
seq,
});
}
fn push(ev: TimelineEvent) {
TIMELINE.with(|t| {
let mut buf = t.borrow_mut();
if buf.len() == CAP {
buf.pop_front();
}
buf.push_back(ev);
});
}
#[allow(dead_code)] pub fn snapshot() -> Vec<TimelineEvent> {
TIMELINE.with(|t| t.borrow().iter().cloned().collect())
}
#[allow(dead_code)] pub fn len() -> usize {
TIMELINE.with(|t| t.borrow().len())
}
#[allow(dead_code)] pub fn last_seq() -> u64 {
TIMELINE.with(|t| t.borrow().back().map(TimelineEvent::seq).unwrap_or(0))
}
#[doc(hidden)]
pub fn _clear() {
TIMELINE.with(|t| t.borrow_mut().clear());
SEQ.with(|c| c.set(0));
}
#[cfg(test)]
pub(crate) fn clear() {
_clear();
}
fn now_ms() -> f64 {
now_ms_for_scope()
}
pub fn now_ms_for_scope() -> f64 {
#[cfg(target_arch = "wasm32")]
{
web_sys::window()
.and_then(|w| w.performance())
.map(|p| p.now())
.unwrap_or(0.0)
}
#[cfg(not(target_arch = "wasm32"))]
{
0.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn push_appends_and_len_grows() {
clear();
push_effect_run(EffectId(1), None, Duration::from_micros(10));
push_effect_run(EffectId(2), None, Duration::from_micros(20));
assert_eq!(len(), 2);
}
#[test]
fn seq_increments_monotonically() {
clear();
push_effect_run(EffectId(1), None, Duration::ZERO);
let first = last_seq();
push_effect_run(EffectId(2), None, Duration::ZERO);
let second = last_seq();
assert!(second > first, "seq must be monotonic");
}
#[test]
fn ring_caps_at_cap_and_drops_oldest() {
clear();
for i in 0..(CAP + 50) {
push_effect_run(EffectId(i as u64), None, Duration::ZERO);
}
assert_eq!(len(), CAP, "ring must cap at CAP");
let snap = snapshot();
let first_seq = snap.first().unwrap().seq();
let last_seq_val = snap.last().unwrap().seq();
assert_eq!(last_seq_val - first_seq, (CAP as u64) - 1);
}
#[test]
fn handler_event_carries_args_summary() {
clear();
push_handler(
ScopeId(7),
"on_click",
"[MouseEvent]".into(),
Duration::from_micros(42),
);
let snap = snapshot();
match snap.first().unwrap() {
TimelineEvent::Handler {
scope,
name,
args_summary,
dur_us,
..
} => {
assert_eq!(*scope, ScopeId(7));
assert_eq!(name, "on_click");
assert_eq!(args_summary, "[MouseEvent]");
assert_eq!(*dur_us, 42);
}
_ => panic!("expected Handler variant"),
}
}
}