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:
Martin Robinson 2025-03-21 15:55:00 +01:00 committed by GitHub
parent 1f232eb17c
commit 5424479768
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 416 additions and 787 deletions

View file

@ -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());
}