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_performance_paint_metric(
212 &self,
213 paint_time: CrossProcessInstant,
214 first_reflow: bool,
215 metric_type: ProgressiveWebMetricType,
216 ) {
217 match metric_type {
218 ProgressiveWebMetricType::FirstPaint => self.set_first_paint(paint_time, first_reflow),
219 ProgressiveWebMetricType::FirstContentfulPaint => {
220 self.set_first_contentful_paint(paint_time, first_reflow)
221 },
222 _ => {},
223 }
224 }
225
226 fn set_first_paint(&self, paint_time: CrossProcessInstant, first_reflow: bool) {
227 set_metric(
228 self,
229 Some(self.make_metadata(first_reflow)),
230 ProgressiveWebMetricType::FirstPaint,
231 ProfilerCategory::TimeToFirstPaint,
232 &self.first_paint,
233 paint_time,
234 &self.url,
235 );
236 }
237
238 fn set_first_contentful_paint(&self, paint_time: CrossProcessInstant, first_reflow: bool) {
239 set_metric(
240 self,
241 Some(self.make_metadata(first_reflow)),
242 ProgressiveWebMetricType::FirstContentfulPaint,
243 ProfilerCategory::TimeToFirstContentfulPaint,
244 &self.first_contentful_paint,
245 paint_time,
246 &self.url,
247 );
248 }
249
250 pub fn set_largest_contentful_paint(
251 &self,
252 paint_time: CrossProcessInstant,
253 area: usize,
254 lcp_type: LargestContentfulPaintType,
255 ) {
256 set_metric(
257 self,
258 Some(self.make_metadata(false)),
259 ProgressiveWebMetricType::LargestContentfulPaint { area, lcp_type },
260 ProfilerCategory::TimeToLargestContentfulPaint,
261 &self.largest_contentful_paint,
262 paint_time,
263 &self.url,
264 );
265 }
266
267 pub fn maybe_set_tti(&self, metric: InteractiveFlag) {
270 if self.get_tti().is_some() {
271 return;
272 }
273 match metric {
274 InteractiveFlag::DOMContentLoaded => self.set_dom_content_loaded(),
275 InteractiveFlag::TimeToInteractive(time) => self.set_main_thread_available(time),
276 }
277
278 let dcl = self.dom_content_loaded.get();
279 let mta = self.main_thread_available.get();
280 let (dcl, mta) = match (dcl, mta) {
281 (Some(dcl), Some(mta)) => (dcl, mta),
282 _ => return,
283 };
284 let metric_time = match dcl.partial_cmp(&mta) {
285 Some(Ordering::Less) => mta,
286 Some(_) => dcl,
287 None => panic!("no ordering possible. something bad happened"),
288 };
289 set_metric(
290 self,
291 Some(self.make_metadata(true)),
292 ProgressiveWebMetricType::TimeToInteractive,
293 ProfilerCategory::TimeToInteractive,
294 &self.time_to_interactive,
295 metric_time,
296 &self.url,
297 );
298 }
299
300 pub fn get_tti(&self) -> Option<CrossProcessInstant> {
301 self.time_to_interactive.get()
302 }
303
304 pub fn needs_tti(&self) -> bool {
305 self.get_tti().is_none()
306 }
307
308 pub fn navigation_start(&self) -> Option<CrossProcessInstant> {
309 self.navigation_start
310 }
311
312 pub fn set_navigation_start(&mut self, time: CrossProcessInstant) {
313 self.navigation_start = Some(time);
314 }
315
316 pub fn time_profiler_chan(&self) -> &ProfilerChan {
317 &self.time_profiler_chan
318 }
319}
320
321#[cfg(test)]
322mod test {
323 use super::*;
324
325 fn test_metrics() -> ProgressiveWebMetrics {
326 let (sender, _) = ipc_channel::ipc::channel().unwrap();
327 let profiler_chan = ProfilerChan(sender);
328 let mut metrics = ProgressiveWebMetrics::new(
329 profiler_chan,
330 ServoUrl::parse("about:blank").unwrap(),
331 TimerMetadataFrameType::RootWindow,
332 );
333
334 assert!((&metrics).navigation_start().is_none());
335 assert!(metrics.get_tti().is_none());
336 assert!(metrics.first_contentful_paint().is_none());
337 assert!(metrics.first_paint().is_none());
338
339 metrics.set_navigation_start(CrossProcessInstant::now());
340
341 metrics
342 }
343
344 #[test]
345 fn test_set_dcl() {
346 let metrics = test_metrics();
347 metrics.maybe_set_tti(InteractiveFlag::DOMContentLoaded);
348 let dcl = metrics.dom_content_loaded();
349 assert!(dcl.is_some());
350
351 metrics.maybe_set_tti(InteractiveFlag::DOMContentLoaded);
353 assert_eq!(metrics.dom_content_loaded(), dcl);
354 assert_eq!(metrics.get_tti(), None);
355 }
356
357 #[test]
358 fn test_set_mta() {
359 let metrics = test_metrics();
360 let now = CrossProcessInstant::now();
361 metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive(now));
362 let main_thread_available_time = metrics.main_thread_available();
363 assert!(main_thread_available_time.is_some());
364 assert_eq!(main_thread_available_time, Some(now));
365
366 metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive(
368 CrossProcessInstant::now(),
369 ));
370 assert_eq!(metrics.main_thread_available(), main_thread_available_time);
371 assert_eq!(metrics.get_tti(), None);
372 }
373
374 #[test]
375 fn test_set_tti_dcl() {
376 let metrics = test_metrics();
377 let now = CrossProcessInstant::now();
378 metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive(now));
379 let main_thread_available_time = metrics.main_thread_available();
380 assert!(main_thread_available_time.is_some());
381
382 metrics.maybe_set_tti(InteractiveFlag::DOMContentLoaded);
383 let dom_content_loaded_time = metrics.dom_content_loaded();
384 assert!(dom_content_loaded_time.is_some());
385
386 assert_eq!(metrics.get_tti(), dom_content_loaded_time);
387 }
388
389 #[test]
390 fn test_set_tti_mta() {
391 let metrics = test_metrics();
392 metrics.maybe_set_tti(InteractiveFlag::DOMContentLoaded);
393 let dcl = metrics.dom_content_loaded();
394 assert!(dcl.is_some());
395
396 let time = CrossProcessInstant::now();
397 metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive(time));
398 let mta = metrics.main_thread_available();
399 assert!(mta.is_some());
400
401 assert_eq!(metrics.get_tti(), mta);
402 }
403
404 #[test]
405 fn test_first_paint_setter() {
406 let metrics = test_metrics();
407 metrics.set_first_paint(CrossProcessInstant::now(), false);
408 assert!(metrics.first_paint().is_some());
409 }
410
411 #[test]
412 fn test_first_contentful_paint_setter() {
413 let metrics = test_metrics();
414 metrics.set_first_contentful_paint(CrossProcessInstant::now(), false);
415 assert!(metrics.first_contentful_paint().is_some());
416 }
417}