use std::sync::{Arc, Mutex};
use std::time::Duration;
use web_time::Instant;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(usize)]
pub enum Phase {
Style,
Damage,
Construct,
PostConstruct,
BoxLayout,
Transforms,
PaintTree,
Cleanup,
Hover,
Subdocuments,
TextShape,
TextBreak,
TextAlign,
}
impl Phase {
pub const ALL: [Self; 13] = [
Self::Style,
Self::Damage,
Self::Construct,
Self::PostConstruct,
Self::BoxLayout,
Self::Transforms,
Self::PaintTree,
Self::Cleanup,
Self::Hover,
Self::Subdocuments,
Self::TextShape,
Self::TextBreak,
Self::TextAlign,
];
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Sample {
pub calls: u64,
pub units: u64,
pub elapsed: Duration,
pub max: Duration,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Snapshot([Sample; Phase::ALL.len()]);
impl Snapshot {
pub fn sample(&self, phase: Phase) -> Sample {
self.0[phase as usize]
}
pub fn iter(&self) -> impl Iterator<Item = (Phase, Sample)> + '_ {
Phase::ALL
.into_iter()
.map(|phase| (phase, self.sample(phase)))
}
}
#[derive(Default)]
struct State {
epoch: u64,
samples: Snapshot,
}
#[derive(Clone, Default)]
pub struct Profile(Arc<Mutex<State>>);
impl Profile {
pub fn snapshot(&self) -> Snapshot {
self.0.lock().unwrap_or_else(|e| e.into_inner()).samples
}
pub fn reset(&self) -> bool {
let mut state = self.0.lock().unwrap_or_else(|e| e.into_inner());
let Some(epoch) = state.epoch.checked_add(1) else {
return false;
};
state.epoch = epoch;
state.samples = Snapshot::default();
true
}
pub fn span(&self, phase: Phase) -> Span {
let epoch = self.0.lock().unwrap_or_else(|e| e.into_inner()).epoch;
Span {
profile: self.clone(),
epoch,
phase,
start: Instant::now(),
units: 1,
}
}
}
#[must_use]
pub struct Span {
profile: Profile,
epoch: u64,
phase: Phase,
start: Instant,
units: u64,
}
impl Span {
pub fn set_work(&mut self, units: u64) {
self.units = units;
}
}
impl Drop for Span {
fn drop(&mut self) {
let elapsed = self.start.elapsed();
let mut state = self.profile.0.lock().unwrap_or_else(|e| e.into_inner());
if state.epoch != self.epoch {
return;
}
let sample = &mut state.samples.0[self.phase as usize];
sample.calls = sample.calls.saturating_add(1);
sample.units = sample.units.saturating_add(self.units);
sample.elapsed = sample.elapsed.saturating_add(elapsed);
sample.max = sample.max.max(elapsed);
}
}
impl crate::BaseDocument {
pub fn set_profile(&mut self, profile: Option<Profile>) -> Option<Profile> {
std::mem::replace(&mut self.profile, profile)
}
pub fn profile(&self) -> Option<Profile> {
self.profile.clone()
}
pub(crate) fn profile_span(&self, phase: Phase) -> Option<Span> {
self.profile.as_ref().map(|profile| profile.span(phase))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reset_retires_old_spans_and_unwind_records_once() {
let profile = Profile::default();
let old = profile.span(Phase::Style);
assert!(profile.reset());
drop(old);
assert_eq!(profile.snapshot(), Snapshot::default());
let _ = std::panic::catch_unwind(|| {
let mut span = profile.span(Phase::TextShape);
span.set_work(7);
panic!("profile unwind");
});
let sample = profile.snapshot().sample(Phase::TextShape);
assert_eq!((sample.calls, sample.units), (1, 7));
assert!(sample.max <= sample.elapsed);
}
#[test]
fn parallel_workers_share_bounded_samples_without_losing_work() {
let profile = Profile::default();
let handles: Vec<_> = (0..4)
.map(|_| {
let profile = profile.clone();
std::thread::spawn(move || {
for _ in 0..1_000 {
let mut span = profile.span(Phase::TextShape);
span.set_work(8);
}
})
})
.collect();
for handle in handles {
handle.join().unwrap();
}
let snapshot = profile.snapshot();
let sample = snapshot.sample(Phase::TextShape);
assert_eq!((sample.calls, sample.units), (4_000, 32_000));
assert_eq!(snapshot.iter().count(), 13);
assert!(sample.max <= sample.elapsed);
}
#[test]
fn exhausted_epoch_is_refused_without_clearing_or_reusing_it() {
let profile = Profile::default();
profile.0.lock().unwrap().epoch = u64::MAX;
drop(profile.span(Phase::BoxLayout));
let before = profile.snapshot();
assert!(!profile.reset());
assert_eq!(profile.snapshot(), before);
}
#[test]
fn a_document_defaults_to_no_instrumentation_and_empty_resolve_invents_no_stages() {
let mut doc = crate::BaseDocument::new(crate::DocumentConfig::default());
assert!(doc.profile().is_none());
assert!(doc.profile_span(Phase::Style).is_none());
let profile = Profile::default();
assert!(doc.set_profile(Some(profile.clone())).is_none());
doc.resolve(0.);
assert_eq!(profile.snapshot(), Snapshot::default());
assert!(doc.set_profile(None).is_some());
}
#[test]
#[cfg(feature = "system-fonts")]
fn attachment_preserves_cached_text_and_mutation_measures_actual_parley_work() {
use crate::{Attribute, qual_name};
let mut doc = crate::BaseDocument::new(crate::DocumentConfig {
viewport: Some(apace_traits::shell::Viewport::new(
400,
300,
1.,
Default::default(),
)),
..Default::default()
});
let root = doc.root_node().id;
let mut mutation = doc.mutate();
let html = mutation.create_element(qual_name!("html", html), vec![]);
let body = mutation.create_element(qual_name!("body", html), vec![]);
let paragraph = mutation.create_element(
qual_name!("div", html),
vec![Attribute {
name: qual_name!("style"),
value: "display:block;width:160px;font-size:16px".into(),
}],
);
let text = mutation.create_text_node("alpha beta gamma delta");
mutation.append_children(root, &[html]);
mutation.append_children(html, &[body]);
mutation.append_children(body, &[paragraph]);
mutation.append_children(paragraph, &[text]);
drop(mutation);
doc.resolve(0.);
let size = doc.nodes[paragraph].final_layout().size;
assert!(size.height > 0., "real system font/layout required");
let profile = Profile::default();
doc.set_profile(Some(profile.clone()));
doc.resolve(0.);
assert_eq!(doc.nodes[paragraph].final_layout().size, size);
assert_eq!(profile.snapshot().sample(Phase::Style).calls, 1);
assert_eq!(profile.snapshot().sample(Phase::TextShape).calls, 0);
assert!(profile.reset());
doc.mutate()
.set_node_text(text, "changed text needs actual shaping");
doc.resolve(0.);
let snapshot = profile.snapshot();
for phase in Phase::ALL {
assert!(snapshot.sample(phase).calls > 0, "{phase:?}");
}
assert!(
snapshot.sample(Phase::TextShape).units
>= "changed text needs actual shaping".len() as u64
);
doc.set_profile(None);
doc.mutate().set_node_text(text, "unprofiled update");
doc.resolve(0.);
assert_eq!(profile.snapshot(), snapshot);
}
}