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",
]
[[package]]
name = "metrics_tests"
version = "0.0.1"
dependencies = [
"base",
"ipc-channel",
"metrics",
"profile_traits",
"servo_url",
]
[[package]]
name = "mime"
version = "0.3.17"
@ -5846,8 +5835,10 @@ dependencies = [
"crossbeam-channel",
"ipc-channel",
"log",
"malloc_size_of_derive",
"serde",
"servo_config",
"servo_malloc_size_of",
"signpost",
"strum_macros",
"time",

View file

@ -18,7 +18,8 @@ use base::id::{PipelineId, WebViewId};
use base::{Epoch, WebRenderEpochToU16};
use bitflags::bitflags;
use compositing_traits::{
CompositionPipeline, CompositorMsg, CompositorReceiver, ConstellationMsg, SendableFrameTree,
CompositionPipeline, CompositorMsg, CompositorReceiver, ConstellationMsg, PaintMetricEvent,
SendableFrameTree,
};
use crossbeam_channel::Sender;
use dpi::PhysicalSize;
@ -33,9 +34,7 @@ use log::{debug, info, trace, warn};
use pixels::{CorsStatus, Image, ImageFrame, PixelFormat};
use profile_traits::time::{self as profile_time, ProfilerCategory};
use profile_traits::time_profile;
use script_traits::{
AnimationState, AnimationTickType, ScriptThreadMessage, WindowSizeData, WindowSizeType,
};
use script_traits::{AnimationState, AnimationTickType, WindowSizeData, WindowSizeType};
use servo_config::opts;
use servo_geometry::DeviceIndependentPixel;
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 {
/// The pipeline associated with this PipelineDetails object.
pub pipeline: Option<CompositionPipeline>,
@ -231,12 +245,11 @@ pub(crate) struct PipelineDetails {
/// nodes in the compositor before forwarding new offsets to WebRender.
pub scroll_tree: ScrollTree,
/// A per-pipeline queue of display lists that have not yet been rendered by WebRender. Layout
/// expects WebRender to paint each given epoch. Once the compositor paints a frame with that
/// 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
/// recorded.
pub pending_paint_metrics: Vec<Epoch>,
/// The paint metric status of the first paint.
pub first_paint_metric: PaintMetricState,
/// The paint metric status of the first contentful paint.
pub first_contentful_paint_metric: PaintMetricState,
}
impl PipelineDetails {
@ -287,7 +300,8 @@ impl PipelineDetails {
throttled: false,
hit_test_items: Vec::new(),
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 }));
},
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) => {
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.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();
transaction
.set_display_list(display_list_info.epoch, (pipeline_id, built_display_list));
@ -1529,16 +1549,8 @@ impl IOCompositor {
let paint_time = CrossProcessInstant::now();
let document_id = self.webrender_document();
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() {
if pipeline.pending_paint_metrics.is_empty() {
continue;
}
let Some(composition_pipeline) = pipeline.pipeline.as_ref() else {
continue;
};
let Some(WebRenderEpoch(current_epoch)) = self
let Some(current_epoch) = self
.webrender
.as_ref()
.and_then(|wr| wr.current_epoch(document_id, pipeline_id.into()))
@ -1546,29 +1558,43 @@ impl IOCompositor {
continue;
};
let current_epoch = Epoch(current_epoch);
let Some(index) = pipeline
.pending_paint_metrics
.iter()
.position(|epoch| *epoch == current_epoch)
else {
continue;
};
// 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(
match pipeline.first_paint_metric {
// We need to check whether the current epoch is later, because
// CrossProcessCompositorMessage::SendInitialTransaction sends an
// empty display list to WebRender which can happen before we receive
// the first "real" display list.
PaintMetricState::Seen(epoch, first_reflow) if epoch <= current_epoch => {
assert!(epoch <= current_epoch);
if let Err(error) = self.global.borrow().constellation_sender.send(
ConstellationMsg::PaintMetric(
*pipeline_id,
current_epoch,
paint_time,
))
{
warn!("Sending RequestLayoutPaintMetric message to layout failed ({error:?}).");
PaintMetricEvent::FirstPaint(paint_time, first_reflow),
),
) {
warn!(
"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::NewWebRenderFrameReady(..) => target!("NewWebRenderFrameReady"),
Self::PipelineExited(..) => target!("PipelineExited"),
Self::PendingPaintMetric(..) => target!("PendingPaintMetric"),
Self::LoadComplete(..) => target!("LoadComplete"),
Self::WebDriverMouseButtonEvent(..) => target!("WebDriverMouseButtonEvent"),
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)
pub fn on_vsync(&mut self) {
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::webgl::WebGLThreads;
use compositing_traits::{
CompositorMsg, CompositorProxy, ConstellationMsg as FromCompositorMsg, SendableFrameTree,
CompositorMsg, CompositorProxy, ConstellationMsg as FromCompositorMsg, PaintMetricEvent,
SendableFrameTree,
};
use crossbeam_channel::{Receiver, Sender, select, unbounded};
use devtools_traits::{
@ -141,8 +142,8 @@ use script_traits::{
AnimationState, AnimationTickType, AuxiliaryWebViewCreationRequest,
AuxiliaryWebViewCreationResponse, BroadcastMsg, ConstellationInputEvent,
DiscardBrowsingContext, DocumentActivity, DocumentState, IFrameLoadInfo,
IFrameLoadInfoWithData, IFrameSandboxState, IFrameSizeMsg, Job, LayoutMsg as FromLayoutMsg,
LoadData, LoadOrigin, LogEntry, MessagePortMsg, NavigationHistoryBehavior, PortMessageTask,
IFrameLoadInfoWithData, IFrameSandboxState, IFrameSizeMsg, Job, LoadData, LoadOrigin, LogEntry,
MessagePortMsg, NavigationHistoryBehavior, PortMessageTask, ProgressiveWebMetricType,
SWManagerMsg, SWManagerSenders, ScriptMsg as FromScriptMsg, ScriptThreadMessage,
ScriptToConstellationChan, ServiceWorkerManagerFactory, ServiceWorkerMsg,
StructuredSerializedData, UpdatePipelineIdReason, WindowSizeData, WindowSizeType,
@ -309,14 +310,6 @@ pub struct Constellation<STF, SWF> {
/// dependency between script and layout.
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.
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 =
route_ipc_receiver_to_new_crossbeam_receiver_preserving_errors(
swmanager_ipc_receiver,
@ -693,11 +679,9 @@ where
background_hang_monitor_receiver,
background_monitor_register,
background_monitor_control_senders: background_hang_monitor_control_ipc_senders,
layout_sender: layout_ipc_sender,
script_receiver,
compositor_receiver,
layout_factory,
layout_receiver,
embedder_proxy: state.embedder_proxy,
compositor_proxy: state.compositor_proxy,
webviews: WebViewManager::default(),
@ -967,7 +951,6 @@ where
background_hang_monitor_to_constellation_chan: self
.background_hang_monitor_sender
.clone(),
layout_to_constellation_chan: self.layout_sender.clone(),
layout_factory: self.layout_factory.clone(),
compositor_proxy: self.compositor_proxy.clone(),
devtools_sender: self.devtools_sender.clone(),
@ -1132,7 +1115,6 @@ where
Script((PipelineId, FromScriptMsg)),
BackgroundHangMonitor(HangMonitorAlert),
Compositor(FromCompositorMsg),
Layout(FromLayoutMsg),
FromSWManager(SWManagerMsg),
}
// Get one incoming request.
@ -1163,9 +1145,6 @@ where
recv(self.compositor_receiver) -> msg => {
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 => {
msg.expect("Unexpected SW channel panic in constellation").map(Request::FromSWManager)
}
@ -1188,9 +1167,6 @@ where
Request::BackgroundHangMonitor(message) => {
self.handle_request_from_background_hang_monitor(message);
},
Request::Layout(message) => {
self.handle_request_from_layout(message);
},
Request::FromSWManager(message) => {
self.handle_request_from_swmanager(message);
},
@ -1409,6 +1385,9 @@ where
FromCompositorMsg::SetScrollStates(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(
feature = "tracing",
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(
feature = "tracing",
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:?}");
}
}
#[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 script_layout_interface::{LayoutFactory, ScriptThreadFactory};
use script_traits::{
AnimationState, DiscardBrowsingContext, DocumentActivity, InitialScriptState, LayoutMsg,
LoadData, NewLayoutInfo, SWManagerMsg, ScriptThreadMessage, ScriptToConstellationChan,
WindowSizeData,
AnimationState, DiscardBrowsingContext, DocumentActivity, InitialScriptState, LoadData,
NewLayoutInfo, SWManagerMsg, ScriptThreadMessage, ScriptToConstellationChan, WindowSizeData,
};
use serde::{Deserialize, Serialize};
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.
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.
pub layout_factory: Arc<dyn LayoutFactory>,
@ -279,7 +275,6 @@ impl Pipeline {
time_profiler_chan: state.time_profiler_chan,
mem_profiler_chan: state.mem_profiler_chan,
window_size: state.window_size,
layout_to_constellation_chan: state.layout_to_constellation_chan,
script_chan: script_chan.clone(),
load_data: state.load_data.clone(),
script_port,
@ -481,7 +476,6 @@ pub struct UnprivilegedPipelineContent {
script_to_constellation_chan: ScriptToConstellationChan,
background_hang_monitor_to_constellation_chan: IpcSender<HangMonitorAlert>,
bhm_control_port: Option<IpcReceiver<BackgroundHangMonitorControlMsg>>,
layout_to_constellation_chan: IpcSender<LayoutMsg>,
devtools_ipc_sender: Option<IpcSender<ScriptToDevtoolsControlMsg>>,
#[cfg(feature = "bluetooth")]
bluetooth_thread: IpcSender<BluetoothRequest>,
@ -533,7 +527,6 @@ impl UnprivilegedPipelineContent {
constellation_receiver: self.script_port,
pipeline_to_constellation_sender: self.script_to_constellation_chan.clone(),
background_hang_monitor_register: background_hang_monitor_register.clone(),
layout_to_constellation_ipc_sender: self.layout_to_constellation_chan.clone(),
#[cfg(feature = "bluetooth")]
bluetooth_sender: self.bluetooth_thread,
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.
pub(crate) trait LogTarget {
fn log_target(&self) -> &'static str;
@ -87,6 +76,7 @@ mod from_compositor {
Self::MediaSessionAction(_) => target!("MediaSessionAction"),
Self::SetWebViewThrottled(_, _) => target!("SetWebViewThrottled"),
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,
epoch: wr::Epoch,
viewport_scroll_sensitivity: AxesScrollSensitivity,
first_reflow: bool,
) -> Self {
Self {
wr: wr::DisplayListBuilder::new(pipeline_id),
@ -111,6 +112,7 @@ impl DisplayList {
pipeline_id,
epoch,
viewport_scroll_sensitivity,
first_reflow,
),
spatial_tree_count: 0,
}
@ -161,11 +163,6 @@ pub(crate) struct DisplayListBuilder<'a> {
/// The [DisplayList] used to collect display list items and metadata.
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 {
@ -175,7 +172,7 @@ impl DisplayList {
context: &LayoutContext,
fragment_tree: &FragmentTree,
root_stacking_context: &StackingContext,
) -> bool {
) {
#[cfg(feature = "tracing")]
let _span = tracing::trace_span!("display_list::build", servo_profiling = true).entered();
let mut builder = DisplayListBuilder {
@ -183,12 +180,10 @@ impl DisplayList {
current_reference_frame_scroll_node_id: self.compositor_info.root_reference_frame_id,
current_clip_chain_id: ClipChainId::INVALID,
element_for_canvas_background: fragment_tree.canvas_background.from_element,
is_contentful: false,
context,
display_list: self,
};
fragment_tree.build_display_list(&mut builder, root_stacking_context);
builder.is_contentful
}
}
@ -197,6 +192,10 @@ impl DisplayListBuilder<'_> {
&mut self.display_list.wr
}
fn mark_is_contentful(&mut self) {
self.display_list.compositor_info.is_contentful = true;
}
fn common_properties(
&self,
clip_rect: units::LayoutRect,
@ -282,7 +281,7 @@ impl Fragment {
let image = image.borrow();
match image.style.get_inherited_box().visibility {
Visibility::Visible => {
builder.is_contentful = true;
builder.mark_is_contentful();
let image_rendering = image
.style
@ -318,7 +317,7 @@ impl Fragment {
let iframe = iframe.borrow();
match iframe.style.get_inherited_box().visibility {
Visibility::Visible => {
builder.is_contentful = true;
builder.mark_is_contentful();
let rect = iframe.rect.translate(containing_block.origin.to_vector());
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:
// 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 mut baseline_origin = rect.origin;

View file

@ -16,7 +16,6 @@ use std::sync::{Arc, LazyLock};
use app_units::Au;
use base::Epoch;
use base::cross_process_instant::CrossProcessInstant;
use base::id::{PipelineId, WebViewId};
use embedder_traits::resources::{self, Resource};
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 log::{debug, error};
use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
use metrics::{PaintTimeMetrics, ProfilerMetadataFactory};
use net_traits::image_cache::{ImageCache, UsePlaceholder};
use parking_lot::{Mutex, RwLock};
use profile_traits::mem::{Report, ReportKind};
@ -160,9 +158,6 @@ pub struct LayoutThread {
/// Cross-process access to the Compositor API.
compositor_api: CrossProcessCompositorApi,
/// Paint time metrics.
paint_time_metrics: PaintTimeMetrics,
/// Debug options, copied from configuration to this `LayoutThread` in order
/// to avoid having to constantly access the thread-safe global options.
debug: DebugOptions,
@ -462,10 +457,6 @@ impl Layout for LayoutThread {
.map(|scroll_state| (scroll_state.scroll_id, scroll_state.scroll_offset))
.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 {
@ -520,7 +511,6 @@ impl LayoutThread {
scroll_offsets: Default::default(),
stylist: Stylist::new(device, QuirksMode::NoQuirks),
webrender_image_cache: Default::default(),
paint_time_metrics: config.paint_time_metrics,
debug: opts::get().debug.clone(),
}
}
@ -856,6 +846,7 @@ impl LayoutThread {
self.id.into(),
epoch.into(),
fragment_tree.viewport_scroll_sensitivity,
self.first_reflow.get(),
);
display_list.wr.begin();
@ -874,7 +865,7 @@ impl LayoutThread {
display_list.build_stacking_context_tree(&fragment_tree, &self.debug);
// 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 {
fragment_tree.print();
@ -884,12 +875,6 @@ impl LayoutThread {
}
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() {
self.compositor_api.send_display_list(
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 parse_ua_stylesheet(
shared_lock: &SharedRwLock,

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,
}
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,
),
#[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,
);
// 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);
}
}
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());
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;
}
metrics.set_navigation_start(CrossProcessInstant::now());
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()
}
metrics
}
impl ProgressiveWebMetric for PaintTimeMetrics {
fn get_navigation_start(&self) -> Option<CrossProcessInstant> {
Some(self.navigation_start)
}
#[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());
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::DOMContentLoaded);
assert_eq!(metrics.dom_content_loaded(), dcl);
assert_eq!(metrics.get_tti(), None);
}
#[test]
fn test_set_mta() {
let metrics = test_metrics();
let now = CrossProcessInstant::now();
metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive(now));
let main_thread_available_time = metrics.main_thread_available();
assert!(main_thread_available_time.is_some());
assert_eq!(main_thread_available_time, Some(now));
//try to overwrite
metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive(
CrossProcessInstant::now(),
));
assert_eq!(metrics.main_thread_available(), main_thread_available_time);
assert_eq!(metrics.get_tti(), None);
}
#[test]
fn test_set_tti_dcl() {
let metrics = test_metrics();
let now = CrossProcessInstant::now();
metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive(now));
let main_thread_available_time = metrics.main_thread_available();
assert!(main_thread_available_time.is_some());
metrics.maybe_set_tti(InteractiveFlag::DOMContentLoaded);
let dom_content_loaded_time = metrics.dom_content_loaded();
assert!(dom_content_loaded_time.is_some());
assert_eq!(metrics.get_tti(), dom_content_loaded_time);
}
#[test]
fn test_set_tti_mta() {
let metrics = test_metrics();
metrics.maybe_set_tti(InteractiveFlag::DOMContentLoaded);
let dcl = metrics.dom_content_loaded();
assert!(dcl.is_some());
let time = CrossProcessInstant::now();
metrics.maybe_set_tti(InteractiveFlag::TimeToInteractive(time));
let mta = metrics.main_thread_available();
assert!(mta.is_some());
assert_eq!(metrics.get_tti(), mta);
}
#[test]
fn test_first_paint_setter() {
let metrics = test_metrics();
metrics.set_first_paint(CrossProcessInstant::now(), false);
assert!(metrics.first_paint().is_some());
}
#[test]
fn test_first_contentful_paint_setter() {
let metrics = test_metrics();
metrics.set_first_contentful_paint(CrossProcessInstant::now(), false);
assert!(metrics.first_contentful_paint().is_some());
}

View file

@ -36,10 +36,7 @@ use hyper_serde::Serde;
use ipc_channel::ipc;
use js::rust::{HandleObject, HandleValue};
use keyboard_types::{Code, Key, KeyState};
use metrics::{
InteractiveFlag, InteractiveMetrics, InteractiveWindow, ProfilerMetadataFactory,
ProgressiveWebMetric,
};
use metrics::{InteractiveFlag, InteractiveWindow, ProgressiveWebMetrics};
use mime::{self, Mime};
use net_traits::CookieSource::NonHTTP;
use net_traits::CoreResourceMsg::{GetCookiesForUrl, SetCookiesForUrl};
@ -51,10 +48,11 @@ use net_traits::{FetchResponseListener, IpcSend, ReferrerPolicy};
use num_traits::ToPrimitive;
use percent_encoding::percent_decode;
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_traits::{
AnimationState, AnimationTickType, ConstellationInputEvent, DocumentActivity, ScriptMsg,
AnimationState, AnimationTickType, ConstellationInputEvent, DocumentActivity,
ProgressiveWebMetricType, ScriptMsg,
};
use servo_arc::Arc;
use servo_config::pref;
@ -78,6 +76,7 @@ use webrender_traits::CompositorHitTestResult;
use super::bindings::codegen::Bindings::XPathEvaluatorBinding::XPathEvaluatorMethods;
use super::clipboardevent::ClipboardEventType;
use super::performancepainttiming::PerformancePaintTiming;
use crate::DomTypes;
use crate::animation_timeline::AnimationTimeline;
use crate::animations::Animations;
@ -432,7 +431,7 @@ pub(crate) struct Document {
/// See <https://html.spec.whatwg.org/multipage/#form-owner>
form_id_listener_map: DomRefCell<HashMapTracedValues<Atom, HashSet<Dom<Element>>>>,
#[no_trace]
interactive_time: DomRefCell<InteractiveMetrics>,
interactive_time: DomRefCell<ProgressiveWebMetrics>,
#[no_trace]
tti_window: DomRefCell<InteractiveWindow>,
/// RAII canceller for Fetch
@ -3010,7 +3009,7 @@ impl Document {
// html parsing has finished - set dom content loaded
self.interactive_time
.borrow()
.maybe_set_tti(self, InteractiveFlag::DOMContentLoaded);
.maybe_set_tti(InteractiveFlag::DOMContentLoaded);
// Step 4.2.
// TODO: client message queue.
@ -3095,7 +3094,7 @@ impl Document {
.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()
}
@ -3149,10 +3148,10 @@ impl Document {
return;
}
if self.tti_window.borrow().needs_check() {
self.get_interactive_metrics().maybe_set_tti(
self,
InteractiveFlag::TimeToInteractive(self.tti_window.borrow().get_start()),
);
self.get_interactive_metrics()
.maybe_set_tti(InteractiveFlag::TimeToInteractive(
self.tti_window.borrow().get_start(),
));
}
}
@ -3532,6 +3531,37 @@ impl Document {
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 {
@ -3688,8 +3718,15 @@ impl Document {
(DocumentReadyState::Complete, true)
};
let interactive_time =
InteractiveMetrics::new(window.time_profiler_chan().clone(), url.clone());
let frame_type = match window.is_top_level() {
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(|| {
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)]
impl DocumentMethods<crate::DomTypeHolder> for 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 profile_traits::mem::{self as profile_mem, OpaqueSender, ReportsChan};
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 timers::TimerScheduler;
#[cfg(feature = "webgpu")]
@ -88,7 +88,6 @@ impl MixedMessage {
#[cfg(feature = "webgpu")]
ScriptThreadMessage::SetWebGPUPort(..) => None,
ScriptThreadMessage::SetScrollStates(id, ..) => Some(*id),
ScriptThreadMessage::SetEpochPaintTime(id, ..) => Some(*id),
},
MixedMessage::FromScript(inner_msg) => match inner_msg {
MainThreadScriptMsg::Common(CommonScriptMsg::Task(_, _, pipeline_id, _)) => {
@ -318,10 +317,6 @@ pub(crate) struct ScriptThreadSenders {
#[no_trace]
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
/// messages on this channel are routed to crossbeam [`Sender`] on the router thread, which
/// in turn sends messages to [`ScriptThreadReceivers::image_cache_receiver`].

View file

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

View file

@ -7,6 +7,7 @@ use std::fmt;
use std::time::Duration;
use base::Epoch;
use base::cross_process_instant::CrossProcessInstant;
use base::id::{PipelineId, WebViewId};
use embedder_traits::{
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
/// given pipeline via the constellation.
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 {

View file

@ -8,9 +8,8 @@ mod constellation_msg;
use std::fmt::{Debug, Error, Formatter};
use base::Epoch;
use base::id::{PipelineId, WebViewId};
pub use constellation_msg::ConstellationMsg;
pub use constellation_msg::{ConstellationMsg, PaintMetricEvent};
use crossbeam_channel::{Receiver, Sender};
use embedder_traits::{EventLoopWaker, MouseButton, MouseButtonAction};
use euclid::Rect;
@ -84,10 +83,6 @@ pub enum CompositorMsg {
// sends a reply on the IpcSender, the constellation knows it's safe to
// tear down the other threads associated with this pipeline.
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
LoadComplete(WebViewId),
/// WebDriver mouse button event

View file

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

View file

@ -5,6 +5,7 @@
use base::cross_process_instant::CrossProcessInstant;
use ipc_channel::ipc::IpcSender;
use log::warn;
use malloc_size_of_derive::MallocSizeOf;
use serde::{Deserialize, Serialize};
use servo_config::opts;
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 {
RootWindow,
IFrame,

View file

@ -19,7 +19,6 @@ use std::fmt;
use std::sync::Arc;
use background_hang_monitor_api::BackgroundHangMonitorRegister;
use base::Epoch;
use base::cross_process_instant::CrossProcessInstant;
use base::id::{
BlobId, BrowsingContextId, HistoryStateId, MessagePortId, PipelineId, PipelineNamespaceId,
@ -64,9 +63,8 @@ use webrender_traits::{
};
pub use crate::script_msg::{
DOMMessage, IFrameSizeMsg, Job, JobError, JobResult, JobResultValue, JobType, LayoutMsg,
LogEntry, SWManagerMsg, SWManagerSenders, ScopeThings, ScriptMsg, ServiceWorkerMsg,
TouchEventResult,
DOMMessage, IFrameSizeMsg, Job, JobError, JobResult, JobResultValue, JobType, LogEntry,
SWManagerMsg, SWManagerSenders, ScopeThings, ScriptMsg, ServiceWorkerMsg, TouchEventResult,
};
use crate::serializable::BlobImpl;
use crate::transferable::MessagePortImpl;
@ -395,7 +393,12 @@ pub enum ScriptThreadMessage {
/// Reload the given page.
Reload(PipelineId),
/// 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.
MediaSessionAction(PipelineId, MediaSessionActionType),
/// 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
/// pipeline via the Constellation.
SetScrollStates(PipelineId, Vec<ScrollState>),
/// Send the paint time for a specific epoch.
SetEpochPaintTime(PipelineId, Epoch, CrossProcessInstant),
}
impl fmt::Debug for ScriptThreadMessage {
@ -476,8 +477,6 @@ pub struct InitialScriptState {
pub pipeline_to_constellation_sender: ScriptToConstellationChan,
/// A handle to register script-(and associated layout-)threads for hang monitoring.
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.
pub resource_threads: ResourceThreads,
/// A channel to the bluetooth thread.

View file

@ -46,21 +46,6 @@ pub struct IFrameSizeMsg {
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
#[derive(Debug, Deserialize, Serialize)]
pub enum TouchEventResult {

View file

@ -18,7 +18,6 @@ use std::sync::atomic::{AtomicIsize, AtomicU64, Ordering};
use app_units::Au;
use atomic_refcell::AtomicRefCell;
use base::Epoch;
use base::cross_process_instant::CrossProcessInstant;
use base::id::{BrowsingContextId, PipelineId, WebViewId};
use canvas_traits::canvas::{CanvasId, CanvasMsg};
use euclid::Size2D;
@ -28,7 +27,6 @@ use fonts::{FontContext, SystemFontServiceProxy};
use ipc_channel::ipc::IpcSender;
use libc::c_void;
use malloc_size_of_derive::MallocSizeOf;
use metrics::PaintTimeMetrics;
use net_traits::image_cache::{ImageCache, PendingImageId};
use profile_traits::mem::Report;
use profile_traits::time;
@ -189,7 +187,6 @@ pub struct LayoutConfig {
pub font_context: Arc<FontContext>,
pub time_profiler_chan: time::ProfilerChan,
pub compositor_api: CrossProcessCompositorApi,
pub paint_time_metrics: PaintTimeMetrics,
pub window_size: WindowSizeData,
}
@ -245,9 +242,6 @@ pub trait Layout {
/// Set the scroll states of this layout after a compositor scroll.
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_boxes(&self, node: OpaqueNode) -> Vec<Rect<Au>>;
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
/// tree.
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 {
@ -316,6 +325,7 @@ impl CompositorDisplayListInfo {
pipeline_id: PipelineId,
epoch: Epoch,
viewport_scroll_sensitivity: AxesScrollSensitivity,
first_reflow: bool,
) -> Self {
let mut scroll_tree = ScrollTree::default();
let root_reference_frame_id = scroll_tree.add_scroll_tree_node(
@ -343,6 +353,8 @@ impl CompositorDisplayListInfo {
scroll_tree,
root_reference_frame_id,
root_scroll_node_id,
is_contentful: false,
first_reflow,
}
}

View file

@ -166,6 +166,7 @@ class MachCommands(CommandBase):
"hyper_serde",
"layout_2020",
"libservo",
"metrics",
"net",
"net_traits",
"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"
);
}