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;
pub const MAX_TASK_NS: u64 = 50000000;
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_profile_data(
category,
metadata,
pwm.time_profiler_chan(),
metric_time,
metric_time,
);
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()
);
}
}
#[derive(MallocSizeOf)]
pub struct ProgressiveWebMetrics {
frame_type: TimerMetadataFrameType,
navigation_start: Option<CrossProcessInstant>,
dom_content_loaded: Cell<Option<CrossProcessInstant>>,
main_thread_available: Cell<Option<CrossProcessInstant>>,
time_to_interactive: Cell<Option<CrossProcessInstant>>,
first_paint: Cell<Option<CrossProcessInstant>>,
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 {
pub fn start_window(&mut self) {
self.start = CrossProcessInstant::now();
}
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,
);
}
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());
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));
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());
}