1use std::sync::{Arc, Mutex};
4use std::time::Duration;
5use web_time::Instant;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8#[repr(usize)]
9pub enum Phase {
10 Style,
11 Damage,
12 Construct,
13 PostConstruct,
14 BoxLayout,
15 Transforms,
16 PaintTree,
17 Cleanup,
18 Hover,
19 Subdocuments,
20 TextShape,
21 TextBreak,
22 TextAlign,
23}
24impl Phase {
25 pub const ALL: [Self; 13] = [
26 Self::Style,
27 Self::Damage,
28 Self::Construct,
29 Self::PostConstruct,
30 Self::BoxLayout,
31 Self::Transforms,
32 Self::PaintTree,
33 Self::Cleanup,
34 Self::Hover,
35 Self::Subdocuments,
36 Self::TextShape,
37 Self::TextBreak,
38 Self::TextAlign,
39 ];
40}
41#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
42pub struct Sample {
43 pub calls: u64,
44 pub units: u64,
46 pub elapsed: Duration,
47 pub max: Duration,
48}
49#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
50pub struct Snapshot([Sample; Phase::ALL.len()]);
51impl Snapshot {
52 pub fn sample(&self, phase: Phase) -> Sample {
53 self.0[phase as usize]
54 }
55 pub fn iter(&self) -> impl Iterator<Item = (Phase, Sample)> + '_ {
56 Phase::ALL
57 .into_iter()
58 .map(|phase| (phase, self.sample(phase)))
59 }
60}
61#[derive(Default)]
62struct State {
63 epoch: u64,
64 samples: Snapshot,
65}
66#[derive(Clone, Default)]
70pub struct Profile(Arc<Mutex<State>>);
71impl Profile {
72 pub fn snapshot(&self) -> Snapshot {
73 self.0.lock().unwrap_or_else(|e| e.into_inner()).samples
74 }
75 pub fn reset(&self) -> bool {
77 let mut state = self.0.lock().unwrap_or_else(|e| e.into_inner());
78 let Some(epoch) = state.epoch.checked_add(1) else {
79 return false;
80 };
81 state.epoch = epoch;
82 state.samples = Snapshot::default();
83 true
84 }
85 pub fn span(&self, phase: Phase) -> Span {
86 let epoch = self.0.lock().unwrap_or_else(|e| e.into_inner()).epoch;
87 Span {
88 profile: self.clone(),
89 epoch,
90 phase,
91 start: Instant::now(),
92 units: 1,
93 }
94 }
95}
96#[must_use]
97pub struct Span {
98 profile: Profile,
99 epoch: u64,
100 phase: Phase,
101 start: Instant,
102 units: u64,
103}
104impl Span {
105 pub fn set_work(&mut self, units: u64) {
106 self.units = units;
107 }
108}
109impl Drop for Span {
110 fn drop(&mut self) {
111 let elapsed = self.start.elapsed();
112 let mut state = self.profile.0.lock().unwrap_or_else(|e| e.into_inner());
113 if state.epoch != self.epoch {
114 return;
115 }
116 let sample = &mut state.samples.0[self.phase as usize];
117 sample.calls = sample.calls.saturating_add(1);
118 sample.units = sample.units.saturating_add(self.units);
119 sample.elapsed = sample.elapsed.saturating_add(elapsed);
120 sample.max = sample.max.max(elapsed);
121 }
122}
123
124impl crate::BaseDocument {
125 pub fn set_profile(&mut self, profile: Option<Profile>) -> Option<Profile> {
129 std::mem::replace(&mut self.profile, profile)
130 }
131 pub fn profile(&self) -> Option<Profile> {
132 self.profile.clone()
133 }
134 pub(crate) fn profile_span(&self, phase: Phase) -> Option<Span> {
135 self.profile.as_ref().map(|profile| profile.span(phase))
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 #[test]
143 fn reset_retires_old_spans_and_unwind_records_once() {
144 let profile = Profile::default();
145 let old = profile.span(Phase::Style);
146 assert!(profile.reset());
147 drop(old);
148 assert_eq!(profile.snapshot(), Snapshot::default());
149 let _ = std::panic::catch_unwind(|| {
150 let mut span = profile.span(Phase::TextShape);
151 span.set_work(7);
152 panic!("profile unwind");
153 });
154 let sample = profile.snapshot().sample(Phase::TextShape);
155 assert_eq!((sample.calls, sample.units), (1, 7));
156 assert!(sample.max <= sample.elapsed);
157 }
158 #[test]
159 fn parallel_workers_share_bounded_samples_without_losing_work() {
160 let profile = Profile::default();
161 let handles: Vec<_> = (0..4)
162 .map(|_| {
163 let profile = profile.clone();
164 std::thread::spawn(move || {
165 for _ in 0..1_000 {
166 let mut span = profile.span(Phase::TextShape);
167 span.set_work(8);
168 }
169 })
170 })
171 .collect();
172 for handle in handles {
173 handle.join().unwrap();
174 }
175 let snapshot = profile.snapshot();
176 let sample = snapshot.sample(Phase::TextShape);
177 assert_eq!((sample.calls, sample.units), (4_000, 32_000));
178 assert_eq!(snapshot.iter().count(), 13);
179 assert!(sample.max <= sample.elapsed);
180 }
181 #[test]
182 fn exhausted_epoch_is_refused_without_clearing_or_reusing_it() {
183 let profile = Profile::default();
184 profile.0.lock().unwrap().epoch = u64::MAX;
185 drop(profile.span(Phase::BoxLayout));
186 let before = profile.snapshot();
187 assert!(!profile.reset());
188 assert_eq!(profile.snapshot(), before);
189 }
190 #[test]
191 fn a_document_defaults_to_no_instrumentation_and_empty_resolve_invents_no_stages() {
192 let mut doc = crate::BaseDocument::new(crate::DocumentConfig::default());
193 assert!(doc.profile().is_none());
194 assert!(doc.profile_span(Phase::Style).is_none());
195 let profile = Profile::default();
196 assert!(doc.set_profile(Some(profile.clone())).is_none());
197 doc.resolve(0.);
198 assert_eq!(profile.snapshot(), Snapshot::default());
199 assert!(doc.set_profile(None).is_some());
200 }
201 #[test]
202 #[cfg(feature = "system-fonts")]
203 fn attachment_preserves_cached_text_and_mutation_measures_actual_parley_work() {
204 use crate::{Attribute, qual_name};
205 let mut doc = crate::BaseDocument::new(crate::DocumentConfig {
206 viewport: Some(apace_traits::shell::Viewport::new(
207 400,
208 300,
209 1.,
210 Default::default(),
211 )),
212 ..Default::default()
213 });
214 let root = doc.root_node().id;
215 let mut mutation = doc.mutate();
216 let html = mutation.create_element(qual_name!("html", html), vec![]);
217 let body = mutation.create_element(qual_name!("body", html), vec![]);
218 let paragraph = mutation.create_element(
219 qual_name!("div", html),
220 vec![Attribute {
221 name: qual_name!("style"),
222 value: "display:block;width:160px;font-size:16px".into(),
223 }],
224 );
225 let text = mutation.create_text_node("alpha beta gamma delta");
226 mutation.append_children(root, &[html]);
227 mutation.append_children(html, &[body]);
228 mutation.append_children(body, &[paragraph]);
229 mutation.append_children(paragraph, &[text]);
230 drop(mutation);
231 doc.resolve(0.);
232 let size = doc.nodes[paragraph].final_layout().size;
233 assert!(size.height > 0., "real system font/layout required");
234 let profile = Profile::default();
235 doc.set_profile(Some(profile.clone()));
236 doc.resolve(0.);
237 assert_eq!(doc.nodes[paragraph].final_layout().size, size);
238 assert_eq!(profile.snapshot().sample(Phase::Style).calls, 1);
239 assert_eq!(profile.snapshot().sample(Phase::TextShape).calls, 0);
240 assert!(profile.reset());
241 doc.mutate()
242 .set_node_text(text, "changed text needs actual shaping");
243 doc.resolve(0.);
244 let snapshot = profile.snapshot();
245 for phase in Phase::ALL {
246 assert!(snapshot.sample(phase).calls > 0, "{phase:?}");
247 }
248 assert!(
249 snapshot.sample(Phase::TextShape).units
250 >= "changed text needs actual shaping".len() as u64
251 );
252 doc.set_profile(None);
253 doc.mutate().set_node_text(text, "unprofiled update");
254 doc.resolve(0.);
255 assert_eq!(profile.snapshot(), snapshot);
256 }
257}