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