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 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
20/// TODO make this configurable
21/// maximum task time is 50ms (in ns)
22pub const MAX_TASK_NS: u128 = 50000000;
23/// 10 second window
24const 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 the metric to the time profiler.
48    send_profile_data(
49        category,
50        metadata,
51        pwm.time_profiler_chan(),
52        metric_time,
53        metric_time,
54    );
55
56    // Print the metric to console if the print-pwm option was given.
57    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/// A data structure to track web metrics dfined in various specifications:
71///
72///  - <https://w3c.github.io/paint-timing/>
73///  - <https://github.com/WICG/time-to-interactive> / <https://github.com/GoogleChrome/lighthouse/issues/27>
74///
75///  We can look at three different metrics here:
76///    - navigation start -> visually ready (dom content loaded)
77///    - navigation start -> thread ready (main thread available)
78///    - visually ready -> thread ready
79#[derive(MallocSizeOf)]
80pub struct ProgressiveWebMetrics {
81    /// Whether or not this metric is for an `<iframe>` or a top level frame.
82    frame_type: TimerMetadataFrameType,
83    /// when we navigated to the page
84    navigation_start: Option<CrossProcessInstant>,
85    /// indicates if the page is visually ready
86    dom_content_loaded: Cell<Option<CrossProcessInstant>>,
87    /// main thread is available -- there's been a 10s window with no tasks longer than 50ms
88    main_thread_available: Cell<Option<CrossProcessInstant>>,
89    // max(main_thread_available, dom_content_loaded)
90    time_to_interactive: Cell<Option<CrossProcessInstant>>,
91    /// The first paint of a particular document.
92    /// TODO(mrobinson): It's unclear if this particular metric is reflected in the specification.
93    ///
94    /// See <https://w3c.github.io/paint-timing/#sec-reporting-paint-timing>.
95    first_paint: Cell<Option<CrossProcessInstant>>,
96    /// The first "contentful" paint of a particular document.
97    ///
98    /// See <https://w3c.github.io/paint-timing/#first-contentful-paint>
99    first_contentful_paint: Cell<Option<CrossProcessInstant>>,
100    /// The time at which the largest contentful paint was rendered.
101    ///
102    /// See <https://www.w3.org/TR/largest-contentful-paint/>
103    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    // We need to either start or restart the 10s window
124    //   start: we've added a new document
125    //   restart: there was a task > 50ms
126    //   not all documents are interactive
127    pub fn start_window(&mut self) {
128        self.start = CrossProcessInstant::now();
129    }
130
131    /// check if 10s has elapsed since start
132    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    // can set either dlc or tti first, but both must be set to actually calc metric
268    // when the second is set, set_tti is called with appropriate time
269    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        // try to overwrite
352        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        // try to overwrite
367        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}