Skip to main content

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