Compare commits

...

2 Commits

Author SHA1 Message Date
Martin Robinson
3f09d2008c constellation: Remove BrowsingContext::throttled
This is now longer necessary now that `WebView`s track visibility.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
2026-01-04 10:28:34 +01:00
Martin Robinson
177978fe18 Do not animate hidden WebViews
When a `WebView` is hidden it should not process animations. This change
makes it so that this does not happen by:

- Tracking visibility in the Constellation. Hidden WebViews are
  throttled and hiding and showing WebViews does not force Pipelines
  into a non-throttled state.
- Tracking visibility in the embedder-side `WebView` data structure.
  Hidden `WebView`s do not get new frame notifications and do not
  trigger animation ticks in the `RefreshDriver`.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
2026-01-04 10:28:34 +01:00
10 changed files with 157 additions and 114 deletions

View File

@@ -173,8 +173,10 @@ impl WebViewRenderer {
.any(PipelineDetails::animation_callbacks_running)
}
/// Whether this [`WebViewRenderer`] is animating. Note that hidden `WebView`s
/// are never considered to be animating.
pub(crate) fn animating(&self) -> bool {
self.animating
self.animating && !self.hidden()
}
pub(crate) fn hidden(&self) -> bool {
@@ -185,7 +187,14 @@ impl WebViewRenderer {
/// value changed or `false` otherwise.
pub(crate) fn set_hidden(&mut self, new_value: bool) -> bool {
let old_value = std::mem::replace(&mut self.hidden, new_value);
new_value != old_value
if new_value == old_value {
return false;
}
let _ = self.embedder_to_constellation_sender.send(
EmbedderToConstellationMessage::SetWebViewHidden(self.id, new_value),
);
true
}
/// Returns the [`PipelineDetails`] for the given [`PipelineId`], creating it if needed.

View File

@@ -26,10 +26,6 @@ pub struct NewBrowsingContextInfo {
/// Whether this browsing context inherits a secure context.
pub inherited_secure_context: Option<bool>,
/// Whether this browsing context should be throttled, using less resources
/// by stopping animations and running timers at a heavily limited rate.
pub throttled: bool,
}
/// The constellation's view of a browsing context.
@@ -57,10 +53,6 @@ pub struct BrowsingContext {
/// Whether this browsing context inherits a secure context.
pub inherited_secure_context: Option<bool>,
/// Whether this browsing context should be throttled, using less resources
/// by stopping animations and running timers at a heavily limited rate.
pub throttled: bool,
/// The pipeline for the current session history entry.
pub pipeline_id: PipelineId,
@@ -86,7 +78,6 @@ impl BrowsingContext {
viewport_details: ViewportDetails,
is_private: bool,
inherited_secure_context: Option<bool>,
throttled: bool,
) -> BrowsingContext {
let mut pipelines = FxHashSet::default();
pipelines.insert(pipeline_id);
@@ -97,7 +88,6 @@ impl BrowsingContext {
viewport_details,
is_private,
inherited_secure_context,
throttled,
pipeline_id,
parent_pipeline_id,
pipelines,

View File

@@ -1030,20 +1030,18 @@ where
// https://github.com/servo/ipc-channel/issues/138
load_data: LoadData,
is_private: bool,
throttled: bool,
) {
if self.shutting_down {
return;
}
debug!("Creating new pipeline ({new_pipeline_id:?}) in {browsing_context_id}");
let Some(theme) = self
.webviews
.get(&webview_id)
.map(ConstellationWebView::theme)
else {
warn!("Tried to create Pipeline for uknown WebViewId: {webview_id:?}");
return;
let (webview_hidden, theme) = {
let Some(webview) = self.webviews.get(&webview_id) else {
warn!("Tried to create Pipeline for uknown WebViewId: {webview_id:?}");
return;
};
(webview.hidden(), webview.theme())
};
let event_loop = match self.get_or_create_event_loop_for_new_pipeline(
@@ -1073,7 +1071,7 @@ where
user_content_manager_id,
theme,
};
let pipeline = match Pipeline::spawn(new_pipeline_info, event_loop, self, throttled) {
let pipeline = match Pipeline::spawn(new_pipeline_info, event_loop, self, webview_hidden) {
Ok(pipeline) => pipeline,
Err(error) => return self.handle_send_error(new_pipeline_id, error),
};
@@ -1163,7 +1161,6 @@ where
viewport_details: ViewportDetails,
is_private: bool,
inherited_secure_context: Option<bool>,
throttled: bool,
) {
debug!("{}: Creating new browsing context", browsing_context_id);
let bc_group_id = match self
@@ -1202,7 +1199,6 @@ where
viewport_details,
is_private,
inherited_secure_context,
throttled,
);
self.browsing_contexts
.insert(browsing_context_id, browsing_context);
@@ -1503,8 +1499,8 @@ where
EmbedderToConstellationMessage::MediaSessionAction(action) => {
self.handle_media_session_action_msg(action);
},
EmbedderToConstellationMessage::SetWebViewThrottled(webview_id, throttled) => {
self.set_webview_throttled(webview_id, throttled);
EmbedderToConstellationMessage::SetWebViewHidden(webview_id, hidden) => {
self.set_webview_hidden(webview_id, hidden);
},
EmbedderToConstellationMessage::SetScrollStates(pipeline_id, scroll_states) => {
self.handle_set_scroll_states(pipeline_id, scroll_states)
@@ -2855,7 +2851,6 @@ where
};
let viewport_details = browsing_context.viewport_details;
let pipeline_id = browsing_context.pipeline_id;
let throttled = browsing_context.throttled;
let Some(pipeline) = self.pipelines.get(&pipeline_id) else {
return warn!("failed pipeline is missing");
@@ -2900,7 +2895,6 @@ where
viewport_details,
new_load_data,
is_private,
throttled,
);
self.add_pending_change(SessionHistoryChange {
webview_id,
@@ -3062,7 +3056,6 @@ where
let browsing_context_id = BrowsingContextId::from(webview_id);
let load_data = LoadData::new_for_new_unrelated_webview(url);
let is_private = false;
let throttled = false;
// Register this new top-level browsing context id as a webview and set
// its focused browsing context to be itself.
@@ -3089,7 +3082,6 @@ where
viewport_details,
load_data,
is_private,
throttled,
);
self.add_pending_change(SessionHistoryChange {
webview_id,
@@ -3100,7 +3092,6 @@ where
parent_pipeline_id: None,
is_private,
inherited_secure_context: None,
throttled,
}),
viewport_details,
});
@@ -3290,7 +3281,6 @@ where
};
let browsing_context_size = browsing_context.viewport_details;
let browsing_context_throttled = browsing_context.throttled;
// TODO(servo#30571) revert to debug_assert_eq!() once underlying bug is fixed
#[cfg(debug_assertions)]
if !(browsing_context_size == load_info.viewport_details) {
@@ -3309,7 +3299,6 @@ where
browsing_context_size,
load_info.load_data,
is_private,
browsing_context_throttled,
);
self.add_pending_change(SessionHistoryChange {
webview_id,
@@ -3343,9 +3332,9 @@ where
);
},
};
let (is_parent_private, is_parent_throttled, is_parent_secure) =
let (is_parent_private, is_parent_secure) =
match self.browsing_contexts.get(&parent_browsing_context_id) {
Some(ctx) => (ctx.is_private, ctx.throttled, ctx.inherited_secure_context),
Some(ctx) => (ctx.is_private, ctx.inherited_secure_context),
None => {
return warn!(
"{}: New iframe {} loaded in closed parent browsing context",
@@ -3353,6 +3342,11 @@ where
);
},
};
let webview_hidden = self
.webviews
.get(&webview_id)
.is_none_or(|webview| webview.hidden());
let is_private = is_private || is_parent_private;
let pipeline = Pipeline::new_already_spawned(
new_pipeline_id,
@@ -3361,7 +3355,7 @@ where
None,
script_sender,
self.paint_proxy.clone(),
is_parent_throttled,
webview_hidden,
load_info.load_data,
);
@@ -3377,7 +3371,6 @@ where
parent_pipeline_id: Some(parent_pipeline_id),
is_private,
inherited_secure_context: is_parent_secure,
throttled: is_parent_throttled,
}),
viewport_details: load_info.viewport_details,
});
@@ -3424,9 +3417,9 @@ where
);
},
};
let (is_opener_private, is_opener_throttled, is_opener_secure) =
let (is_opener_private, is_opener_secure) =
match self.browsing_contexts.get(&opener_browsing_context_id) {
Some(ctx) => (ctx.is_private, ctx.throttled, ctx.inherited_secure_context),
Some(ctx) => (ctx.is_private, ctx.inherited_secure_context),
None => {
return warn!(
"{}: New auxiliary {} loaded in closed opener browsing context",
@@ -3442,7 +3435,9 @@ where
Some(opener_browsing_context_id),
script_sender,
self.paint_proxy.clone(),
is_opener_throttled,
// This means that the WebView isn't hidden. If it does become hidden
// at a later time, a followup message will arrive to the Constellation.
false,
load_data,
);
let _ = response_sender.send(Some(AuxiliaryWebViewCreationResponse {
@@ -3483,7 +3478,6 @@ where
parent_pipeline_id: None,
is_private: is_opener_private,
inherited_secure_context: is_opener_secure,
throttled: is_opener_throttled,
}),
viewport_details,
});
@@ -3622,14 +3616,13 @@ where
return None;
},
};
let (viewport_details, pipeline_id, parent_pipeline_id, is_private, is_throttled) =
let (viewport_details, pipeline_id, parent_pipeline_id, is_private) =
match self.browsing_contexts.get(&browsing_context_id) {
Some(ctx) => (
ctx.viewport_details,
ctx.pipeline_id,
ctx.parent_pipeline_id,
ctx.is_private,
ctx.throttled,
),
None => {
// This should technically never happen (since `load_url` is
@@ -3716,7 +3709,6 @@ where
viewport_details,
load_data,
is_private,
is_throttled,
);
self.add_pending_change(SessionHistoryChange {
webview_id,
@@ -3962,24 +3954,17 @@ where
browsing_context_id, pipeline_id,
);
let (
webview_id,
old_pipeline_id,
parent_pipeline_id,
viewport_details,
is_private,
throttled,
) = match self.browsing_contexts.get(&browsing_context_id) {
Some(ctx) => (
ctx.webview_id,
ctx.pipeline_id,
ctx.parent_pipeline_id,
ctx.viewport_details,
ctx.is_private,
ctx.throttled,
),
None => return warn!("No browsing context to traverse!"),
};
let (webview_id, old_pipeline_id, parent_pipeline_id, viewport_details, is_private) =
match self.browsing_contexts.get(&browsing_context_id) {
Some(ctx) => (
ctx.webview_id,
ctx.pipeline_id,
ctx.parent_pipeline_id,
ctx.viewport_details,
ctx.is_private,
),
None => return warn!("No browsing context to traverse!"),
};
let opener = match self.pipelines.get(&old_pipeline_id) {
Some(pipeline) => pipeline.opener,
None => None,
@@ -3994,7 +3979,6 @@ where
viewport_details,
load_data.clone(),
is_private,
throttled,
);
self.add_pending_change(SessionHistoryChange {
webview_id,
@@ -4027,7 +4011,7 @@ where
self.unload_document(old_pipeline_id);
if let Some(new_pipeline) = self.pipelines.get(&new_pipeline_id) {
if let Some(new_pipeline) = self.pipelines.get_mut(&new_pipeline_id) {
if let Some(ref chan) = self.devtools_sender {
let state = NavigationState::Start(new_pipeline.url.clone());
let _ = chan.send(DevtoolsControlMsg::FromScript(
@@ -4044,7 +4028,7 @@ where
));
}
new_pipeline.set_throttled(false);
new_pipeline.set_has_active_document(true);
self.notify_focus_state(new_pipeline_id);
}
@@ -4572,19 +4556,29 @@ where
}
}
#[servo_tracing::instrument(skip_all)]
fn set_webview_throttled(&mut self, webview_id: WebViewId, throttled: bool) {
fn root_pipeline_for_webview(&mut self, webview_id: WebViewId) -> Option<&Pipeline> {
let browsing_context_id = BrowsingContextId::from(webview_id);
let pipeline_id = match self.browsing_contexts.get(&browsing_context_id) {
Some(browsing_context) => browsing_context.pipeline_id,
None => {
return warn!("{browsing_context_id}: Tried to SetWebViewThrottled after closure");
},
};
match self.pipelines.get(&pipeline_id) {
None => warn!("{pipeline_id}: Tried to SetWebViewThrottled after closure"),
Some(pipeline) => pipeline.set_throttled(throttled),
let browsing_context = self.browsing_contexts.get(&browsing_context_id)?;
self.pipelines.get(&browsing_context.pipeline_id)
}
#[servo_tracing::instrument(skip_all)]
fn set_webview_hidden(&mut self, webview_id: WebViewId, hidden: bool) {
if self
.webviews
.get_mut(&webview_id)
.is_none_or(|webview| !webview.set_hidden(hidden))
{
return;
}
let Some(root_pipeline) = self.root_pipeline_for_webview(webview_id) else {
warn!("Tried to send SetThrottled to WebView ({webview_id:?} after closure");
return;
};
// Throttle the root pipeline, recursively throttling descendant pipelines as well.
root_pipeline.send_throttle_messages(hidden);
}
#[servo_tracing::instrument(skip_all)]
@@ -4743,7 +4737,6 @@ where
change.viewport_details,
new_context_info.is_private,
new_context_info.inherited_secure_context,
new_context_info.throttled,
);
self.update_activity(change.new_pipeline_id);
},
@@ -5396,11 +5389,12 @@ where
/// Send a message to script requesting the document associated with this pipeline runs the 'unload' algorithm.
#[servo_tracing::instrument(skip_all)]
fn unload_document(&self, pipeline_id: PipelineId) {
if let Some(pipeline) = self.pipelines.get(&pipeline_id) {
pipeline.set_throttled(true);
let msg = ScriptThreadMessage::UnloadDocument(pipeline_id);
let _ = pipeline.event_loop.send(msg);
fn unload_document(&mut self, pipeline_id: PipelineId) {
if let Some(pipeline) = self.pipelines.get_mut(&pipeline_id) {
pipeline.set_has_active_document(false);
let _ = pipeline
.event_loop
.send(ScriptThreadMessage::UnloadDocument(pipeline_id));
}
}

View File

@@ -45,6 +45,10 @@ pub(crate) struct ConstellationWebView {
/// The [`Theme`] that this [`ConstellationWebView`] uses. This is communicated to all
/// `ScriptThread`s so that they know how to render the contents of a particular `WebView.
theme: Theme,
/// Whether or not this entire [`ConstellationWebView`] is hiden. `WebView`s that
/// are hidden will be throttled.
hidden: bool,
}
impl ConstellationWebView {
@@ -61,6 +65,7 @@ impl ConstellationWebView {
last_mouse_move_point: Default::default(),
session_history: JointSessionHistory::new(),
theme: Theme::Light,
hidden: false,
}
}
@@ -75,6 +80,17 @@ impl ConstellationWebView {
self.theme
}
/// Whether or not the WebView is hidden.
pub(crate) fn hidden(&self) -> bool {
self.hidden
}
/// Set whether or not this [`ConstellationWebView`] is hidden, returning true if the value changed.
pub(crate) fn set_hidden(&mut self, hidden: bool) -> bool {
let old_hidden = std::mem::replace(&mut self.hidden, hidden);
old_hidden != self.hidden
}
fn target_pipeline_id_for_input_event(
&self,
event: &ConstellationInputEvent,

View File

@@ -67,7 +67,13 @@ pub struct Pipeline {
/// The title of this pipeline's document.
pub title: String,
/// The [`FocusSequenceNumber`] of this [`Pipeline`].
pub focus_sequence: FocusSequenceNumber,
/// Whether or not this [`Pipeline`] has an actively loading/loaded. When there is no
/// active document, the [`Pipeline`] will never be unthrottled. Throttled pipelines
/// do not update animations and their timers are slowed.
has_active_document: bool,
}
impl Pipeline {
@@ -76,7 +82,7 @@ impl Pipeline {
new_pipeline_info: NewPipelineInfo,
event_loop: Rc<EventLoop>,
constellation: &Constellation<STF, SWF>,
throttled: bool,
webview_hidden: bool,
) -> Result<Self, Error> {
if let Err(error) = event_loop.send(ScriptThreadMessage::SpawnPipeline(
new_pipeline_info.clone(),
@@ -92,7 +98,7 @@ impl Pipeline {
new_pipeline_info.opener,
event_loop,
constellation.paint_proxy.clone(),
throttled,
webview_hidden,
new_pipeline_info.load_data,
))
}
@@ -106,7 +112,7 @@ impl Pipeline {
opener: Option<BrowsingContextId>,
event_loop: Rc<EventLoop>,
paint_proxy: PaintProxy,
throttled: bool,
webview_hidden: bool,
load_data: LoadData,
) -> Self {
let pipeline = Self {
@@ -125,8 +131,14 @@ impl Pipeline {
completely_loaded: false,
title: String::new(),
focus_sequence: FocusSequenceNumber::default(),
// Assume that every new Pipeline has an active document until told otherwise.
has_active_document: true,
};
pipeline.set_throttled(throttled);
if webview_hidden {
pipeline.send_throttle_messages(true);
}
pipeline
}
@@ -187,15 +199,37 @@ impl Pipeline {
}
}
/// Set whether to make pipeline use less resources, by stopping animations and
/// running timers at a heavily limited rate.
pub fn set_throttled(&self, throttled: bool) {
let script_msg = ScriptThreadMessage::SetThrottled(self.webview_id, self.id, throttled);
let paint_message = PaintMessage::SetThrottled(self.webview_id, self.id, throttled);
let err = self.event_loop.send(script_msg);
if let Err(e) = err {
warn!("Sending SetThrottled to script failed ({}).", e);
/// Set whether or not this Pipeline has an active document.
pub(crate) fn set_has_active_document(&mut self, has_active_document: bool) {
if self.has_active_document == has_active_document {
return;
}
self.paint_proxy.send(paint_message);
self.has_active_document = has_active_document;
// If the active document has gone away, throttle the WebView.
if !self.has_active_document {
self.send_throttle_messages(true);
}
}
/// Set whether this Pipeline is throttled or unthrottled. If the Pipeline
/// does not have an active Document, it will not be unthrottled until it does.
pub(crate) fn send_throttle_messages(&self, throttled: bool) {
// Never unthrottled Pipelines that do not have an active Document.
let throttled = !self.has_active_document || throttled;
if let Err(error) = self.event_loop.send(ScriptThreadMessage::SetThrottled(
self.webview_id,
self.id,
throttled,
)) {
warn!("Sending SetThrottled to script failed ({error}).");
}
self.paint_proxy.send(PaintMessage::SetThrottled(
self.webview_id,
self.id,
throttled,
));
}
}

View File

@@ -69,7 +69,7 @@ mod from_embedder {
Self::ToggleProfiler(..) => target!("EnableProfiler"),
Self::ExitFullScreen(_) => target!("ExitFullScreen"),
Self::MediaSessionAction(_) => target!("MediaSessionAction"),
Self::SetWebViewThrottled(_, _) => target!("SetWebViewThrottled"),
Self::SetWebViewHidden(_, _) => target!("SetWebViewHidden"),
Self::SetScrollStates(..) => target!("SetScrollStates"),
Self::PaintMetric(..) => target!("PaintMetric"),
Self::EvaluateJavaScript(..) => target!("EvaluateJavaScript"),

View File

@@ -430,6 +430,9 @@ pub(crate) struct Window {
#[no_trace]
player_context: WindowGLContext,
/// Whether or no this [`Window`] is "throttled." When this is true animations will not run
/// and timers will be slowed down. [`Window`]s become throttled when their [`Document`] is
/// no longer active or when the `WebView` that contains them is hidden.
throttled: Cell<bool>,
/// A shared marker for the validity of any cached layout values. A value of true

View File

@@ -1169,21 +1169,24 @@ impl ScriptThread {
document.react_to_environment_changes()
}
// > 11. For each doc of docs, update animations and send events for doc, passing
// > in relative high resolution time given frameTimestamp and doc's relevant
// > global object as the timestamp [WEBANIMATIONS]
document.update_animations_and_send_events(can_gc);
// Do not update animations or run rAFs if a Document is throttled.
if !document.window().throttled() {
// > 11. For each doc of docs, update animations and send events for doc, passing
// > in relative high resolution time given frameTimestamp and doc's relevant
// > global object as the timestamp [WEBANIMATIONS]
document.update_animations_and_send_events(can_gc);
// TODO(#31866): Implement "run the fullscreen steps" from
// https://fullscreen.spec.whatwg.org/multipage/#run-the-fullscreen-steps.
// TODO(#31866): Implement "run the fullscreen steps" from
// https://fullscreen.spec.whatwg.org/multipage/#run-the-fullscreen-steps.
// TODO(#31868): Implement the "context lost steps" from
// https://html.spec.whatwg.org/multipage/#context-lost-steps.
// TODO(#31868): Implement the "context lost steps" from
// https://html.spec.whatwg.org/multipage/#context-lost-steps.
// > 14. For each doc of docs, run the animation frame callbacks for doc, passing
// > in the relative high resolution time given frameTimestamp and doc's
// > relevant global object as the timestamp.
document.run_the_animation_frame_callbacks(can_gc);
// > 14. For each doc of docs, run the animation frame callbacks for doc, passing
// > in the relative high resolution time given frameTimestamp and doc's
// > relevant global object as the timestamp.
document.run_the_animation_frame_callbacks(can_gc);
}
// Run the resize observer steps.
let _realm = enter_realm(&*document);

View File

@@ -567,12 +567,6 @@ impl WebView {
.send(EmbedderToConstellationMessage::ExitFullScreen(self.id()));
}
pub fn set_throttled(&self, throttled: bool) {
self.inner().servo.constellation_proxy().send(
EmbedderToConstellationMessage::SetWebViewThrottled(self.id(), throttled),
);
}
pub fn toggle_webrender_debugging(&self, debugging: WebRenderDebugOption) {
self.inner().servo.paint().toggle_webrender_debug(debugging);
}

View File

@@ -90,7 +90,7 @@ pub enum EmbedderToConstellationMessage {
/// Media session action.
MediaSessionAction(MediaSessionActionType),
/// Set whether to use less resources, by stopping animations and running timers at a heavily limited rate.
SetWebViewThrottled(WebViewId, bool),
SetWebViewHidden(WebViewId, bool),
/// The Servo renderer scrolled and is updating the scroll states of the nodes in the
/// given pipeline via the constellation.
SetScrollStates(PipelineId, FxHashMap<ExternalScrollId, LayoutVector2D>),