mirror of
https://github.com/servo/servo.git
synced 2025-06-06 16:45:39 +00:00
374 lines
12 KiB
Rust
374 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: 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,
|
|
);
|
|
|
|
// 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)]
|
|
mod tests {
|
|
#[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());
|
|
}
|
|
}
|