metrics/
lib.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::Cell;
6use std::cmp::Ordering;
7use std::time::Duration;
8
9use base::cross_process_instant::CrossProcessInstant;
10use malloc_size_of_derive::MallocSizeOf;
11use profile_traits::time::{
12    ProfilerCategory, ProfilerChan, TimerMetadata, TimerMetadataFrameType, TimerMetadataReflowType,
13    send_profile_data,
14};
15use script_traits::ProgressiveWebMetricType;
16use servo_config::opts;
17use servo_url::ServoUrl;
18
19/// TODO make this configurable
20/// maximum task time is 50ms (in ns)
21pub const MAX_TASK_NS: u128 = 50000000;
22/// 10 second window
23const INTERACTIVE_WINDOW_SECONDS: Duration = Duration::from_secs(10);
24
25pub trait ToMs<T> {
26    fn to_ms(&self) -> T;
27}
28
29impl ToMs<f64> for u64 {
30    fn to_ms(&self) -> f64 {
31        *self as f64 / 1000000.
32    }
33}
34
35fn set_metric(
36    pwm: &ProgressiveWebMetrics,
37    metadata: Option<TimerMetadata>,
38    metric_type: ProgressiveWebMetricType,
39    category: ProfilerCategory,
40    attr: &Cell<Option<CrossProcessInstant>>,
41    metric_time: CrossProcessInstant,
42    url: &ServoUrl,
43) {
44    attr.set(Some(metric_time));
45
46    // Send the metric to the time profiler.
47    send_profile_data(
48        category,
49        metadata,
50        pwm.time_profiler_chan(),
51        metric_time,
52        metric_time,
53    );
54
55    // Print the metric to console if the print-pwm option was given.
56    if opts::get().print_pwm {
57        let navigation_start = pwm
58            .navigation_start()
59            .unwrap_or_else(CrossProcessInstant::epoch);
60        println!(
61            "{:?} {:?} {:?}",
62            url,
63            metric_type,
64            (metric_time - navigation_start).as_seconds_f64()
65        );
66    }
67}
68
69/// A data structure to track web metrics dfined in various specifications:
70///
71///  - <https://w3c.github.io/paint-timing/>
72///  - <https://github.com/WICG/time-to-interactive> / <https://github.com/GoogleChrome/lighthouse/issues/27>
73///
74///  We can look at three different metrics here:
75///    - navigation start -> visually ready (dom content loaded)
76///    - navigation start -> thread ready (main thread available)
77///    - visually ready -> thread ready
78#[derive(MallocSizeOf)]
79pub struct ProgressiveWebMetrics {
80    /// Whether or not this metric is for an `<iframe>` or a top level frame.
81    frame_type: TimerMetadataFrameType,
82    /// when we navigated to the page
83    navigation_start: Option<CrossProcessInstant>,
84    /// indicates if the page is visually ready
85    dom_content_loaded: Cell<Option<CrossProcessInstant>>,
86    /// main thread is available -- there's been a 10s window with no tasks longer than 50ms
87    main_thread_available: Cell<Option<CrossProcessInstant>>,
88    // max(main_thread_available, dom_content_loaded)
89    time_to_interactive: Cell<Option<CrossProcessInstant>>,
90    /// The first paint of a particular document.
91    /// TODO(mrobinson): It's unclear if this particular metric is reflected in the specification.
92    ///
93    /// See <https://w3c.github.io/paint-timing/#sec-reporting-paint-timing>.
94    first_paint: Cell<Option<CrossProcessInstant>>,
95    /// The first "contentful" paint of a particular document.
96    ///
97    /// See <https://w3c.github.io/paint-timing/#first-contentful-paint>
98    first_contentful_paint: Cell<Option<CrossProcessInstant>>,
99    #[ignore_malloc_size_of = "can't measure channels"]
100    time_profiler_chan: ProfilerChan,
101    url: ServoUrl,
102}
103
104#[derive(Clone, Copy, Debug, MallocSizeOf)]
105pub struct InteractiveWindow {
106    start: CrossProcessInstant,
107}
108
109impl Default for InteractiveWindow {
110    fn default() -> Self {
111        Self {
112            start: CrossProcessInstant::now(),
113        }
114    }
115}
116
117impl InteractiveWindow {
118    // We need to either start or restart the 10s window
119    //   start: we've added a new document
120    //   restart: there was a task > 50ms
121    //   not all documents are interactive
122    pub fn start_window(&mut self) {
123        self.start = CrossProcessInstant::now();
124    }
125
126    /// check if 10s has elapsed since start
127    pub fn needs_check(&self) -> bool {
128        CrossProcessInstant::now() - self.start > INTERACTIVE_WINDOW_SECONDS
129    }
130
131    pub fn get_start(&self) -> CrossProcessInstant {
132        self.start
133    }
134}
135
136#[derive(Debug)]
137pub enum InteractiveFlag {
138    DOMContentLoaded,
139    TimeToInteractive(CrossProcessInstant),
140}
141
142impl ProgressiveWebMetrics {
143    pub fn new(
144        time_profiler_chan: ProfilerChan,
145        url: ServoUrl,
146        frame_type: TimerMetadataFrameType,
147    ) -> ProgressiveWebMetrics {
148        ProgressiveWebMetrics {
149            frame_type,
150            navigation_start: None,
151            dom_content_loaded: Cell::new(None),
152            main_thread_available: Cell::new(None),
153            time_to_interactive: Cell::new(None),
154            first_paint: Cell::new(None),
155            first_contentful_paint: Cell::new(None),
156            time_profiler_chan,
157            url,
158        }
159    }
160
161    fn make_metadata(&self, first_reflow: bool) -> TimerMetadata {
162        TimerMetadata {
163            url: self.url.to_string(),
164            iframe: self.frame_type.clone(),
165            incremental: match first_reflow {
166                true => TimerMetadataReflowType::FirstReflow,
167                false => TimerMetadataReflowType::Incremental,
168            },
169        }
170    }
171
172    pub fn set_dom_content_loaded(&self) {
173        if self.dom_content_loaded.get().is_none() {
174            self.dom_content_loaded
175                .set(Some(CrossProcessInstant::now()));
176        }
177    }
178
179    pub fn set_main_thread_available(&self, time: CrossProcessInstant) {
180        if self.main_thread_available.get().is_none() {
181            self.main_thread_available.set(Some(time));
182        }
183    }
184
185    pub fn dom_content_loaded(&self) -> Option<CrossProcessInstant> {
186        self.dom_content_loaded.get()
187    }
188
189    pub fn first_paint(&self) -> Option<CrossProcessInstant> {
190        self.first_paint.get()
191    }
192
193    pub fn first_contentful_paint(&self) -> Option<CrossProcessInstant> {
194        self.first_contentful_paint.get()
195    }
196
197    pub fn main_thread_available(&self) -> Option<CrossProcessInstant> {
198        self.main_thread_available.get()
199    }
200
201    pub fn set_first_paint(&self, paint_time: CrossProcessInstant, first_reflow: bool) {
202        set_metric(
203            self,
204            Some(self.make_metadata(first_reflow)),
205            ProgressiveWebMetricType::FirstPaint,
206            ProfilerCategory::TimeToFirstPaint,
207            &self.first_paint,
208            paint_time,
209            &self.url,
210        );
211    }
212
213    pub fn set_first_contentful_paint(&self, paint_time: CrossProcessInstant, first_reflow: bool) {
214        set_metric(
215            self,
216            Some(self.make_metadata(first_reflow)),
217            ProgressiveWebMetricType::FirstContentfulPaint,
218            ProfilerCategory::TimeToFirstContentfulPaint,
219            &self.first_contentful_paint,
220            paint_time,
221            &self.url,
222        );
223    }
224
225    // can set either dlc or tti first, but both must be set to actually calc metric
226    // when the second is set, set_tti is called with appropriate time
227    pub fn maybe_set_tti(&self, metric: InteractiveFlag) {
228        if self.get_tti().is_some() {
229            return;
230        }
231        match metric {
232            InteractiveFlag::DOMContentLoaded => self.set_dom_content_loaded(),
233            InteractiveFlag::TimeToInteractive(time) => self.set_main_thread_available(time),
234        }
235
236        let dcl = self.dom_content_loaded.get();
237        let mta = self.main_thread_available.get();
238        let (dcl, mta) = match (dcl, mta) {
239            (Some(dcl), Some(mta)) => (dcl, mta),
240            _ => return,
241        };
242        let metric_time = match dcl.partial_cmp(&mta) {
243            Some(Ordering::Less) => mta,
244            Some(_) => dcl,
245            None => panic!("no ordering possible. something bad happened"),
246        };
247        set_metric(
248            self,
249            Some(self.make_metadata(true)),
250            ProgressiveWebMetricType::TimeToInteractive,
251            ProfilerCategory::TimeToInteractive,
252            &self.time_to_interactive,
253            metric_time,
254            &self.url,
255        );
256    }
257
258    pub fn get_tti(&self) -> Option<CrossProcessInstant> {
259        self.time_to_interactive.get()
260    }
261
262    pub fn needs_tti(&self) -> bool {
263        self.get_tti().is_none()
264    }
265
266    pub fn navigation_start(&self) -> Option<CrossProcessInstant> {
267        self.navigation_start
268    }
269
270    pub fn set_navigation_start(&mut self, time: CrossProcessInstant) {
271        self.navigation_start = Some(time);
272    }
273
274    pub fn time_profiler_chan(&self) -> &ProfilerChan {
275        &self.time_profiler_chan
276    }
277}
278
279#[cfg(test)]
280fn test_metrics() -> ProgressiveWebMetrics {
281    let (sender, _) = ipc_channel::ipc::channel().unwrap();
282    let profiler_chan = ProfilerChan(sender);
283    let mut metrics = ProgressiveWebMetrics::new(
284        profiler_chan,
285        ServoUrl::parse("about:blank").unwrap(),
286        TimerMetadataFrameType::RootWindow,
287    );
288
289    assert!((&metrics).navigation_start().is_none());
290    assert!(metrics.get_tti().is_none());
291    assert!(metrics.first_contentful_paint().is_none());
292    assert!(metrics.first_paint().is_none());
293
294    metrics.set_navigation_start(CrossProcessInstant::now());
295
296    metrics
297}
298
299#[test]
300fn test_set_dcl() {
301    let metrics = test_metrics();
302    metrics.maybe_set_tti(InteractiveFlag::DOMContentLoaded);
303    let dcl = metrics.dom_content_loaded();
304    assert!(dcl.is_some());
305
306    // try to overwrite
307    metrics.maybe_set_tti(InteractiveFlag::DOMContentLoaded);
308    assert_eq!(metrics.dom_content_loaded(), dcl);
309    assert_eq!(metrics.get_tti(), None);
310}
311
312#[test]
313fn test_set_mta() {
314    let metrics = test_metrics();
315    let now = CrossProcessInstant::now();
316    metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive(now));
317    let main_thread_available_time = metrics.main_thread_available();
318    assert!(main_thread_available_time.is_some());
319    assert_eq!(main_thread_available_time, Some(now));
320
321    // try to overwrite
322    metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive(
323        CrossProcessInstant::now(),
324    ));
325    assert_eq!(metrics.main_thread_available(), main_thread_available_time);
326    assert_eq!(metrics.get_tti(), None);
327}
328
329#[test]
330fn test_set_tti_dcl() {
331    let metrics = test_metrics();
332    let now = CrossProcessInstant::now();
333    metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive(now));
334    let main_thread_available_time = metrics.main_thread_available();
335    assert!(main_thread_available_time.is_some());
336
337    metrics.maybe_set_tti(InteractiveFlag::DOMContentLoaded);
338    let dom_content_loaded_time = metrics.dom_content_loaded();
339    assert!(dom_content_loaded_time.is_some());
340
341    assert_eq!(metrics.get_tti(), dom_content_loaded_time);
342}
343
344#[test]
345fn test_set_tti_mta() {
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    let time = CrossProcessInstant::now();
352    metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive(time));
353    let mta = metrics.main_thread_available();
354    assert!(mta.is_some());
355
356    assert_eq!(metrics.get_tti(), mta);
357}
358
359#[test]
360fn test_first_paint_setter() {
361    let metrics = test_metrics();
362    metrics.set_first_paint(CrossProcessInstant::now(), false);
363    assert!(metrics.first_paint().is_some());
364}
365
366#[test]
367fn test_first_contentful_paint_setter() {
368    let metrics = test_metrics();
369    metrics.set_first_contentful_paint(CrossProcessInstant::now(), false);
370    assert!(metrics.first_contentful_paint().is_some());
371}