script: Move navigation fetching to the ScriptThread (#34919)

This allows reusing the asynchrnous fetch mechanism that we use for page
resources and is likely a step toward removing the `FetchThread`.

Benefits:
 - Reduces IPC traffic during navigation. Now instead of bouncing
   between the constellation and the `ScriptThread` responses are sent
   directly to the `ScriptThread`.
 - Allows cancelling loads after redirects, which was not possible
   before.

There is the question of what to do when a redirect is cross-origin
(#23037). This currently isn't handled properly as the `Constellation`
sends data to the same `Pipeline` that initiated the load. This change
doesn't fix this issue, but does make it more possible for the
`ScriptThread` to shut down the pipeline and ask the `Constellation` to
replace it with a new one.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
Martin Robinson 2025-01-10 13:19:40 +01:00 committed by GitHub
parent 73c0701c83
commit fbd77b4524
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 420 additions and 460 deletions

View file

@ -129,9 +129,9 @@ use keyboard_types::{CompositionEvent, KeyboardEvent};
use log::{debug, error, info, trace, warn};
use media::{GLPlayerThreads, WindowGLContext};
use net_traits::pub_domains::reg_host;
use net_traits::request::{Referrer, RequestBuilder};
use net_traits::request::Referrer;
use net_traits::storage_thread::{StorageThreadMsg, StorageType};
use net_traits::{self, FetchResponseMsg, IpcSend, ReferrerPolicy, ResourceThreads};
use net_traits::{self, IpcSend, ReferrerPolicy, ResourceThreads};
use profile_traits::{mem, time};
use script_layout_interface::{LayoutFactory, ScriptThreadFactory};
use script_traits::CompositorEvent::{MouseButtonEvent, MouseMoveEvent};
@ -166,7 +166,6 @@ use crate::browsingcontext::{
NewBrowsingContextInfo,
};
use crate::event_loop::EventLoop;
use crate::network_listener::NetworkListener;
use crate::pipeline::{InitialPipelineState, Pipeline};
use crate::serviceworker::ServiceWorkerUnprivilegedContent;
use crate::session_history::{
@ -317,12 +316,6 @@ pub struct Constellation<STF, SWF> {
/// This is the constellation's view of `layout_sender`.
layout_receiver: Receiver<Result<FromLayoutMsg, IpcError>>,
/// A channel for network listener to send messages to the constellation.
network_listener_sender: Sender<(PipelineId, FetchResponseMsg)>,
/// A channel for the constellation to receive messages from network listener.
network_listener_receiver: Receiver<(PipelineId, FetchResponseMsg)>,
/// A channel for the constellation to receive messages from the compositor thread.
compositor_receiver: Receiver<FromCompositorMsg>,
@ -685,8 +678,6 @@ where
layout_ipc_receiver,
);
let (network_listener_sender, network_listener_receiver) = unbounded();
let swmanager_receiver =
route_ipc_receiver_to_new_crossbeam_receiver_preserving_errors(
swmanager_ipc_receiver,
@ -715,8 +706,6 @@ where
compositor_receiver,
layout_factory,
layout_receiver,
network_listener_sender,
network_listener_receiver,
embedder_proxy: state.embedder_proxy,
compositor_proxy: state.compositor_proxy,
webviews: WebViewManager::default(),
@ -1156,15 +1145,12 @@ where
)]
fn handle_request(&mut self) {
#[derive(Debug)]
// FIXME: https://github.com/servo/servo/issues/34591
#[expect(clippy::large_enum_variant)]
enum Request {
PipelineNamespace(PipelineNamespaceRequest),
Script((PipelineId, FromScriptMsg)),
BackgroundHangMonitor(HangMonitorAlert),
Compositor(FromCompositorMsg),
Layout(FromLayoutMsg),
NetworkListener((PipelineId, FetchResponseMsg)),
FromSWManager(SWManagerMsg),
}
// Get one incoming request.
@ -1198,11 +1184,6 @@ where
recv(self.layout_receiver) -> msg => {
msg.expect("Unexpected layout channel panic in constellation").map(Request::Layout)
}
recv(self.network_listener_receiver) -> msg => {
Ok(Request::NetworkListener(
msg.expect("Unexpected network listener channel panic in constellation")
))
}
recv(self.swmanager_receiver) -> msg => {
msg.expect("Unexpected SW channel panic in constellation").map(Request::FromSWManager)
}
@ -1228,9 +1209,6 @@ where
Request::Layout(message) => {
self.handle_request_from_layout(message);
},
Request::NetworkListener(message) => {
self.handle_request_from_network_listener(message);
},
Request::FromSWManager(message) => {
self.handle_request_from_swmanager(message);
},
@ -1263,26 +1241,6 @@ where
}
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace")
)]
fn handle_request_from_network_listener(&mut self, message: (PipelineId, FetchResponseMsg)) {
let (id, message_) = message;
let result = match self.pipelines.get(&id) {
Some(pipeline) => {
let msg = ConstellationControlMsg::NavigationResponse(id, message_);
pipeline.event_loop.send(msg)
},
None => {
return warn!("{}: Got fetch data after closure!", id);
},
};
if let Err(e) = result {
self.handle_send_error(id, e);
}
}
fn handle_request_from_swmanager(&mut self, message: SWManagerMsg) {
match message {
SWManagerMsg::PostMessageToClient => {
@ -1615,10 +1573,6 @@ where
FromScriptMsg::DiscardTopLevelBrowsingContext => {
self.handle_close_top_level_browsing_context(source_top_ctx_id);
},
FromScriptMsg::InitiateNavigateRequest(req_init, cancel_chan) => {
self.handle_navigate_request(source_pipeline_id, req_init, cancel_chan);
},
FromScriptMsg::ScriptLoadedURLInIFrame(load_info) => {
self.handle_script_loaded_url_in_iframe_msg(load_info);
},
@ -3202,26 +3156,6 @@ where
}
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace")
)]
fn handle_navigate_request(
&self,
id: PipelineId,
request_builder: RequestBuilder,
cancel_chan: IpcReceiver<()>,
) {
let listener = NetworkListener::new(
request_builder,
id,
self.public_resource_threads.clone(),
self.network_listener_sender.clone(),
);
listener.initiate_fetch(Some(cancel_chan));
}
// The script thread associated with pipeline_id has loaded a URL in an
// iframe via script. This will result in a new pipeline being spawned and
// a child being added to the parent browsing context. This message is never

View file

@ -11,7 +11,6 @@ mod browsingcontext;
mod constellation;
mod event_loop;
mod logging;
mod network_listener;
mod pipeline;
mod sandboxing;
mod serviceworker;

View file

@ -1,192 +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/. */
//! The listener that encapsulates all state for an in-progress document request.
//! Any redirects that are encountered are followed. Whenever a non-redirect
//! response is received, it is forwarded to the appropriate script thread.
use base::id::PipelineId;
use crossbeam_channel::Sender;
use http::{header, HeaderMap};
use ipc_channel::ipc;
use ipc_channel::router::ROUTER;
use log::warn;
use net::http_loader::{set_default_accept_language, DOCUMENT_ACCEPT_HEADER_VALUE};
use net_traits::request::{Referrer, RequestBuilder, RequestId};
use net_traits::response::ResponseInit;
use net_traits::{
CoreResourceMsg, FetchChannels, FetchMetadata, FetchResponseMsg, IpcSend, NetworkError,
ResourceThreads,
};
pub struct NetworkListener {
res_init: Option<ResponseInit>,
request_builder: RequestBuilder,
pipeline_id: PipelineId,
resource_threads: ResourceThreads,
sender: Sender<(PipelineId, FetchResponseMsg)>,
should_send: bool,
}
impl NetworkListener {
pub fn new(
request_builder: RequestBuilder,
pipeline_id: PipelineId,
resource_threads: ResourceThreads,
sender: Sender<(PipelineId, FetchResponseMsg)>,
) -> NetworkListener {
NetworkListener {
res_init: None,
request_builder,
pipeline_id,
resource_threads,
sender,
should_send: false,
}
}
pub fn initiate_fetch(&self, cancel_chan: Option<ipc::IpcReceiver<()>>) {
let (ipc_sender, ipc_receiver) = ipc::channel().expect("Failed to create IPC channel!");
let mut listener = NetworkListener {
res_init: self.res_init.clone(),
request_builder: self.request_builder.clone(),
resource_threads: self.resource_threads.clone(),
sender: self.sender.clone(),
pipeline_id: self.pipeline_id,
should_send: false,
};
let msg = match self.res_init {
Some(ref res_init_) => CoreResourceMsg::FetchRedirect(
self.request_builder.clone(),
res_init_.clone(),
ipc_sender,
None,
),
None => {
if !listener
.request_builder
.headers
.contains_key(header::ACCEPT)
{
listener
.request_builder
.headers
.insert(header::ACCEPT, DOCUMENT_ACCEPT_HEADER_VALUE);
}
set_default_accept_language(&mut listener.request_builder.headers);
CoreResourceMsg::Fetch(
listener.request_builder.clone(),
FetchChannels::ResponseMsg(ipc_sender, cancel_chan),
)
},
};
ROUTER.add_typed_route(
ipc_receiver,
Box::new(move |message| {
match message {
Ok(FetchResponseMsg::ProcessResponse(request_id, res)) => {
listener.check_redirect(request_id, res)
},
Ok(msg_) => listener.send(msg_),
Err(e) => warn!("Error while receiving network listener message: {}", e),
};
}),
);
if let Err(e) = self.resource_threads.sender().send(msg) {
warn!("Resource thread unavailable ({})", e);
}
}
fn check_redirect(
&mut self,
request_id: RequestId,
message: Result<FetchMetadata, NetworkError>,
) {
match message {
Ok(res_metadata) => {
let metadata = match res_metadata {
FetchMetadata::Filtered { ref unsafe_, .. } => unsafe_,
FetchMetadata::Unfiltered(ref m) => m,
};
match metadata.location_url {
// https://html.spec.whatwg.org/multipage/#process-a-navigate-fetch
// Step 7-4.
Some(Ok(ref location_url))
if matches!(location_url.scheme(), "http" | "https") =>
{
if self.request_builder.url_list.is_empty() {
self.request_builder
.url_list
.push(self.request_builder.url.clone());
}
self.request_builder
.url_list
.push(metadata.final_url.clone());
self.request_builder.referrer = metadata
.referrer
.clone()
.map(Referrer::ReferrerUrl)
.unwrap_or(Referrer::NoReferrer);
self.request_builder.referrer_policy = metadata.referrer_policy;
let headers = if let Some(ref headers) = metadata.headers {
headers.clone().into_inner()
} else {
HeaderMap::new()
};
self.res_init = Some(ResponseInit {
url: metadata.final_url.clone(),
location_url: metadata.location_url.clone(),
headers,
referrer: metadata.referrer.clone(),
status_code: metadata
.status
.try_code()
.map(|code| code.as_u16())
.unwrap_or(200),
});
// XXXManishearth we don't have the cancel_chan anymore and
// can't use it here.
//
// Ideally the Fetch code would handle manual redirects on its own
self.initiate_fetch(None);
},
_ => {
// Response should be processed by script thread.
self.should_send = true;
self.send(FetchResponseMsg::ProcessResponse(
request_id,
Ok(res_metadata),
));
},
};
},
Err(e) => {
self.should_send = true;
self.send(FetchResponseMsg::ProcessResponse(request_id, Err(e)))
},
};
}
fn send(&mut self, msg: FetchResponseMsg) {
if self.should_send {
if let Err(e) = self.sender.send((self.pipeline_id, msg)) {
warn!(
"Failed to forward network message to pipeline {:?}: {:?}",
self.pipeline_id, e
);
}
}
}
}

View file

@ -150,7 +150,6 @@ mod from_script {
},
Self::ScheduleBroadcast(..) => target!("ScheduleBroadcast"),
Self::ForwardToEmbedder(msg) => msg.log_target(),
Self::InitiateNavigateRequest(..) => target!("InitiateNavigateRequest"),
Self::BroadcastStorageEvent(..) => target!("BroadcastStorageEvent"),
Self::ChangeRunningAnimationsState(..) => target!("ChangeRunningAnimationsState"),
Self::CreateCanvasPaintThread(..) => target!("CreateCanvasPaintThread"),