Skip to main content

servo_constellation/
pipeline.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::collections::HashSet;
6use std::rc::Rc;
7
8use embedder_traits::{AnimationState, FocusSequenceNumber};
9use layout_api::ScriptThreadFactory;
10use log::{debug, error, warn};
11use paint_api::{CompositionPipeline, PaintMessage, PaintProxy};
12use rustc_hash::FxHashSet;
13use script_traits::{
14    DiscardBrowsingContext, DocumentActivity, NewPipelineInfo, ScriptThreadMessage,
15};
16use servo_base::generic_channel::SendError;
17use servo_base::id::{BrowsingContextId, HistoryStateId, PipelineId, WebViewId, WorkerId};
18use servo_constellation_traits::{LoadData, ServiceWorkerManagerFactory};
19use servo_url::ServoUrl;
20
21use crate::Constellation;
22use crate::event_loop::EventLoop;
23
24/// A `Pipeline` is the constellation's view of a `Window`. Each pipeline has an event loop
25/// (executed by a script thread). A script thread may be responsible for many pipelines.
26pub struct Pipeline {
27    /// The ID of the pipeline.
28    pub id: PipelineId,
29
30    /// The ID of the browsing context that contains this Pipeline.
31    pub browsing_context_id: BrowsingContextId,
32
33    /// The [`WebViewId`] of the `WebView` that contains this Pipeline.
34    pub webview_id: WebViewId,
35
36    pub opener: Option<BrowsingContextId>,
37
38    /// The event loop handling this pipeline.
39    pub event_loop: Rc<EventLoop>,
40
41    /// A channel to `Paint`.
42    pub paint_proxy: PaintProxy,
43
44    /// The most recently loaded URL in this pipeline.
45    /// Note that this URL can change, for example if the page navigates
46    /// to a hash URL.
47    pub url: ServoUrl,
48
49    /// Whether this pipeline is currently running animations. Pipelines that are running
50    /// animations cause composites to be continually scheduled.
51    pub animation_state: AnimationState,
52
53    /// Whether the document for this pipeline has active animation frame callbacks.
54    pub document_callbacks_active: bool,
55
56    /// Workers in this pipeline with active animation frame callbacks.
57    pub worker_callbacks_active: FxHashSet<WorkerId>,
58
59    /// The last aggregate animation-callback state sent to Paint.
60    pub last_callbacks_active_sent_to_paint: bool,
61
62    /// The child browsing contexts of this pipeline (these are iframes in the document).
63    pub children: Vec<BrowsingContextId>,
64
65    /// The Load Data used to create this pipeline.
66    pub load_data: LoadData,
67
68    /// The active history state for this pipeline.
69    pub history_state_id: Option<HistoryStateId>,
70
71    /// The history states owned by this pipeline.
72    pub history_states: HashSet<HistoryStateId>,
73
74    /// Has this pipeline received a notification that it is completely loaded?
75    pub completely_loaded: bool,
76
77    /// The title of this pipeline's document.
78    pub title: String,
79
80    /// The [`FocusSequenceNumber`] of this [`Pipeline`].
81    pub focus_sequence: FocusSequenceNumber,
82
83    /// Whether or not this [`Pipeline`] has an actively loading/loaded document. When there is
84    /// no active document, the [`Pipeline`] will never be unthrottled. Throttled pipelines do
85    /// not update animations and their timers are slowed.
86    has_active_document: bool,
87}
88
89impl Pipeline {
90    /// Possibly starts a script thread, in a new process if requested.
91    pub(crate) fn spawn<STF: ScriptThreadFactory, SWF: ServiceWorkerManagerFactory>(
92        new_pipeline_info: NewPipelineInfo,
93        event_loop: Rc<EventLoop>,
94        constellation: &Constellation<STF, SWF>,
95        webview_hidden: bool,
96    ) -> Result<Self, SendError> {
97        if let Err(error) = event_loop.send(ScriptThreadMessage::SpawnPipeline(
98            new_pipeline_info.clone(),
99        )) {
100            error!("Could not spawn Pipeline in EventLoop: {error}");
101            return Err(error);
102        }
103
104        Ok(Self::new_already_spawned(
105            new_pipeline_info.new_pipeline_id,
106            new_pipeline_info.browsing_context_id,
107            new_pipeline_info.webview_state.id,
108            new_pipeline_info.opener,
109            event_loop,
110            constellation.paint_proxy.clone(),
111            webview_hidden,
112            new_pipeline_info.load_data,
113        ))
114    }
115
116    /// Creates a new `Pipeline`, after it has been spawned in its [`EventLoop`].
117    #[expect(clippy::too_many_arguments)]
118    pub fn new_already_spawned(
119        id: PipelineId,
120        browsing_context_id: BrowsingContextId,
121        webview_id: WebViewId,
122        opener: Option<BrowsingContextId>,
123        event_loop: Rc<EventLoop>,
124        paint_proxy: PaintProxy,
125        webview_hidden: bool,
126        load_data: LoadData,
127    ) -> Self {
128        let pipeline = Self {
129            id,
130            browsing_context_id,
131            webview_id,
132            opener,
133            event_loop,
134            paint_proxy,
135            url: load_data.url.clone(),
136            children: vec![],
137            animation_state: AnimationState::NoAnimationsPresent,
138            document_callbacks_active: false,
139            worker_callbacks_active: FxHashSet::default(),
140            last_callbacks_active_sent_to_paint: false,
141            load_data,
142            history_state_id: None,
143            history_states: HashSet::new(),
144            completely_loaded: false,
145            title: String::new(),
146            focus_sequence: FocusSequenceNumber::default(),
147            // Assume that every new Pipeline has an active document until told otherwise.
148            has_active_document: true,
149        };
150
151        if webview_hidden {
152            pipeline.send_throttle_messages(true);
153        }
154
155        pipeline
156    }
157
158    /// Let the `ScriptThread` for this [`Pipeline`] know that it has exited. If the `ScriptThread` hasn't
159    /// panicked and is still alive, it will send a `PipelineExited` message back to the `Constellation`
160    /// when it finishes cleaning up.
161    ///
162    /// Returns `true` if the channel is still alive or `false` otherwise.
163    pub fn send_exit_message_to_script(&self, discard_bc: DiscardBrowsingContext) -> bool {
164        debug!("{:?} Sending exit message to script", self.id);
165
166        // Script thread handles shutting down layout, and layout handles shutting down the painter.
167        // For now, if the script thread has failed, we give up on clean shutdown.
168        let result = self
169            .event_loop
170            .send(ScriptThreadMessage::ExitPipeline(
171                self.webview_id,
172                self.id,
173                discard_bc,
174            ))
175            .inspect_err(|error| warn!("Sending script exit message failed ({error})."));
176        !matches!(result, Err(SendError::Disconnected))
177    }
178
179    /// Notify this pipeline of its activity.
180    pub fn set_activity(&self, activity: DocumentActivity) {
181        let msg = ScriptThreadMessage::SetDocumentActivity(self.id, activity);
182        if let Err(e) = self.event_loop.send(msg) {
183            warn!("Sending activity message failed ({}).", e);
184        }
185    }
186
187    /// `Paint`'s view of a pipeline.
188    pub fn to_sendable(&self) -> CompositionPipeline {
189        CompositionPipeline {
190            id: self.id,
191            webview_id: self.webview_id,
192        }
193    }
194
195    /// Add a new child browsing context.
196    pub fn add_child(&mut self, browsing_context_id: BrowsingContextId) {
197        self.children.push(browsing_context_id);
198    }
199
200    /// Remove a child browsing context.
201    pub fn remove_child(&mut self, browsing_context_id: BrowsingContextId) {
202        match self
203            .children
204            .iter()
205            .position(|id| *id == browsing_context_id)
206        {
207            None => {
208                warn!(
209                    "Pipeline remove child already removed ({:?}).",
210                    browsing_context_id
211                )
212            },
213            Some(index) => {
214                self.children.remove(index);
215            },
216        }
217    }
218
219    /// Set whether or not this [`Pipeline`] has an active document.
220    pub(crate) fn set_has_active_document(&mut self, has_active_document: bool) {
221        if self.has_active_document == has_active_document {
222            return;
223        }
224
225        self.has_active_document = has_active_document;
226
227        // If the active document has gone away, throttle the WebView.
228        if !self.has_active_document {
229            self.send_throttle_messages(true);
230        }
231    }
232
233    /// Set whether this [`Pipeline`] is throttled or unthrottled. If the Pipeline
234    /// does not have an active Document, it will not be unthrottled until it does.
235    pub(crate) fn send_throttle_messages(&self, throttled: bool) {
236        // Never unthrottle Pipelines that do not have an active Document.
237        let throttled = !self.has_active_document || throttled;
238
239        if let Err(error) = self
240            .event_loop
241            .send(ScriptThreadMessage::SetThrottled(self.id, throttled))
242        {
243            warn!("Sending SetThrottled to script failed ({error}).");
244        }
245        self.paint_proxy.send(PaintMessage::SetThrottled(
246            self.webview_id,
247            self.id,
248            throttled,
249        ));
250    }
251}