mirror of
https://github.com/servo/servo.git
synced 2025-10-10 21:40:22 +01:00
Simply how `ProgressiveWebMetrics` works: 1. Keep only a single struct instead of one in layout and one script that both implement the `ProgressiveWebMetrics` trait. Since layout and script are the same thread these can now just be a single `ProgressiveWebMetrics` struct stored in script. 2. Have the compositor be responsible for informing the Constellation (which informs the ScripThread) about paint metrics. This makes communication flow one way and removes one dependency between the compositor and script (of two). 3. All units tests are moved into the `metrics` crate itself since there is only one struct there now. Signed-off-by: Martin Robinson <mrobinson@igalia.com>
371 lines
12 KiB
Rust
371 lines
12 KiB
Rust
/* 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 base::cross_process_instant::CrossProcessInstant;
|
|
use malloc_size_of_derive::MallocSizeOf;
|
|
use profile_traits::time::{
|
|
ProfilerCategory, ProfilerChan, TimerMetadata, TimerMetadataFrameType, TimerMetadataReflowType,
|
|
send_profile_data,
|
|
};
|
|
use script_traits::ProgressiveWebMetricType;
|
|
use servo_config::opts;
|
|
use servo_url::ServoUrl;
|
|
|
|
/// TODO make this configurable
|
|
/// maximum task time is 50ms (in ns)
|
|
pub const MAX_TASK_NS: u64 = 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,
|
|
);
|
|
|
|
// Print the metric to console if the print-pwm option was given.
|
|
if opts::get().print_pwm {
|
|
let navigation_start = pwm
|
|
.navigation_start()
|
|
.unwrap_or_else(CrossProcessInstant::epoch);
|
|
println!(
|
|
"{:?} {:?} {:?}",
|
|
url,
|
|
metric_type,
|
|
(metric_time - navigation_start).as_seconds_f64()
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 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>>,
|
|
#[ignore_malloc_size_of = "can't measure channels"]
|
|
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),
|
|
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 main_thread_available(&self) -> Option<CrossProcessInstant> {
|
|
self.main_thread_available.get()
|
|
}
|
|
|
|
pub 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,
|
|
);
|
|
}
|
|
|
|
pub 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,
|
|
);
|
|
}
|
|
|
|
// 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)]
|
|
fn test_metrics() -> ProgressiveWebMetrics {
|
|
let (sender, _) = ipc_channel::ipc::channel().unwrap();
|
|
let profiler_chan = ProfilerChan(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());
|
|
}
|