mirror of
https://github.com/servo/servo.git
synced 2025-07-24 15:50:21 +01:00
metrics: Simplify ProgressiveWebMetrics
(#35985)
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>
This commit is contained in:
parent
1f232eb17c
commit
5424479768
26 changed files with 416 additions and 787 deletions
|
@ -2,38 +2,20 @@
|
|||
* 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, RefCell};
|
||||
use std::cell::Cell;
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
use base::Epoch;
|
||||
use base::cross_process_instant::CrossProcessInstant;
|
||||
use base::id::{PipelineId, WebViewId};
|
||||
use ipc_channel::ipc::IpcSender;
|
||||
use log::warn;
|
||||
use malloc_size_of_derive::MallocSizeOf;
|
||||
use profile_traits::time::{ProfilerCategory, ProfilerChan, TimerMetadata, send_profile_data};
|
||||
use script_traits::{LayoutMsg, ProgressiveWebMetricType, ScriptThreadMessage};
|
||||
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 trait ProfilerMetadataFactory {
|
||||
fn new_metadata(&self) -> Option<TimerMetadata>;
|
||||
}
|
||||
|
||||
pub trait ProgressiveWebMetric {
|
||||
fn get_navigation_start(&self) -> Option<CrossProcessInstant>;
|
||||
fn set_navigation_start(&mut self, time: CrossProcessInstant);
|
||||
fn get_time_profiler_chan(&self) -> &ProfilerChan;
|
||||
fn send_queued_constellation_msg(
|
||||
&self,
|
||||
name: ProgressiveWebMetricType,
|
||||
time: CrossProcessInstant,
|
||||
);
|
||||
fn get_url(&self) -> &ServoUrl;
|
||||
}
|
||||
|
||||
/// TODO make this configurable
|
||||
/// maximum task time is 50ms (in ns)
|
||||
pub const MAX_TASK_NS: u64 = 50000000;
|
||||
|
@ -50,8 +32,8 @@ impl ToMs<f64> for u64 {
|
|||
}
|
||||
}
|
||||
|
||||
fn set_metric<U: ProgressiveWebMetric>(
|
||||
pwm: &U,
|
||||
fn set_metric(
|
||||
pwm: &ProgressiveWebMetrics,
|
||||
metadata: Option<TimerMetadata>,
|
||||
metric_type: ProgressiveWebMetricType,
|
||||
category: ProfilerCategory,
|
||||
|
@ -61,14 +43,11 @@ fn set_metric<U: ProgressiveWebMetric>(
|
|||
) {
|
||||
attr.set(Some(metric_time));
|
||||
|
||||
// Queue performance observer notification.
|
||||
pwm.send_queued_constellation_msg(metric_type, metric_time);
|
||||
|
||||
// Send the metric to the time profiler.
|
||||
send_profile_data(
|
||||
category,
|
||||
metadata,
|
||||
pwm.get_time_profiler_chan(),
|
||||
pwm.time_profiler_chan(),
|
||||
metric_time,
|
||||
metric_time,
|
||||
);
|
||||
|
@ -76,7 +55,7 @@ fn set_metric<U: ProgressiveWebMetric>(
|
|||
// Print the metric to console if the print-pwm option was given.
|
||||
if opts::get().print_pwm {
|
||||
let navigation_start = pwm
|
||||
.get_navigation_start()
|
||||
.navigation_start()
|
||||
.unwrap_or_else(CrossProcessInstant::epoch);
|
||||
println!(
|
||||
"{:?} {:?} {:?}",
|
||||
|
@ -87,14 +66,19 @@ fn set_metric<U: ProgressiveWebMetric>(
|
|||
}
|
||||
}
|
||||
|
||||
// spec: 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
|
||||
/// 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 InteractiveMetrics {
|
||||
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
|
||||
|
@ -103,6 +87,15 @@ pub struct InteractiveMetrics {
|
|||
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,
|
||||
|
@ -146,18 +139,36 @@ pub enum InteractiveFlag {
|
|||
TimeToInteractive(CrossProcessInstant),
|
||||
}
|
||||
|
||||
impl InteractiveMetrics {
|
||||
pub fn new(time_profiler_chan: ProfilerChan, url: ServoUrl) -> InteractiveMetrics {
|
||||
InteractiveMetrics {
|
||||
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
|
||||
|
@ -171,20 +182,49 @@ impl InteractiveMetrics {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn get_dom_content_loaded(&self) -> Option<CrossProcessInstant> {
|
||||
pub fn dom_content_loaded(&self) -> Option<CrossProcessInstant> {
|
||||
self.dom_content_loaded.get()
|
||||
}
|
||||
|
||||
pub fn get_main_thread_available(&self) -> Option<CrossProcessInstant> {
|
||||
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<T>(&self, profiler_metadata_factory: &T, metric: InteractiveFlag)
|
||||
where
|
||||
T: ProfilerMetadataFactory,
|
||||
{
|
||||
pub fn maybe_set_tti(&self, metric: InteractiveFlag) {
|
||||
if self.get_tti().is_some() {
|
||||
return;
|
||||
}
|
||||
|
@ -206,7 +246,7 @@ impl InteractiveMetrics {
|
|||
};
|
||||
set_metric(
|
||||
self,
|
||||
profiler_metadata_factory.new_metadata(),
|
||||
Some(self.make_metadata(true)),
|
||||
ProgressiveWebMetricType::TimeToInteractive,
|
||||
ProfilerCategory::TimeToInteractive,
|
||||
&self.time_to_interactive,
|
||||
|
@ -222,167 +262,110 @@ impl InteractiveMetrics {
|
|||
pub fn needs_tti(&self) -> bool {
|
||||
self.get_tti().is_none()
|
||||
}
|
||||
}
|
||||
|
||||
impl ProgressiveWebMetric for InteractiveMetrics {
|
||||
fn get_navigation_start(&self) -> Option<CrossProcessInstant> {
|
||||
pub fn navigation_start(&self) -> Option<CrossProcessInstant> {
|
||||
self.navigation_start
|
||||
}
|
||||
|
||||
fn set_navigation_start(&mut self, time: CrossProcessInstant) {
|
||||
pub fn set_navigation_start(&mut self, time: CrossProcessInstant) {
|
||||
self.navigation_start = Some(time);
|
||||
}
|
||||
|
||||
fn send_queued_constellation_msg(
|
||||
&self,
|
||||
_name: ProgressiveWebMetricType,
|
||||
_time: CrossProcessInstant,
|
||||
) {
|
||||
}
|
||||
|
||||
fn get_time_profiler_chan(&self) -> &ProfilerChan {
|
||||
pub fn time_profiler_chan(&self) -> &ProfilerChan {
|
||||
&self.time_profiler_chan
|
||||
}
|
||||
|
||||
fn get_url(&self) -> &ServoUrl {
|
||||
&self.url
|
||||
}
|
||||
}
|
||||
|
||||
// https://w3c.github.io/paint-timing/
|
||||
pub struct PaintTimeMetrics {
|
||||
pending_metrics: RefCell<HashMap<Epoch, (Option<TimerMetadata>, bool)>>,
|
||||
navigation_start: CrossProcessInstant,
|
||||
first_paint: Cell<Option<CrossProcessInstant>>,
|
||||
first_contentful_paint: Cell<Option<CrossProcessInstant>>,
|
||||
webview_id: WebViewId,
|
||||
pipeline_id: PipelineId,
|
||||
time_profiler_chan: ProfilerChan,
|
||||
constellation_chan: IpcSender<LayoutMsg>,
|
||||
script_chan: IpcSender<ScriptThreadMessage>,
|
||||
url: ServoUrl,
|
||||
#[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
|
||||
}
|
||||
|
||||
impl PaintTimeMetrics {
|
||||
pub fn new(
|
||||
webview_id: WebViewId,
|
||||
pipeline_id: PipelineId,
|
||||
time_profiler_chan: ProfilerChan,
|
||||
constellation_chan: IpcSender<LayoutMsg>,
|
||||
script_chan: IpcSender<ScriptThreadMessage>,
|
||||
url: ServoUrl,
|
||||
navigation_start: CrossProcessInstant,
|
||||
) -> PaintTimeMetrics {
|
||||
PaintTimeMetrics {
|
||||
pending_metrics: RefCell::new(HashMap::new()),
|
||||
navigation_start,
|
||||
first_paint: Cell::new(None),
|
||||
first_contentful_paint: Cell::new(None),
|
||||
webview_id,
|
||||
pipeline_id,
|
||||
time_profiler_chan,
|
||||
constellation_chan,
|
||||
script_chan,
|
||||
url,
|
||||
}
|
||||
}
|
||||
#[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());
|
||||
|
||||
pub fn maybe_observe_paint_time<T>(
|
||||
&self,
|
||||
profiler_metadata_factory: &T,
|
||||
epoch: Epoch,
|
||||
display_list_is_contentful: bool,
|
||||
) where
|
||||
T: ProfilerMetadataFactory,
|
||||
{
|
||||
if self.first_paint.get().is_some() && self.first_contentful_paint.get().is_some() {
|
||||
// If we already set all paint metrics, we just bail out.
|
||||
return;
|
||||
}
|
||||
|
||||
self.pending_metrics.borrow_mut().insert(
|
||||
epoch,
|
||||
(
|
||||
profiler_metadata_factory.new_metadata(),
|
||||
display_list_is_contentful,
|
||||
),
|
||||
);
|
||||
|
||||
// Send the pending metric information to the compositor thread.
|
||||
// The compositor will record the current time after painting the
|
||||
// frame with the given ID and will send the metric back to us.
|
||||
let msg = LayoutMsg::PendingPaintMetric(self.webview_id, self.pipeline_id, epoch);
|
||||
if let Err(e) = self.constellation_chan.send(msg) {
|
||||
warn!("Failed to send PendingPaintMetric {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn maybe_set_metric(&self, epoch: Epoch, paint_time: CrossProcessInstant) {
|
||||
if self.first_paint.get().is_some() && self.first_contentful_paint.get().is_some() {
|
||||
// If we already set all paint metrics we just bail out.
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(pending_metric) = self.pending_metrics.borrow_mut().remove(&epoch) {
|
||||
let profiler_metadata = pending_metric.0;
|
||||
set_metric(
|
||||
self,
|
||||
profiler_metadata.clone(),
|
||||
ProgressiveWebMetricType::FirstPaint,
|
||||
ProfilerCategory::TimeToFirstPaint,
|
||||
&self.first_paint,
|
||||
paint_time,
|
||||
&self.url,
|
||||
);
|
||||
|
||||
if pending_metric.1 {
|
||||
set_metric(
|
||||
self,
|
||||
profiler_metadata,
|
||||
ProgressiveWebMetricType::FirstContentfulPaint,
|
||||
ProfilerCategory::TimeToFirstContentfulPaint,
|
||||
&self.first_contentful_paint,
|
||||
paint_time,
|
||||
&self.url,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_first_paint(&self) -> Option<CrossProcessInstant> {
|
||||
self.first_paint.get()
|
||||
}
|
||||
|
||||
pub fn get_first_contentful_paint(&self) -> Option<CrossProcessInstant> {
|
||||
self.first_contentful_paint.get()
|
||||
}
|
||||
//try to overwrite
|
||||
metrics.maybe_set_tti(InteractiveFlag::DOMContentLoaded);
|
||||
assert_eq!(metrics.dom_content_loaded(), dcl);
|
||||
assert_eq!(metrics.get_tti(), None);
|
||||
}
|
||||
|
||||
impl ProgressiveWebMetric for PaintTimeMetrics {
|
||||
fn get_navigation_start(&self) -> Option<CrossProcessInstant> {
|
||||
Some(self.navigation_start)
|
||||
}
|
||||
#[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));
|
||||
|
||||
fn set_navigation_start(&mut self, time: CrossProcessInstant) {
|
||||
self.navigation_start = time;
|
||||
}
|
||||
|
||||
fn send_queued_constellation_msg(
|
||||
&self,
|
||||
name: ProgressiveWebMetricType,
|
||||
time: CrossProcessInstant,
|
||||
) {
|
||||
let msg = ScriptThreadMessage::PaintMetric(self.pipeline_id, name, time);
|
||||
if let Err(e) = self.script_chan.send(msg) {
|
||||
warn!("Sending metric to script thread failed ({}).", e);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_time_profiler_chan(&self) -> &ProfilerChan {
|
||||
&self.time_profiler_chan
|
||||
}
|
||||
|
||||
fn get_url(&self) -> &ServoUrl {
|
||||
&self.url
|
||||
}
|
||||
//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());
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue