1use std::cell::Cell;
6use std::cmp::Ordering;
7use std::time::Duration;
8
9use base::cross_process_instant::CrossProcessInstant;
10use compositing_traits::largest_contentful_paint_candidate::LargestContentfulPaintType;
11use malloc_size_of_derive::MallocSizeOf;
12use profile_traits::time::{
13 ProfilerCategory, ProfilerChan, TimerMetadata, TimerMetadataFrameType, TimerMetadataReflowType,
14 send_profile_data,
15};
16use script_traits::ProgressiveWebMetricType;
17use servo_config::opts;
18use servo_url::ServoUrl;
19
20pub const MAX_TASK_NS: u128 = 50000000;
23const INTERACTIVE_WINDOW_SECONDS: Duration = Duration::from_secs(10);
25
26pub trait ToMs<T> {
27 fn to_ms(&self) -> T;
28}
29
30impl ToMs<f64> for u64 {
31 fn to_ms(&self) -> f64 {
32 *self as f64 / 1000000.
33 }
34}
35
36fn set_metric(
37 pwm: &ProgressiveWebMetrics,
38 metadata: Option<TimerMetadata>,
39 metric_type: ProgressiveWebMetricType,
40 category: ProfilerCategory,
41 attr: &Cell<Option<CrossProcessInstant>>,
42 metric_time: CrossProcessInstant,
43 url: &ServoUrl,
44) {
45 attr.set(Some(metric_time));
46
47 send_profile_data(
49 category,
50 metadata,
51 pwm.time_profiler_chan(),
52 metric_time,
53 metric_time,
54 );
55
56 if opts::get().print_pwm {
58 let navigation_start = pwm
59 .navigation_start()
60 .unwrap_or_else(CrossProcessInstant::epoch);
61 println!(
62 "{:?} {:?} {:?}",
63 url,
64 metric_type,
65 (metric_time - navigation_start).as_seconds_f64()
66 );
67 }
68}
69
70#[derive(MallocSizeOf)]
80pub struct ProgressiveWebMetrics {
81 frame_type: TimerMetadataFrameType,
83 navigation_start: Option<CrossProcessInstant>,
85 dom_content_loaded: Cell<Option<CrossProcessInstant>>,
87 main_thread_available: Cell<Option<CrossProcessInstant>>,
89 time_to_interactive: Cell<Option<CrossProcessInstant>>,
91 first_paint: Cell<Option<CrossProcessInstant>>,
96 first_contentful_paint: Cell<Option<CrossProcessInstant>>,
100 largest_contentful_paint: Cell<Option<CrossProcessInstant>>,
104 #[ignore_malloc_size_of = "can't measure channels"]
105 time_profiler_chan: ProfilerChan,
106 url: ServoUrl,
107}
108
109#[derive(Clone, Copy, Debug, MallocSizeOf)]
110pub struct InteractiveWindow {
111 start: CrossProcessInstant,
112}
113
114impl Default for InteractiveWindow {
115 fn default() -> Self {
116 Self {
117 start: CrossProcessInstant::now(),
118 }
119 }
120}
121
122impl InteractiveWindow {
123 pub fn start_window(&mut self) {
128 self.start = CrossProcessInstant::now();
129 }
130
131 pub fn needs_check(&self) -> bool {
133 CrossProcessInstant::now() - self.start > INTERACTIVE_WINDOW_SECONDS
134 }
135
136 pub fn get_start(&self) -> CrossProcessInstant {
137 self.start
138 }
139}
140
141#[derive(Debug)]
142pub enum InteractiveFlag {
143 DOMContentLoaded,
144 TimeToInteractive(CrossProcessInstant),
145}
146
147impl ProgressiveWebMetrics {
148 pub fn new(
149 time_profiler_chan: ProfilerChan,
150 url: ServoUrl,
151 frame_type: TimerMetadataFrameType,
152 ) -> ProgressiveWebMetrics {
153 ProgressiveWebMetrics {
154 frame_type,
155 navigation_start: None,
156 dom_content_loaded: Cell::new(None),
157 main_thread_available: Cell::new(None),
158 time_to_interactive: Cell::new(None),
159 first_paint: Cell::new(None),
160 first_contentful_paint: Cell::new(None),
161 largest_contentful_paint: Cell::new(None),
162 time_profiler_chan,
163 url,
164 }
165 }
166
167 fn make_metadata(&self, first_reflow: bool) -> TimerMetadata {
168 TimerMetadata {
169 url: self.url.to_string(),
170 iframe: self.frame_type.clone(),
171 incremental: match first_reflow {
172 true => TimerMetadataReflowType::FirstReflow,
173 false => TimerMetadataReflowType::Incremental,
174 },
175 }
176 }
177
178 pub fn set_dom_content_loaded(&self) {
179 if self.dom_content_loaded.get().is_none() {
180 self.dom_content_loaded
181 .set(Some(CrossProcessInstant::now()));
182 }
183 }
184
185 pub fn set_main_thread_available(&self, time: CrossProcessInstant) {
186 if self.main_thread_available.get().is_none() {
187 self.main_thread_available.set(Some(time));
188 }
189 }
190
191 pub fn dom_content_loaded(&self) -> Option<CrossProcessInstant> {
192 self.dom_content_loaded.get()
193 }
194
195 pub fn first_paint(&self) -> Option<CrossProcessInstant> {
196 self.first_paint.get()
197 }
198
199 pub fn first_contentful_paint(&self) -> Option<CrossProcessInstant> {
200 self.first_contentful_paint.get()
201 }
202
203 pub fn largest_contentful_paint(&self) -> Option<CrossProcessInstant> {
204 self.largest_contentful_paint.get()
205 }
206
207 pub fn main_thread_available(&self) -> Option<CrossProcessInstant> {
208 self.main_thread_available.get()
209 }
210
211 pub fn set_first_paint(&self, paint_time: CrossProcessInstant, first_reflow: bool) {
212 set_metric(
213 self,
214 Some(self.make_metadata(first_reflow)),
215 ProgressiveWebMetricType::FirstPaint,
216 ProfilerCategory::TimeToFirstPaint,
217 &self.first_paint,
218 paint_time,
219 &self.url,
220 );
221 }
222
223 pub fn set_first_contentful_paint(&self, paint_time: CrossProcessInstant, first_reflow: bool) {
224 set_metric(
225 self,
226 Some(self.make_metadata(first_reflow)),
227 ProgressiveWebMetricType::FirstContentfulPaint,
228 ProfilerCategory::TimeToFirstContentfulPaint,
229 &self.first_contentful_paint,
230 paint_time,
231 &self.url,
232 );
233 }
234
235 pub fn set_largest_contentful_paint(
236 &self,
237 paint_time: CrossProcessInstant,
238 area: usize,
239 lcp_type: LargestContentfulPaintType,
240 ) {
241 set_metric(
242 self,
243 Some(self.make_metadata(false)),
244 ProgressiveWebMetricType::LargestContentfulPaint { area, lcp_type },
245 ProfilerCategory::TimeToLargestContentfulPaint,
246 &self.largest_contentful_paint,
247 paint_time,
248 &self.url,
249 );
250 }
251
252 pub fn maybe_set_tti(&self, metric: InteractiveFlag) {
255 if self.get_tti().is_some() {
256 return;
257 }
258 match metric {
259 InteractiveFlag::DOMContentLoaded => self.set_dom_content_loaded(),
260 InteractiveFlag::TimeToInteractive(time) => self.set_main_thread_available(time),
261 }
262
263 let dcl = self.dom_content_loaded.get();
264 let mta = self.main_thread_available.get();
265 let (dcl, mta) = match (dcl, mta) {
266 (Some(dcl), Some(mta)) => (dcl, mta),
267 _ => return,
268 };
269 let metric_time = match dcl.partial_cmp(&mta) {
270 Some(Ordering::Less) => mta,
271 Some(_) => dcl,
272 None => panic!("no ordering possible. something bad happened"),
273 };
274 set_metric(
275 self,
276 Some(self.make_metadata(true)),
277 ProgressiveWebMetricType::TimeToInteractive,
278 ProfilerCategory::TimeToInteractive,
279 &self.time_to_interactive,
280 metric_time,
281 &self.url,
282 );
283 }
284
285 pub fn get_tti(&self) -> Option<CrossProcessInstant> {
286 self.time_to_interactive.get()
287 }
288
289 pub fn needs_tti(&self) -> bool {
290 self.get_tti().is_none()
291 }
292
293 pub fn navigation_start(&self) -> Option<CrossProcessInstant> {
294 self.navigation_start
295 }
296
297 pub fn set_navigation_start(&mut self, time: CrossProcessInstant) {
298 self.navigation_start = Some(time);
299 }
300
301 pub fn time_profiler_chan(&self) -> &ProfilerChan {
302 &self.time_profiler_chan
303 }
304}
305
306#[cfg(test)]
307mod test {
308 use super::*;
309
310 fn test_metrics() -> ProgressiveWebMetrics {
311 let (sender, _) = ipc_channel::ipc::channel().unwrap();
312 let profiler_chan = ProfilerChan(sender);
313 let mut metrics = ProgressiveWebMetrics::new(
314 profiler_chan,
315 ServoUrl::parse("about:blank").unwrap(),
316 TimerMetadataFrameType::RootWindow,
317 );
318
319 assert!((&metrics).navigation_start().is_none());
320 assert!(metrics.get_tti().is_none());
321 assert!(metrics.first_contentful_paint().is_none());
322 assert!(metrics.first_paint().is_none());
323
324 metrics.set_navigation_start(CrossProcessInstant::now());
325
326 metrics
327 }
328
329 #[test]
330 fn test_set_dcl() {
331 let metrics = test_metrics();
332 metrics.maybe_set_tti(InteractiveFlag::DOMContentLoaded);
333 let dcl = metrics.dom_content_loaded();
334 assert!(dcl.is_some());
335
336 metrics.maybe_set_tti(InteractiveFlag::DOMContentLoaded);
338 assert_eq!(metrics.dom_content_loaded(), dcl);
339 assert_eq!(metrics.get_tti(), None);
340 }
341
342 #[test]
343 fn test_set_mta() {
344 let metrics = test_metrics();
345 let now = CrossProcessInstant::now();
346 metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive(now));
347 let main_thread_available_time = metrics.main_thread_available();
348 assert!(main_thread_available_time.is_some());
349 assert_eq!(main_thread_available_time, Some(now));
350
351 metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive(
353 CrossProcessInstant::now(),
354 ));
355 assert_eq!(metrics.main_thread_available(), main_thread_available_time);
356 assert_eq!(metrics.get_tti(), None);
357 }
358
359 #[test]
360 fn test_set_tti_dcl() {
361 let metrics = test_metrics();
362 let now = CrossProcessInstant::now();
363 metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive(now));
364 let main_thread_available_time = metrics.main_thread_available();
365 assert!(main_thread_available_time.is_some());
366
367 metrics.maybe_set_tti(InteractiveFlag::DOMContentLoaded);
368 let dom_content_loaded_time = metrics.dom_content_loaded();
369 assert!(dom_content_loaded_time.is_some());
370
371 assert_eq!(metrics.get_tti(), dom_content_loaded_time);
372 }
373
374 #[test]
375 fn test_set_tti_mta() {
376 let metrics = test_metrics();
377 metrics.maybe_set_tti(InteractiveFlag::DOMContentLoaded);
378 let dcl = metrics.dom_content_loaded();
379 assert!(dcl.is_some());
380
381 let time = CrossProcessInstant::now();
382 metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive(time));
383 let mta = metrics.main_thread_available();
384 assert!(mta.is_some());
385
386 assert_eq!(metrics.get_tti(), mta);
387 }
388
389 #[test]
390 fn test_first_paint_setter() {
391 let metrics = test_metrics();
392 metrics.set_first_paint(CrossProcessInstant::now(), false);
393 assert!(metrics.first_paint().is_some());
394 }
395
396 #[test]
397 fn test_first_contentful_paint_setter() {
398 let metrics = test_metrics();
399 metrics.set_first_contentful_paint(CrossProcessInstant::now(), false);
400 assert!(metrics.first_contentful_paint().is_some());
401 }
402}