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