/
githubmirror
/
servo
Обзор
Документация
Войти
/
githubmirror
/
servo
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
components/metrics/lib.rs
420 строк
13 KB
Shubham Gupta
script: Wire `element` for `LargestContentfulPaint` (#46553)
01 авг 2026, 15:19
Не верифицирован
01 авг 2026, 15:19
3f08ca6
Код
Авторство
О чём код?
/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use std::cell::Cell; use std::cmp::Ordering; use std::time::Duration; use malloc_size_of_derive::MallocSizeOf; use paint_api::largest_contentful_paint_candidate::LCPCandidateID; use profile_traits::time::{ ProfilerCategory, ProfilerChan, TimerMetadata, TimerMetadataFrameType, TimerMetadataReflowType, send_profile_data, }; use script_traits::ProgressiveWebMetricType; use servo_base::cross_process_instant::CrossProcessInstant; use servo_config::opts::{self, DiagnosticsLoggingOption}; use servo_url::ServoUrl; /// TODO make this configurable /// maximum task time is 50ms (in ns) pub const MAX_TASK_NS: u128 = 50000000; /// 10 second window const INTERACTIVE_WINDOW_SECONDS: Duration = Duration::from_secs(10); pub trait ToMs<T> { fn to_ms(&self) -> T; } impl ToMs<f64> for u64 { fn to_ms(&self) -> f64 { *self as f64 / 1000000. } } fn set_metric( pwm: &ProgressiveWebMetrics, metadata: Option<TimerMetadata>, metric_type: ProgressiveWebMetricType, category: ProfilerCategory, attr: &Cell<Option<CrossProcessInstant>>, metric_time: CrossProcessInstant, url: &ServoUrl, ) { attr.set(Some(metric_time)); // Send the metric to the time profiler. send_profile_data( category, metadata, pwm.time_profiler_chan(), metric_time, metric_time, ); if opts::get() .debug .is_enabled(DiagnosticsLoggingOption::ProgressiveWebMetrics) { let navigation_start = pwm .navigation_start() .unwrap_or_else(CrossProcessInstant::epoch); let duration = (metric_time - navigation_start).as_seconds_f64(); println!("{url:?} {metric_type:?} {duration:?}s"); } } /// A data structure to track web metrics dfined in various specifications: /// /// - <https://w3c.github.io/paint-timing/> /// - <https://github.com/WICG/time-to-interactive> / <https://github.com/GoogleChrome/lighthouse/issues/27> /// /// We can look at three different metrics here: /// - navigation start -> visually ready (dom content loaded) /// - navigation start -> thread ready (main thread available) /// - visually ready -> thread ready #[derive(MallocSizeOf)] pub struct ProgressiveWebMetrics { /// Whether or not this metric is for an `<iframe>` or a top level frame. frame_type: TimerMetadataFrameType, /// when we navigated to the page navigation_start: Option<CrossProcessInstant>, /// indicates if the page is visually ready dom_content_loaded: Cell<Option<CrossProcessInstant>>, /// main thread is available -- there's been a 10s window with no tasks longer than 50ms main_thread_available: Cell<Option<CrossProcessInstant>>, // max(main_thread_available, dom_content_loaded) time_to_interactive: Cell<Option<CrossProcessInstant>>, /// The first paint of a particular document. /// TODO(mrobinson): It's unclear if this particular metric is reflected in the specification. /// /// See <https://w3c.github.io/paint-timing/#sec-reporting-paint-timing>. first_paint: Cell<Option<CrossProcessInstant>>, /// The first "contentful" paint of a particular document. /// /// See <https://w3c.github.io/paint-timing/#first-contentful-paint> first_contentful_paint: Cell<Option<CrossProcessInstant>>, /// The time at which the largest contentful paint was rendered. /// /// See <https://www.w3.org/TR/largest-contentful-paint/> largest_contentful_paint: Cell<Option<CrossProcessInstant>>, time_profiler_chan: ProfilerChan, url: ServoUrl, } #[derive(Clone, Copy, Debug, MallocSizeOf)] pub struct InteractiveWindow { start: CrossProcessInstant, } impl Default for InteractiveWindow { fn default() -> Self { Self { start: CrossProcessInstant::now(), } } } impl InteractiveWindow { // We need to either start or restart the 10s window // start: we've added a new document // restart: there was a task > 50ms // not all documents are interactive pub fn start_window(&mut self) { self.start = CrossProcessInstant::now(); } /// check if 10s has elapsed since start pub fn needs_check(&self) -> bool { CrossProcessInstant::now() - self.start > INTERACTIVE_WINDOW_SECONDS } pub fn get_start(&self) -> CrossProcessInstant { self.start } } #[derive(Debug)] pub enum InteractiveFlag { DOMContentLoaded, TimeToInteractive(CrossProcessInstant), } impl ProgressiveWebMetrics { pub fn new( time_profiler_chan: ProfilerChan, url: ServoUrl, frame_type: TimerMetadataFrameType, ) -> ProgressiveWebMetrics { ProgressiveWebMetrics { frame_type, navigation_start: None, dom_content_loaded: Cell::new(None), main_thread_available: Cell::new(None), time_to_interactive: Cell::new(None), first_paint: Cell::new(None), first_contentful_paint: Cell::new(None), largest_contentful_paint: Cell::new(None), time_profiler_chan, url, } } fn make_metadata(&self, first_reflow: bool) -> TimerMetadata { TimerMetadata { url: self.url.to_string(), iframe: self.frame_type.clone(), incremental: match first_reflow { true => TimerMetadataReflowType::FirstReflow, false => TimerMetadataReflowType::Incremental, }, } } pub fn set_dom_content_loaded(&self) { if self.dom_content_loaded.get().is_none() { self.dom_content_loaded .set(Some(CrossProcessInstant::now())); } } pub fn set_main_thread_available(&self, time: CrossProcessInstant) { if self.main_thread_available.get().is_none() { self.main_thread_available.set(Some(time)); } } pub fn dom_content_loaded(&self) -> Option<CrossProcessInstant> { self.dom_content_loaded.get() } pub fn first_paint(&self) -> Option<CrossProcessInstant> { self.first_paint.get() } pub fn first_contentful_paint(&self) -> Option<CrossProcessInstant> { self.first_contentful_paint.get() } pub fn largest_contentful_paint(&self) -> Option<CrossProcessInstant> { self.largest_contentful_paint.get() } pub fn main_thread_available(&self) -> Option<CrossProcessInstant> { self.main_thread_available.get() } pub fn set_performance_paint_metric( &self, paint_time: CrossProcessInstant, first_reflow: bool, metric_type: ProgressiveWebMetricType, ) { match metric_type { ProgressiveWebMetricType::FirstPaint => self.set_first_paint(paint_time, first_reflow), ProgressiveWebMetricType::FirstContentfulPaint => { self.set_first_contentful_paint(paint_time, first_reflow) }, _ => {}, } } fn set_first_paint(&self, paint_time: CrossProcessInstant, first_reflow: bool) { set_metric( self, Some(self.make_metadata(first_reflow)), ProgressiveWebMetricType::FirstPaint, ProfilerCategory::TimeToFirstPaint, &self.first_paint, paint_time, &self.url, ); } fn set_first_contentful_paint(&self, paint_time: CrossProcessInstant, first_reflow: bool) { set_metric( self, Some(self.make_metadata(first_reflow)), ProgressiveWebMetricType::FirstContentfulPaint, ProfilerCategory::TimeToFirstContentfulPaint, &self.first_contentful_paint, paint_time, &self.url, ); } pub fn set_largest_contentful_paint( &self, id: LCPCandidateID, paint_time: CrossProcessInstant, area: usize, ) { set_metric( self, Some(self.make_metadata(false)), ProgressiveWebMetricType::LargestContentfulPaint { id, area, url: None, }, ProfilerCategory::TimeToLargestContentfulPaint, &self.largest_contentful_paint, paint_time, &self.url, ); } // can set either dlc or tti first, but both must be set to actually calc metric // when the second is set, set_tti is called with appropriate time pub fn maybe_set_tti(&self, metric: InteractiveFlag) { if self.get_tti().is_some() { return; } match metric { InteractiveFlag::DOMContentLoaded => self.set_dom_content_loaded(), InteractiveFlag::TimeToInteractive(time) => self.set_main_thread_available(time), } let dcl = self.dom_content_loaded.get(); let mta = self.main_thread_available.get(); let (dcl, mta) = match (dcl, mta) { (Some(dcl), Some(mta)) => (dcl, mta), _ => return, }; let metric_time = match dcl.partial_cmp(&mta) { Some(Ordering::Less) => mta, Some(_) => dcl, None => panic!("no ordering possible. something bad happened"), }; set_metric( self, Some(self.make_metadata(true)), ProgressiveWebMetricType::TimeToInteractive, ProfilerCategory::TimeToInteractive, &self.time_to_interactive, metric_time, &self.url, ); } pub fn get_tti(&self) -> Option<CrossProcessInstant> { self.time_to_interactive.get() } pub fn needs_tti(&self) -> bool { self.get_tti().is_none() } pub fn navigation_start(&self) -> Option<CrossProcessInstant> { self.navigation_start } pub fn set_navigation_start(&mut self, time: CrossProcessInstant) { self.navigation_start = Some(time); } pub fn time_profiler_chan(&self) -> &ProfilerChan { &self.time_profiler_chan } } #[cfg(test)] mod test { use servo_base::generic_channel; use super::*; fn test_metrics() -> ProgressiveWebMetrics { let (sender, _) = generic_channel::channel().unwrap(); let profiler_chan = ProfilerChan(Some(sender)); let mut metrics = ProgressiveWebMetrics::new( profiler_chan, ServoUrl::parse("about:blank").unwrap(), TimerMetadataFrameType::RootWindow, ); assert!((&metrics).navigation_start().is_none()); assert!(metrics.get_tti().is_none()); assert!(metrics.first_contentful_paint().is_none()); assert!(metrics.first_paint().is_none()); metrics.set_navigation_start(CrossProcessInstant::now()); metrics } #[test] fn test_set_dcl() { let metrics = test_metrics(); metrics.maybe_set_tti(InteractiveFlag::DOMContentLoaded); let dcl = metrics.dom_content_loaded(); assert!(dcl.is_some()); // try to overwrite metrics.maybe_set_tti(InteractiveFlag::DOMContentLoaded); assert_eq!(metrics.dom_content_loaded(), dcl); assert_eq!(metrics.get_tti(), None); } #[test] fn test_set_mta() { let metrics = test_metrics(); let now = CrossProcessInstant::now(); metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive(now)); let main_thread_available_time = metrics.main_thread_available(); assert!(main_thread_available_time.is_some()); assert_eq!(main_thread_available_time, Some(now)); // try to overwrite metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive( CrossProcessInstant::now(), )); assert_eq!(metrics.main_thread_available(), main_thread_available_time); assert_eq!(metrics.get_tti(), None); } #[test] fn test_set_tti_dcl() { let metrics = test_metrics(); let now = CrossProcessInstant::now(); metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive(now)); let main_thread_available_time = metrics.main_thread_available(); assert!(main_thread_available_time.is_some()); metrics.maybe_set_tti(InteractiveFlag::DOMContentLoaded); let dom_content_loaded_time = metrics.dom_content_loaded(); assert!(dom_content_loaded_time.is_some()); assert_eq!(metrics.get_tti(), dom_content_loaded_time); } #[test] fn test_set_tti_mta() { let metrics = test_metrics(); metrics.maybe_set_tti(InteractiveFlag::DOMContentLoaded); let dcl = metrics.dom_content_loaded(); assert!(dcl.is_some()); let time = CrossProcessInstant::now(); metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive(time)); let mta = metrics.main_thread_available(); assert!(mta.is_some()); assert_eq!(metrics.get_tti(), mta); } #[test] fn test_first_paint_setter() { let metrics = test_metrics(); metrics.set_first_paint(CrossProcessInstant::now(), false); assert!(metrics.first_paint().is_some()); } #[test] fn test_first_contentful_paint_setter() { let metrics = test_metrics(); metrics.set_first_contentful_paint(CrossProcessInstant::now(), false); assert!(metrics.first_contentful_paint().is_some()); } }