Skip to main content

script/dom/servoparser/
prefetch.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::cell::{Cell, RefCell};
6use std::ops::Deref;
7
8use html5ever::buffer_queue::BufferQueue;
9use html5ever::tokenizer::states::RawKind;
10use html5ever::tokenizer::{
11    Tag, TagKind, Token, TokenSink, TokenSinkResult, Tokenizer as HtmlTokenizer,
12};
13use html5ever::{Attribute, LocalName, local_name};
14use js::jsapi::JSTracer;
15use markup5ever::TokenizerResult;
16use net_traits::request::{
17    CorsSettings, CredentialsMode, Destination, ParserMetadata, Referrer, RequestClient,
18};
19use net_traits::{CoreResourceMsg, FetchChannels, ReferrerPolicy, ResourceThreads};
20use servo_base::generic_channel::GenericSend;
21use servo_base::id::{PipelineId, WebViewId};
22use servo_url::ServoUrl;
23
24use crate::dom::bindings::reflector::DomGlobal;
25use crate::dom::bindings::trace::{CustomTraceable, JSTraceable};
26use crate::dom::document::Document;
27use crate::dom::html::htmlscriptelement::script_fetch_request;
28use crate::dom::processingoptions::determine_cors_settings_for_token;
29use crate::fetch::create_a_potential_cors_request;
30use crate::script_module::ScriptFetchOptions;
31
32#[derive(JSTraceable, MallocSizeOf)]
33#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
34pub(crate) struct Tokenizer {
35    #[ignore_malloc_size_of = "Defined in html5ever"]
36    inner: TraceableTokenizer,
37}
38
39struct TraceableTokenizer(HtmlTokenizer<PrefetchSink>);
40
41impl Deref for TraceableTokenizer {
42    type Target = HtmlTokenizer<PrefetchSink>;
43    fn deref(&self) -> &Self::Target {
44        &self.0
45    }
46}
47
48#[expect(unsafe_code)]
49unsafe impl JSTraceable for TraceableTokenizer {
50    unsafe fn trace(&self, trc: *mut JSTracer) {
51        unsafe { CustomTraceable::trace(&self.0, trc) }
52    }
53}
54
55#[expect(unsafe_code)]
56unsafe impl CustomTraceable for PrefetchSink {
57    unsafe fn trace(&self, trc: *mut JSTracer) {
58        unsafe { <Self as JSTraceable>::trace(self, trc) }
59    }
60}
61
62impl Tokenizer {
63    pub(crate) fn new(document: &Document) -> Self {
64        let global = document.global();
65        let sink = PrefetchSink {
66            pipeline_id: global.pipeline_id(),
67            webview_id: document.webview_id(),
68            base_url: RefCell::new(None),
69            document_url: document.url(),
70            referrer: global.get_referrer(),
71            referrer_policy: document.get_referrer_policy(),
72            resource_threads: document.loader().resource_threads().clone(),
73            // Initially we set prefetching to false, and only set it
74            // true after the first script tag, since that is what will
75            // block the main parser.
76            prefetching: Cell::new(false),
77            request_client: global.request_client(None),
78        };
79        let options = Default::default();
80        let inner = TraceableTokenizer(HtmlTokenizer::new(sink, options));
81        Tokenizer { inner }
82    }
83
84    pub(crate) fn feed(&self, input: &BufferQueue) {
85        while let TokenizerResult::Script(PrefetchHandle) = self.inner.feed(input) {}
86    }
87}
88
89#[derive(JSTraceable)]
90struct PrefetchSink {
91    #[no_trace]
92    pipeline_id: PipelineId,
93    #[no_trace]
94    webview_id: WebViewId,
95    #[no_trace]
96    document_url: ServoUrl,
97    #[no_trace]
98    base_url: RefCell<Option<ServoUrl>>,
99    #[no_trace]
100    referrer: Referrer,
101    #[no_trace]
102    referrer_policy: ReferrerPolicy,
103    #[no_trace]
104    resource_threads: ResourceThreads,
105    prefetching: Cell<bool>,
106    #[no_trace]
107    request_client: RequestClient,
108}
109
110/// The prefetch tokenizer produces trivial results
111#[derive(Clone, Copy, JSTraceable)]
112struct PrefetchHandle;
113
114impl TokenSink for PrefetchSink {
115    type Handle = PrefetchHandle;
116    fn process_token(&self, token: Token, _line_number: u64) -> TokenSinkResult<PrefetchHandle> {
117        let tag = match token {
118            Token::TagToken(ref tag) => tag,
119            _ => return TokenSinkResult::Continue,
120        };
121        match (tag.kind, &tag.name) {
122            (TagKind::StartTag, &local_name!("script")) if self.prefetching.get() => {
123                if let Some(url) = self.get_url(tag, local_name!("src")) {
124                    debug!("Prefetch script {}", url);
125                    let cors_setting = self.get_cors_settings(tag, local_name!("crossorigin"));
126                    let integrity_metadata = self
127                        .get_attr(tag, local_name!("integrity"))
128                        .map(|attr| String::from(&attr.value))
129                        .unwrap_or_default();
130                    let cryptographic_nonce = self
131                        .get_attr(tag, local_name!("nonce"))
132                        .map(|attr| String::from(&attr.value))
133                        .unwrap_or_default();
134                    let request = script_fetch_request(
135                        self.webview_id,
136                        url,
137                        cors_setting,
138                        ScriptFetchOptions {
139                            referrer_policy: self.referrer_policy,
140                            integrity_metadata,
141                            cryptographic_nonce,
142                            credentials_mode: CredentialsMode::CredentialsSameOrigin,
143                            parser_metadata: ParserMetadata::ParserInserted,
144                            render_blocking: false,
145                        },
146                        self.referrer.clone(),
147                    )
148                    .client(self.request_client.clone())
149                    .pipeline_id(Some(self.pipeline_id));
150                    let _ = self
151                        .resource_threads
152                        .send(CoreResourceMsg::Fetch(request, FetchChannels::Prefetch));
153                }
154                TokenSinkResult::RawData(RawKind::ScriptData)
155            },
156            (TagKind::StartTag, &local_name!("img")) if self.prefetching.get() => {
157                if let Some(url) = self.get_url(tag, local_name!("src")) {
158                    debug!("Prefetch {} {}", tag.name, url);
159                    let request = create_a_potential_cors_request(
160                        Some(self.webview_id),
161                        url,
162                        Destination::Image,
163                        self.get_cors_settings(tag, local_name!("crossorigin")),
164                        None,
165                        self.referrer.clone(),
166                    )
167                    .client(self.request_client.clone())
168                    .pipeline_id(Some(self.pipeline_id))
169                    .referrer_policy(self.get_referrer_policy(tag, local_name!("referrerpolicy")));
170
171                    let _ = self
172                        .resource_threads
173                        .send(CoreResourceMsg::Fetch(request, FetchChannels::Prefetch));
174                }
175                TokenSinkResult::Continue
176            },
177            (TagKind::StartTag, &local_name!("link")) if self.prefetching.get() => {
178                if let Some(rel) = self.get_attr(tag, local_name!("rel")) &&
179                    rel.value.eq_ignore_ascii_case("stylesheet") &&
180                    let Some(url) = self.get_url(tag, local_name!("href"))
181                {
182                    debug!("Prefetch {} {}", tag.name, url);
183                    let cors_setting = self.get_cors_settings(tag, local_name!("crossorigin"));
184                    let referrer_policy =
185                        self.get_referrer_policy(tag, local_name!("referrerpolicy"));
186                    let integrity_metadata = self
187                        .get_attr(tag, local_name!("integrity"))
188                        .map(|attr| String::from(&attr.value))
189                        .unwrap_or_default();
190
191                    // https://html.spec.whatwg.org/multipage/#default-fetch-and-process-the-linked-resource
192                    let request = create_a_potential_cors_request(
193                        Some(self.webview_id),
194                        url,
195                        Destination::Style,
196                        cors_setting,
197                        None,
198                        self.referrer.clone(),
199                    )
200                    .client(self.request_client.clone())
201                    .pipeline_id(Some(self.pipeline_id))
202                    .referrer_policy(referrer_policy)
203                    .integrity_metadata(integrity_metadata);
204
205                    let _ = self
206                        .resource_threads
207                        .send(CoreResourceMsg::Fetch(request, FetchChannels::Prefetch));
208                }
209                TokenSinkResult::Continue
210            },
211            (TagKind::StartTag, &local_name!("script")) => {
212                TokenSinkResult::RawData(RawKind::ScriptData)
213            },
214            (TagKind::EndTag, &local_name!("script")) => {
215                // After the first script tag, the main parser is blocked, so it's worth prefetching.
216                self.prefetching.set(true);
217                TokenSinkResult::Script(PrefetchHandle)
218            },
219            (TagKind::StartTag, &local_name!("base")) => {
220                if let Some(url) = self.get_url(tag, local_name!("href")) &&
221                    self.base_url.borrow().is_none()
222                {
223                    debug!("Setting base {}", url);
224                    *self.base_url.borrow_mut() = Some(url);
225                }
226                TokenSinkResult::Continue
227            },
228            _ => TokenSinkResult::Continue,
229        }
230    }
231}
232
233impl PrefetchSink {
234    fn get_attr<'a>(&'a self, tag: &'a Tag, name: LocalName) -> Option<&'a Attribute> {
235        tag.attrs.iter().find(|attr| attr.name.local == name)
236    }
237
238    fn get_url(&self, tag: &Tag, name: LocalName) -> Option<ServoUrl> {
239        let attr = self.get_attr(tag, name)?;
240        let base_url = self.base_url.borrow();
241        let base = base_url.as_ref().unwrap_or(&self.document_url);
242        ServoUrl::parse_with_base(Some(base), &attr.value).ok()
243    }
244
245    fn get_referrer_policy(&self, tag: &Tag, name: LocalName) -> ReferrerPolicy {
246        self.get_attr(tag, name)
247            .map(|attr| ReferrerPolicy::from(&*attr.value))
248            .unwrap_or(self.referrer_policy)
249    }
250
251    fn get_cors_settings(&self, tag: &Tag, name: LocalName) -> Option<CorsSettings> {
252        let attr = self.get_attr(tag, name)?;
253        determine_cors_settings_for_token(&attr.value)
254    }
255}