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    if opts::get().debug.progressive_web_metrics {
56        let navigation_start = pwm
57            .navigation_start()
58            .unwrap_or_else(CrossProcessInstant::epoch);
59        let duration = (metric_time - navigation_start).as_seconds_f64();
60        println!("{url:?} {metric_type:?} {duration:?}s");
61    }
62}
63
64/// A data structure to track web metrics dfined in various specifications:
65///
66///  - <https://w3c.github.io/paint-timing/>
67///  - <https://github.com/WICG/time-to-interactive> / <https://github.com/GoogleChrome/lighthouse/issues/27>
68///
69///  We can look at three different metrics here:
70///    - navigation start -> visually ready (dom content loaded)
71///    - navigation start -> thread ready (main thread available)
72///    - visually ready -> thread ready
73#[derive(MallocSizeOf)]
74pub struct ProgressiveWebMetrics {
75    /// Whether or not this metric is for an `<iframe>` or a top level frame.
76    frame_type: TimerMetadataFrameType,
77    /// when we navigated to the page
78    navigation_start: Option<CrossProcessInstant>,
79    /// indicates if the page is visually ready
80    dom_content_loaded: Cell<Option<CrossProcessInstant>>,
81    /// main thread is available -- there's been a 10s window with no tasks longer than 50ms
82    main_thread_available: Cell<Option<CrossProcessInstant>>,
83    // max(main_thread_available, dom_content_loaded)
84    time_to_interactive: Cell<Option<CrossProcessInstant>>,
85    /// The first paint of a particular document.
86    /// TODO(mrobinson): It's unclear if this particular metric is reflected in the specification.
87    ///
88    /// See <https://w3c.github.io/paint-timing/#sec-reporting-paint-timing>.
89    first_paint: Cell<Option<CrossProcessInstant>>,
90    /// The first "contentful" paint of a particular document.
91    ///
92    /// See <https://w3c.github.io/paint-timing/#first-contentful-paint>
93    first_contentful_paint: Cell<Option<CrossProcessInstant>>,
94    /// The time at which the largest contentful paint was rendered.
95    ///
96    /// See <https://www.w3.org/TR/largest-contentful-paint/>
97    largest_contentful_paint: Cell<Option<CrossProcessInstant>>,
98    time_profiler_chan: ProfilerChan,
99    url: ServoUrl,
100}
101
102#[derive(Clone, Copy, Debug, MallocSizeOf)]
103pub struct InteractiveWindow {
104    start: CrossProcessInstant,
105}
106
107impl Default for InteractiveWindow {
108    fn default() -> Self {
109        Self {
110            start: CrossProcessInstant::now(),
111        }
112    }
113}
114
115impl InteractiveWindow {
116    // We need to either start or restart the 10s window
117    //   start: we've added a new document
118    //   restart: there was a task > 50ms
119    //   not all documents are interactive
120    pub fn start_window(&mut self) {
121        self.start = CrossProcessInstant::now();
122    }
123
124    /// check if 10s has elapsed since start
125    pub fn needs_check(&self) -> bool {
126        CrossProcessInstant::now() - self.start > INTERACTIVE_WINDOW_SECONDS
127    }
128
129    pub fn get_start(&self) -> CrossProcessInstant {
130        self.start
131    }
132}
133
134#[derive(Debug)]
135pub enum InteractiveFlag {
136    DOMContentLoaded,
137    TimeToInteractive(CrossProcessInstant),
138}
139
140impl ProgressiveWebMetrics {
141    pub fn new(
142        time_profiler_chan: ProfilerChan,
143        url: ServoUrl,
144        frame_type: TimerMetadataFrameType,
145    ) -> ProgressiveWebMetrics {
146        ProgressiveWebMetrics {
147            frame_type,
148            navigation_start: None,
149            dom_content_loaded: Cell::new(None),
150            main_thread_available: Cell::new(None),
151            time_to_interactive: Cell::new(None),
152            first_paint: Cell::new(None),
153            first_contentful_paint: Cell::new(None),
154            largest_contentful_paint: Cell::new(None),
155            time_profiler_chan,
156            url,
157        }
158    }
159
160    fn make_metadata(&self, first_reflow: bool) -> TimerMetadata {
161        TimerMetadata {
162            url: self.url.to_string(),
163            iframe: self.frame_type.clone(),
164            incremental: match first_reflow {
165                true => TimerMetadataReflowType::FirstReflow,
166                false => TimerMetadataReflowType::Incremental,
167            },
168        }
169    }
170
171    pub fn set_dom_content_loaded(&self) {
172        if self.dom_content_loaded.get().is_none() {
173            self.dom_content_loaded
174                .set(Some(CrossProcessInstant::now()));
175        }
176    }
177
178    pub fn set_main_thread_available(&self, time: CrossProcessInstant) {
179        if self.main_thread_available.get().is_none() {
180            self.main_thread_available.set(Some(time));
181        }
182    }
183
184    pub fn dom_content_loaded(&self) -> Option<CrossProcessInstant> {
185        self.dom_content_loaded.get()
186    }
187
188    pub fn first_paint(&self) -> Option<CrossProcessInstant> {
189        self.first_paint.get()
190    }
191
192    pub fn first_contentful_paint(&self) -> Option<CrossProcessInstant> {
193        self.first_contentful_paint.get()
194    }
195
196    pub fn largest_contentful_paint(&self) -> Option<CrossProcessInstant> {
197        self.largest_contentful_paint.get()
198    }
199
200    pub fn main_thread_available(&self) -> Option<CrossProcessInstant> {
201        self.main_thread_available.get()
202    }
203
204    pub fn set_performance_paint_metric(
205        &self,
206        paint_time: CrossProcessInstant,
207        first_reflow: bool,
208        metric_type: ProgressiveWebMetricType,
209    ) {
210        match metric_type {
211            ProgressiveWebMetricType::FirstPaint => self.set_first_paint(paint_time, first_reflow),
212            ProgressiveWebMetricType::FirstContentfulPaint => {
213                self.set_first_contentful_paint(paint_time, first_reflow)
214            },
215            _ => {},
216        }
217    }
218
219    fn set_first_paint(&self, paint_time: CrossProcessInstant, first_reflow: bool) {
220        set_metric(
221            self,
222            Some(self.make_metadata(first_reflow)),
223            ProgressiveWebMetricType::FirstPaint,
224            ProfilerCategory::TimeToFirstPaint,
225            &self.first_paint,
226            paint_time,
227            &self.url,
228        );
229    }
230
231    fn set_first_contentful_paint(&self, paint_time: CrossProcessInstant, first_reflow: bool) {
232        set_metric(
233            self,
234            Some(self.make_metadata(first_reflow)),
235            ProgressiveWebMetricType::FirstContentfulPaint,
236            ProfilerCategory::TimeToFirstContentfulPaint,
237            &self.first_contentful_paint,
238            paint_time,
239            &self.url,
240        );
241    }
242
243    pub fn set_largest_contentful_paint(&self, paint_time: CrossProcessInstant, area: usize) {
244        set_metric(
245            self,
246            Some(self.make_metadata(false)),
247            ProgressiveWebMetricType::LargestContentfulPaint { area, url: None },
248            ProfilerCategory::TimeToLargestContentfulPaint,
249            &self.largest_contentful_paint,
250            paint_time,
251            &self.url,
252        );
253    }
254
255    // can set either dlc or tti first, but both must be set to actually calc metric
256    // when the second is set, set_tti is called with appropriate time
257    pub fn maybe_set_tti(&self, metric: InteractiveFlag) {
258        if self.get_tti().is_some() {
259            return;
260        }
261        match metric {
262            InteractiveFlag::DOMContentLoaded => self.set_dom_content_loaded(),
263            InteractiveFlag::TimeToInteractive(time) => self.set_main_thread_available(time),
264        }
265
266        let dcl = self.dom_content_loaded.get();
267        let mta = self.main_thread_available.get();
268        let (dcl, mta) = match (dcl, mta) {
269            (Some(dcl), Some(mta)) => (dcl, mta),
270            _ => return,
271        };
272        let metric_time = match dcl.partial_cmp(&mta) {
273            Some(Ordering::Less) => mta,
274            Some(_) => dcl,
275            None => panic!("no ordering possible. something bad happened"),
276        };
277        set_metric(
278            self,
279            Some(self.make_metadata(true)),
280            ProgressiveWebMetricType::TimeToInteractive,
281            ProfilerCategory::TimeToInteractive,
282            &self.time_to_interactive,
283            metric_time,
284            &self.url,
285        );
286    }
287
288    pub fn get_tti(&self) -> Option<CrossProcessInstant> {
289        self.time_to_interactive.get()
290    }
291
292    pub fn needs_tti(&self) -> bool {
293        self.get_tti().is_none()
294    }
295
296    pub fn navigation_start(&self) -> Option<CrossProcessInstant> {
297        self.navigation_start
298    }
299
300    pub fn set_navigation_start(&mut self, time: CrossProcessInstant) {
301        self.navigation_start = Some(time);
302    }
303
304    pub fn time_profiler_chan(&self) -> &ProfilerChan {
305        &self.time_profiler_chan
306    }
307}
308
309#[cfg(test)]
310mod test {
311    use base::generic_channel;
312
313    use super::*;
314
315    fn test_metrics() -> ProgressiveWebMetrics {
316        let (sender, _) = generic_channel::channel().unwrap();
317        let profiler_chan = ProfilerChan(Some(sender));
318        let mut metrics = ProgressiveWebMetrics::new(
319            profiler_chan,
320            ServoUrl::parse("about:blank").unwrap(),
321            TimerMetadataFrameType::RootWindow,
322        );
323
324        assert!((&metrics).navigation_start().is_none());
325        assert!(metrics.get_tti().is_none());
326        assert!(metrics.first_contentful_paint().is_none());
327        assert!(metrics.first_paint().is_none());
328
329        metrics.set_navigation_start(CrossProcessInstant::now());
330
331        metrics
332    }
333
334    #[test]
335    fn test_set_dcl() {
336        let metrics = test_metrics();
337        metrics.maybe_set_tti(InteractiveFlag::DOMContentLoaded);
338        let dcl = metrics.dom_content_loaded();
339        assert!(dcl.is_some());
340
341        // try to overwrite
342        metrics.maybe_set_tti(InteractiveFlag::DOMContentLoaded);
343        assert_eq!(metrics.dom_content_loaded(), dcl);
344        assert_eq!(metrics.get_tti(), None);
345    }
346
347    #[test]
348    fn test_set_mta() {
349        let metrics = test_metrics();
350        let now = CrossProcessInstant::now();
351        metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive(now));
352        let main_thread_available_time = metrics.main_thread_available();
353        assert!(main_thread_available_time.is_some());
354        assert_eq!(main_thread_available_time, Some(now));
355
356        // try to overwrite
357        metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive(
358            CrossProcessInstant::now(),
359        ));
360        assert_eq!(metrics.main_thread_available(), main_thread_available_time);
361        assert_eq!(metrics.get_tti(), None);
362    }
363
364    #[test]
365    fn test_set_tti_dcl() {
366        let metrics = test_metrics();
367        let now = CrossProcessInstant::now();
368        metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive(now));
369        let main_thread_available_time = metrics.main_thread_available();
370        assert!(main_thread_available_time.is_some());
371
372        metrics.maybe_set_tti(InteractiveFlag::DOMContentLoaded);
373        let dom_content_loaded_time = metrics.dom_content_loaded();
374        assert!(dom_content_loaded_time.is_some());
375
376        assert_eq!(metrics.get_tti(), dom_content_loaded_time);
377    }
378
379    #[test]
380    fn test_set_tti_mta() {
381        let metrics = test_metrics();
382        metrics.maybe_set_tti(InteractiveFlag::DOMContentLoaded);
383        let dcl = metrics.dom_content_loaded();
384        assert!(dcl.is_some());
385
386        let time = CrossProcessInstant::now();
387        metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive(time));
388        let mta = metrics.main_thread_available();
389        assert!(mta.is_some());
390
391        assert_eq!(metrics.get_tti(), mta);
392    }
393
394    #[test]
395    fn test_first_paint_setter() {
396        let metrics = test_metrics();
397        metrics.set_first_paint(CrossProcessInstant::now(), false);
398        assert!(metrics.first_paint().is_some());
399    }
400
401    #[test]
402    fn test_first_contentful_paint_setter() {
403        let metrics = test_metrics();
404        metrics.set_first_contentful_paint(CrossProcessInstant::now(), false);
405        assert!(metrics.first_contentful_paint().is_some());
406    }
407}