Skip to main content

script/
document_loader.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
5//! Tracking of pending loads in a document.
6//!
7//! <https://html.spec.whatwg.org/multipage/#the-end>
8
9use std::collections::HashMap;
10
11use net_traits::request::RequestBuilder;
12use net_traits::{BoxedFetchCallback, ResourceThreads, fetch_async};
13use script_bindings::cell::DomRefCell;
14use script_bindings::script_runtime::{during_gc_collection, runtime_is_alive};
15use servo_url::ServoUrl;
16
17use crate::dom::bindings::root::Dom;
18use crate::dom::document::Document;
19use crate::fetch::FetchCanceller;
20
21#[derive(Clone, Debug, Eq, Hash, JSTraceable, MallocSizeOf, PartialEq)]
22pub(crate) enum LoadType {
23    Image(#[no_trace] ServoUrl),
24    Script(#[no_trace] ServoUrl),
25    Subframe(#[no_trace] ServoUrl),
26    Stylesheet(#[no_trace] ServoUrl),
27    PageSource(#[no_trace] ServoUrl),
28    Media,
29}
30
31/// Canary value ensuring that manually added blocking loads (ie. ones that weren't
32/// created via DocumentLoader::fetch_async) are always removed by the time
33/// that the owner is destroyed.
34#[derive(JSTraceable, MallocSizeOf)]
35#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
36pub(crate) struct LoadBlocker {
37    /// The document whose load event is blocked by this object existing.
38    doc: Dom<Document>,
39    /// The load that is blocking the document's load event.
40    load: Option<LoadType>,
41}
42
43impl LoadBlocker {
44    /// Mark the document's load event as blocked on this new load.
45    pub(crate) fn new(doc: &Document, load: LoadType) -> LoadBlocker {
46        doc.loader_mut().add_blocking_load(load.clone());
47        LoadBlocker {
48            doc: Dom::from_ref(doc),
49            load: Some(load),
50        }
51    }
52
53    /// Remove this load from the associated document's list of blocking loads.
54    pub(crate) fn terminate(
55        blocker: &DomRefCell<Option<LoadBlocker>>,
56        cx: &mut js::context::JSContext,
57    ) {
58        let Some(load) = blocker
59            .safe_borrow_mut(cx.no_gc())
60            .as_mut()
61            .and_then(|blocker| blocker.load.take())
62        else {
63            return;
64        };
65
66        if let Some(blocker) = blocker.borrow().as_ref() {
67            blocker.doc.finish_load(load, cx);
68        }
69
70        *blocker.safe_borrow_mut(cx.no_gc()) = None;
71    }
72}
73
74impl Drop for LoadBlocker {
75    fn drop(&mut self) {
76        // We need to check here if the whole runtime is alive, otherwise
77        // we interact with a document that is also being dropped. That
78        // would panic, since we should no longer schedule a task for
79        // a dropped runtime. Therefore, we should only run the drop logic
80        // in case this element is dropped, but its containing document
81        // is still alive.
82        //
83        // This destructor can also run from during GC (see #46207), which can lead to
84        // accessing already freed memory if we run `finish_load_for_dropped_blocker`.
85        if runtime_is_alive() &&
86            !during_gc_collection() &&
87            let Some(load) = self.load.take()
88        {
89            self.doc.finish_load_for_dropped_blocker(load);
90        }
91    }
92}
93
94#[derive(JSTraceable, MallocSizeOf)]
95pub(crate) struct DocumentLoader {
96    #[no_trace]
97    resource_threads: ResourceThreads,
98    /// A map from [`LoadType`] to the number of blocking loads. When a particular [`LoadType`]
99    /// reaches zero, it is removed from the map and no longer blocks the load.
100    blocking_loads: HashMap<LoadType, u32>,
101    events_inhibited: bool,
102    cancellers: Vec<FetchCanceller>,
103}
104
105impl DocumentLoader {
106    pub(crate) fn new(existing: &DocumentLoader) -> DocumentLoader {
107        DocumentLoader::new_with_threads(existing.resource_threads.clone(), None)
108    }
109
110    pub(crate) fn new_with_threads(
111        resource_threads: ResourceThreads,
112        initial_load: Option<ServoUrl>,
113    ) -> DocumentLoader {
114        debug!("Initial blocking load {:?}.", initial_load);
115
116        let initial_loads = initial_load
117            .into_iter()
118            .map(|url| (LoadType::PageSource(url), 1))
119            .collect();
120
121        DocumentLoader {
122            resource_threads,
123            blocking_loads: initial_loads,
124            events_inhibited: false,
125            cancellers: Vec::new(),
126        }
127    }
128
129    /// <https://fetch.spec.whatwg.org/#concept-fetch-group-terminate>
130    pub(crate) fn cancel_all_loads(&mut self) -> Vec<FetchCanceller> {
131        self.cancellers.drain(..).collect()
132    }
133
134    /// Add a load to the list of blocking loads.
135    fn add_blocking_load(&mut self, load: LoadType) {
136        debug!(
137            "Adding blocking load {:?} ({}).",
138            load,
139            self.blocking_loads.len()
140        );
141        self.blocking_loads
142            .entry(load)
143            .and_modify(|load_number| *load_number += 1)
144            .or_insert(1);
145    }
146
147    /// Initiate a new fetch given a response callback.
148    pub(crate) fn fetch_async_with_callback(
149        &mut self,
150        load: LoadType,
151        request: RequestBuilder,
152        callback: BoxedFetchCallback,
153    ) {
154        self.add_blocking_load(load);
155        self.fetch_async_background(request, callback);
156    }
157
158    /// Initiate a new fetch that does not block the document load event.
159    pub(crate) fn fetch_async_background(
160        &mut self,
161        request: RequestBuilder,
162        callback: BoxedFetchCallback,
163    ) {
164        self.cancellers.push(FetchCanceller::new(
165            request.id,
166            request.keep_alive,
167            self.resource_threads.core_thread.clone(),
168        ));
169        fetch_async(&self.resource_threads.core_thread, request, None, callback);
170    }
171
172    /// Mark an in-progress network request complete.
173    pub(crate) fn finish_load(&mut self, load: &LoadType) {
174        debug!(
175            "Removing blocking load {:?} ({}).",
176            load,
177            self.blocking_loads.len()
178        );
179
180        let Some(entry) = self.blocking_loads.get_mut(load) else {
181            warn!("unknown completed load {load:?}");
182            return;
183        };
184
185        *entry = entry.saturating_sub(1);
186        if *entry == 0 {
187            self.blocking_loads.remove(load);
188        }
189    }
190
191    pub(crate) fn is_blocked(&self) -> bool {
192        // TODO: Ensure that we report blocked if parsing is still ongoing.
193        !self.blocking_loads.is_empty()
194    }
195
196    pub(crate) fn is_only_blocked_by_iframes(&self) -> bool {
197        self.blocking_loads
198            .keys()
199            .all(|load| matches!(*load, LoadType::Subframe(_)))
200    }
201
202    pub(crate) fn inhibit_events(&mut self) {
203        self.events_inhibited = true;
204    }
205
206    pub(crate) fn events_inhibited(&self) -> bool {
207        self.events_inhibited
208    }
209
210    pub(crate) fn resource_threads(&self) -> &ResourceThreads {
211        &self.resource_threads
212    }
213}