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

13
Cargo.lock generated
View file

@ -4612,17 +4612,6 @@ dependencies = [
"servo_url", "servo_url",
] ]
[[package]]
name = "metrics_tests"
version = "0.0.1"
dependencies = [
"base",
"ipc-channel",
"metrics",
"profile_traits",
"servo_url",
]
[[package]] [[package]]
name = "mime" name = "mime"
version = "0.3.17" version = "0.3.17"
@ -5846,8 +5835,10 @@ dependencies = [
"crossbeam-channel", "crossbeam-channel",
"ipc-channel", "ipc-channel",
"log", "log",
"malloc_size_of_derive",
"serde", "serde",
"servo_config", "servo_config",
"servo_malloc_size_of",
"signpost", "signpost",
"strum_macros", "strum_macros",
"time", "time",

View file

@ -18,7 +18,8 @@ use base::id::{PipelineId, WebViewId};
use base::{Epoch, WebRenderEpochToU16}; use base::{Epoch, WebRenderEpochToU16};
use bitflags::bitflags; use bitflags::bitflags;
use compositing_traits::{ use compositing_traits::{
CompositionPipeline, CompositorMsg, CompositorReceiver, ConstellationMsg, SendableFrameTree, CompositionPipeline, CompositorMsg, CompositorReceiver, ConstellationMsg, PaintMetricEvent,
SendableFrameTree,
}; };
use crossbeam_channel::Sender; use crossbeam_channel::Sender;
use dpi::PhysicalSize; use dpi::PhysicalSize;
@ -33,9 +34,7 @@ use log::{debug, info, trace, warn};
use pixels::{CorsStatus, Image, ImageFrame, PixelFormat}; use pixels::{CorsStatus, Image, ImageFrame, PixelFormat};
use profile_traits::time::{self as profile_time, ProfilerCategory}; use profile_traits::time::{self as profile_time, ProfilerCategory};
use profile_traits::time_profile; use profile_traits::time_profile;
use script_traits::{ use script_traits::{AnimationState, AnimationTickType, WindowSizeData, WindowSizeType};
AnimationState, AnimationTickType, ScriptThreadMessage, WindowSizeData, WindowSizeType,
};
use servo_config::opts; use servo_config::opts;
use servo_geometry::DeviceIndependentPixel; use servo_geometry::DeviceIndependentPixel;
use style_traits::{CSSPixel, PinchZoomFactor}; use style_traits::{CSSPixel, PinchZoomFactor};
@ -200,6 +199,21 @@ bitflags! {
} }
} }
/// The paint status of a particular pipeline in the Servo renderer. This is used to trigger metrics
/// in script (via the constellation) when display lists are received.
///
/// See <https://w3c.github.io/paint-timing/#first-contentful-paint>.
#[derive(PartialEq)]
pub(crate) enum PaintMetricState {
/// The renderer is still waiting to process a display list which triggers this metric.
Waiting,
/// The renderer has processed the display list which will trigger this event, marked the Servo
/// instance ready to paint, and is waiting for the given epoch to actually be rendered.
Seen(WebRenderEpoch, bool /* first_reflow */),
/// The metric has been sent to the constellation and no more work needs to be done.
Sent,
}
pub(crate) struct PipelineDetails { pub(crate) struct PipelineDetails {
/// The pipeline associated with this PipelineDetails object. /// The pipeline associated with this PipelineDetails object.
pub pipeline: Option<CompositionPipeline>, pub pipeline: Option<CompositionPipeline>,
@ -231,12 +245,11 @@ pub(crate) struct PipelineDetails {
/// nodes in the compositor before forwarding new offsets to WebRender. /// nodes in the compositor before forwarding new offsets to WebRender.
pub scroll_tree: ScrollTree, pub scroll_tree: ScrollTree,
/// A per-pipeline queue of display lists that have not yet been rendered by WebRender. Layout /// The paint metric status of the first paint.
/// expects WebRender to paint each given epoch. Once the compositor paints a frame with that pub first_paint_metric: PaintMetricState,
/// epoch's display list, it will be removed from the queue and the paint time will be recorded
/// as a metric. In case new display lists come faster than painting a metric might never be /// The paint metric status of the first contentful paint.
/// recorded. pub first_contentful_paint_metric: PaintMetricState,
pub pending_paint_metrics: Vec<Epoch>,
} }
impl PipelineDetails { impl PipelineDetails {
@ -287,7 +300,8 @@ impl PipelineDetails {
throttled: false, throttled: false,
hit_test_items: Vec::new(), hit_test_items: Vec::new(),
scroll_tree: ScrollTree::default(), scroll_tree: ScrollTree::default(),
pending_paint_metrics: Vec::new(), first_paint_metric: PaintMetricState::Waiting,
first_contentful_paint_metric: PaintMetricState::Waiting,
} }
} }
@ -648,12 +662,6 @@ impl IOCompositor {
webview.dispatch_input_event(InputEvent::MouseMove(MouseMoveEvent { point })); webview.dispatch_input_event(InputEvent::MouseMove(MouseMoveEvent { point }));
}, },
CompositorMsg::PendingPaintMetric(webview_id, pipeline_id, epoch) => {
if let Some(webview) = self.webviews.get_mut(webview_id) {
webview.add_pending_paint_metric(pipeline_id, epoch);
}
},
CompositorMsg::CrossProcess(cross_proces_message) => { CompositorMsg::CrossProcess(cross_proces_message) => {
self.handle_cross_process_message(cross_proces_message); self.handle_cross_process_message(cross_proces_message);
}, },
@ -771,6 +779,18 @@ impl IOCompositor {
details.hit_test_items = display_list_info.hit_test_info; details.hit_test_items = display_list_info.hit_test_info;
details.install_new_scroll_tree(display_list_info.scroll_tree); details.install_new_scroll_tree(display_list_info.scroll_tree);
let epoch = display_list_info.epoch;
let first_reflow = display_list_info.first_reflow;
if details.first_paint_metric == PaintMetricState::Waiting {
details.first_paint_metric = PaintMetricState::Seen(epoch, first_reflow);
}
if details.first_contentful_paint_metric == PaintMetricState::Waiting &&
display_list_info.is_contentful
{
details.first_contentful_paint_metric =
PaintMetricState::Seen(epoch, first_reflow);
}
let mut transaction = Transaction::new(); let mut transaction = Transaction::new();
transaction transaction
.set_display_list(display_list_info.epoch, (pipeline_id, built_display_list)); .set_display_list(display_list_info.epoch, (pipeline_id, built_display_list));
@ -1529,16 +1549,8 @@ impl IOCompositor {
let paint_time = CrossProcessInstant::now(); let paint_time = CrossProcessInstant::now();
let document_id = self.webrender_document(); let document_id = self.webrender_document();
for webview_details in self.webviews.iter_mut() { for webview_details in self.webviews.iter_mut() {
// For each pipeline, determine the current epoch and update paint timing if necessary.
for (pipeline_id, pipeline) in webview_details.pipelines.iter_mut() { for (pipeline_id, pipeline) in webview_details.pipelines.iter_mut() {
if pipeline.pending_paint_metrics.is_empty() { let Some(current_epoch) = self
continue;
}
let Some(composition_pipeline) = pipeline.pipeline.as_ref() else {
continue;
};
let Some(WebRenderEpoch(current_epoch)) = self
.webrender .webrender
.as_ref() .as_ref()
.and_then(|wr| wr.current_epoch(document_id, pipeline_id.into())) .and_then(|wr| wr.current_epoch(document_id, pipeline_id.into()))
@ -1546,29 +1558,43 @@ impl IOCompositor {
continue; continue;
}; };
let current_epoch = Epoch(current_epoch); match pipeline.first_paint_metric {
let Some(index) = pipeline // We need to check whether the current epoch is later, because
.pending_paint_metrics // CrossProcessCompositorMessage::SendInitialTransaction sends an
.iter() // empty display list to WebRender which can happen before we receive
.position(|epoch| *epoch == current_epoch) // the first "real" display list.
else { PaintMetricState::Seen(epoch, first_reflow) if epoch <= current_epoch => {
continue; assert!(epoch <= current_epoch);
}; if let Err(error) = self.global.borrow().constellation_sender.send(
ConstellationMsg::PaintMetric(
// Remove all epochs that were pending before the current epochs. They were not and will not,
// be painted.
pipeline.pending_paint_metrics.drain(0..index);
if let Err(error) =
composition_pipeline
.script_chan
.send(ScriptThreadMessage::SetEpochPaintTime(
*pipeline_id, *pipeline_id,
current_epoch, PaintMetricEvent::FirstPaint(paint_time, first_reflow),
paint_time, ),
)) ) {
{ warn!(
warn!("Sending RequestLayoutPaintMetric message to layout failed ({error:?})."); "Sending paint metric event to constellation failed ({error:?})."
);
}
pipeline.first_paint_metric = PaintMetricState::Sent;
},
_ => {},
}
match pipeline.first_contentful_paint_metric {
PaintMetricState::Seen(epoch, first_reflow) if epoch <= current_epoch => {
if let Err(error) = self.global.borrow().constellation_sender.send(
ConstellationMsg::PaintMetric(
*pipeline_id,
PaintMetricEvent::FirstContentfulPaint(paint_time, first_reflow),
),
) {
warn!(
"Sending paint metric event to constellation failed ({error:?})."
);
}
pipeline.first_contentful_paint_metric = PaintMetricState::Sent;
},
_ => {},
} }
} }
} }

View file

@ -39,7 +39,6 @@ mod from_constellation {
Self::SetThrottled(..) => target!("SetThrottled"), Self::SetThrottled(..) => target!("SetThrottled"),
Self::NewWebRenderFrameReady(..) => target!("NewWebRenderFrameReady"), Self::NewWebRenderFrameReady(..) => target!("NewWebRenderFrameReady"),
Self::PipelineExited(..) => target!("PipelineExited"), Self::PipelineExited(..) => target!("PipelineExited"),
Self::PendingPaintMetric(..) => target!("PendingPaintMetric"),
Self::LoadComplete(..) => target!("LoadComplete"), Self::LoadComplete(..) => target!("LoadComplete"),
Self::WebDriverMouseButtonEvent(..) => target!("WebDriverMouseButtonEvent"), Self::WebDriverMouseButtonEvent(..) => target!("WebDriverMouseButtonEvent"),
Self::WebDriverMouseMoveEvent(..) => target!("WebDriverMouseMoveEvent"), Self::WebDriverMouseMoveEvent(..) => target!("WebDriverMouseMoveEvent"),

View file

@ -252,12 +252,6 @@ impl WebView {
} }
} }
pub(crate) fn add_pending_paint_metric(&mut self, pipeline_id: PipelineId, epoch: base::Epoch) {
self.ensure_pipeline_details(pipeline_id)
.pending_paint_metrics
.push(epoch);
}
/// On a Window refresh tick (e.g. vsync) /// On a Window refresh tick (e.g. vsync)
pub fn on_vsync(&mut self) { pub fn on_vsync(&mut self) {
if let Some(fling_action) = self.touch_handler.on_vsync() { if let Some(fling_action) = self.touch_handler.on_vsync() {

View file

@ -109,7 +109,8 @@ use canvas_traits::ConstellationCanvasMsg;
use canvas_traits::canvas::{CanvasId, CanvasMsg}; use canvas_traits::canvas::{CanvasId, CanvasMsg};
use canvas_traits::webgl::WebGLThreads; use canvas_traits::webgl::WebGLThreads;
use compositing_traits::{ use compositing_traits::{
CompositorMsg, CompositorProxy, ConstellationMsg as FromCompositorMsg, SendableFrameTree, CompositorMsg, CompositorProxy, ConstellationMsg as FromCompositorMsg, PaintMetricEvent,
SendableFrameTree,
}; };
use crossbeam_channel::{Receiver, Sender, select, unbounded}; use crossbeam_channel::{Receiver, Sender, select, unbounded};
use devtools_traits::{ use devtools_traits::{
@ -141,8 +142,8 @@ use script_traits::{
AnimationState, AnimationTickType, AuxiliaryWebViewCreationRequest, AnimationState, AnimationTickType, AuxiliaryWebViewCreationRequest,
AuxiliaryWebViewCreationResponse, BroadcastMsg, ConstellationInputEvent, AuxiliaryWebViewCreationResponse, BroadcastMsg, ConstellationInputEvent,
DiscardBrowsingContext, DocumentActivity, DocumentState, IFrameLoadInfo, DiscardBrowsingContext, DocumentActivity, DocumentState, IFrameLoadInfo,
IFrameLoadInfoWithData, IFrameSandboxState, IFrameSizeMsg, Job, LayoutMsg as FromLayoutMsg, IFrameLoadInfoWithData, IFrameSandboxState, IFrameSizeMsg, Job, LoadData, LoadOrigin, LogEntry,
LoadData, LoadOrigin, LogEntry, MessagePortMsg, NavigationHistoryBehavior, PortMessageTask, MessagePortMsg, NavigationHistoryBehavior, PortMessageTask, ProgressiveWebMetricType,
SWManagerMsg, SWManagerSenders, ScriptMsg as FromScriptMsg, ScriptThreadMessage, SWManagerMsg, SWManagerSenders, ScriptMsg as FromScriptMsg, ScriptThreadMessage,
ScriptToConstellationChan, ServiceWorkerManagerFactory, ServiceWorkerMsg, ScriptToConstellationChan, ServiceWorkerManagerFactory, ServiceWorkerMsg,
StructuredSerializedData, UpdatePipelineIdReason, WindowSizeData, WindowSizeType, StructuredSerializedData, UpdatePipelineIdReason, WindowSizeData, WindowSizeType,
@ -309,14 +310,6 @@ pub struct Constellation<STF, SWF> {
/// dependency between script and layout. /// dependency between script and layout.
layout_factory: Arc<dyn LayoutFactory>, layout_factory: Arc<dyn LayoutFactory>,
/// An IPC channel for layout to send messages to the constellation.
/// This is the layout's view of `layout_receiver`.
layout_sender: IpcSender<FromLayoutMsg>,
/// A channel for the constellation to receive messages from layout.
/// This is the constellation's view of `layout_sender`.
layout_receiver: Receiver<Result<FromLayoutMsg, IpcError>>,
/// A channel for the constellation to receive messages from the compositor thread. /// A channel for the constellation to receive messages from the compositor thread.
compositor_receiver: Receiver<FromCompositorMsg>, compositor_receiver: Receiver<FromCompositorMsg>,
@ -661,13 +654,6 @@ where
) )
}; };
let (layout_ipc_sender, layout_ipc_receiver) =
ipc::channel().expect("ipc channel failure");
let layout_receiver =
route_ipc_receiver_to_new_crossbeam_receiver_preserving_errors(
layout_ipc_receiver,
);
let swmanager_receiver = let swmanager_receiver =
route_ipc_receiver_to_new_crossbeam_receiver_preserving_errors( route_ipc_receiver_to_new_crossbeam_receiver_preserving_errors(
swmanager_ipc_receiver, swmanager_ipc_receiver,
@ -693,11 +679,9 @@ where
background_hang_monitor_receiver, background_hang_monitor_receiver,
background_monitor_register, background_monitor_register,
background_monitor_control_senders: background_hang_monitor_control_ipc_senders, background_monitor_control_senders: background_hang_monitor_control_ipc_senders,
layout_sender: layout_ipc_sender,
script_receiver, script_receiver,
compositor_receiver, compositor_receiver,
layout_factory, layout_factory,
layout_receiver,
embedder_proxy: state.embedder_proxy, embedder_proxy: state.embedder_proxy,
compositor_proxy: state.compositor_proxy, compositor_proxy: state.compositor_proxy,
webviews: WebViewManager::default(), webviews: WebViewManager::default(),
@ -967,7 +951,6 @@ where
background_hang_monitor_to_constellation_chan: self background_hang_monitor_to_constellation_chan: self
.background_hang_monitor_sender .background_hang_monitor_sender
.clone(), .clone(),
layout_to_constellation_chan: self.layout_sender.clone(),
layout_factory: self.layout_factory.clone(), layout_factory: self.layout_factory.clone(),
compositor_proxy: self.compositor_proxy.clone(), compositor_proxy: self.compositor_proxy.clone(),
devtools_sender: self.devtools_sender.clone(), devtools_sender: self.devtools_sender.clone(),
@ -1132,7 +1115,6 @@ where
Script((PipelineId, FromScriptMsg)), Script((PipelineId, FromScriptMsg)),
BackgroundHangMonitor(HangMonitorAlert), BackgroundHangMonitor(HangMonitorAlert),
Compositor(FromCompositorMsg), Compositor(FromCompositorMsg),
Layout(FromLayoutMsg),
FromSWManager(SWManagerMsg), FromSWManager(SWManagerMsg),
} }
// Get one incoming request. // Get one incoming request.
@ -1163,9 +1145,6 @@ where
recv(self.compositor_receiver) -> msg => { recv(self.compositor_receiver) -> msg => {
Ok(Request::Compositor(msg.expect("Unexpected compositor channel panic in constellation"))) Ok(Request::Compositor(msg.expect("Unexpected compositor channel panic in constellation")))
} }
recv(self.layout_receiver) -> msg => {
msg.expect("Unexpected layout channel panic in constellation").map(Request::Layout)
}
recv(self.swmanager_receiver) -> msg => { recv(self.swmanager_receiver) -> msg => {
msg.expect("Unexpected SW channel panic in constellation").map(Request::FromSWManager) msg.expect("Unexpected SW channel panic in constellation").map(Request::FromSWManager)
} }
@ -1188,9 +1167,6 @@ where
Request::BackgroundHangMonitor(message) => { Request::BackgroundHangMonitor(message) => {
self.handle_request_from_background_hang_monitor(message); self.handle_request_from_background_hang_monitor(message);
}, },
Request::Layout(message) => {
self.handle_request_from_layout(message);
},
Request::FromSWManager(message) => { Request::FromSWManager(message) => {
self.handle_request_from_swmanager(message); self.handle_request_from_swmanager(message);
}, },
@ -1409,6 +1385,9 @@ where
FromCompositorMsg::SetScrollStates(pipeline_id, scroll_states) => { FromCompositorMsg::SetScrollStates(pipeline_id, scroll_states) => {
self.handle_set_scroll_states(pipeline_id, scroll_states) self.handle_set_scroll_states(pipeline_id, scroll_states)
}, },
FromCompositorMsg::PaintMetric(pipeline_id, paint_metric_event) => {
self.handle_paint_metric(pipeline_id, paint_metric_event);
},
} }
} }
@ -1980,19 +1959,6 @@ where
} }
} }
#[cfg_attr(
feature = "tracing",
tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace")
)]
fn handle_request_from_layout(&mut self, message: FromLayoutMsg) {
trace_layout_msg!(message, "{message:?}");
match message {
FromLayoutMsg::PendingPaintMetric(webview_id, pipeline_id, epoch) => {
self.handle_pending_paint_metric(webview_id, pipeline_id, epoch);
},
}
}
#[cfg_attr( #[cfg_attr(
feature = "tracing", feature = "tracing",
tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace") tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace")
@ -3401,24 +3367,6 @@ where
}); });
} }
#[cfg_attr(
feature = "tracing",
tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace")
)]
fn handle_pending_paint_metric(
&self,
webview_id: WebViewId,
pipeline_id: PipelineId,
epoch: Epoch,
) {
self.compositor_proxy
.send(CompositorMsg::PendingPaintMetric(
webview_id,
pipeline_id,
epoch,
))
}
#[cfg_attr( #[cfg_attr(
feature = "tracing", feature = "tracing",
tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace") tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace")
@ -5585,4 +5533,35 @@ where
warn!("Could not send scroll offsets to pipeline: {pipeline_id:?}: {error:?}"); warn!("Could not send scroll offsets to pipeline: {pipeline_id:?}: {error:?}");
} }
} }
#[cfg_attr(
feature = "tracing",
tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace")
)]
fn handle_paint_metric(&mut self, pipeline_id: PipelineId, event: PaintMetricEvent) {
let Some(pipeline) = self.pipelines.get(&pipeline_id) else {
warn!("Discarding paint metric event for unknown pipeline");
return;
};
let (metric_type, metric_value, first_reflow) = match event {
PaintMetricEvent::FirstPaint(metric_value, first_reflow) => (
ProgressiveWebMetricType::FirstPaint,
metric_value,
first_reflow,
),
PaintMetricEvent::FirstContentfulPaint(metric_value, first_reflow) => (
ProgressiveWebMetricType::FirstContentfulPaint,
metric_value,
first_reflow,
),
};
if let Err(error) = pipeline.event_loop.send(ScriptThreadMessage::PaintMetric(
pipeline_id,
metric_type,
metric_value,
first_reflow,
)) {
warn!("Could not sent paint metric event to pipeline: {pipeline_id:?}: {error:?}");
}
}
} }

View file

@ -34,9 +34,8 @@ use net_traits::image_cache::ImageCache;
use profile_traits::{mem as profile_mem, time}; use profile_traits::{mem as profile_mem, time};
use script_layout_interface::{LayoutFactory, ScriptThreadFactory}; use script_layout_interface::{LayoutFactory, ScriptThreadFactory};
use script_traits::{ use script_traits::{
AnimationState, DiscardBrowsingContext, DocumentActivity, InitialScriptState, LayoutMsg, AnimationState, DiscardBrowsingContext, DocumentActivity, InitialScriptState, LoadData,
LoadData, NewLayoutInfo, SWManagerMsg, ScriptThreadMessage, ScriptToConstellationChan, NewLayoutInfo, SWManagerMsg, ScriptThreadMessage, ScriptToConstellationChan, WindowSizeData,
WindowSizeData,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use servo_config::opts::{self, Opts}; use servo_config::opts::{self, Opts};
@ -133,9 +132,6 @@ pub struct InitialPipelineState {
/// A channel for the background hang monitor to send messages to the constellation. /// A channel for the background hang monitor to send messages to the constellation.
pub background_hang_monitor_to_constellation_chan: IpcSender<HangMonitorAlert>, pub background_hang_monitor_to_constellation_chan: IpcSender<HangMonitorAlert>,
/// A channel for the layout to send messages to the constellation.
pub layout_to_constellation_chan: IpcSender<LayoutMsg>,
/// A fatory for creating layouts to be used by the ScriptThread. /// A fatory for creating layouts to be used by the ScriptThread.
pub layout_factory: Arc<dyn LayoutFactory>, pub layout_factory: Arc<dyn LayoutFactory>,
@ -279,7 +275,6 @@ impl Pipeline {
time_profiler_chan: state.time_profiler_chan, time_profiler_chan: state.time_profiler_chan,
mem_profiler_chan: state.mem_profiler_chan, mem_profiler_chan: state.mem_profiler_chan,
window_size: state.window_size, window_size: state.window_size,
layout_to_constellation_chan: state.layout_to_constellation_chan,
script_chan: script_chan.clone(), script_chan: script_chan.clone(),
load_data: state.load_data.clone(), load_data: state.load_data.clone(),
script_port, script_port,
@ -481,7 +476,6 @@ pub struct UnprivilegedPipelineContent {
script_to_constellation_chan: ScriptToConstellationChan, script_to_constellation_chan: ScriptToConstellationChan,
background_hang_monitor_to_constellation_chan: IpcSender<HangMonitorAlert>, background_hang_monitor_to_constellation_chan: IpcSender<HangMonitorAlert>,
bhm_control_port: Option<IpcReceiver<BackgroundHangMonitorControlMsg>>, bhm_control_port: Option<IpcReceiver<BackgroundHangMonitorControlMsg>>,
layout_to_constellation_chan: IpcSender<LayoutMsg>,
devtools_ipc_sender: Option<IpcSender<ScriptToDevtoolsControlMsg>>, devtools_ipc_sender: Option<IpcSender<ScriptToDevtoolsControlMsg>>,
#[cfg(feature = "bluetooth")] #[cfg(feature = "bluetooth")]
bluetooth_thread: IpcSender<BluetoothRequest>, bluetooth_thread: IpcSender<BluetoothRequest>,
@ -533,7 +527,6 @@ impl UnprivilegedPipelineContent {
constellation_receiver: self.script_port, constellation_receiver: self.script_port,
pipeline_to_constellation_sender: self.script_to_constellation_chan.clone(), pipeline_to_constellation_sender: self.script_to_constellation_chan.clone(),
background_hang_monitor_register: background_hang_monitor_register.clone(), background_hang_monitor_register: background_hang_monitor_register.clone(),
layout_to_constellation_ipc_sender: self.layout_to_constellation_chan.clone(),
#[cfg(feature = "bluetooth")] #[cfg(feature = "bluetooth")]
bluetooth_sender: self.bluetooth_thread, bluetooth_sender: self.bluetooth_thread,
resource_threads: self.resource_threads, resource_threads: self.resource_threads,

View file

@ -30,17 +30,6 @@ macro_rules! trace_script_msg {
}; };
} }
/// Log an event from layout at trace level.
/// - To disable tracing: RUST_LOG='constellation<layout@=off'
/// - To enable tracing: RUST_LOG='constellation<layout@'
macro_rules! trace_layout_msg {
// This macro only exists to put the docs in the same file as the target prefix,
// so the macro definition is always the same.
($event:expr, $($rest:tt)+) => {
::log::trace!(target: $crate::tracing::LogTarget::log_target(&$event), $($rest)+)
};
}
/// Get the log target for an event, as a static string. /// Get the log target for an event, as a static string.
pub(crate) trait LogTarget { pub(crate) trait LogTarget {
fn log_target(&self) -> &'static str; fn log_target(&self) -> &'static str;
@ -87,6 +76,7 @@ mod from_compositor {
Self::MediaSessionAction(_) => target!("MediaSessionAction"), Self::MediaSessionAction(_) => target!("MediaSessionAction"),
Self::SetWebViewThrottled(_, _) => target!("SetWebViewThrottled"), Self::SetWebViewThrottled(_, _) => target!("SetWebViewThrottled"),
Self::SetScrollStates(..) => target!("SetScrollStates"), Self::SetScrollStates(..) => target!("SetScrollStates"),
Self::PaintMetric(..) => target!("PaintMetric"),
} }
} }
} }
@ -249,21 +239,3 @@ mod from_script {
} }
} }
} }
mod from_layout {
use super::LogTarget;
macro_rules! target {
($($name:literal)+) => {
concat!("constellation<layout@", $($name),+)
};
}
impl LogTarget for script_traits::LayoutMsg {
fn log_target(&self) -> &'static str {
match self {
Self::PendingPaintMetric(..) => target!("PendingPaintMetric"),
}
}
}
}

View file

@ -102,6 +102,7 @@ impl DisplayList {
pipeline_id: wr::PipelineId, pipeline_id: wr::PipelineId,
epoch: wr::Epoch, epoch: wr::Epoch,
viewport_scroll_sensitivity: AxesScrollSensitivity, viewport_scroll_sensitivity: AxesScrollSensitivity,
first_reflow: bool,
) -> Self { ) -> Self {
Self { Self {
wr: wr::DisplayListBuilder::new(pipeline_id), wr: wr::DisplayListBuilder::new(pipeline_id),
@ -111,6 +112,7 @@ impl DisplayList {
pipeline_id, pipeline_id,
epoch, epoch,
viewport_scroll_sensitivity, viewport_scroll_sensitivity,
first_reflow,
), ),
spatial_tree_count: 0, spatial_tree_count: 0,
} }
@ -161,11 +163,6 @@ pub(crate) struct DisplayListBuilder<'a> {
/// The [DisplayList] used to collect display list items and metadata. /// The [DisplayList] used to collect display list items and metadata.
pub display_list: &'a mut DisplayList, pub display_list: &'a mut DisplayList,
/// Contentful paint i.e. whether the display list contains items of type
/// text, image, non-white canvas or SVG). Used by metrics.
/// See <https://w3c.github.io/paint-timing/#first-contentful-paint>.
is_contentful: bool,
} }
impl DisplayList { impl DisplayList {
@ -175,7 +172,7 @@ impl DisplayList {
context: &LayoutContext, context: &LayoutContext,
fragment_tree: &FragmentTree, fragment_tree: &FragmentTree,
root_stacking_context: &StackingContext, root_stacking_context: &StackingContext,
) -> bool { ) {
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
let _span = tracing::trace_span!("display_list::build", servo_profiling = true).entered(); let _span = tracing::trace_span!("display_list::build", servo_profiling = true).entered();
let mut builder = DisplayListBuilder { let mut builder = DisplayListBuilder {
@ -183,12 +180,10 @@ impl DisplayList {
current_reference_frame_scroll_node_id: self.compositor_info.root_reference_frame_id, current_reference_frame_scroll_node_id: self.compositor_info.root_reference_frame_id,
current_clip_chain_id: ClipChainId::INVALID, current_clip_chain_id: ClipChainId::INVALID,
element_for_canvas_background: fragment_tree.canvas_background.from_element, element_for_canvas_background: fragment_tree.canvas_background.from_element,
is_contentful: false,
context, context,
display_list: self, display_list: self,
}; };
fragment_tree.build_display_list(&mut builder, root_stacking_context); fragment_tree.build_display_list(&mut builder, root_stacking_context);
builder.is_contentful
} }
} }
@ -197,6 +192,10 @@ impl DisplayListBuilder<'_> {
&mut self.display_list.wr &mut self.display_list.wr
} }
fn mark_is_contentful(&mut self) {
self.display_list.compositor_info.is_contentful = true;
}
fn common_properties( fn common_properties(
&self, &self,
clip_rect: units::LayoutRect, clip_rect: units::LayoutRect,
@ -282,7 +281,7 @@ impl Fragment {
let image = image.borrow(); let image = image.borrow();
match image.style.get_inherited_box().visibility { match image.style.get_inherited_box().visibility {
Visibility::Visible => { Visibility::Visible => {
builder.is_contentful = true; builder.mark_is_contentful();
let image_rendering = image let image_rendering = image
.style .style
@ -318,7 +317,7 @@ impl Fragment {
let iframe = iframe.borrow(); let iframe = iframe.borrow();
match iframe.style.get_inherited_box().visibility { match iframe.style.get_inherited_box().visibility {
Visibility::Visible => { Visibility::Visible => {
builder.is_contentful = true; builder.mark_is_contentful();
let rect = iframe.rect.translate(containing_block.origin.to_vector()); let rect = iframe.rect.translate(containing_block.origin.to_vector());
let common = builder.common_properties(rect.to_webrender(), &iframe.style); let common = builder.common_properties(rect.to_webrender(), &iframe.style);
@ -384,7 +383,7 @@ impl Fragment {
// NB: The order of painting text components (CSS Text Decoration Module Level 3) is: // NB: The order of painting text components (CSS Text Decoration Module Level 3) is:
// shadows, underline, overline, text, text-emphasis, and then line-through. // shadows, underline, overline, text, text-emphasis, and then line-through.
builder.is_contentful = true; builder.mark_is_contentful();
let rect = fragment.rect.translate(containing_block.origin.to_vector()); let rect = fragment.rect.translate(containing_block.origin.to_vector());
let mut baseline_origin = rect.origin; let mut baseline_origin = rect.origin;

View file

@ -16,7 +16,6 @@ use std::sync::{Arc, LazyLock};
use app_units::Au; use app_units::Au;
use base::Epoch; use base::Epoch;
use base::cross_process_instant::CrossProcessInstant;
use base::id::{PipelineId, WebViewId}; use base::id::{PipelineId, WebViewId};
use embedder_traits::resources::{self, Resource}; use embedder_traits::resources::{self, Resource};
use euclid::default::{Point2D as UntypedPoint2D, Rect as UntypedRect, Size2D as UntypedSize2D}; use euclid::default::{Point2D as UntypedPoint2D, Rect as UntypedRect, Size2D as UntypedSize2D};
@ -37,7 +36,6 @@ use layout::traversal::RecalcStyle;
use layout::{BoxTree, FragmentTree}; use layout::{BoxTree, FragmentTree};
use log::{debug, error}; use log::{debug, error};
use malloc_size_of::{MallocSizeOf, MallocSizeOfOps}; use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
use metrics::{PaintTimeMetrics, ProfilerMetadataFactory};
use net_traits::image_cache::{ImageCache, UsePlaceholder}; use net_traits::image_cache::{ImageCache, UsePlaceholder};
use parking_lot::{Mutex, RwLock}; use parking_lot::{Mutex, RwLock};
use profile_traits::mem::{Report, ReportKind}; use profile_traits::mem::{Report, ReportKind};
@ -160,9 +158,6 @@ pub struct LayoutThread {
/// Cross-process access to the Compositor API. /// Cross-process access to the Compositor API.
compositor_api: CrossProcessCompositorApi, compositor_api: CrossProcessCompositorApi,
/// Paint time metrics.
paint_time_metrics: PaintTimeMetrics,
/// Debug options, copied from configuration to this `LayoutThread` in order /// Debug options, copied from configuration to this `LayoutThread` in order
/// to avoid having to constantly access the thread-safe global options. /// to avoid having to constantly access the thread-safe global options.
debug: DebugOptions, debug: DebugOptions,
@ -462,10 +457,6 @@ impl Layout for LayoutThread {
.map(|scroll_state| (scroll_state.scroll_id, scroll_state.scroll_offset)) .map(|scroll_state| (scroll_state.scroll_id, scroll_state.scroll_offset))
.collect(); .collect();
} }
fn set_epoch_paint_time(&mut self, epoch: Epoch, paint_time: CrossProcessInstant) {
self.paint_time_metrics.maybe_set_metric(epoch, paint_time);
}
} }
impl LayoutThread { impl LayoutThread {
@ -520,7 +511,6 @@ impl LayoutThread {
scroll_offsets: Default::default(), scroll_offsets: Default::default(),
stylist: Stylist::new(device, QuirksMode::NoQuirks), stylist: Stylist::new(device, QuirksMode::NoQuirks),
webrender_image_cache: Default::default(), webrender_image_cache: Default::default(),
paint_time_metrics: config.paint_time_metrics,
debug: opts::get().debug.clone(), debug: opts::get().debug.clone(),
} }
} }
@ -856,6 +846,7 @@ impl LayoutThread {
self.id.into(), self.id.into(),
epoch.into(), epoch.into(),
fragment_tree.viewport_scroll_sensitivity, fragment_tree.viewport_scroll_sensitivity,
self.first_reflow.get(),
); );
display_list.wr.begin(); display_list.wr.begin();
@ -874,7 +865,7 @@ impl LayoutThread {
display_list.build_stacking_context_tree(&fragment_tree, &self.debug); display_list.build_stacking_context_tree(&fragment_tree, &self.debug);
// Build the rest of the display list which inclues all of the WebRender primitives. // Build the rest of the display list which inclues all of the WebRender primitives.
let is_contentful = display_list.build(context, &fragment_tree, &root_stacking_context); display_list.build(context, &fragment_tree, &root_stacking_context);
if self.debug.dump_flow_tree { if self.debug.dump_flow_tree {
fragment_tree.print(); fragment_tree.print();
@ -884,12 +875,6 @@ impl LayoutThread {
} }
debug!("Layout done!"); debug!("Layout done!");
// Observe notifications about rendered frames if needed right before
// sending the display list to WebRender in order to set time related
// Progressive Web Metrics.
self.paint_time_metrics
.maybe_observe_paint_time(self, epoch, is_contentful);
if reflow_goal.needs_display() { if reflow_goal.needs_display() {
self.compositor_api.send_display_list( self.compositor_api.send_display_list(
self.webview_id, self.webview_id,
@ -991,12 +976,6 @@ impl LayoutThread {
} }
} }
impl ProfilerMetadataFactory for LayoutThread {
fn new_metadata(&self) -> Option<TimerMetadata> {
self.profiler_metadata()
}
}
fn get_ua_stylesheets() -> Result<UserAgentStylesheets, &'static str> { fn get_ua_stylesheets() -> Result<UserAgentStylesheets, &'static str> {
fn parse_ua_stylesheet( fn parse_ua_stylesheet(
shared_lock: &SharedRwLock, shared_lock: &SharedRwLock,

View file

@ -2,38 +2,20 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * 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::cmp::Ordering;
use std::collections::HashMap;
use std::time::Duration; use std::time::Duration;
use base::Epoch;
use base::cross_process_instant::CrossProcessInstant; 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 malloc_size_of_derive::MallocSizeOf;
use profile_traits::time::{ProfilerCategory, ProfilerChan, TimerMetadata, send_profile_data}; use profile_traits::time::{
use script_traits::{LayoutMsg, ProgressiveWebMetricType, ScriptThreadMessage}; ProfilerCategory, ProfilerChan, TimerMetadata, TimerMetadataFrameType, TimerMetadataReflowType,
send_profile_data,
};
use script_traits::ProgressiveWebMetricType;
use servo_config::opts; use servo_config::opts;
use servo_url::ServoUrl; 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 /// TODO make this configurable
/// maximum task time is 50ms (in ns) /// maximum task time is 50ms (in ns)
pub const MAX_TASK_NS: u64 = 50000000; pub const MAX_TASK_NS: u64 = 50000000;
@ -50,8 +32,8 @@ impl ToMs<f64> for u64 {
} }
} }
fn set_metric<U: ProgressiveWebMetric>( fn set_metric(
pwm: &U, pwm: &ProgressiveWebMetrics,
metadata: Option<TimerMetadata>, metadata: Option<TimerMetadata>,
metric_type: ProgressiveWebMetricType, metric_type: ProgressiveWebMetricType,
category: ProfilerCategory, category: ProfilerCategory,
@ -61,14 +43,11 @@ fn set_metric<U: ProgressiveWebMetric>(
) { ) {
attr.set(Some(metric_time)); 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 the metric to the time profiler.
send_profile_data( send_profile_data(
category, category,
metadata, metadata,
pwm.get_time_profiler_chan(), pwm.time_profiler_chan(),
metric_time, metric_time,
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. // Print the metric to console if the print-pwm option was given.
if opts::get().print_pwm { if opts::get().print_pwm {
let navigation_start = pwm let navigation_start = pwm
.get_navigation_start() .navigation_start()
.unwrap_or_else(CrossProcessInstant::epoch); .unwrap_or_else(CrossProcessInstant::epoch);
println!( println!(
"{:?} {:?} {:?}", "{:?} {:?} {:?}",
@ -87,14 +66,19 @@ fn set_metric<U: ProgressiveWebMetric>(
} }
} }
// spec: https://github.com/WICG/time-to-interactive /// A data structure to track web metrics dfined in various specifications:
// https://github.com/GoogleChrome/lighthouse/issues/27 ///
// we can look at three different metrics here: /// - <https://w3c.github.io/paint-timing/>
// navigation start -> visually ready (dom content loaded) /// - <https://github.com/WICG/time-to-interactive> / <https://github.com/GoogleChrome/lighthouse/issues/27>
// navigation start -> thread ready (main thread available) ///
// visually ready -> thread ready /// 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)] #[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 /// when we navigated to the page
navigation_start: Option<CrossProcessInstant>, navigation_start: Option<CrossProcessInstant>,
/// indicates if the page is visually ready /// indicates if the page is visually ready
@ -103,6 +87,15 @@ pub struct InteractiveMetrics {
main_thread_available: Cell<Option<CrossProcessInstant>>, main_thread_available: Cell<Option<CrossProcessInstant>>,
// max(main_thread_available, dom_content_loaded) // max(main_thread_available, dom_content_loaded)
time_to_interactive: Cell<Option<CrossProcessInstant>>, 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"] #[ignore_malloc_size_of = "can't measure channels"]
time_profiler_chan: ProfilerChan, time_profiler_chan: ProfilerChan,
url: ServoUrl, url: ServoUrl,
@ -146,18 +139,36 @@ pub enum InteractiveFlag {
TimeToInteractive(CrossProcessInstant), TimeToInteractive(CrossProcessInstant),
} }
impl InteractiveMetrics { impl ProgressiveWebMetrics {
pub fn new(time_profiler_chan: ProfilerChan, url: ServoUrl) -> InteractiveMetrics { pub fn new(
InteractiveMetrics { time_profiler_chan: ProfilerChan,
url: ServoUrl,
frame_type: TimerMetadataFrameType,
) -> ProgressiveWebMetrics {
ProgressiveWebMetrics {
frame_type,
navigation_start: None, navigation_start: None,
dom_content_loaded: Cell::new(None), dom_content_loaded: Cell::new(None),
main_thread_available: Cell::new(None), main_thread_available: Cell::new(None),
time_to_interactive: Cell::new(None), time_to_interactive: Cell::new(None),
first_paint: Cell::new(None),
first_contentful_paint: Cell::new(None),
time_profiler_chan, time_profiler_chan,
url, 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) { pub fn set_dom_content_loaded(&self) {
if self.dom_content_loaded.get().is_none() { if self.dom_content_loaded.get().is_none() {
self.dom_content_loaded 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() 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() 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 // 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 // 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) pub fn maybe_set_tti(&self, metric: InteractiveFlag) {
where
T: ProfilerMetadataFactory,
{
if self.get_tti().is_some() { if self.get_tti().is_some() {
return; return;
} }
@ -206,7 +246,7 @@ impl InteractiveMetrics {
}; };
set_metric( set_metric(
self, self,
profiler_metadata_factory.new_metadata(), Some(self.make_metadata(true)),
ProgressiveWebMetricType::TimeToInteractive, ProgressiveWebMetricType::TimeToInteractive,
ProfilerCategory::TimeToInteractive, ProfilerCategory::TimeToInteractive,
&self.time_to_interactive, &self.time_to_interactive,
@ -222,167 +262,110 @@ impl InteractiveMetrics {
pub fn needs_tti(&self) -> bool { pub fn needs_tti(&self) -> bool {
self.get_tti().is_none() self.get_tti().is_none()
} }
}
impl ProgressiveWebMetric for InteractiveMetrics { pub fn navigation_start(&self) -> Option<CrossProcessInstant> {
fn get_navigation_start(&self) -> Option<CrossProcessInstant> {
self.navigation_start 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); self.navigation_start = Some(time);
} }
fn send_queued_constellation_msg( pub fn time_profiler_chan(&self) -> &ProfilerChan {
&self,
_name: ProgressiveWebMetricType,
_time: CrossProcessInstant,
) {
}
fn get_time_profiler_chan(&self) -> &ProfilerChan {
&self.time_profiler_chan &self.time_profiler_chan
} }
fn get_url(&self) -> &ServoUrl {
&self.url
}
} }
// https://w3c.github.io/paint-timing/ #[cfg(test)]
pub struct PaintTimeMetrics { fn test_metrics() -> ProgressiveWebMetrics {
pending_metrics: RefCell<HashMap<Epoch, (Option<TimerMetadata>, bool)>>, let (sender, _) = ipc_channel::ipc::channel().unwrap();
navigation_start: CrossProcessInstant, let profiler_chan = ProfilerChan(sender);
first_paint: Cell<Option<CrossProcessInstant>>, let mut metrics = ProgressiveWebMetrics::new(
first_contentful_paint: Cell<Option<CrossProcessInstant>>, profiler_chan,
webview_id: WebViewId, ServoUrl::parse("about:blank").unwrap(),
pipeline_id: PipelineId, TimerMetadataFrameType::RootWindow,
time_profiler_chan: ProfilerChan,
constellation_chan: IpcSender<LayoutMsg>,
script_chan: IpcSender<ScriptThreadMessage>,
url: ServoUrl,
}
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,
}
}
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. assert!((&metrics).navigation_start().is_none());
// The compositor will record the current time after painting the assert!(metrics.get_tti().is_none());
// frame with the given ID and will send the metric back to us. assert!(metrics.first_contentful_paint().is_none());
let msg = LayoutMsg::PendingPaintMetric(self.webview_id, self.pipeline_id, epoch); assert!(metrics.first_paint().is_none());
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) { metrics.set_navigation_start(CrossProcessInstant::now());
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) { metrics
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()
}
} }
impl ProgressiveWebMetric for PaintTimeMetrics { #[test]
fn get_navigation_start(&self) -> Option<CrossProcessInstant> { fn test_set_dcl() {
Some(self.navigation_start) let metrics = test_metrics();
} metrics.maybe_set_tti(InteractiveFlag::DOMContentLoaded);
let dcl = metrics.dom_content_loaded();
assert!(dcl.is_some());
fn set_navigation_start(&mut self, time: CrossProcessInstant) { //try to overwrite
self.navigation_start = time; metrics.maybe_set_tti(InteractiveFlag::DOMContentLoaded);
} assert_eq!(metrics.dom_content_loaded(), dcl);
assert_eq!(metrics.get_tti(), None);
fn send_queued_constellation_msg( }
&self,
name: ProgressiveWebMetricType, #[test]
time: CrossProcessInstant, fn test_set_mta() {
) { let metrics = test_metrics();
let msg = ScriptThreadMessage::PaintMetric(self.pipeline_id, name, time); let now = CrossProcessInstant::now();
if let Err(e) = self.script_chan.send(msg) { metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive(now));
warn!("Sending metric to script thread failed ({}).", e); 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 get_time_profiler_chan(&self) -> &ProfilerChan { //try to overwrite
&self.time_profiler_chan metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive(
} CrossProcessInstant::now(),
));
fn get_url(&self) -> &ServoUrl { assert_eq!(metrics.main_thread_available(), main_thread_available_time);
&self.url 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());
} }

View file

@ -36,10 +36,7 @@ use hyper_serde::Serde;
use ipc_channel::ipc; use ipc_channel::ipc;
use js::rust::{HandleObject, HandleValue}; use js::rust::{HandleObject, HandleValue};
use keyboard_types::{Code, Key, KeyState}; use keyboard_types::{Code, Key, KeyState};
use metrics::{ use metrics::{InteractiveFlag, InteractiveWindow, ProgressiveWebMetrics};
InteractiveFlag, InteractiveMetrics, InteractiveWindow, ProfilerMetadataFactory,
ProgressiveWebMetric,
};
use mime::{self, Mime}; use mime::{self, Mime};
use net_traits::CookieSource::NonHTTP; use net_traits::CookieSource::NonHTTP;
use net_traits::CoreResourceMsg::{GetCookiesForUrl, SetCookiesForUrl}; use net_traits::CoreResourceMsg::{GetCookiesForUrl, SetCookiesForUrl};
@ -51,10 +48,11 @@ use net_traits::{FetchResponseListener, IpcSend, ReferrerPolicy};
use num_traits::ToPrimitive; use num_traits::ToPrimitive;
use percent_encoding::percent_decode; use percent_encoding::percent_decode;
use profile_traits::ipc as profile_ipc; use profile_traits::ipc as profile_ipc;
use profile_traits::time::{TimerMetadata, TimerMetadataFrameType, TimerMetadataReflowType}; use profile_traits::time::TimerMetadataFrameType;
use script_layout_interface::{PendingRestyle, TrustedNodeAddress}; use script_layout_interface::{PendingRestyle, TrustedNodeAddress};
use script_traits::{ use script_traits::{
AnimationState, AnimationTickType, ConstellationInputEvent, DocumentActivity, ScriptMsg, AnimationState, AnimationTickType, ConstellationInputEvent, DocumentActivity,
ProgressiveWebMetricType, ScriptMsg,
}; };
use servo_arc::Arc; use servo_arc::Arc;
use servo_config::pref; use servo_config::pref;
@ -78,6 +76,7 @@ use webrender_traits::CompositorHitTestResult;
use super::bindings::codegen::Bindings::XPathEvaluatorBinding::XPathEvaluatorMethods; use super::bindings::codegen::Bindings::XPathEvaluatorBinding::XPathEvaluatorMethods;
use super::clipboardevent::ClipboardEventType; use super::clipboardevent::ClipboardEventType;
use super::performancepainttiming::PerformancePaintTiming;
use crate::DomTypes; use crate::DomTypes;
use crate::animation_timeline::AnimationTimeline; use crate::animation_timeline::AnimationTimeline;
use crate::animations::Animations; use crate::animations::Animations;
@ -432,7 +431,7 @@ pub(crate) struct Document {
/// See <https://html.spec.whatwg.org/multipage/#form-owner> /// See <https://html.spec.whatwg.org/multipage/#form-owner>
form_id_listener_map: DomRefCell<HashMapTracedValues<Atom, HashSet<Dom<Element>>>>, form_id_listener_map: DomRefCell<HashMapTracedValues<Atom, HashSet<Dom<Element>>>>,
#[no_trace] #[no_trace]
interactive_time: DomRefCell<InteractiveMetrics>, interactive_time: DomRefCell<ProgressiveWebMetrics>,
#[no_trace] #[no_trace]
tti_window: DomRefCell<InteractiveWindow>, tti_window: DomRefCell<InteractiveWindow>,
/// RAII canceller for Fetch /// RAII canceller for Fetch
@ -3010,7 +3009,7 @@ impl Document {
// html parsing has finished - set dom content loaded // html parsing has finished - set dom content loaded
self.interactive_time self.interactive_time
.borrow() .borrow()
.maybe_set_tti(self, InteractiveFlag::DOMContentLoaded); .maybe_set_tti(InteractiveFlag::DOMContentLoaded);
// Step 4.2. // Step 4.2.
// TODO: client message queue. // TODO: client message queue.
@ -3095,7 +3094,7 @@ impl Document {
.set_navigation_start(navigation_start); .set_navigation_start(navigation_start);
} }
pub(crate) fn get_interactive_metrics(&self) -> Ref<InteractiveMetrics> { pub(crate) fn get_interactive_metrics(&self) -> Ref<ProgressiveWebMetrics> {
self.interactive_time.borrow() self.interactive_time.borrow()
} }
@ -3149,10 +3148,10 @@ impl Document {
return; return;
} }
if self.tti_window.borrow().needs_check() { if self.tti_window.borrow().needs_check() {
self.get_interactive_metrics().maybe_set_tti( self.get_interactive_metrics()
self, .maybe_set_tti(InteractiveFlag::TimeToInteractive(
InteractiveFlag::TimeToInteractive(self.tti_window.borrow().get_start()), self.tti_window.borrow().get_start(),
); ));
} }
} }
@ -3532,6 +3531,37 @@ impl Document {
document.root().notify_intersection_observers(CanGc::note()); document.root().notify_intersection_observers(CanGc::note());
})); }));
} }
pub(crate) fn handle_paint_metric(
&self,
metric_type: ProgressiveWebMetricType,
metric_value: CrossProcessInstant,
first_reflow: bool,
can_gc: CanGc,
) {
let metrics = self.interactive_time.borrow();
match metric_type {
ProgressiveWebMetricType::FirstPaint => {
metrics.set_first_paint(metric_value, first_reflow)
},
ProgressiveWebMetricType::FirstContentfulPaint => {
metrics.set_first_contentful_paint(metric_value, first_reflow)
},
ProgressiveWebMetricType::TimeToInteractive => {
unreachable!("Unexpected non-paint metric.")
},
}
let entry = PerformancePaintTiming::new(
self.window.as_global_scope(),
metric_type,
metric_value,
can_gc,
);
self.window
.Performance()
.queue_entry(entry.upcast::<PerformanceEntry>(), can_gc);
}
} }
fn is_character_value_key(key: &Key) -> bool { fn is_character_value_key(key: &Key) -> bool {
@ -3688,8 +3718,15 @@ impl Document {
(DocumentReadyState::Complete, true) (DocumentReadyState::Complete, true)
}; };
let interactive_time = let frame_type = match window.is_top_level() {
InteractiveMetrics::new(window.time_profiler_chan().clone(), url.clone()); true => TimerMetadataFrameType::RootWindow,
false => TimerMetadataFrameType::IFrame,
};
let interactive_time = ProgressiveWebMetrics::new(
window.time_profiler_chan().clone(),
url.clone(),
frame_type,
);
let content_type = content_type.unwrap_or_else(|| { let content_type = content_type.unwrap_or_else(|| {
match is_html_document { match is_html_document {
@ -4704,16 +4741,6 @@ impl Document {
} }
} }
impl ProfilerMetadataFactory for Document {
fn new_metadata(&self) -> Option<TimerMetadata> {
Some(TimerMetadata {
url: String::from(self.url().as_str()),
iframe: TimerMetadataFrameType::RootWindow,
incremental: TimerMetadataReflowType::Incremental,
})
}
}
#[allow(non_snake_case)] #[allow(non_snake_case)]
impl DocumentMethods<crate::DomTypeHolder> for Document { impl DocumentMethods<crate::DomTypeHolder> for Document {
// https://dom.spec.whatwg.org/#dom-document-document // https://dom.spec.whatwg.org/#dom-document-document

View file

@ -18,7 +18,7 @@ use net_traits::FetchResponseMsg;
use net_traits::image_cache::PendingImageResponse; use net_traits::image_cache::PendingImageResponse;
use profile_traits::mem::{self as profile_mem, OpaqueSender, ReportsChan}; use profile_traits::mem::{self as profile_mem, OpaqueSender, ReportsChan};
use profile_traits::time::{self as profile_time}; use profile_traits::time::{self as profile_time};
use script_traits::{LayoutMsg, Painter, ScriptMsg, ScriptThreadMessage}; use script_traits::{Painter, ScriptMsg, ScriptThreadMessage};
use stylo_atoms::Atom; use stylo_atoms::Atom;
use timers::TimerScheduler; use timers::TimerScheduler;
#[cfg(feature = "webgpu")] #[cfg(feature = "webgpu")]
@ -88,7 +88,6 @@ impl MixedMessage {
#[cfg(feature = "webgpu")] #[cfg(feature = "webgpu")]
ScriptThreadMessage::SetWebGPUPort(..) => None, ScriptThreadMessage::SetWebGPUPort(..) => None,
ScriptThreadMessage::SetScrollStates(id, ..) => Some(*id), ScriptThreadMessage::SetScrollStates(id, ..) => Some(*id),
ScriptThreadMessage::SetEpochPaintTime(id, ..) => Some(*id),
}, },
MixedMessage::FromScript(inner_msg) => match inner_msg { MixedMessage::FromScript(inner_msg) => match inner_msg {
MainThreadScriptMsg::Common(CommonScriptMsg::Task(_, _, pipeline_id, _)) => { MainThreadScriptMsg::Common(CommonScriptMsg::Task(_, _, pipeline_id, _)) => {
@ -318,10 +317,6 @@ pub(crate) struct ScriptThreadSenders {
#[no_trace] #[no_trace]
pub(crate) pipeline_to_constellation_sender: IpcSender<(PipelineId, ScriptMsg)>, pub(crate) pipeline_to_constellation_sender: IpcSender<(PipelineId, ScriptMsg)>,
/// A sender for layout to communicate to the constellation.
#[no_trace]
pub(crate) layout_to_constellation_ipc_sender: IpcSender<LayoutMsg>,
/// The shared [`IpcSender`] which is sent to the `ImageCache` when requesting an image. The /// The shared [`IpcSender`] which is sent to the `ImageCache` when requesting an image. The
/// messages on this channel are routed to crossbeam [`Sender`] on the router thread, which /// messages on this channel are routed to crossbeam [`Sender`] on the router thread, which
/// in turn sends messages to [`ScriptThreadReceivers::image_cache_receiver`]. /// in turn sends messages to [`ScriptThreadReceivers::image_cache_receiver`].

View file

@ -33,7 +33,6 @@ use background_hang_monitor_api::{
BackgroundHangMonitor, BackgroundHangMonitorExitSignal, HangAnnotation, MonitoredComponentId, BackgroundHangMonitor, BackgroundHangMonitorExitSignal, HangAnnotation, MonitoredComponentId,
MonitoredComponentType, MonitoredComponentType,
}; };
use base::Epoch;
use base::cross_process_instant::CrossProcessInstant; use base::cross_process_instant::CrossProcessInstant;
use base::id::{BrowsingContextId, HistoryStateId, PipelineId, PipelineNamespace, WebViewId}; use base::id::{BrowsingContextId, HistoryStateId, PipelineId, PipelineNamespace, WebViewId};
use canvas_traits::webgl::WebGLPipeline; use canvas_traits::webgl::WebGLPipeline;
@ -60,7 +59,7 @@ use js::jsapi::{
use js::jsval::UndefinedValue; use js::jsval::UndefinedValue;
use js::rust::ParentRuntime; use js::rust::ParentRuntime;
use media::WindowGLContext; use media::WindowGLContext;
use metrics::{MAX_TASK_NS, PaintTimeMetrics}; use metrics::MAX_TASK_NS;
use mime::{self, Mime}; use mime::{self, Mime};
use net_traits::image_cache::{ImageCache, PendingImageResponse}; use net_traits::image_cache::{ImageCache, PendingImageResponse};
use net_traits::request::{Referrer, RequestId}; use net_traits::request::{Referrer, RequestId};
@ -128,8 +127,6 @@ use crate::dom::htmliframeelement::HTMLIFrameElement;
use crate::dom::htmlslotelement::HTMLSlotElement; use crate::dom::htmlslotelement::HTMLSlotElement;
use crate::dom::mutationobserver::MutationObserver; use crate::dom::mutationobserver::MutationObserver;
use crate::dom::node::{Node, NodeTraits, ShadowIncluding}; use crate::dom::node::{Node, NodeTraits, ShadowIncluding};
use crate::dom::performanceentry::PerformanceEntry;
use crate::dom::performancepainttiming::PerformancePaintTiming;
use crate::dom::servoparser::{ParserContext, ServoParser}; use crate::dom::servoparser::{ParserContext, ServoParser};
#[cfg(feature = "webgpu")] #[cfg(feature = "webgpu")]
use crate::dom::webgpu::identityhub::IdentityHub; use crate::dom::webgpu::identityhub::IdentityHub;
@ -896,7 +893,6 @@ impl ScriptThread {
bluetooth_sender: state.bluetooth_sender, bluetooth_sender: state.bluetooth_sender,
constellation_sender: state.constellation_sender, constellation_sender: state.constellation_sender,
pipeline_to_constellation_sender: state.pipeline_to_constellation_sender.sender.clone(), pipeline_to_constellation_sender: state.pipeline_to_constellation_sender.sender.clone(),
layout_to_constellation_ipc_sender: state.layout_to_constellation_ipc_sender,
image_cache_sender: ipc_image_cache_sender, image_cache_sender: ipc_image_cache_sender,
time_profiler_sender: state.time_profiler_sender, time_profiler_sender: state.time_profiler_sender,
memory_profiler_sender: state.memory_profiler_sender, memory_profiler_sender: state.memory_profiler_sender,
@ -1849,9 +1845,18 @@ impl ScriptThread {
ScriptThreadMessage::ExitPipeline(pipeline_id, discard_browsing_context) => { ScriptThreadMessage::ExitPipeline(pipeline_id, discard_browsing_context) => {
self.handle_exit_pipeline_msg(pipeline_id, discard_browsing_context, can_gc) self.handle_exit_pipeline_msg(pipeline_id, discard_browsing_context, can_gc)
}, },
ScriptThreadMessage::PaintMetric(pipeline_id, metric_type, metric_value) => { ScriptThreadMessage::PaintMetric(
self.handle_paint_metric(pipeline_id, metric_type, metric_value, can_gc) pipeline_id,
}, metric_type,
metric_value,
first_reflow,
) => self.handle_paint_metric(
pipeline_id,
metric_type,
metric_value,
first_reflow,
can_gc,
),
ScriptThreadMessage::MediaSessionAction(pipeline_id, action) => { ScriptThreadMessage::MediaSessionAction(pipeline_id, action) => {
self.handle_media_session_action(pipeline_id, action, can_gc) self.handle_media_session_action(pipeline_id, action, can_gc)
}, },
@ -1872,9 +1877,6 @@ impl ScriptThread {
ScriptThreadMessage::SetScrollStates(pipeline_id, scroll_states) => { ScriptThreadMessage::SetScrollStates(pipeline_id, scroll_states) => {
self.handle_set_scroll_states_offsets(pipeline_id, scroll_states) self.handle_set_scroll_states_offsets(pipeline_id, scroll_states)
}, },
ScriptThreadMessage::SetEpochPaintTime(pipeline_id, epoch, time) => {
self.handle_set_epoch_paint_time(pipeline_id, epoch, time)
},
} }
} }
@ -1910,19 +1912,6 @@ impl ScriptThread {
) )
} }
fn handle_set_epoch_paint_time(
&self,
pipeline_id: PipelineId,
epoch: Epoch,
time: CrossProcessInstant,
) {
let Some(window) = self.documents.borrow().find_window(pipeline_id) else {
warn!("Received set epoch paint time message for closed pipeline {pipeline_id}.");
return;
};
window.layout_mut().set_epoch_paint_time(epoch, time);
}
#[cfg(feature = "webgpu")] #[cfg(feature = "webgpu")]
fn handle_msg_from_webgpu_server(&self, msg: WebGPUMsg, can_gc: CanGc) { fn handle_msg_from_webgpu_server(&self, msg: WebGPUMsg, can_gc: CanGc) {
match msg { match msg {
@ -3056,16 +3045,6 @@ impl ScriptThread {
pipeline_id: incomplete.pipeline_id, pipeline_id: incomplete.pipeline_id,
}; };
let paint_time_metrics = PaintTimeMetrics::new(
incomplete.webview_id,
incomplete.pipeline_id,
self.senders.time_profiler_sender.clone(),
self.senders.layout_to_constellation_ipc_sender.clone(),
self.senders.constellation_sender.clone(),
final_url.clone(),
incomplete.navigation_start,
);
let font_context = Arc::new(FontContext::new( let font_context = Arc::new(FontContext::new(
self.system_font_service.clone(), self.system_font_service.clone(),
self.compositor_api.clone(), self.compositor_api.clone(),
@ -3082,7 +3061,6 @@ impl ScriptThread {
font_context: font_context.clone(), font_context: font_context.clone(),
time_profiler_chan: self.senders.time_profiler_sender.clone(), time_profiler_chan: self.senders.time_profiler_sender.clone(),
compositor_api: self.compositor_api.clone(), compositor_api: self.compositor_api.clone(),
paint_time_metrics,
window_size: incomplete.window_size, window_size: incomplete.window_size,
}; };
@ -3619,19 +3597,16 @@ impl ScriptThread {
pipeline_id: PipelineId, pipeline_id: PipelineId,
metric_type: ProgressiveWebMetricType, metric_type: ProgressiveWebMetricType,
metric_value: CrossProcessInstant, metric_value: CrossProcessInstant,
first_reflow: bool,
can_gc: CanGc, can_gc: CanGc,
) { ) {
let window = self.documents.borrow().find_window(pipeline_id); match self.documents.borrow().find_document(pipeline_id) {
if let Some(window) = window { Some(document) => {
let entry = PerformancePaintTiming::new( document.handle_paint_metric(metric_type, metric_value, first_reflow, can_gc)
window.as_global_scope(), },
metric_type, None => warn!(
metric_value, "Received paint metric ({metric_type:?}) for unknown document: {pipeline_id:?}"
can_gc, ),
);
window
.Performance()
.queue_entry(entry.upcast::<PerformanceEntry>(), can_gc);
} }
} }

View file

@ -7,6 +7,7 @@ use std::fmt;
use std::time::Duration; use std::time::Duration;
use base::Epoch; use base::Epoch;
use base::cross_process_instant::CrossProcessInstant;
use base::id::{PipelineId, WebViewId}; use base::id::{PipelineId, WebViewId};
use embedder_traits::{ use embedder_traits::{
Cursor, InputEvent, MediaSessionActionType, Theme, TraversalDirection, WebDriverCommandMsg, Cursor, InputEvent, MediaSessionActionType, Theme, TraversalDirection, WebDriverCommandMsg,
@ -72,6 +73,15 @@ pub enum ConstellationMsg {
/// The Servo renderer scrolled and is updating the scroll states of the nodes in the /// The Servo renderer scrolled and is updating the scroll states of the nodes in the
/// given pipeline via the constellation. /// given pipeline via the constellation.
SetScrollStates(PipelineId, Vec<ScrollState>), SetScrollStates(PipelineId, Vec<ScrollState>),
/// Notify the constellation that a particular paint metric event has happened for the given pipeline.
PaintMetric(PipelineId, PaintMetricEvent),
}
/// A description of a paint metric that is sent from the Servo renderer to the
/// constellation.
pub enum PaintMetricEvent {
FirstPaint(CrossProcessInstant, bool /* first_reflow */),
FirstContentfulPaint(CrossProcessInstant, bool /* first_reflow */),
} }
impl fmt::Debug for ConstellationMsg { impl fmt::Debug for ConstellationMsg {

View file

@ -8,9 +8,8 @@ mod constellation_msg;
use std::fmt::{Debug, Error, Formatter}; use std::fmt::{Debug, Error, Formatter};
use base::Epoch;
use base::id::{PipelineId, WebViewId}; use base::id::{PipelineId, WebViewId};
pub use constellation_msg::ConstellationMsg; pub use constellation_msg::{ConstellationMsg, PaintMetricEvent};
use crossbeam_channel::{Receiver, Sender}; use crossbeam_channel::{Receiver, Sender};
use embedder_traits::{EventLoopWaker, MouseButton, MouseButtonAction}; use embedder_traits::{EventLoopWaker, MouseButton, MouseButtonAction};
use euclid::Rect; use euclid::Rect;
@ -84,10 +83,6 @@ pub enum CompositorMsg {
// sends a reply on the IpcSender, the constellation knows it's safe to // sends a reply on the IpcSender, the constellation knows it's safe to
// tear down the other threads associated with this pipeline. // tear down the other threads associated with this pipeline.
PipelineExited(WebViewId, PipelineId, IpcSender<()>), PipelineExited(WebViewId, PipelineId, IpcSender<()>),
/// Indicates to the compositor that it needs to record the time when the frame with
/// the given ID (epoch) is painted and report it to the layout of the given
/// WebViewId and PipelienId.
PendingPaintMetric(WebViewId, PipelineId, Epoch),
/// The load of a page has completed /// The load of a page has completed
LoadComplete(WebViewId), LoadComplete(WebViewId),
/// WebDriver mouse button event /// WebDriver mouse button event

View file

@ -19,6 +19,8 @@ base = { workspace = true }
crossbeam-channel = { workspace = true } crossbeam-channel = { workspace = true }
ipc-channel = { workspace = true } ipc-channel = { workspace = true }
log = { workspace = true } log = { workspace = true }
malloc_size_of = { workspace = true }
malloc_size_of_derive = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
servo_config = { path = "../../config" } servo_config = { path = "../../config" }
signpost = { git = "https://github.com/pcwalton/signpost.git" } signpost = { git = "https://github.com/pcwalton/signpost.git" }

View file

@ -5,6 +5,7 @@
use base::cross_process_instant::CrossProcessInstant; use base::cross_process_instant::CrossProcessInstant;
use ipc_channel::ipc::IpcSender; use ipc_channel::ipc::IpcSender;
use log::warn; use log::warn;
use malloc_size_of_derive::MallocSizeOf;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use servo_config::opts; use servo_config::opts;
use strum_macros::IntoStaticStr; use strum_macros::IntoStaticStr;
@ -188,7 +189,7 @@ impl ProfilerCategory {
} }
} }
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] #[derive(Clone, Debug, Deserialize, Eq, MallocSizeOf, Ord, PartialEq, PartialOrd, Serialize)]
pub enum TimerMetadataFrameType { pub enum TimerMetadataFrameType {
RootWindow, RootWindow,
IFrame, IFrame,

View file

@ -19,7 +19,6 @@ use std::fmt;
use std::sync::Arc; use std::sync::Arc;
use background_hang_monitor_api::BackgroundHangMonitorRegister; use background_hang_monitor_api::BackgroundHangMonitorRegister;
use base::Epoch;
use base::cross_process_instant::CrossProcessInstant; use base::cross_process_instant::CrossProcessInstant;
use base::id::{ use base::id::{
BlobId, BrowsingContextId, HistoryStateId, MessagePortId, PipelineId, PipelineNamespaceId, BlobId, BrowsingContextId, HistoryStateId, MessagePortId, PipelineId, PipelineNamespaceId,
@ -64,9 +63,8 @@ use webrender_traits::{
}; };
pub use crate::script_msg::{ pub use crate::script_msg::{
DOMMessage, IFrameSizeMsg, Job, JobError, JobResult, JobResultValue, JobType, LayoutMsg, DOMMessage, IFrameSizeMsg, Job, JobError, JobResult, JobResultValue, JobType, LogEntry,
LogEntry, SWManagerMsg, SWManagerSenders, ScopeThings, ScriptMsg, ServiceWorkerMsg, SWManagerMsg, SWManagerSenders, ScopeThings, ScriptMsg, ServiceWorkerMsg, TouchEventResult,
TouchEventResult,
}; };
use crate::serializable::BlobImpl; use crate::serializable::BlobImpl;
use crate::transferable::MessagePortImpl; use crate::transferable::MessagePortImpl;
@ -395,7 +393,12 @@ pub enum ScriptThreadMessage {
/// Reload the given page. /// Reload the given page.
Reload(PipelineId), Reload(PipelineId),
/// Notifies the script thread about a new recorded paint metric. /// Notifies the script thread about a new recorded paint metric.
PaintMetric(PipelineId, ProgressiveWebMetricType, CrossProcessInstant), PaintMetric(
PipelineId,
ProgressiveWebMetricType,
CrossProcessInstant,
bool, /* first_reflow */
),
/// Notifies the media session about a user requested media session action. /// Notifies the media session about a user requested media session action.
MediaSessionAction(PipelineId, MediaSessionActionType), MediaSessionAction(PipelineId, MediaSessionActionType),
/// Notifies script thread that WebGPU server has started /// Notifies script thread that WebGPU server has started
@ -404,8 +407,6 @@ pub enum ScriptThreadMessage {
/// The compositor scrolled and is updating the scroll states of the nodes in the given /// The compositor scrolled and is updating the scroll states of the nodes in the given
/// pipeline via the Constellation. /// pipeline via the Constellation.
SetScrollStates(PipelineId, Vec<ScrollState>), SetScrollStates(PipelineId, Vec<ScrollState>),
/// Send the paint time for a specific epoch.
SetEpochPaintTime(PipelineId, Epoch, CrossProcessInstant),
} }
impl fmt::Debug for ScriptThreadMessage { impl fmt::Debug for ScriptThreadMessage {
@ -476,8 +477,6 @@ pub struct InitialScriptState {
pub pipeline_to_constellation_sender: ScriptToConstellationChan, pub pipeline_to_constellation_sender: ScriptToConstellationChan,
/// A handle to register script-(and associated layout-)threads for hang monitoring. /// A handle to register script-(and associated layout-)threads for hang monitoring.
pub background_hang_monitor_register: Box<dyn BackgroundHangMonitorRegister>, pub background_hang_monitor_register: Box<dyn BackgroundHangMonitorRegister>,
/// A sender layout to communicate to the constellation.
pub layout_to_constellation_ipc_sender: IpcSender<LayoutMsg>,
/// A channel to the resource manager thread. /// A channel to the resource manager thread.
pub resource_threads: ResourceThreads, pub resource_threads: ResourceThreads,
/// A channel to the bluetooth thread. /// A channel to the bluetooth thread.

View file

@ -46,21 +46,6 @@ pub struct IFrameSizeMsg {
pub type_: WindowSizeType, pub type_: WindowSizeType,
} }
/// Messages from the layout to the constellation.
#[derive(Deserialize, IntoStaticStr, Serialize)]
pub enum LayoutMsg {
/// Requests that the constellation inform the compositor that it needs to record
/// the time when the frame with the given ID (epoch) is painted.
PendingPaintMetric(WebViewId, PipelineId, Epoch),
}
impl fmt::Debug for LayoutMsg {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
let variant_string: &'static str = self.into();
write!(formatter, "LayoutMsg::{variant_string}")
}
}
/// Whether the default action for a touch event was prevented by web content /// Whether the default action for a touch event was prevented by web content
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub enum TouchEventResult { pub enum TouchEventResult {

View file

@ -18,7 +18,6 @@ use std::sync::atomic::{AtomicIsize, AtomicU64, Ordering};
use app_units::Au; use app_units::Au;
use atomic_refcell::AtomicRefCell; use atomic_refcell::AtomicRefCell;
use base::Epoch; use base::Epoch;
use base::cross_process_instant::CrossProcessInstant;
use base::id::{BrowsingContextId, PipelineId, WebViewId}; use base::id::{BrowsingContextId, PipelineId, WebViewId};
use canvas_traits::canvas::{CanvasId, CanvasMsg}; use canvas_traits::canvas::{CanvasId, CanvasMsg};
use euclid::Size2D; use euclid::Size2D;
@ -28,7 +27,6 @@ use fonts::{FontContext, SystemFontServiceProxy};
use ipc_channel::ipc::IpcSender; use ipc_channel::ipc::IpcSender;
use libc::c_void; use libc::c_void;
use malloc_size_of_derive::MallocSizeOf; use malloc_size_of_derive::MallocSizeOf;
use metrics::PaintTimeMetrics;
use net_traits::image_cache::{ImageCache, PendingImageId}; use net_traits::image_cache::{ImageCache, PendingImageId};
use profile_traits::mem::Report; use profile_traits::mem::Report;
use profile_traits::time; use profile_traits::time;
@ -189,7 +187,6 @@ pub struct LayoutConfig {
pub font_context: Arc<FontContext>, pub font_context: Arc<FontContext>,
pub time_profiler_chan: time::ProfilerChan, pub time_profiler_chan: time::ProfilerChan,
pub compositor_api: CrossProcessCompositorApi, pub compositor_api: CrossProcessCompositorApi,
pub paint_time_metrics: PaintTimeMetrics,
pub window_size: WindowSizeData, pub window_size: WindowSizeData,
} }
@ -245,9 +242,6 @@ pub trait Layout {
/// Set the scroll states of this layout after a compositor scroll. /// Set the scroll states of this layout after a compositor scroll.
fn set_scroll_offsets(&mut self, scroll_states: &[ScrollState]); fn set_scroll_offsets(&mut self, scroll_states: &[ScrollState]);
/// Set the paint time for a specific epoch.
fn set_epoch_paint_time(&mut self, epoch: Epoch, paint_time: CrossProcessInstant);
fn query_content_box(&self, node: OpaqueNode) -> Option<Rect<Au>>; fn query_content_box(&self, node: OpaqueNode) -> Option<Rect<Au>>;
fn query_content_boxes(&self, node: OpaqueNode) -> Vec<Rect<Au>>; fn query_content_boxes(&self, node: OpaqueNode) -> Vec<Rect<Au>>;
fn query_client_rect(&self, node: OpaqueNode) -> Rect<i32>; fn query_client_rect(&self, node: OpaqueNode) -> Rect<i32>;

View file

@ -305,6 +305,15 @@ pub struct CompositorDisplayListInfo {
/// The `ScrollTreeNodeId` of the topmost scrolling frame of this info's scroll /// The `ScrollTreeNodeId` of the topmost scrolling frame of this info's scroll
/// tree. /// tree.
pub root_scroll_node_id: ScrollTreeNodeId, pub root_scroll_node_id: ScrollTreeNodeId,
/// Contentful paint i.e. whether the display list contains items of type
/// text, image, non-white canvas or SVG). Used by metrics.
/// See <https://w3c.github.io/paint-timing/#first-contentful-paint>.
pub is_contentful: bool,
/// Whether the first layout or a subsequent (incremental) layout triggered this
/// display list creation.
pub first_reflow: bool,
} }
impl CompositorDisplayListInfo { impl CompositorDisplayListInfo {
@ -316,6 +325,7 @@ impl CompositorDisplayListInfo {
pipeline_id: PipelineId, pipeline_id: PipelineId,
epoch: Epoch, epoch: Epoch,
viewport_scroll_sensitivity: AxesScrollSensitivity, viewport_scroll_sensitivity: AxesScrollSensitivity,
first_reflow: bool,
) -> Self { ) -> Self {
let mut scroll_tree = ScrollTree::default(); let mut scroll_tree = ScrollTree::default();
let root_reference_frame_id = scroll_tree.add_scroll_tree_node( let root_reference_frame_id = scroll_tree.add_scroll_tree_node(
@ -343,6 +353,8 @@ impl CompositorDisplayListInfo {
scroll_tree, scroll_tree,
root_reference_frame_id, root_reference_frame_id,
root_scroll_node_id, root_scroll_node_id,
is_contentful: false,
first_reflow,
} }
} }

View file

@ -166,6 +166,7 @@ class MachCommands(CommandBase):
"hyper_serde", "hyper_serde",
"layout_2020", "layout_2020",
"libservo", "libservo",
"metrics",
"net", "net",
"net_traits", "net_traits",
"pixels", "pixels",

View file

@ -1,20 +0,0 @@
[package]
name = "metrics_tests"
version.workspace = true
authors.workspace = true
license.workspace = true
edition.workspace = true
publish.workspace = true
rust-version.workspace = true
[lib]
name = "metrics_tests"
path = "lib.rs"
doctest = false
[dependencies]
base = { workspace = true }
ipc-channel = { workspace = true }
metrics = { path = "../../../components/metrics" }
profile_traits = { workspace = true }
servo_url = { path = "../../../components/url" }

View file

@ -1,125 +0,0 @@
/* 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 base::cross_process_instant::CrossProcessInstant;
use ipc_channel::ipc;
use metrics::{InteractiveFlag, InteractiveMetrics, ProfilerMetadataFactory, ProgressiveWebMetric};
use profile_traits::time::{ProfilerChan, TimerMetadata};
use servo_url::ServoUrl;
struct DummyProfilerMetadataFactory {}
impl ProfilerMetadataFactory for DummyProfilerMetadataFactory {
fn new_metadata(&self) -> Option<TimerMetadata> {
None
}
}
fn test_interactive() -> InteractiveMetrics {
let (sender, _) = ipc::channel().unwrap();
let profiler_chan = ProfilerChan(sender);
let mut interactive =
InteractiveMetrics::new(profiler_chan, ServoUrl::parse("about:blank").unwrap());
assert_eq!((&interactive).get_navigation_start(), None);
assert_eq!(interactive.get_tti(), None);
interactive.set_navigation_start(CrossProcessInstant::now());
interactive
}
#[test]
fn test_set_dcl() {
let profiler_metadata_factory = DummyProfilerMetadataFactory {};
let interactive = test_interactive();
interactive.maybe_set_tti(
&profiler_metadata_factory,
InteractiveFlag::DOMContentLoaded,
);
let dcl = interactive.get_dom_content_loaded();
assert!(dcl.is_some());
//try to overwrite
interactive.maybe_set_tti(
&profiler_metadata_factory,
InteractiveFlag::DOMContentLoaded,
);
assert_eq!(interactive.get_dom_content_loaded(), dcl);
assert_eq!(interactive.get_tti(), None);
}
#[test]
fn test_set_mta() {
let profiler_metadata_factory = DummyProfilerMetadataFactory {};
let interactive = test_interactive();
let now = CrossProcessInstant::now();
interactive.maybe_set_tti(
&profiler_metadata_factory,
InteractiveFlag::TimeToInteractive(now),
);
let main_thread_available_time = interactive.get_main_thread_available();
assert!(main_thread_available_time.is_some());
assert_eq!(main_thread_available_time, Some(now));
//try to overwrite
interactive.maybe_set_tti(
&profiler_metadata_factory,
InteractiveFlag::TimeToInteractive(CrossProcessInstant::now()),
);
assert_eq!(
interactive.get_main_thread_available(),
main_thread_available_time
);
assert_eq!(interactive.get_tti(), None);
}
#[test]
fn test_set_tti_dcl() {
let profiler_metadata_factory = DummyProfilerMetadataFactory {};
let interactive = test_interactive();
let now = CrossProcessInstant::now();
interactive.maybe_set_tti(
&profiler_metadata_factory,
InteractiveFlag::TimeToInteractive(now),
);
let main_thread_available_time = interactive.get_main_thread_available();
assert!(main_thread_available_time.is_some());
interactive.maybe_set_tti(
&profiler_metadata_factory,
InteractiveFlag::DOMContentLoaded,
);
let dom_content_loaded_time = interactive.get_dom_content_loaded();
assert!(dom_content_loaded_time.is_some());
assert_eq!(interactive.get_tti(), dom_content_loaded_time);
}
#[test]
fn test_set_tti_mta() {
let profiler_metadata_factory = DummyProfilerMetadataFactory {};
let interactive = test_interactive();
interactive.maybe_set_tti(
&profiler_metadata_factory,
InteractiveFlag::DOMContentLoaded,
);
let dcl = interactive.get_dom_content_loaded();
assert!(dcl.is_some());
let time = CrossProcessInstant::now();
interactive.maybe_set_tti(
&profiler_metadata_factory,
InteractiveFlag::TimeToInteractive(time),
);
let mta = interactive.get_main_thread_available();
assert!(mta.is_some());
assert_eq!(interactive.get_tti(), mta);
}
// TODO InteractiveWindow tests

View file

@ -1,8 +0,0 @@
/* 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/. */
#![cfg(test)]
mod interactive_time;
mod paint_time;

View file

@ -1,129 +0,0 @@
/* 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 base::Epoch;
use base::cross_process_instant::CrossProcessInstant;
use base::id::{TEST_PIPELINE_ID, TEST_WEBVIEW_ID};
use ipc_channel::ipc;
use metrics::{PaintTimeMetrics, ProfilerMetadataFactory, ProgressiveWebMetric};
use profile_traits::time::{ProfilerChan, TimerMetadata};
use servo_url::ServoUrl;
struct DummyProfilerMetadataFactory {}
impl ProfilerMetadataFactory for DummyProfilerMetadataFactory {
fn new_metadata(&self) -> Option<TimerMetadata> {
None
}
}
#[test]
fn test_paint_metrics_construction() {
let (sender, _) = ipc::channel().unwrap();
let profiler_chan = ProfilerChan(sender);
let (layout_sender, _) = ipc::channel().unwrap();
let (script_sender, _) = ipc::channel().unwrap();
let start_time = CrossProcessInstant::now();
let paint_time_metrics = PaintTimeMetrics::new(
TEST_WEBVIEW_ID,
TEST_PIPELINE_ID,
profiler_chan,
layout_sender,
script_sender,
ServoUrl::parse("about:blank").unwrap(),
start_time,
);
assert_eq!(
(&paint_time_metrics).get_navigation_start(),
Some(start_time),
"navigation start is set properly"
);
assert_eq!(
paint_time_metrics.get_first_paint(),
None,
"first paint is None"
);
assert_eq!(
paint_time_metrics.get_first_contentful_paint(),
None,
"first contentful paint is None"
);
}
fn test_common(display_list_is_contentful: bool, epoch: Epoch) -> PaintTimeMetrics {
let (sender, _) = ipc::channel().unwrap();
let profiler_chan = ProfilerChan(sender);
let (layout_sender, _) = ipc::channel().unwrap();
let (script_sender, _) = ipc::channel().unwrap();
let start_time = CrossProcessInstant::now();
let mut paint_time_metrics = PaintTimeMetrics::new(
TEST_WEBVIEW_ID,
TEST_PIPELINE_ID,
profiler_chan,
layout_sender,
script_sender,
ServoUrl::parse("about:blank").unwrap(),
start_time,
);
let dummy_profiler_metadata_factory = DummyProfilerMetadataFactory {};
paint_time_metrics.maybe_observe_paint_time(
&dummy_profiler_metadata_factory,
epoch,
display_list_is_contentful,
);
assert_eq!(
paint_time_metrics.get_first_paint(),
None,
"first paint is None"
);
assert_eq!(
paint_time_metrics.get_first_contentful_paint(),
None,
"first contentful paint is None"
);
let navigation_start = CrossProcessInstant::now();
paint_time_metrics.set_navigation_start(navigation_start);
assert_eq!(
(&paint_time_metrics).get_navigation_start().unwrap(),
navigation_start,
"navigation start is set"
);
paint_time_metrics
}
#[test]
fn test_first_paint_setter() {
let epoch = Epoch(0);
let paint_time_metrics = test_common(false, epoch);
let now = CrossProcessInstant::now();
paint_time_metrics.maybe_set_metric(epoch, now);
assert!(
paint_time_metrics.get_first_paint().is_some(),
"first paint is set"
);
assert_eq!(
paint_time_metrics.get_first_contentful_paint(),
None,
"first contentful paint is None"
);
}
#[test]
fn test_first_contentful_paint_setter() {
let epoch = Epoch(0);
let paint_time_metrics = test_common(true, epoch);
let now = CrossProcessInstant::now();
paint_time_metrics.maybe_set_metric(epoch, now);
assert!(
paint_time_metrics.get_first_contentful_paint().is_some(),
"first contentful paint is set"
);
assert!(
paint_time_metrics.get_first_paint().is_some(),
"first paint is set"
);
}