| /* 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 script thread is the thread that owns the DOM in memory, runs JavaScript, and triggers |
| //! layout. It's in charge of processing events for all same-origin pages in a frame |
| //! tree, and manages the entire lifetime of pages in the frame tree from initial request to |
| //! teardown. |
| //! |
| //! Page loads follow a two-step process. When a request for a new page load is received, the |
| //! network request is initiated and the relevant data pertaining to the new page is stashed. |
| //! While the non-blocking request is ongoing, the script thread is free to process further events, |
| //! noting when they pertain to ongoing loads (such as resizes/viewport adjustments). When the |
| //! initial response is received for an ongoing load, the second phase starts - the frame tree |
| //! entry is created, along with the Window and Document objects, and the appropriate parser |
| //! takes over the response body. Once parsing is complete, the document lifecycle for loading |
| //! a page runs its course and the script thread returns to processing events in the main event |
| //! loop. |
| |
| use std::cell::{Cell, RefCell}; |
| use std::collections::{HashMap, HashSet}; |
| use std::default::Default; |
| use std::option::Option; |
| use std::rc::Rc; |
| use std::result::Result; |
| use std::sync::Arc; |
| use std::sync::atomic::{AtomicBool, Ordering}; |
| use std::thread::{self, JoinHandle}; |
| use std::time::{Duration, Instant, SystemTime}; |
| |
| use background_hang_monitor_api::{ |
| BackgroundHangMonitor, BackgroundHangMonitorExitSignal, HangAnnotation, MonitoredComponentId, |
| MonitoredComponentType, |
| }; |
| use base::cross_process_instant::CrossProcessInstant; |
| use base::id::{BrowsingContextId, HistoryStateId, PipelineId, PipelineNamespace, WebViewId}; |
| use canvas_traits::webgl::WebGLPipeline; |
| use chrono::{DateTime, Local}; |
| use compositing_traits::{CrossProcessCompositorApi, PipelineExitSource}; |
| use constellation_traits::{ |
| JsEvalResult, LoadData, LoadOrigin, NavigationHistoryBehavior, ScriptToConstellationChan, |
| ScriptToConstellationMessage, StructuredSerializedData, WindowSizeType, |
| }; |
| use crossbeam_channel::unbounded; |
| use data_url::mime::Mime; |
| use devtools_traits::{ |
| CSSError, DevtoolScriptControlMsg, DevtoolsPageInfo, NavigationState, |
| ScriptToDevtoolsControlMsg, WorkerId, |
| }; |
| use embedder_traits::user_content_manager::UserContentManager; |
| use embedder_traits::{ |
| FocusSequenceNumber, InputEvent, JavaScriptEvaluationError, JavaScriptEvaluationId, |
| MediaSessionActionType, MouseButton, MouseButtonAction, MouseButtonEvent, Theme, |
| ViewportDetails, WebDriverScriptCommand, |
| }; |
| use euclid::Point2D; |
| use euclid::default::Rect; |
| use fnv::FnvHashMap; |
| use fonts::{FontContext, SystemFontServiceProxy}; |
| use headers::{HeaderMapExt, LastModified, ReferrerPolicy as ReferrerPolicyHeader}; |
| use http::header::REFRESH; |
| use hyper_serde::Serde; |
| use ipc_channel::ipc; |
| use ipc_channel::router::ROUTER; |
| use js::glue::GetWindowProxyClass; |
| use js::jsapi::{ |
| JS_AddInterruptCallback, JSContext as UnsafeJSContext, JSTracer, SetWindowProxyClass, |
| }; |
| use js::jsval::UndefinedValue; |
| use js::rust::ParentRuntime; |
| use layout_api::{LayoutConfig, LayoutFactory, RestyleReason, ScriptThreadFactory}; |
| use media::WindowGLContext; |
| use metrics::MAX_TASK_NS; |
| use net_traits::image_cache::{ImageCache, ImageCacheResponseMessage}; |
| use net_traits::request::{Referrer, RequestId}; |
| use net_traits::response::ResponseInit; |
| use net_traits::storage_thread::StorageType; |
| use net_traits::{ |
| FetchMetadata, FetchResponseListener, FetchResponseMsg, Metadata, NetworkError, |
| ResourceFetchTiming, ResourceThreads, ResourceTimingType, |
| }; |
| use percent_encoding::percent_decode; |
| use profile_traits::mem::{ProcessReports, ReportsChan, perform_memory_report}; |
| use profile_traits::time::ProfilerCategory; |
| use profile_traits::time_profile; |
| use script_traits::{ |
| ConstellationInputEvent, DiscardBrowsingContext, DocumentActivity, InitialScriptState, |
| NewLayoutInfo, Painter, ProgressiveWebMetricType, ScriptThreadMessage, UpdatePipelineIdReason, |
| }; |
| use servo_config::{opts, prefs}; |
| use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl}; |
| use style::thread_state::{self, ThreadState}; |
| use stylo_atoms::Atom; |
| use timers::{TimerEventRequest, TimerId, TimerScheduler}; |
| use url::Position; |
| #[cfg(feature = "webgpu")] |
| use webgpu_traits::{WebGPUDevice, WebGPUMsg}; |
| use webrender_api::ExternalScrollId; |
| use webrender_api::units::{DevicePixel, LayoutVector2D}; |
| |
| use crate::document_collection::DocumentCollection; |
| use crate::document_loader::DocumentLoader; |
| use crate::dom::bindings::cell::DomRefCell; |
| use crate::dom::bindings::codegen::Bindings::DocumentBinding::{ |
| DocumentMethods, DocumentReadyState, |
| }; |
| use crate::dom::bindings::codegen::Bindings::NavigatorBinding::NavigatorMethods; |
| use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods; |
| use crate::dom::bindings::conversions::{ |
| ConversionResult, SafeFromJSValConvertible, StringificationBehavior, |
| }; |
| use crate::dom::bindings::inheritance::Castable; |
| use crate::dom::bindings::refcounted::Trusted; |
| use crate::dom::bindings::reflector::DomGlobal; |
| use crate::dom::bindings::root::{Dom, DomRoot}; |
| use crate::dom::bindings::settings_stack::AutoEntryScript; |
| use crate::dom::bindings::str::DOMString; |
| use crate::dom::bindings::trace::{HashMapTracedValues, JSTraceable}; |
| use crate::dom::csp::{CspReporting, GlobalCspReporting, Violation}; |
| use crate::dom::customelementregistry::{ |
| CallbackReaction, CustomElementDefinition, CustomElementReactionStack, |
| }; |
| use crate::dom::document::{ |
| Document, DocumentSource, FocusInitiator, HasBrowsingContext, IsHTMLDocument, |
| }; |
| use crate::dom::element::Element; |
| use crate::dom::globalscope::GlobalScope; |
| use crate::dom::html::htmliframeelement::HTMLIFrameElement; |
| use crate::dom::html::htmlslotelement::HTMLSlotElement; |
| use crate::dom::mutationobserver::MutationObserver; |
| use crate::dom::node::NodeTraits; |
| use crate::dom::servoparser::{ParserContext, ServoParser}; |
| use crate::dom::types::DebuggerGlobalScope; |
| #[cfg(feature = "webgpu")] |
| use crate::dom::webgpu::identityhub::IdentityHub; |
| use crate::dom::window::Window; |
| use crate::dom::windowproxy::{CreatorBrowsingContextInfo, WindowProxy}; |
| use crate::dom::worklet::WorkletThreadPool; |
| use crate::dom::workletglobalscope::WorkletGlobalScopeInit; |
| use crate::fetch::FetchCanceller; |
| use crate::messaging::{ |
| CommonScriptMsg, MainThreadScriptMsg, MixedMessage, ScriptEventLoopSender, |
| ScriptThreadReceivers, ScriptThreadSenders, |
| }; |
| use crate::microtask::{Microtask, MicrotaskQueue}; |
| use crate::mime::{APPLICATION, MimeExt, TEXT, XML}; |
| use crate::navigation::{InProgressLoad, NavigationListener}; |
| use crate::realms::enter_realm; |
| use crate::script_module::ScriptFetchOptions; |
| use crate::script_runtime::{ |
| CanGc, IntroductionType, JSContext, JSContextHelper, Runtime, ScriptThreadEventCategory, |
| ThreadSafeJSContext, |
| }; |
| use crate::task_queue::TaskQueue; |
| use crate::task_source::{SendableTaskSource, TaskSourceName}; |
| use crate::webdriver_handlers::jsval_to_webdriver; |
| use crate::{devtools, webdriver_handlers}; |
| |
| thread_local!(static SCRIPT_THREAD_ROOT: Cell<Option<*const ScriptThread>> = const { Cell::new(None) }); |
| |
| fn with_optional_script_thread<R>(f: impl FnOnce(Option<&ScriptThread>) -> R) -> R { |
| SCRIPT_THREAD_ROOT.with(|root| { |
| f(root |
| .get() |
| .and_then(|script_thread| unsafe { script_thread.as_ref() })) |
| }) |
| } |
| |
| pub(crate) fn with_script_thread<R: Default>(f: impl FnOnce(&ScriptThread) -> R) -> R { |
| with_optional_script_thread(|script_thread| script_thread.map(f).unwrap_or_default()) |
| } |
| |
| /// # Safety |
| /// |
| /// The `JSTracer` argument must point to a valid `JSTracer` in memory. In addition, |
| /// implementors of this method must ensure that all active objects are properly traced |
| /// or else the garbage collector may end up collecting objects that are still reachable. |
| pub(crate) unsafe fn trace_thread(tr: *mut JSTracer) { |
| with_script_thread(|script_thread| { |
| trace!("tracing fields of ScriptThread"); |
| unsafe { script_thread.trace(tr) }; |
| }) |
| } |
| |
| // We borrow the incomplete parser contexts mutably during parsing, |
| // which is fine except that parsing can trigger evaluation, |
| // which can trigger GC, and so we can end up tracing the script |
| // thread during parsing. For this reason, we don't trace the |
| // incomplete parser contexts during GC. |
| pub(crate) struct IncompleteParserContexts(RefCell<Vec<(PipelineId, ParserContext)>>); |
| |
| unsafe_no_jsmanaged_fields!(TaskQueue<MainThreadScriptMsg>); |
| |
| type NodeIdSet = HashSet<String>; |
| |
| #[derive(JSTraceable)] |
| // ScriptThread instances are rooted on creation, so this is okay |
| #[cfg_attr(crown, allow(crown::unrooted_must_root))] |
| pub struct ScriptThread { |
| /// <https://html.spec.whatwg.org/multipage/#last-render-opportunity-time> |
| last_render_opportunity_time: Cell<Option<Instant>>, |
| |
| /// The documents for pipelines managed by this thread |
| documents: DomRefCell<DocumentCollection>, |
| /// The window proxies known by this thread |
| /// TODO: this map grows, but never shrinks. Issue #15258. |
| window_proxies: DomRefCell<HashMapTracedValues<BrowsingContextId, Dom<WindowProxy>>>, |
| /// A list of data pertaining to loads that have not yet received a network response |
| incomplete_loads: DomRefCell<Vec<InProgressLoad>>, |
| /// A vector containing parser contexts which have not yet been fully processed |
| incomplete_parser_contexts: IncompleteParserContexts, |
| /// Image cache for this script thread. |
| #[no_trace] |
| image_cache: Arc<dyn ImageCache>, |
| |
| /// A [`ScriptThreadReceivers`] holding all of the incoming `Receiver`s for messages |
| /// to this [`ScriptThread`]. |
| receivers: ScriptThreadReceivers, |
| |
| /// A [`ScriptThreadSenders`] that holds all outgoing sending channels necessary to communicate |
| /// to other parts of Servo. |
| senders: ScriptThreadSenders, |
| |
| /// A handle to the resource thread. This is an `Arc` to avoid running out of file descriptors if |
| /// there are many iframes. |
| #[no_trace] |
| resource_threads: ResourceThreads, |
| |
| /// A queue of tasks to be executed in this script-thread. |
| task_queue: TaskQueue<MainThreadScriptMsg>, |
| |
| /// The dedicated means of communication with the background-hang-monitor for this script-thread. |
| #[no_trace] |
| background_hang_monitor: Box<dyn BackgroundHangMonitor>, |
| /// A flag set to `true` by the BHM on exit, and checked from within the interrupt handler. |
| closing: Arc<AtomicBool>, |
| |
| /// A [`TimerScheduler`] used to schedule timers for this [`ScriptThread`]. Timers are handled |
| /// in the [`ScriptThread`] event loop. |
| #[no_trace] |
| timer_scheduler: RefCell<TimerScheduler>, |
| |
| /// A proxy to the `SystemFontService` to use for accessing system font lists. |
| #[no_trace] |
| system_font_service: Arc<SystemFontServiceProxy>, |
| |
| /// The JavaScript runtime. |
| js_runtime: Rc<Runtime>, |
| |
| /// List of pipelines that have been owned and closed by this script thread. |
| #[no_trace] |
| closed_pipelines: DomRefCell<HashSet<PipelineId>>, |
| |
| /// <https://html.spec.whatwg.org/multipage/#microtask-queue> |
| microtask_queue: Rc<MicrotaskQueue>, |
| |
| /// Microtask Queue for adding support for mutation observer microtasks |
| mutation_observer_microtask_queued: Cell<bool>, |
| |
| /// The unit of related similar-origin browsing contexts' list of MutationObserver objects |
| mutation_observers: DomRefCell<Vec<Dom<MutationObserver>>>, |
| |
| /// <https://dom.spec.whatwg.org/#signal-slot-list> |
| signal_slots: DomRefCell<Vec<Dom<HTMLSlotElement>>>, |
| |
| /// A handle to the WebGL thread |
| #[no_trace] |
| webgl_chan: Option<WebGLPipeline>, |
| |
| /// The WebXR device registry |
| #[no_trace] |
| #[cfg(feature = "webxr")] |
| webxr_registry: Option<webxr_api::Registry>, |
| |
| /// The worklet thread pool |
| worklet_thread_pool: DomRefCell<Option<Rc<WorkletThreadPool>>>, |
| |
| /// A list of pipelines containing documents that finished loading all their blocking |
| /// resources during a turn of the event loop. |
| docs_with_no_blocking_loads: DomRefCell<HashSet<Dom<Document>>>, |
| |
| /// <https://html.spec.whatwg.org/multipage/#custom-element-reactions-stack> |
| custom_element_reaction_stack: Rc<CustomElementReactionStack>, |
| |
| /// Cross-process access to the compositor's API. |
| #[no_trace] |
| compositor_api: CrossProcessCompositorApi, |
| |
| /// Periodically print out on which events script threads spend their processing time. |
| profile_script_events: bool, |
| |
| /// Print Progressive Web Metrics to console. |
| print_pwm: bool, |
| |
| /// Unminify Javascript. |
| unminify_js: bool, |
| |
| /// Directory with stored unminified scripts |
| local_script_source: Option<String>, |
| |
| /// Unminify Css. |
| unminify_css: bool, |
| |
| /// User content manager |
| #[no_trace] |
| user_content_manager: UserContentManager, |
| |
| /// Application window's GL Context for Media player |
| #[no_trace] |
| player_context: WindowGLContext, |
| |
| /// A map from pipelines to all owned nodes ever created in this script thread |
| #[no_trace] |
| pipeline_to_node_ids: DomRefCell<HashMap<PipelineId, NodeIdSet>>, |
| |
| /// Code is running as a consequence of a user interaction |
| is_user_interacting: Cell<bool>, |
| |
| /// Identity manager for WebGPU resources |
| #[no_trace] |
| #[cfg(feature = "webgpu")] |
| gpu_id_hub: Arc<IdentityHub>, |
| |
| // Secure context |
| inherited_secure_context: Option<bool>, |
| |
| /// A factory for making new layouts. This allows layout to depend on script. |
| #[no_trace] |
| layout_factory: Arc<dyn LayoutFactory>, |
| |
| /// The screen coordinates where the primary mouse button was pressed. |
| #[no_trace] |
| relative_mouse_down_point: Cell<Point2D<f32, DevicePixel>>, |
| |
| /// The [`TimerId`] of a ScriptThread-scheduled "update the rendering" call, if any. |
| /// The ScriptThread schedules calls to "update the rendering," but the renderer can |
| /// also do this when animating. Renderer-based calls always take precedence. |
| #[no_trace] |
| scheduled_update_the_rendering: RefCell<Option<TimerId>>, |
| |
| /// Whether an animation tick or ScriptThread-triggered rendering update is pending. This might |
| /// either be because the Servo renderer is managing animations and the [`ScriptThread`] has |
| /// received a [`ScriptThreadMessage::TickAllAnimations`] message, because the [`ScriptThread`] |
| /// itself is managing animations the the timer fired triggering a [`ScriptThread`]-based |
| /// animation tick, or if there are no animations running and the [`ScriptThread`] has noticed a |
| /// change that requires a rendering update. |
| needs_rendering_update: Arc<AtomicBool>, |
| |
| debugger_global: Dom<DebuggerGlobalScope>, |
| |
| /// A list of URLs that can access privileged internal APIs. |
| #[no_trace] |
| privileged_urls: Vec<ServoUrl>, |
| } |
| |
| struct BHMExitSignal { |
| closing: Arc<AtomicBool>, |
| js_context: ThreadSafeJSContext, |
| } |
| |
| impl BackgroundHangMonitorExitSignal for BHMExitSignal { |
| fn signal_to_exit(&self) { |
| self.closing.store(true, Ordering::SeqCst); |
| self.js_context.request_interrupt_callback(); |
| } |
| } |
| |
| #[allow(unsafe_code)] |
| unsafe extern "C" fn interrupt_callback(_cx: *mut UnsafeJSContext) -> bool { |
| let res = ScriptThread::can_continue_running(); |
| if !res { |
| ScriptThread::prepare_for_shutdown(); |
| } |
| res |
| } |
| |
| /// In the event of thread panic, all data on the stack runs its destructor. However, there |
| /// are no reachable, owning pointers to the DOM memory, so it never gets freed by default |
| /// when the script thread fails. The ScriptMemoryFailsafe uses the destructor bomb pattern |
| /// to forcibly tear down the JS realms for pages associated with the failing ScriptThread. |
| struct ScriptMemoryFailsafe<'a> { |
| owner: Option<&'a ScriptThread>, |
| } |
| |
| impl<'a> ScriptMemoryFailsafe<'a> { |
| fn neuter(&mut self) { |
| self.owner = None; |
| } |
| |
| fn new(owner: &'a ScriptThread) -> ScriptMemoryFailsafe<'a> { |
| ScriptMemoryFailsafe { owner: Some(owner) } |
| } |
| } |
| |
| impl Drop for ScriptMemoryFailsafe<'_> { |
| #[cfg_attr(crown, allow(crown::unrooted_must_root))] |
| fn drop(&mut self) { |
| if let Some(owner) = self.owner { |
| for (_, document) in owner.documents.borrow().iter() { |
| document.window().clear_js_runtime_for_script_deallocation(); |
| } |
| } |
| } |
| } |
| |
| impl ScriptThreadFactory for ScriptThread { |
| fn create( |
| state: InitialScriptState, |
| layout_factory: Arc<dyn LayoutFactory>, |
| system_font_service: Arc<SystemFontServiceProxy>, |
| load_data: LoadData, |
| ) -> JoinHandle<()> { |
| thread::Builder::new() |
| .name(format!("Script{:?}", state.id)) |
| .spawn(move || { |
| thread_state::initialize(ThreadState::SCRIPT | ThreadState::LAYOUT); |
| PipelineNamespace::install(state.pipeline_namespace_id); |
| WebViewId::install(state.webview_id); |
| let memory_profiler_sender = state.memory_profiler_sender.clone(); |
| |
| let in_progress_load = InProgressLoad::new( |
| state.id, |
| state.browsing_context_id, |
| state.webview_id, |
| state.parent_info, |
| state.opener, |
| state.viewport_details, |
| state.theme, |
| MutableOrigin::new(load_data.url.origin()), |
| load_data, |
| ); |
| let reporter_name = format!("script-reporter-{:?}", state.id); |
| let script_thread = ScriptThread::new(state, layout_factory, system_font_service); |
| |
| SCRIPT_THREAD_ROOT.with(|root| { |
| root.set(Some(&script_thread as *const _)); |
| }); |
| |
| let mut failsafe = ScriptMemoryFailsafe::new(&script_thread); |
| |
| script_thread.pre_page_load(in_progress_load); |
| |
| memory_profiler_sender.run_with_memory_reporting( |
| || { |
| script_thread.start(CanGc::note()); |
| |
| let _ = script_thread |
| .senders |
| .content_process_shutdown_sender |
| .send(()); |
| }, |
| reporter_name, |
| ScriptEventLoopSender::MainThread(script_thread.senders.self_sender.clone()), |
| CommonScriptMsg::CollectReports, |
| ); |
| |
| // This must always be the very last operation performed before the thread completes |
| failsafe.neuter(); |
| }) |
| .expect("Thread spawning failed") |
| } |
| } |
| |
| impl ScriptThread { |
| pub(crate) fn runtime_handle() -> ParentRuntime { |
| with_optional_script_thread(|script_thread| { |
| script_thread.unwrap().js_runtime.prepare_for_new_child() |
| }) |
| } |
| |
| pub(crate) fn can_continue_running() -> bool { |
| with_script_thread(|script_thread| script_thread.can_continue_running_inner()) |
| } |
| |
| pub(crate) fn prepare_for_shutdown() { |
| with_script_thread(|script_thread| { |
| script_thread.prepare_for_shutdown_inner(); |
| }) |
| } |
| |
| pub(crate) fn set_mutation_observer_microtask_queued(value: bool) { |
| with_script_thread(|script_thread| { |
| script_thread.mutation_observer_microtask_queued.set(value); |
| }) |
| } |
| |
| pub(crate) fn is_mutation_observer_microtask_queued() -> bool { |
| with_script_thread(|script_thread| script_thread.mutation_observer_microtask_queued.get()) |
| } |
| |
| pub(crate) fn add_mutation_observer(observer: &MutationObserver) { |
| with_script_thread(|script_thread| { |
| script_thread |
| .mutation_observers |
| .borrow_mut() |
| .push(Dom::from_ref(observer)); |
| }) |
| } |
| |
| pub(crate) fn get_mutation_observers() -> Vec<DomRoot<MutationObserver>> { |
| with_script_thread(|script_thread| { |
| script_thread |
| .mutation_observers |
| .borrow() |
| .iter() |
| .map(|o| DomRoot::from_ref(&**o)) |
| .collect() |
| }) |
| } |
| |
| pub(crate) fn add_signal_slot(observer: &HTMLSlotElement) { |
| with_script_thread(|script_thread| { |
| script_thread |
| .signal_slots |
| .borrow_mut() |
| .push(Dom::from_ref(observer)); |
| }) |
| } |
| |
| pub(crate) fn take_signal_slots() -> Vec<DomRoot<HTMLSlotElement>> { |
| with_script_thread(|script_thread| { |
| script_thread |
| .signal_slots |
| .take() |
| .into_iter() |
| .inspect(|slot| { |
| slot.remove_from_signal_slots(); |
| }) |
| .map(|slot| slot.as_rooted()) |
| .collect() |
| }) |
| } |
| |
| pub(crate) fn mark_document_with_no_blocked_loads(doc: &Document) { |
| with_script_thread(|script_thread| { |
| script_thread |
| .docs_with_no_blocking_loads |
| .borrow_mut() |
| .insert(Dom::from_ref(doc)); |
| }) |
| } |
| |
| pub(crate) fn page_headers_available( |
| id: &PipelineId, |
| metadata: Option<Metadata>, |
| can_gc: CanGc, |
| ) -> Option<DomRoot<ServoParser>> { |
| with_script_thread(|script_thread| { |
| script_thread.handle_page_headers_available(id, metadata, can_gc) |
| }) |
| } |
| |
| /// Process a single event as if it were the next event |
| /// in the queue for this window event-loop. |
| /// Returns a boolean indicating whether further events should be processed. |
| pub(crate) fn process_event(msg: CommonScriptMsg) -> bool { |
| with_script_thread(|script_thread| { |
| if !script_thread.can_continue_running_inner() { |
| return false; |
| } |
| script_thread.handle_msg_from_script(MainThreadScriptMsg::Common(msg)); |
| true |
| }) |
| } |
| |
| /// Schedule a [`TimerEventRequest`] on this [`ScriptThread`]'s [`TimerScheduler`]. |
| pub(crate) fn schedule_timer(&self, request: TimerEventRequest) -> TimerId { |
| self.timer_scheduler.borrow_mut().schedule_timer(request) |
| } |
| |
| /// Cancel a the [`TimerEventRequest`] for the given [`TimerId`] on this |
| /// [`ScriptThread`]'s [`TimerScheduler`]. |
| pub(crate) fn cancel_timer(&self, timer_id: TimerId) { |
| self.timer_scheduler.borrow_mut().cancel_timer(timer_id) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#await-a-stable-state |
| pub(crate) fn await_stable_state(task: Microtask) { |
| with_script_thread(|script_thread| { |
| script_thread |
| .microtask_queue |
| .enqueue(task, script_thread.get_cx()); |
| }); |
| } |
| |
| /// Check that two origins are "similar enough", |
| /// for now only used to prevent cross-origin JS url evaluation. |
| /// |
| /// <https://github.com/whatwg/html/issues/2591> |
| pub(crate) fn check_load_origin(source: &LoadOrigin, target: &ImmutableOrigin) -> bool { |
| match (source, target) { |
| (LoadOrigin::Constellation, _) | (LoadOrigin::WebDriver, _) => { |
| // Always allow loads initiated by the constellation or webdriver. |
| true |
| }, |
| (_, ImmutableOrigin::Opaque(_)) => { |
| // If the target is opaque, allow. |
| // This covers newly created about:blank auxiliaries, and iframe with no src. |
| // TODO: https://github.com/servo/servo/issues/22879 |
| true |
| }, |
| (LoadOrigin::Script(source_origin), _) => source_origin == target, |
| } |
| } |
| |
| /// Inform the `ScriptThread` that it should make a call to |
| /// [`ScriptThread::update_the_rendering`] as soon as possible, as the rendering |
| /// update timer has fired or the renderer has asked us for a new rendering update. |
| pub(crate) fn set_needs_rendering_update(&self) { |
| self.needs_rendering_update.store(true, Ordering::Relaxed); |
| } |
| |
| /// Step 13 of <https://html.spec.whatwg.org/multipage/#navigate> |
| pub(crate) fn navigate( |
| pipeline_id: PipelineId, |
| mut load_data: LoadData, |
| history_handling: NavigationHistoryBehavior, |
| ) { |
| with_script_thread(|script_thread| { |
| let is_javascript = load_data.url.scheme() == "javascript"; |
| // If resource is a request whose url's scheme is "javascript" |
| // https://html.spec.whatwg.org/multipage/#navigate-to-a-javascript:-url |
| if is_javascript { |
| let window = match script_thread.documents.borrow().find_window(pipeline_id) { |
| None => return, |
| Some(window) => window, |
| }; |
| let global = window.as_global_scope(); |
| let trusted_global = Trusted::new(global); |
| let sender = script_thread |
| .senders |
| .pipeline_to_constellation_sender |
| .clone(); |
| let task = task!(navigate_javascript: move || { |
| // Important re security. See https://github.com/servo/servo/issues/23373 |
| if trusted_global.root().is::<Window>() { |
| let global = &trusted_global.root(); |
| if Self::navigate_to_javascript_url(global, global, &mut load_data, None, CanGc::note()) { |
| sender |
| .send((pipeline_id, ScriptToConstellationMessage::LoadUrl(load_data, history_handling))) |
| .unwrap(); |
| } |
| } |
| }); |
| // Step 19 of <https://html.spec.whatwg.org/multipage/#navigate> |
| global |
| .task_manager() |
| .dom_manipulation_task_source() |
| .queue(task); |
| } else { |
| script_thread |
| .senders |
| .pipeline_to_constellation_sender |
| .send(( |
| pipeline_id, |
| ScriptToConstellationMessage::LoadUrl(load_data, history_handling), |
| )) |
| .expect("Sending a LoadUrl message to the constellation failed"); |
| } |
| }); |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#navigate-to-a-javascript:-url> |
| pub(crate) fn navigate_to_javascript_url( |
| global: &GlobalScope, |
| containing_global: &GlobalScope, |
| load_data: &mut LoadData, |
| container: Option<&Element>, |
| can_gc: CanGc, |
| ) -> bool { |
| // Step 3. If initiatorOrigin is not same origin-domain with targetNavigable's active document's origin, then return. |
| // |
| // Important re security. See https://github.com/servo/servo/issues/23373 |
| if !Self::check_load_origin(&load_data.load_origin, &global.get_url().origin()) { |
| return false; |
| } |
| |
| // Step 5: If the result of should navigation request of type be blocked by |
| // Content Security Policy? given request and cspNavigationType is "Blocked", then return. [CSP] |
| if global |
| .get_csp_list() |
| .should_navigation_request_be_blocked(global, load_data, container, can_gc) |
| { |
| return false; |
| } |
| |
| // Step 6. Let newDocument be the result of evaluating a javascript: URL given targetNavigable, |
| // url, initiatorOrigin, and userInvolvement. |
| Self::eval_js_url(containing_global, load_data, can_gc); |
| true |
| } |
| |
| pub(crate) fn process_attach_layout(new_layout_info: NewLayoutInfo, origin: MutableOrigin) { |
| with_script_thread(|script_thread| { |
| let pipeline_id = Some(new_layout_info.new_pipeline_id); |
| script_thread.profile_event( |
| ScriptThreadEventCategory::AttachLayout, |
| pipeline_id, |
| || { |
| script_thread.handle_new_layout(new_layout_info, origin); |
| }, |
| ) |
| }); |
| } |
| |
| pub(crate) fn get_top_level_for_browsing_context( |
| sender_pipeline: PipelineId, |
| browsing_context_id: BrowsingContextId, |
| ) -> Option<WebViewId> { |
| with_script_thread(|script_thread| { |
| script_thread.ask_constellation_for_top_level_info(sender_pipeline, browsing_context_id) |
| }) |
| } |
| |
| pub(crate) fn find_document(id: PipelineId) -> Option<DomRoot<Document>> { |
| with_script_thread(|script_thread| script_thread.documents.borrow().find_document(id)) |
| } |
| |
| pub(crate) fn set_user_interacting(interacting: bool) { |
| with_script_thread(|script_thread| { |
| script_thread.is_user_interacting.set(interacting); |
| }); |
| } |
| |
| pub(crate) fn is_user_interacting() -> bool { |
| with_script_thread(|script_thread| script_thread.is_user_interacting.get()) |
| } |
| |
| pub(crate) fn get_fully_active_document_ids(&self) -> HashSet<PipelineId> { |
| self.documents |
| .borrow() |
| .iter() |
| .filter_map(|(id, document)| { |
| if document.is_fully_active() { |
| Some(id) |
| } else { |
| None |
| } |
| }) |
| .fold(HashSet::new(), |mut set, id| { |
| let _ = set.insert(id); |
| set |
| }) |
| } |
| |
| pub(crate) fn find_window_proxy(id: BrowsingContextId) -> Option<DomRoot<WindowProxy>> { |
| with_script_thread(|script_thread| { |
| script_thread |
| .window_proxies |
| .borrow() |
| .get(&id) |
| .map(|context| DomRoot::from_ref(&**context)) |
| }) |
| } |
| |
| pub(crate) fn find_window_proxy_by_name(name: &DOMString) -> Option<DomRoot<WindowProxy>> { |
| with_script_thread(|script_thread| { |
| for (_, proxy) in script_thread.window_proxies.borrow().iter() { |
| if proxy.get_name() == *name { |
| return Some(DomRoot::from_ref(&**proxy)); |
| } |
| } |
| None |
| }) |
| } |
| |
| /// The worklet will use the given `ImageCache`. |
| pub(crate) fn worklet_thread_pool(image_cache: Arc<dyn ImageCache>) -> Rc<WorkletThreadPool> { |
| with_optional_script_thread(|script_thread| { |
| let script_thread = script_thread.unwrap(); |
| script_thread |
| .worklet_thread_pool |
| .borrow_mut() |
| .get_or_insert_with(|| { |
| let init = WorkletGlobalScopeInit { |
| to_script_thread_sender: script_thread.senders.self_sender.clone(), |
| resource_threads: script_thread.resource_threads.clone(), |
| mem_profiler_chan: script_thread.senders.memory_profiler_sender.clone(), |
| time_profiler_chan: script_thread.senders.time_profiler_sender.clone(), |
| devtools_chan: script_thread.senders.devtools_server_sender.clone(), |
| to_constellation_sender: script_thread |
| .senders |
| .pipeline_to_constellation_sender |
| .clone(), |
| to_embedder_sender: script_thread |
| .senders |
| .pipeline_to_embedder_sender |
| .clone(), |
| image_cache, |
| #[cfg(feature = "webgpu")] |
| gpu_id_hub: script_thread.gpu_id_hub.clone(), |
| inherited_secure_context: script_thread.inherited_secure_context, |
| }; |
| Rc::new(WorkletThreadPool::spawn(init)) |
| }) |
| .clone() |
| }) |
| } |
| |
| fn handle_register_paint_worklet( |
| &self, |
| pipeline_id: PipelineId, |
| name: Atom, |
| properties: Vec<Atom>, |
| painter: Box<dyn Painter>, |
| ) { |
| let Some(window) = self.documents.borrow().find_window(pipeline_id) else { |
| warn!("Paint worklet registered after pipeline {pipeline_id} closed."); |
| return; |
| }; |
| |
| window |
| .layout_mut() |
| .register_paint_worklet_modules(name, properties, painter); |
| } |
| |
| pub(crate) fn custom_element_reaction_stack() -> Rc<CustomElementReactionStack> { |
| with_optional_script_thread(|script_thread| { |
| script_thread |
| .as_ref() |
| .unwrap() |
| .custom_element_reaction_stack |
| .clone() |
| }) |
| } |
| |
| pub(crate) fn enqueue_callback_reaction( |
| element: &Element, |
| reaction: CallbackReaction, |
| definition: Option<Rc<CustomElementDefinition>>, |
| ) { |
| with_script_thread(|script_thread| { |
| script_thread |
| .custom_element_reaction_stack |
| .enqueue_callback_reaction(element, reaction, definition); |
| }) |
| } |
| |
| pub(crate) fn enqueue_upgrade_reaction( |
| element: &Element, |
| definition: Rc<CustomElementDefinition>, |
| ) { |
| with_script_thread(|script_thread| { |
| script_thread |
| .custom_element_reaction_stack |
| .enqueue_upgrade_reaction(element, definition); |
| }) |
| } |
| |
| pub(crate) fn invoke_backup_element_queue(can_gc: CanGc) { |
| with_script_thread(|script_thread| { |
| script_thread |
| .custom_element_reaction_stack |
| .invoke_backup_element_queue(can_gc); |
| }) |
| } |
| |
| pub(crate) fn save_node_id(pipeline: PipelineId, node_id: String) { |
| with_script_thread(|script_thread| { |
| script_thread |
| .pipeline_to_node_ids |
| .borrow_mut() |
| .entry(pipeline) |
| .or_default() |
| .insert(node_id); |
| }) |
| } |
| |
| pub(crate) fn has_node_id(pipeline: PipelineId, node_id: &str) -> bool { |
| with_script_thread(|script_thread| { |
| script_thread |
| .pipeline_to_node_ids |
| .borrow() |
| .get(&pipeline) |
| .is_some_and(|node_ids| node_ids.contains(node_id)) |
| }) |
| } |
| |
| /// Creates a new script thread. |
| pub(crate) fn new( |
| state: InitialScriptState, |
| layout_factory: Arc<dyn LayoutFactory>, |
| system_font_service: Arc<SystemFontServiceProxy>, |
| ) -> ScriptThread { |
| let (self_sender, self_receiver) = unbounded(); |
| let runtime = Runtime::new(Some(SendableTaskSource { |
| sender: ScriptEventLoopSender::MainThread(self_sender.clone()), |
| pipeline_id: state.id, |
| name: TaskSourceName::Networking, |
| canceller: Default::default(), |
| })); |
| let cx = runtime.cx(); |
| |
| unsafe { |
| SetWindowProxyClass(cx, GetWindowProxyClass()); |
| JS_AddInterruptCallback(cx, Some(interrupt_callback)); |
| } |
| |
| let constellation_receiver = state.constellation_receiver.route_preserving_errors(); |
| |
| // Ask the router to proxy IPC messages from the devtools to us. |
| let devtools_server_sender = state.devtools_server_sender; |
| let (ipc_devtools_sender, ipc_devtools_receiver) = ipc::channel().unwrap(); |
| let devtools_server_receiver = devtools_server_sender |
| .as_ref() |
| .map(|_| ROUTER.route_ipc_receiver_to_new_crossbeam_receiver(ipc_devtools_receiver)) |
| .unwrap_or_else(crossbeam_channel::never); |
| |
| let task_queue = TaskQueue::new(self_receiver, self_sender.clone()); |
| |
| let closing = Arc::new(AtomicBool::new(false)); |
| let background_hang_monitor_exit_signal = BHMExitSignal { |
| closing: closing.clone(), |
| js_context: runtime.thread_safe_js_context(), |
| }; |
| |
| let background_hang_monitor = state.background_hang_monitor_register.register_component( |
| MonitoredComponentId(state.id, MonitoredComponentType::Script), |
| Duration::from_millis(1000), |
| Duration::from_millis(5000), |
| Box::new(background_hang_monitor_exit_signal), |
| ); |
| |
| let (image_cache_sender, image_cache_receiver) = unbounded(); |
| let (ipc_image_cache_sender, ipc_image_cache_receiver) = ipc::channel().unwrap(); |
| ROUTER.add_typed_route( |
| ipc_image_cache_receiver, |
| Box::new(move |message| { |
| let _ = image_cache_sender.send(message.unwrap()); |
| }), |
| ); |
| |
| let receivers = ScriptThreadReceivers { |
| constellation_receiver, |
| image_cache_receiver, |
| devtools_server_receiver, |
| // Initialized to `never` until WebGPU is initialized. |
| #[cfg(feature = "webgpu")] |
| webgpu_receiver: RefCell::new(crossbeam_channel::never()), |
| }; |
| |
| let opts = opts::get(); |
| let senders = ScriptThreadSenders { |
| self_sender, |
| #[cfg(feature = "bluetooth")] |
| bluetooth_sender: state.bluetooth_sender, |
| constellation_sender: state.constellation_sender, |
| pipeline_to_constellation_sender: state.pipeline_to_constellation_sender.sender.clone(), |
| pipeline_to_embedder_sender: state.pipeline_to_embedder_sender.clone(), |
| image_cache_sender: ipc_image_cache_sender, |
| time_profiler_sender: state.time_profiler_sender, |
| memory_profiler_sender: state.memory_profiler_sender, |
| devtools_server_sender, |
| devtools_client_to_script_thread_sender: ipc_devtools_sender, |
| content_process_shutdown_sender: state.content_process_shutdown_sender, |
| }; |
| |
| let microtask_queue = runtime.microtask_queue.clone(); |
| let js_runtime = Rc::new(runtime); |
| #[cfg(feature = "webgpu")] |
| let gpu_id_hub = Arc::new(IdentityHub::default()); |
| |
| let pipeline_id = PipelineId::new(); |
| let script_to_constellation_chan = ScriptToConstellationChan { |
| sender: senders.pipeline_to_constellation_sender.clone(), |
| pipeline_id, |
| }; |
| let debugger_global = DebuggerGlobalScope::new( |
| PipelineId::new(), |
| senders.devtools_server_sender.clone(), |
| senders.devtools_client_to_script_thread_sender.clone(), |
| senders.memory_profiler_sender.clone(), |
| senders.time_profiler_sender.clone(), |
| script_to_constellation_chan, |
| senders.pipeline_to_embedder_sender.clone(), |
| state.resource_threads.clone(), |
| #[cfg(feature = "webgpu")] |
| gpu_id_hub.clone(), |
| CanGc::note(), |
| ); |
| debugger_global.execute(CanGc::note()); |
| |
| ScriptThread { |
| documents: DomRefCell::new(DocumentCollection::default()), |
| last_render_opportunity_time: Default::default(), |
| window_proxies: DomRefCell::new(HashMapTracedValues::new()), |
| incomplete_loads: DomRefCell::new(vec![]), |
| incomplete_parser_contexts: IncompleteParserContexts(RefCell::new(vec![])), |
| senders, |
| receivers, |
| image_cache: state.image_cache.clone(), |
| resource_threads: state.resource_threads, |
| task_queue, |
| background_hang_monitor, |
| closing, |
| timer_scheduler: Default::default(), |
| microtask_queue, |
| js_runtime, |
| closed_pipelines: DomRefCell::new(HashSet::new()), |
| mutation_observer_microtask_queued: Default::default(), |
| mutation_observers: Default::default(), |
| signal_slots: Default::default(), |
| system_font_service, |
| webgl_chan: state.webgl_chan, |
| #[cfg(feature = "webxr")] |
| webxr_registry: state.webxr_registry, |
| worklet_thread_pool: Default::default(), |
| docs_with_no_blocking_loads: Default::default(), |
| custom_element_reaction_stack: Rc::new(CustomElementReactionStack::new()), |
| compositor_api: state.compositor_api, |
| profile_script_events: opts.debug.profile_script_events, |
| print_pwm: opts.print_pwm, |
| unminify_js: opts.unminify_js, |
| local_script_source: opts.local_script_source.clone(), |
| unminify_css: opts.unminify_css, |
| user_content_manager: state.user_content_manager, |
| player_context: state.player_context, |
| pipeline_to_node_ids: Default::default(), |
| is_user_interacting: Cell::new(false), |
| #[cfg(feature = "webgpu")] |
| gpu_id_hub, |
| inherited_secure_context: state.inherited_secure_context, |
| layout_factory, |
| relative_mouse_down_point: Cell::new(Point2D::zero()), |
| scheduled_update_the_rendering: Default::default(), |
| needs_rendering_update: Arc::new(AtomicBool::new(false)), |
| debugger_global: debugger_global.as_traced(), |
| privileged_urls: state.privileged_urls, |
| } |
| } |
| |
| #[allow(unsafe_code)] |
| pub(crate) fn get_cx(&self) -> JSContext { |
| unsafe { JSContext::from_ptr(self.js_runtime.cx()) } |
| } |
| |
| /// Check if we are closing. |
| fn can_continue_running_inner(&self) -> bool { |
| if self.closing.load(Ordering::SeqCst) { |
| return false; |
| } |
| true |
| } |
| |
| /// We are closing, ensure no script can run and potentially hang. |
| fn prepare_for_shutdown_inner(&self) { |
| let docs = self.documents.borrow(); |
| for (_, document) in docs.iter() { |
| document |
| .owner_global() |
| .task_manager() |
| .cancel_all_tasks_and_ignore_future_tasks(); |
| } |
| } |
| |
| /// Starts the script thread. After calling this method, the script thread will loop receiving |
| /// messages on its port. |
| pub(crate) fn start(&self, can_gc: CanGc) { |
| debug!("Starting script thread."); |
| while self.handle_msgs(can_gc) { |
| // Go on... |
| debug!("Running script thread."); |
| } |
| debug!("Stopped script thread."); |
| } |
| |
| /// Process compositor events as part of a "update the rendering task". |
| fn process_pending_input_events(&self, pipeline_id: PipelineId, can_gc: CanGc) { |
| let Some(document) = self.documents.borrow().find_document(pipeline_id) else { |
| warn!("Processing pending compositor events for closed pipeline {pipeline_id}."); |
| return; |
| }; |
| // Do not handle events if the BC has been, or is being, discarded |
| if document.window().Closed() { |
| warn!("Compositor event sent to a pipeline with a closed window {pipeline_id}."); |
| return; |
| } |
| ScriptThread::set_user_interacting(true); |
| |
| document.event_handler().handle_pending_input_events(can_gc); |
| ScriptThread::set_user_interacting(false); |
| } |
| |
| fn cancel_scheduled_update_the_rendering(&self) { |
| if let Some(timer_id) = self.scheduled_update_the_rendering.borrow_mut().take() { |
| self.timer_scheduler.borrow_mut().cancel_timer(timer_id); |
| } |
| } |
| |
| fn schedule_update_the_rendering_timer_if_necessary(&self, delay: Duration) { |
| if self.scheduled_update_the_rendering.borrow().is_some() { |
| return; |
| } |
| |
| debug!("Scheduling ScriptThread animation frame."); |
| let trigger_script_thread_animation = self.needs_rendering_update.clone(); |
| let timer_id = self.schedule_timer(TimerEventRequest { |
| callback: Box::new(move || { |
| trigger_script_thread_animation.store(true, Ordering::Relaxed); |
| }), |
| duration: delay, |
| }); |
| |
| *self.scheduled_update_the_rendering.borrow_mut() = Some(timer_id); |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#update-the-rendering> |
| /// |
| /// Attempt to update the rendering and then do a microtask checkpoint if rendering was actually |
| /// updated. |
| /// |
| /// Returns true if any reflows produced a new display list. |
| pub(crate) fn update_the_rendering(&self, can_gc: CanGc) -> bool { |
| self.last_render_opportunity_time.set(Some(Instant::now())); |
| self.cancel_scheduled_update_the_rendering(); |
| self.needs_rendering_update.store(false, Ordering::Relaxed); |
| |
| if !self.can_continue_running_inner() { |
| return false; |
| } |
| |
| // TODO: The specification says to filter out non-renderable documents, |
| // as well as those for which a rendering update would be unnecessary, |
| // but this isn't happening here. |
| |
| // TODO(#31242): the filtering of docs is extended to not exclude the ones that |
| // has pending initial observation targets |
| // https://w3c.github.io/IntersectionObserver/#pending-initial-observation |
| |
| // > 2. Let docs be all fully active Document objects whose relevant agent's event loop |
| // > is eventLoop, sorted arbitrarily except that the following conditions must be |
| // > met: |
| // |
| // > Any Document B whose container document is A must be listed after A in the |
| // > list. |
| // |
| // > If there are two documents A and B that both have the same non-null container |
| // > document C, then the order of A and B in the list must match the |
| // > shadow-including tree order of their respective navigable containers in C's |
| // > node tree. |
| // |
| // > In the steps below that iterate over docs, each Document must be processed in |
| // > the order it is found in the list. |
| let documents_in_order = self.documents.borrow().documents_in_order(); |
| |
| // TODO: The specification reads: "for doc in docs" at each step whereas this runs all |
| // steps per doc in docs. Currently `<iframe>` resizing depends on a parent being able to |
| // queue resize events on a child and have those run in the same call to this method, so |
| // that needs to be sorted out to fix this. |
| let mut should_generate_frame = false; |
| for pipeline_id in documents_in_order.iter() { |
| let document = self |
| .documents |
| .borrow() |
| .find_document(*pipeline_id) |
| .expect("Got pipeline for Document not managed by this ScriptThread."); |
| |
| if !document.is_fully_active() { |
| continue; |
| } |
| |
| if document.waiting_on_canvas_image_updates() { |
| continue; |
| } |
| |
| // TODO(#31581): The steps in the "Revealing the document" section need to be implemented |
| // `process_pending_input_events` handles the focusing steps as well as other events |
| // from the compositor. |
| |
| // TODO: Should this be broken and to match the specification more closely? For instance see |
| // https://html.spec.whatwg.org/multipage/#flush-autofocus-candidates. |
| self.process_pending_input_events(*pipeline_id, can_gc); |
| |
| // > 8. For each doc of docs, run the resize steps for doc. [CSSOMVIEW] |
| let resized = document.window().run_the_resize_steps(can_gc); |
| |
| // > 9. For each doc of docs, run the scroll steps for doc. |
| document.run_the_scroll_steps(can_gc); |
| |
| // Media queries is only relevant when there are resizing. |
| if resized { |
| // 10. For each doc of docs, evaluate media queries and report changes for doc. |
| document |
| .window() |
| .evaluate_media_queries_and_report_changes(can_gc); |
| |
| // https://html.spec.whatwg.org/multipage/#img-environment-changes |
| // As per the spec, this can be run at any time. |
| document.react_to_environment_changes() |
| } |
| |
| // > 11. For each doc of docs, update animations and send events for doc, passing |
| // > in relative high resolution time given frameTimestamp and doc's relevant |
| // > global object as the timestamp [WEBANIMATIONS] |
| document.update_animations_and_send_events(can_gc); |
| |
| // TODO(#31866): Implement "run the fullscreen steps" from |
| // https://fullscreen.spec.whatwg.org/multipage/#run-the-fullscreen-steps. |
| |
| // TODO(#31868): Implement the "context lost steps" from |
| // https://html.spec.whatwg.org/multipage/#context-lost-steps. |
| |
| // > 14. For each doc of docs, run the animation frame callbacks for doc, passing |
| // > in the relative high resolution time given frameTimestamp and doc's |
| // > relevant global object as the timestamp. |
| document.run_the_animation_frame_callbacks(can_gc); |
| |
| // Run the resize observer steps. |
| let _realm = enter_realm(&*document); |
| let mut depth = Default::default(); |
| while document.gather_active_resize_observations_at_depth(&depth) { |
| // Note: this will reflow the doc. |
| depth = document.broadcast_active_resize_observations(can_gc); |
| } |
| |
| if document.has_skipped_resize_observations() { |
| document.deliver_resize_loop_error_notification(can_gc); |
| } |
| document.set_resize_observer_started_observing_target(false); |
| |
| // TODO(#31870): Implement step 17: if the focused area of doc is not a focusable area, |
| // then run the focusing steps for document's viewport. |
| |
| // TODO: Perform pending transition operations from |
| // https://drafts.csswg.org/css-view-transitions/#perform-pending-transition-operations. |
| |
| // > 19. For each doc of docs, run the update intersection observations steps for doc, |
| // > passing in the relative high resolution time given now and |
| // > doc's relevant global object as the timestamp. [INTERSECTIONOBSERVER] |
| // TODO(stevennovaryo): The time attribute should be relative to the time origin of the global object |
| document.update_intersection_observer_steps(CrossProcessInstant::now(), can_gc); |
| |
| // TODO: Mark paint timing from https://w3c.github.io/paint-timing. |
| |
| // > Step 22: For each doc of docs, update the rendering or user interface of |
| // > doc and its node navigable to reflect the current state. |
| should_generate_frame = |
| document.update_the_rendering().needs_frame() || should_generate_frame; |
| |
| // TODO: Process top layer removals according to |
| // https://drafts.csswg.org/css-position-4/#process-top-layer-removals. |
| } |
| |
| if should_generate_frame { |
| self.compositor_api.generate_frame(); |
| } |
| |
| // Perform a microtask checkpoint as the specifications says that *update the rendering* |
| // should be run in a task and a microtask checkpoint is always done when running tasks. |
| self.perform_a_microtask_checkpoint(can_gc); |
| should_generate_frame |
| } |
| |
| /// Schedule a rendering update ("update the rendering"), if necessary. This |
| /// can be necessary for a couple reasons. For instance, when the DOM |
| /// changes a scheduled rendering update becomes necessary if one isn't |
| /// scheduled already. Another example is if rAFs are running but no display |
| /// lists are being produced. In that case the [`ScriptThread`] is |
| /// responsible for scheduling animation ticks. |
| fn maybe_schedule_rendering_opportunity_after_ipc_message( |
| &self, |
| built_any_display_lists: bool, |
| ) { |
| let needs_rendering_update = self |
| .documents |
| .borrow() |
| .iter() |
| .any(|(_, document)| document.needs_rendering_update()); |
| let running_animations = self.documents.borrow().iter().any(|(_, document)| { |
| document.is_fully_active() && |
| !document.window().throttled() && |
| (document.animations().running_animation_count() != 0 || |
| document.has_active_request_animation_frame_callbacks()) |
| }); |
| |
| // If we are not running animations and no rendering update is |
| // necessary, just exit early and schedule the next rendering update |
| // when it becomes necessary. |
| if !needs_rendering_update && !running_animations { |
| return; |
| } |
| |
| // If animations are running and a reflow in this event loop iteration |
| // produced a display list, rely on the renderer to inform us of the |
| // next animation tick / rendering opportunity. |
| if running_animations && built_any_display_lists { |
| return; |
| } |
| |
| // There are two possibilities: rendering needs to be updated or we are |
| // scheduling a new animation tick because animations are running, but |
| // not changing the DOM. In the later case we can wait a bit longer |
| // until the next "update the rendering" call as it's more efficient to |
| // slow down rAFs that don't change the DOM. |
| // |
| // TODO: Should either of these delays be reduced to also reduce update latency? |
| let animation_delay = if running_animations && !needs_rendering_update { |
| // 30 milliseconds (33 FPS) is used here as the rendering isn't changing |
| // so it isn't a problem to slow down rAF callback calls. In addition, this allows |
| // renderer-based ticks to arrive first. |
| Duration::from_millis(30) |
| } else { |
| // 20 milliseconds (50 FPS) is used here in order to allow any renderer-based |
| // animation ticks to arrive first. |
| Duration::from_millis(20) |
| }; |
| |
| let time_since_last_rendering_opportunity = self |
| .last_render_opportunity_time |
| .get() |
| .map(|last_render_opportunity_time| Instant::now() - last_render_opportunity_time) |
| .unwrap_or(Duration::MAX) |
| .min(animation_delay); |
| self.schedule_update_the_rendering_timer_if_necessary( |
| animation_delay - time_since_last_rendering_opportunity, |
| ); |
| } |
| |
| /// Fulfill the possibly-pending pending `document.fonts.ready` promise if |
| /// all web fonts have loaded. |
| fn maybe_fulfill_font_ready_promises(&self, can_gc: CanGc) { |
| let mut sent_message = false; |
| for (_, document) in self.documents.borrow().iter() { |
| sent_message = document.maybe_fulfill_font_ready_promise(can_gc) || sent_message; |
| } |
| |
| if sent_message { |
| self.perform_a_microtask_checkpoint(can_gc); |
| } |
| } |
| |
| /// If waiting for an idle `Pipeline` state in order to dump a screenshot at |
| /// the right time, inform the `Constellation` this `Pipeline` has entered |
| /// the idle state when applicable. |
| fn maybe_send_idle_document_state_to_constellation(&self) { |
| if !opts::get().wait_for_stable_image { |
| return; |
| } |
| for (_, document) in self.documents.borrow().iter() { |
| document |
| .window() |
| .maybe_send_idle_document_state_to_constellation(); |
| } |
| } |
| |
| /// Handle incoming messages from other tasks and the task queue. |
| fn handle_msgs(&self, can_gc: CanGc) -> bool { |
| // Proritize rendering tasks and others, and gather all other events as `sequential`. |
| let mut sequential = vec![]; |
| |
| // Notify the background-hang-monitor we are waiting for an event. |
| self.background_hang_monitor.notify_wait(); |
| |
| // Receive at least one message so we don't spinloop. |
| debug!("Waiting for event."); |
| let fully_active = self.get_fully_active_document_ids(); |
| let mut event = self.receivers.recv( |
| &self.task_queue, |
| &self.timer_scheduler.borrow(), |
| &fully_active, |
| ); |
| |
| loop { |
| debug!("Handling event: {event:?}"); |
| |
| // Dispatch any completed timers, so that their tasks can be run below. |
| self.timer_scheduler |
| .borrow_mut() |
| .dispatch_completed_timers(); |
| |
| let _realm = event.pipeline_id().map(|id| { |
| let global = self.documents.borrow().find_global(id); |
| global.map(|global| enter_realm(&*global)) |
| }); |
| |
| // https://html.spec.whatwg.org/multipage/#event-loop-processing-model step 7 |
| match event { |
| // This has to be handled before the ResizeMsg below, |
| // otherwise the page may not have been added to the |
| // child list yet, causing the find() to fail. |
| MixedMessage::FromConstellation(ScriptThreadMessage::AttachLayout( |
| new_layout_info, |
| )) => { |
| let pipeline_id = new_layout_info.new_pipeline_id; |
| self.profile_event( |
| ScriptThreadEventCategory::AttachLayout, |
| Some(pipeline_id), |
| || { |
| // If this is an about:blank or about:srcdoc load, it must share the |
| // creator's origin. This must match the logic in the constellation |
| // when creating a new pipeline |
| let not_an_about_blank_and_about_srcdoc_load = |
| new_layout_info.load_data.url.as_str() != "about:blank" && |
| new_layout_info.load_data.url.as_str() != "about:srcdoc"; |
| let origin = if not_an_about_blank_and_about_srcdoc_load { |
| MutableOrigin::new(new_layout_info.load_data.url.origin()) |
| } else if let Some(parent) = |
| new_layout_info.parent_info.and_then(|pipeline_id| { |
| self.documents.borrow().find_document(pipeline_id) |
| }) |
| { |
| parent.origin().clone() |
| } else if let Some(creator) = new_layout_info |
| .load_data |
| .creator_pipeline_id |
| .and_then(|pipeline_id| { |
| self.documents.borrow().find_document(pipeline_id) |
| }) |
| { |
| creator.origin().clone() |
| } else { |
| MutableOrigin::new(ImmutableOrigin::new_opaque()) |
| }; |
| |
| self.handle_new_layout(new_layout_info, origin); |
| }, |
| ) |
| }, |
| MixedMessage::FromConstellation(ScriptThreadMessage::Resize( |
| id, |
| size, |
| size_type, |
| )) => { |
| self.handle_resize_message(id, size, size_type); |
| }, |
| MixedMessage::FromConstellation(ScriptThreadMessage::Viewport(id, rect)) => self |
| .profile_event(ScriptThreadEventCategory::SetViewport, Some(id), || { |
| self.handle_viewport(id, rect); |
| }), |
| MixedMessage::FromConstellation(ScriptThreadMessage::TickAllAnimations( |
| _webviews, |
| )) => { |
| self.set_needs_rendering_update(); |
| }, |
| MixedMessage::FromConstellation( |
| ScriptThreadMessage::NoLongerWaitingOnAsychronousImageUpdates(pipeline_id), |
| ) => { |
| if let Some(document) = self.documents.borrow().find_document(pipeline_id) { |
| document.handle_no_longer_waiting_on_asynchronous_image_updates(); |
| } |
| }, |
| MixedMessage::FromConstellation(ScriptThreadMessage::SendInputEvent(id, event)) => { |
| self.handle_input_event(id, event) |
| }, |
| MixedMessage::FromScript(MainThreadScriptMsg::Common(CommonScriptMsg::Task( |
| _, |
| _, |
| _, |
| TaskSourceName::Rendering, |
| ))) => { |
| // Instead of interleaving any number of update the rendering tasks with other |
| // message handling, we run those steps only once at the end of each call of |
| // this function. |
| }, |
| MixedMessage::FromScript(MainThreadScriptMsg::Inactive) => { |
| // An event came-in from a document that is not fully-active, it has been stored by the task-queue. |
| // Continue without adding it to "sequential". |
| }, |
| MixedMessage::FromConstellation(ScriptThreadMessage::ExitFullScreen(id)) => self |
| .profile_event(ScriptThreadEventCategory::ExitFullscreen, Some(id), || { |
| self.handle_exit_fullscreen(id, can_gc); |
| }), |
| _ => { |
| sequential.push(event); |
| }, |
| } |
| |
| // If any of our input sources has an event pending, we'll perform another iteration |
| // and check for more resize events. If there are no events pending, we'll move |
| // on and execute the sequential non-resize events we've seen. |
| match self.receivers.try_recv(&self.task_queue, &fully_active) { |
| Some(new_event) => event = new_event, |
| None => break, |
| } |
| } |
| |
| // Process the gathered events. |
| debug!("Processing events."); |
| for msg in sequential { |
| debug!("Processing event {:?}.", msg); |
| let category = self.categorize_msg(&msg); |
| let pipeline_id = msg.pipeline_id(); |
| let _realm = pipeline_id.and_then(|id| { |
| let global = self.documents.borrow().find_global(id); |
| global.map(|global| enter_realm(&*global)) |
| }); |
| |
| if self.closing.load(Ordering::SeqCst) { |
| // If we've received the closed signal from the BHM, only handle exit messages. |
| match msg { |
| MixedMessage::FromConstellation(ScriptThreadMessage::ExitScriptThread) => { |
| self.handle_exit_script_thread_msg(can_gc); |
| return false; |
| }, |
| MixedMessage::FromConstellation(ScriptThreadMessage::ExitPipeline( |
| webview_id, |
| pipeline_id, |
| discard_browsing_context, |
| )) => { |
| self.handle_exit_pipeline_msg( |
| webview_id, |
| pipeline_id, |
| discard_browsing_context, |
| can_gc, |
| ); |
| }, |
| _ => {}, |
| } |
| continue; |
| } |
| |
| let exiting = self.profile_event(category, pipeline_id, move || { |
| match msg { |
| MixedMessage::FromConstellation(ScriptThreadMessage::ExitScriptThread) => { |
| self.handle_exit_script_thread_msg(can_gc); |
| return true; |
| }, |
| MixedMessage::FromConstellation(inner_msg) => { |
| self.handle_msg_from_constellation(inner_msg, can_gc) |
| }, |
| MixedMessage::FromScript(inner_msg) => self.handle_msg_from_script(inner_msg), |
| MixedMessage::FromDevtools(inner_msg) => { |
| self.handle_msg_from_devtools(inner_msg, can_gc) |
| }, |
| MixedMessage::FromImageCache(inner_msg) => { |
| self.handle_msg_from_image_cache(inner_msg) |
| }, |
| #[cfg(feature = "webgpu")] |
| MixedMessage::FromWebGPUServer(inner_msg) => { |
| self.handle_msg_from_webgpu_server(inner_msg) |
| }, |
| MixedMessage::TimerFired => {}, |
| } |
| |
| false |
| }); |
| |
| // If an `ExitScriptThread` message was handled above, bail out now. |
| if exiting { |
| return false; |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#event-loop-processing-model step 6 |
| // TODO(#32003): A microtask checkpoint is only supposed to be performed after running a task. |
| self.perform_a_microtask_checkpoint(can_gc); |
| } |
| |
| for (_, doc) in self.documents.borrow().iter() { |
| let window = doc.window(); |
| window |
| .upcast::<GlobalScope>() |
| .perform_a_dom_garbage_collection_checkpoint(); |
| } |
| |
| { |
| // https://html.spec.whatwg.org/multipage/#the-end step 6 |
| let mut docs = self.docs_with_no_blocking_loads.borrow_mut(); |
| for document in docs.iter() { |
| let _realm = enter_realm(&**document); |
| document.maybe_queue_document_completion(); |
| } |
| docs.clear(); |
| } |
| |
| let built_any_display_lists = self.needs_rendering_update.load(Ordering::Relaxed) && |
| self.update_the_rendering(can_gc); |
| |
| self.maybe_fulfill_font_ready_promises(can_gc); |
| self.maybe_send_idle_document_state_to_constellation(); |
| |
| // This must happen last to detect if any change above makes a rendering update necessary. |
| self.maybe_schedule_rendering_opportunity_after_ipc_message(built_any_display_lists); |
| |
| true |
| } |
| |
| fn categorize_msg(&self, msg: &MixedMessage) -> ScriptThreadEventCategory { |
| match *msg { |
| MixedMessage::FromConstellation(ref inner_msg) => match *inner_msg { |
| ScriptThreadMessage::SendInputEvent(_, _) => ScriptThreadEventCategory::InputEvent, |
| _ => ScriptThreadEventCategory::ConstellationMsg, |
| }, |
| // TODO https://github.com/servo/servo/issues/18998 |
| MixedMessage::FromDevtools(_) => ScriptThreadEventCategory::DevtoolsMsg, |
| MixedMessage::FromImageCache(_) => ScriptThreadEventCategory::ImageCacheMsg, |
| MixedMessage::FromScript(ref inner_msg) => match *inner_msg { |
| MainThreadScriptMsg::Common(CommonScriptMsg::Task(category, ..)) => category, |
| MainThreadScriptMsg::RegisterPaintWorklet { .. } => { |
| ScriptThreadEventCategory::WorkletEvent |
| }, |
| _ => ScriptThreadEventCategory::ScriptEvent, |
| }, |
| #[cfg(feature = "webgpu")] |
| MixedMessage::FromWebGPUServer(_) => ScriptThreadEventCategory::WebGPUMsg, |
| MixedMessage::TimerFired => ScriptThreadEventCategory::TimerEvent, |
| } |
| } |
| |
| fn profile_event<F, R>( |
| &self, |
| category: ScriptThreadEventCategory, |
| pipeline_id: Option<PipelineId>, |
| f: F, |
| ) -> R |
| where |
| F: FnOnce() -> R, |
| { |
| self.background_hang_monitor |
| .notify_activity(HangAnnotation::Script(category.into())); |
| let start = Instant::now(); |
| let value = if self.profile_script_events { |
| let profiler_chan = self.senders.time_profiler_sender.clone(); |
| match category { |
| ScriptThreadEventCategory::AttachLayout => { |
| time_profile!(ProfilerCategory::ScriptAttachLayout, None, profiler_chan, f) |
| }, |
| ScriptThreadEventCategory::ConstellationMsg => time_profile!( |
| ProfilerCategory::ScriptConstellationMsg, |
| None, |
| profiler_chan, |
| f |
| ), |
| ScriptThreadEventCategory::DatabaseAccessEvent => time_profile!( |
| ProfilerCategory::ScriptDatabaseAccessEvent, |
| None, |
| profiler_chan, |
| f |
| ), |
| ScriptThreadEventCategory::DevtoolsMsg => { |
| time_profile!(ProfilerCategory::ScriptDevtoolsMsg, None, profiler_chan, f) |
| }, |
| ScriptThreadEventCategory::DocumentEvent => time_profile!( |
| ProfilerCategory::ScriptDocumentEvent, |
| None, |
| profiler_chan, |
| f |
| ), |
| ScriptThreadEventCategory::InputEvent => { |
| time_profile!(ProfilerCategory::ScriptInputEvent, None, profiler_chan, f) |
| }, |
| ScriptThreadEventCategory::FileRead => { |
| time_profile!(ProfilerCategory::ScriptFileRead, None, profiler_chan, f) |
| }, |
| ScriptThreadEventCategory::FontLoading => { |
| time_profile!(ProfilerCategory::ScriptFontLoading, None, profiler_chan, f) |
| }, |
| ScriptThreadEventCategory::FormPlannedNavigation => time_profile!( |
| ProfilerCategory::ScriptPlannedNavigation, |
| None, |
| profiler_chan, |
| f |
| ), |
| ScriptThreadEventCategory::HistoryEvent => { |
| time_profile!(ProfilerCategory::ScriptHistoryEvent, None, profiler_chan, f) |
| }, |
| ScriptThreadEventCategory::ImageCacheMsg => time_profile!( |
| ProfilerCategory::ScriptImageCacheMsg, |
| None, |
| profiler_chan, |
| f |
| ), |
| ScriptThreadEventCategory::NetworkEvent => { |
| time_profile!(ProfilerCategory::ScriptNetworkEvent, None, profiler_chan, f) |
| }, |
| ScriptThreadEventCategory::PortMessage => { |
| time_profile!(ProfilerCategory::ScriptPortMessage, None, profiler_chan, f) |
| }, |
| ScriptThreadEventCategory::Resize => { |
| time_profile!(ProfilerCategory::ScriptResize, None, profiler_chan, f) |
| }, |
| ScriptThreadEventCategory::ScriptEvent => { |
| time_profile!(ProfilerCategory::ScriptEvent, None, profiler_chan, f) |
| }, |
| ScriptThreadEventCategory::SetScrollState => time_profile!( |
| ProfilerCategory::ScriptSetScrollState, |
| None, |
| profiler_chan, |
| f |
| ), |
| ScriptThreadEventCategory::UpdateReplacedElement => time_profile!( |
| ProfilerCategory::ScriptUpdateReplacedElement, |
| None, |
| profiler_chan, |
| f |
| ), |
| ScriptThreadEventCategory::StylesheetLoad => time_profile!( |
| ProfilerCategory::ScriptStylesheetLoad, |
| None, |
| profiler_chan, |
| f |
| ), |
| ScriptThreadEventCategory::SetViewport => { |
| time_profile!(ProfilerCategory::ScriptSetViewport, None, profiler_chan, f) |
| }, |
| ScriptThreadEventCategory::TimerEvent => { |
| time_profile!(ProfilerCategory::ScriptTimerEvent, None, profiler_chan, f) |
| }, |
| ScriptThreadEventCategory::WebSocketEvent => time_profile!( |
| ProfilerCategory::ScriptWebSocketEvent, |
| None, |
| profiler_chan, |
| f |
| ), |
| ScriptThreadEventCategory::WorkerEvent => { |
| time_profile!(ProfilerCategory::ScriptWorkerEvent, None, profiler_chan, f) |
| }, |
| ScriptThreadEventCategory::WorkletEvent => { |
| time_profile!(ProfilerCategory::ScriptWorkletEvent, None, profiler_chan, f) |
| }, |
| ScriptThreadEventCategory::ServiceWorkerEvent => time_profile!( |
| ProfilerCategory::ScriptServiceWorkerEvent, |
| None, |
| profiler_chan, |
| f |
| ), |
| ScriptThreadEventCategory::EnterFullscreen => time_profile!( |
| ProfilerCategory::ScriptEnterFullscreen, |
| None, |
| profiler_chan, |
| f |
| ), |
| ScriptThreadEventCategory::ExitFullscreen => time_profile!( |
| ProfilerCategory::ScriptExitFullscreen, |
| None, |
| profiler_chan, |
| f |
| ), |
| ScriptThreadEventCategory::PerformanceTimelineTask => time_profile!( |
| ProfilerCategory::ScriptPerformanceEvent, |
| None, |
| profiler_chan, |
| f |
| ), |
| ScriptThreadEventCategory::Rendering => { |
| time_profile!(ProfilerCategory::ScriptRendering, None, profiler_chan, f) |
| }, |
| #[cfg(feature = "webgpu")] |
| ScriptThreadEventCategory::WebGPUMsg => { |
| time_profile!(ProfilerCategory::ScriptWebGPUMsg, None, profiler_chan, f) |
| }, |
| } |
| } else { |
| f() |
| }; |
| let task_duration = start.elapsed(); |
| for (doc_id, doc) in self.documents.borrow().iter() { |
| if let Some(pipeline_id) = pipeline_id { |
| if pipeline_id == doc_id && task_duration.as_nanos() > MAX_TASK_NS { |
| if self.print_pwm { |
| println!( |
| "Task took longer than max allowed ({:?}) {:?}", |
| category, |
| task_duration.as_nanos() |
| ); |
| } |
| doc.start_tti(); |
| } |
| } |
| doc.record_tti_if_necessary(); |
| } |
| value |
| } |
| |
| fn handle_msg_from_constellation(&self, msg: ScriptThreadMessage, can_gc: CanGc) { |
| match msg { |
| ScriptThreadMessage::StopDelayingLoadEventsMode(pipeline_id) => { |
| self.handle_stop_delaying_load_events_mode(pipeline_id) |
| }, |
| ScriptThreadMessage::NavigateIframe( |
| parent_pipeline_id, |
| browsing_context_id, |
| load_data, |
| history_handling, |
| ) => self.handle_navigate_iframe( |
| parent_pipeline_id, |
| browsing_context_id, |
| load_data, |
| history_handling, |
| can_gc, |
| ), |
| ScriptThreadMessage::UnloadDocument(pipeline_id) => { |
| self.handle_unload_document(pipeline_id, can_gc) |
| }, |
| ScriptThreadMessage::ResizeInactive(id, new_size) => { |
| self.handle_resize_inactive_msg(id, new_size) |
| }, |
| ScriptThreadMessage::ThemeChange(_, theme) => { |
| self.handle_theme_change_msg(theme); |
| }, |
| ScriptThreadMessage::GetTitle(pipeline_id) => self.handle_get_title_msg(pipeline_id), |
| ScriptThreadMessage::SetDocumentActivity(pipeline_id, activity) => { |
| self.handle_set_document_activity_msg(pipeline_id, activity, can_gc) |
| }, |
| ScriptThreadMessage::SetThrottled(pipeline_id, throttled) => { |
| self.handle_set_throttled_msg(pipeline_id, throttled) |
| }, |
| ScriptThreadMessage::SetThrottledInContainingIframe( |
| parent_pipeline_id, |
| browsing_context_id, |
| throttled, |
| ) => self.handle_set_throttled_in_containing_iframe_msg( |
| parent_pipeline_id, |
| browsing_context_id, |
| throttled, |
| ), |
| ScriptThreadMessage::PostMessage { |
| target: target_pipeline_id, |
| source: source_pipeline_id, |
| source_browsing_context, |
| target_origin: origin, |
| source_origin, |
| data, |
| } => self.handle_post_message_msg( |
| target_pipeline_id, |
| source_pipeline_id, |
| source_browsing_context, |
| origin, |
| source_origin, |
| *data, |
| ), |
| ScriptThreadMessage::UpdatePipelineId( |
| parent_pipeline_id, |
| browsing_context_id, |
| webview_id, |
| new_pipeline_id, |
| reason, |
| ) => self.handle_update_pipeline_id( |
| parent_pipeline_id, |
| browsing_context_id, |
| webview_id, |
| new_pipeline_id, |
| reason, |
| can_gc, |
| ), |
| ScriptThreadMessage::UpdateHistoryState(pipeline_id, history_state_id, url) => { |
| self.handle_update_history_state_msg(pipeline_id, history_state_id, url, can_gc) |
| }, |
| ScriptThreadMessage::RemoveHistoryStates(pipeline_id, history_states) => { |
| self.handle_remove_history_states(pipeline_id, history_states) |
| }, |
| ScriptThreadMessage::FocusIFrame(parent_pipeline_id, frame_id, sequence) => { |
| self.handle_focus_iframe_msg(parent_pipeline_id, frame_id, sequence, can_gc) |
| }, |
| ScriptThreadMessage::FocusDocument(pipeline_id, sequence) => { |
| self.handle_focus_document_msg(pipeline_id, sequence, can_gc) |
| }, |
| ScriptThreadMessage::Unfocus(pipeline_id, sequence) => { |
| self.handle_unfocus_msg(pipeline_id, sequence, can_gc) |
| }, |
| ScriptThreadMessage::WebDriverScriptCommand(pipeline_id, msg) => { |
| self.handle_webdriver_msg(pipeline_id, msg, can_gc) |
| }, |
| ScriptThreadMessage::WebFontLoaded(pipeline_id, success) => { |
| self.handle_web_font_loaded(pipeline_id, success) |
| }, |
| ScriptThreadMessage::DispatchIFrameLoadEvent { |
| target: browsing_context_id, |
| parent: parent_id, |
| child: child_id, |
| } => self.handle_iframe_load_event(parent_id, browsing_context_id, child_id, can_gc), |
| ScriptThreadMessage::DispatchStorageEvent( |
| pipeline_id, |
| storage, |
| url, |
| key, |
| old_value, |
| new_value, |
| ) => self.handle_storage_event(pipeline_id, storage, url, key, old_value, new_value), |
| ScriptThreadMessage::ReportCSSError(pipeline_id, filename, line, column, msg) => { |
| self.handle_css_error_reporting(pipeline_id, filename, line, column, msg) |
| }, |
| ScriptThreadMessage::Reload(pipeline_id) => self.handle_reload(pipeline_id, can_gc), |
| ScriptThreadMessage::ExitPipeline( |
| webview_id, |
| pipeline_id, |
| discard_browsing_context, |
| ) => self.handle_exit_pipeline_msg( |
| webview_id, |
| pipeline_id, |
| discard_browsing_context, |
| can_gc, |
| ), |
| ScriptThreadMessage::PaintMetric( |
| pipeline_id, |
| metric_type, |
| metric_value, |
| first_reflow, |
| ) => self.handle_paint_metric( |
| pipeline_id, |
| metric_type, |
| metric_value, |
| first_reflow, |
| can_gc, |
| ), |
| ScriptThreadMessage::MediaSessionAction(pipeline_id, action) => { |
| self.handle_media_session_action(pipeline_id, action, can_gc) |
| }, |
| #[cfg(feature = "webgpu")] |
| ScriptThreadMessage::SetWebGPUPort(port) => { |
| *self.receivers.webgpu_receiver.borrow_mut() = |
| ROUTER.route_ipc_receiver_to_new_crossbeam_receiver(port); |
| }, |
| msg @ ScriptThreadMessage::AttachLayout(..) | |
| msg @ ScriptThreadMessage::Viewport(..) | |
| msg @ ScriptThreadMessage::Resize(..) | |
| msg @ ScriptThreadMessage::ExitFullScreen(..) | |
| msg @ ScriptThreadMessage::SendInputEvent(..) | |
| msg @ ScriptThreadMessage::TickAllAnimations(..) | |
| msg @ ScriptThreadMessage::NoLongerWaitingOnAsychronousImageUpdates(..) | |
| msg @ ScriptThreadMessage::ExitScriptThread => { |
| panic!("should have handled {:?} already", msg) |
| }, |
| ScriptThreadMessage::SetScrollStates(pipeline_id, scroll_states) => { |
| self.handle_set_scroll_states(pipeline_id, scroll_states) |
| }, |
| ScriptThreadMessage::EvaluateJavaScript(pipeline_id, evaluation_id, script) => { |
| self.handle_evaluate_javascript(pipeline_id, evaluation_id, script, can_gc); |
| }, |
| ScriptThreadMessage::SendImageKeysBatch(pipeline_id, image_keys) => { |
| if let Some(window) = self.documents.borrow().find_window(pipeline_id) { |
| window |
| .image_cache() |
| .fill_key_cache_with_batch_of_keys(image_keys); |
| } else { |
| warn!( |
| "Could not find window corresponding to an image cache to send image keys to pipeline {:?}", |
| pipeline_id |
| ); |
| } |
| }, |
| ScriptThreadMessage::RefreshCursor(pipeline_id) => { |
| self.handle_refresh_cursor(pipeline_id); |
| }, |
| ScriptThreadMessage::PreferencesUpdated(updates) => { |
| let mut current_preferences = prefs::get().clone(); |
| for (name, value) in updates { |
| current_preferences.set_value(&name, value); |
| } |
| prefs::set(current_preferences); |
| }, |
| } |
| } |
| |
| fn handle_set_scroll_states( |
| &self, |
| pipeline_id: PipelineId, |
| scroll_states: FnvHashMap<ExternalScrollId, LayoutVector2D>, |
| ) { |
| let Some(window) = self.documents.borrow().find_window(pipeline_id) else { |
| warn!("Received scroll states for closed pipeline {pipeline_id}"); |
| return; |
| }; |
| |
| self.profile_event( |
| ScriptThreadEventCategory::SetScrollState, |
| Some(pipeline_id), |
| || { |
| window |
| .layout_mut() |
| .set_scroll_offsets_from_renderer(&scroll_states); |
| }, |
| ) |
| } |
| |
| #[cfg(feature = "webgpu")] |
| fn handle_msg_from_webgpu_server(&self, msg: WebGPUMsg) { |
| match msg { |
| WebGPUMsg::FreeAdapter(id) => self.gpu_id_hub.free_adapter_id(id), |
| WebGPUMsg::FreeDevice { |
| device_id, |
| pipeline_id, |
| } => { |
| self.gpu_id_hub.free_device_id(device_id); |
| if let Some(global) = self.documents.borrow().find_global(pipeline_id) { |
| global.remove_gpu_device(WebGPUDevice(device_id)); |
| } // page can already be destroyed |
| }, |
| WebGPUMsg::FreeBuffer(id) => self.gpu_id_hub.free_buffer_id(id), |
| WebGPUMsg::FreePipelineLayout(id) => self.gpu_id_hub.free_pipeline_layout_id(id), |
| WebGPUMsg::FreeComputePipeline(id) => self.gpu_id_hub.free_compute_pipeline_id(id), |
| WebGPUMsg::FreeBindGroup(id) => self.gpu_id_hub.free_bind_group_id(id), |
| WebGPUMsg::FreeBindGroupLayout(id) => self.gpu_id_hub.free_bind_group_layout_id(id), |
| WebGPUMsg::FreeCommandBuffer(id) => self |
| .gpu_id_hub |
| .free_command_buffer_id(id.into_command_encoder_id()), |
| WebGPUMsg::FreeSampler(id) => self.gpu_id_hub.free_sampler_id(id), |
| WebGPUMsg::FreeShaderModule(id) => self.gpu_id_hub.free_shader_module_id(id), |
| WebGPUMsg::FreeRenderBundle(id) => self.gpu_id_hub.free_render_bundle_id(id), |
| WebGPUMsg::FreeRenderPipeline(id) => self.gpu_id_hub.free_render_pipeline_id(id), |
| WebGPUMsg::FreeTexture(id) => self.gpu_id_hub.free_texture_id(id), |
| WebGPUMsg::FreeTextureView(id) => self.gpu_id_hub.free_texture_view_id(id), |
| WebGPUMsg::FreeComputePass(id) => self.gpu_id_hub.free_compute_pass_id(id), |
| WebGPUMsg::FreeRenderPass(id) => self.gpu_id_hub.free_render_pass_id(id), |
| WebGPUMsg::Exit => { |
| *self.receivers.webgpu_receiver.borrow_mut() = crossbeam_channel::never() |
| }, |
| WebGPUMsg::DeviceLost { |
| pipeline_id, |
| device, |
| reason, |
| msg, |
| } => { |
| let global = self.documents.borrow().find_global(pipeline_id).unwrap(); |
| global.gpu_device_lost(device, reason, msg); |
| }, |
| WebGPUMsg::UncapturedError { |
| device, |
| pipeline_id, |
| error, |
| } => { |
| let global = self.documents.borrow().find_global(pipeline_id).unwrap(); |
| let _ac = enter_realm(&*global); |
| global.handle_uncaptured_gpu_error(device, error); |
| }, |
| _ => {}, |
| } |
| } |
| |
| fn handle_msg_from_script(&self, msg: MainThreadScriptMsg) { |
| match msg { |
| MainThreadScriptMsg::Common(CommonScriptMsg::Task(_, task, pipeline_id, _)) => { |
| let _realm = pipeline_id.and_then(|id| { |
| let global = self.documents.borrow().find_global(id); |
| global.map(|global| enter_realm(&*global)) |
| }); |
| task.run_box() |
| }, |
| MainThreadScriptMsg::Common(CommonScriptMsg::CollectReports(chan)) => { |
| self.collect_reports(chan) |
| }, |
| MainThreadScriptMsg::Common(CommonScriptMsg::ReportCspViolations( |
| pipeline_id, |
| violations, |
| )) => { |
| if let Some(global) = self.documents.borrow().find_global(pipeline_id) { |
| global.report_csp_violations(violations, None, None); |
| } |
| }, |
| MainThreadScriptMsg::NavigationResponse { |
| pipeline_id, |
| message, |
| } => { |
| self.handle_navigation_response(pipeline_id, *message); |
| }, |
| MainThreadScriptMsg::WorkletLoaded(pipeline_id) => { |
| self.handle_worklet_loaded(pipeline_id) |
| }, |
| MainThreadScriptMsg::RegisterPaintWorklet { |
| pipeline_id, |
| name, |
| properties, |
| painter, |
| } => self.handle_register_paint_worklet(pipeline_id, name, properties, painter), |
| MainThreadScriptMsg::Inactive => {}, |
| MainThreadScriptMsg::WakeUp => {}, |
| } |
| } |
| |
| fn handle_msg_from_devtools(&self, msg: DevtoolScriptControlMsg, can_gc: CanGc) { |
| let documents = self.documents.borrow(); |
| match msg { |
| DevtoolScriptControlMsg::EvaluateJS(id, s, reply) => match documents.find_window(id) { |
| Some(window) => { |
| let global = window.as_global_scope(); |
| let _aes = AutoEntryScript::new(global); |
| devtools::handle_evaluate_js(global, s, reply, can_gc) |
| }, |
| None => warn!("Message sent to closed pipeline {}.", id), |
| }, |
| DevtoolScriptControlMsg::GetRootNode(id, reply) => { |
| devtools::handle_get_root_node(&documents, id, reply, can_gc) |
| }, |
| DevtoolScriptControlMsg::GetDocumentElement(id, reply) => { |
| devtools::handle_get_document_element(&documents, id, reply, can_gc) |
| }, |
| DevtoolScriptControlMsg::GetChildren(id, node_id, reply) => { |
| devtools::handle_get_children(&documents, id, node_id, reply, can_gc) |
| }, |
| DevtoolScriptControlMsg::GetAttributeStyle(id, node_id, reply) => { |
| devtools::handle_get_attribute_style(&documents, id, node_id, reply, can_gc) |
| }, |
| DevtoolScriptControlMsg::GetStylesheetStyle( |
| id, |
| node_id, |
| selector, |
| stylesheet, |
| reply, |
| ) => devtools::handle_get_stylesheet_style( |
| &documents, id, node_id, selector, stylesheet, reply, can_gc, |
| ), |
| DevtoolScriptControlMsg::GetSelectors(id, node_id, reply) => { |
| devtools::handle_get_selectors(&documents, id, node_id, reply, can_gc) |
| }, |
| DevtoolScriptControlMsg::GetComputedStyle(id, node_id, reply) => { |
| devtools::handle_get_computed_style(&documents, id, node_id, reply) |
| }, |
| DevtoolScriptControlMsg::GetLayout(id, node_id, reply) => { |
| devtools::handle_get_layout(&documents, id, node_id, reply, can_gc) |
| }, |
| DevtoolScriptControlMsg::ModifyAttribute(id, node_id, modifications) => { |
| devtools::handle_modify_attribute(&documents, id, node_id, modifications, can_gc) |
| }, |
| DevtoolScriptControlMsg::ModifyRule(id, node_id, modifications) => { |
| devtools::handle_modify_rule(&documents, id, node_id, modifications, can_gc) |
| }, |
| DevtoolScriptControlMsg::WantsLiveNotifications(id, to_send) => match documents |
| .find_window(id) |
| { |
| Some(window) => devtools::handle_wants_live_notifications(window.upcast(), to_send), |
| None => warn!("Message sent to closed pipeline {}.", id), |
| }, |
| DevtoolScriptControlMsg::SetTimelineMarkers(id, marker_types, reply) => { |
| devtools::handle_set_timeline_markers(&documents, id, marker_types, reply) |
| }, |
| DevtoolScriptControlMsg::DropTimelineMarkers(id, marker_types) => { |
| devtools::handle_drop_timeline_markers(&documents, id, marker_types) |
| }, |
| DevtoolScriptControlMsg::RequestAnimationFrame(id, name) => { |
| devtools::handle_request_animation_frame(&documents, id, name) |
| }, |
| DevtoolScriptControlMsg::Reload(id) => devtools::handle_reload(&documents, id, can_gc), |
| DevtoolScriptControlMsg::GetCssDatabase(reply) => { |
| devtools::handle_get_css_database(reply) |
| }, |
| DevtoolScriptControlMsg::SimulateColorScheme(id, theme) => { |
| match documents.find_window(id) { |
| Some(window) => { |
| window.handle_theme_change(theme); |
| }, |
| None => warn!("Message sent to closed pipeline {}.", id), |
| } |
| }, |
| DevtoolScriptControlMsg::HighlightDomNode(id, node_id) => { |
| devtools::handle_highlight_dom_node(&documents, id, node_id) |
| }, |
| DevtoolScriptControlMsg::GetPossibleBreakpoints(spidermonkey_id, result_sender) => { |
| self.debugger_global.fire_get_possible_breakpoints( |
| can_gc, |
| spidermonkey_id, |
| result_sender, |
| ); |
| }, |
| } |
| } |
| |
| fn handle_msg_from_image_cache(&self, response: ImageCacheResponseMessage) { |
| match response { |
| ImageCacheResponseMessage::NotifyPendingImageLoadStatus(pending_image_response) => { |
| let window = self |
| .documents |
| .borrow() |
| .find_window(pending_image_response.pipeline_id); |
| if let Some(ref window) = window { |
| window.pending_image_notification(pending_image_response); |
| } |
| }, |
| ImageCacheResponseMessage::VectorImageRasterizationComplete(response) => { |
| let window = self.documents.borrow().find_window(response.pipeline_id); |
| if let Some(ref window) = window { |
| window.handle_image_rasterization_complete_notification(response); |
| } |
| }, |
| }; |
| } |
| |
| fn handle_webdriver_msg( |
| &self, |
| pipeline_id: PipelineId, |
| msg: WebDriverScriptCommand, |
| can_gc: CanGc, |
| ) { |
| // https://github.com/servo/servo/issues/23535 |
| // These two messages need different treatment since the JS script might mutate |
| // `self.documents`, which would conflict with the immutable borrow of it that |
| // occurs for the rest of the messages |
| match msg { |
| WebDriverScriptCommand::ExecuteScript(script, reply) => { |
| let window = self.documents.borrow().find_window(pipeline_id); |
| return webdriver_handlers::handle_execute_script(window, script, reply, can_gc); |
| }, |
| WebDriverScriptCommand::ExecuteAsyncScript(script, reply) => { |
| let window = self.documents.borrow().find_window(pipeline_id); |
| return webdriver_handlers::handle_execute_async_script( |
| window, script, reply, can_gc, |
| ); |
| }, |
| _ => (), |
| } |
| |
| let documents = self.documents.borrow(); |
| match msg { |
| WebDriverScriptCommand::AddCookie(params, reply) => { |
| webdriver_handlers::handle_add_cookie(&documents, pipeline_id, params, reply) |
| }, |
| WebDriverScriptCommand::DeleteCookies(reply) => { |
| webdriver_handlers::handle_delete_cookies(&documents, pipeline_id, reply) |
| }, |
| WebDriverScriptCommand::DeleteCookie(name, reply) => { |
| webdriver_handlers::handle_delete_cookie(&documents, pipeline_id, name, reply) |
| }, |
| WebDriverScriptCommand::ElementClear(element_id, reply) => { |
| webdriver_handlers::handle_element_clear( |
| &documents, |
| pipeline_id, |
| element_id, |
| reply, |
| can_gc, |
| ) |
| }, |
| WebDriverScriptCommand::FindElementsCSSSelector(selector, reply) => { |
| webdriver_handlers::handle_find_elements_css_selector( |
| &documents, |
| pipeline_id, |
| selector, |
| reply, |
| ) |
| }, |
| WebDriverScriptCommand::FindElementsLinkText(selector, partial, reply) => { |
| webdriver_handlers::handle_find_elements_link_text( |
| &documents, |
| pipeline_id, |
| selector, |
| partial, |
| reply, |
| ) |
| }, |
| WebDriverScriptCommand::FindElementsTagName(selector, reply) => { |
| webdriver_handlers::handle_find_elements_tag_name( |
| &documents, |
| pipeline_id, |
| selector, |
| reply, |
| can_gc, |
| ) |
| }, |
| WebDriverScriptCommand::FindElementsXpathSelector(selector, reply) => { |
| webdriver_handlers::handle_find_elements_xpath_selector( |
| &documents, |
| pipeline_id, |
| selector, |
| reply, |
| can_gc, |
| ) |
| }, |
| WebDriverScriptCommand::FindElementElementsCSSSelector(selector, element_id, reply) => { |
| webdriver_handlers::handle_find_element_elements_css_selector( |
| &documents, |
| pipeline_id, |
| element_id, |
| selector, |
| reply, |
| ) |
| }, |
| WebDriverScriptCommand::FindElementElementsLinkText( |
| selector, |
| element_id, |
| partial, |
| reply, |
| ) => webdriver_handlers::handle_find_element_elements_link_text( |
| &documents, |
| pipeline_id, |
| element_id, |
| selector, |
| partial, |
| reply, |
| ), |
| WebDriverScriptCommand::FindElementElementsTagName(selector, element_id, reply) => { |
| webdriver_handlers::handle_find_element_elements_tag_name( |
| &documents, |
| pipeline_id, |
| element_id, |
| selector, |
| reply, |
| can_gc, |
| ) |
| }, |
| WebDriverScriptCommand::FindElementElementsXPathSelector( |
| selector, |
| element_id, |
| reply, |
| ) => webdriver_handlers::handle_find_element_elements_xpath_selector( |
| &documents, |
| pipeline_id, |
| element_id, |
| selector, |
| reply, |
| can_gc, |
| ), |
| WebDriverScriptCommand::FindShadowElementsCSSSelector( |
| selector, |
| shadow_root_id, |
| reply, |
| ) => webdriver_handlers::handle_find_shadow_elements_css_selector( |
| &documents, |
| pipeline_id, |
| shadow_root_id, |
| selector, |
| reply, |
| ), |
| WebDriverScriptCommand::FindShadowElementsLinkText( |
| selector, |
| shadow_root_id, |
| partial, |
| reply, |
| ) => webdriver_handlers::handle_find_shadow_elements_link_text( |
| &documents, |
| pipeline_id, |
| shadow_root_id, |
| selector, |
| partial, |
| reply, |
| ), |
| WebDriverScriptCommand::FindShadowElementsTagName(selector, shadow_root_id, reply) => { |
| webdriver_handlers::handle_find_shadow_elements_tag_name( |
| &documents, |
| pipeline_id, |
| shadow_root_id, |
| selector, |
| reply, |
| ) |
| }, |
| WebDriverScriptCommand::FindShadowElementsXPathSelector( |
| selector, |
| shadow_root_id, |
| reply, |
| ) => webdriver_handlers::handle_find_shadow_elements_xpath_selector( |
| &documents, |
| pipeline_id, |
| shadow_root_id, |
| selector, |
| reply, |
| can_gc, |
| ), |
| WebDriverScriptCommand::GetElementShadowRoot(element_id, reply) => { |
| webdriver_handlers::handle_get_element_shadow_root( |
| &documents, |
| pipeline_id, |
| element_id, |
| reply, |
| ) |
| }, |
| WebDriverScriptCommand::ElementClick(element_id, reply) => { |
| webdriver_handlers::handle_element_click( |
| &documents, |
| pipeline_id, |
| element_id, |
| reply, |
| can_gc, |
| ) |
| }, |
| WebDriverScriptCommand::GetKnownElement(element_id, reply) => { |
| webdriver_handlers::handle_get_known_element( |
| &documents, |
| pipeline_id, |
| element_id, |
| reply, |
| ) |
| }, |
| WebDriverScriptCommand::GetKnownShadowRoot(element_id, reply) => { |
| webdriver_handlers::handle_get_known_shadow_root( |
| &documents, |
| pipeline_id, |
| element_id, |
| reply, |
| ) |
| }, |
| WebDriverScriptCommand::GetActiveElement(reply) => { |
| webdriver_handlers::handle_get_active_element(&documents, pipeline_id, reply) |
| }, |
| WebDriverScriptCommand::GetComputedRole(node_id, reply) => { |
| webdriver_handlers::handle_get_computed_role( |
| &documents, |
| pipeline_id, |
| node_id, |
| reply, |
| ) |
| }, |
| WebDriverScriptCommand::GetPageSource(reply) => { |
| webdriver_handlers::handle_get_page_source(&documents, pipeline_id, reply, can_gc) |
| }, |
| WebDriverScriptCommand::GetCookies(reply) => { |
| webdriver_handlers::handle_get_cookies(&documents, pipeline_id, reply) |
| }, |
| WebDriverScriptCommand::GetCookie(name, reply) => { |
| webdriver_handlers::handle_get_cookie(&documents, pipeline_id, name, reply) |
| }, |
| WebDriverScriptCommand::GetElementTagName(node_id, reply) => { |
| webdriver_handlers::handle_get_name(&documents, pipeline_id, node_id, reply) |
| }, |
| WebDriverScriptCommand::GetElementAttribute(node_id, name, reply) => { |
| webdriver_handlers::handle_get_attribute( |
| &documents, |
| pipeline_id, |
| node_id, |
| name, |
| reply, |
| ) |
| }, |
| WebDriverScriptCommand::GetElementProperty(node_id, name, reply) => { |
| webdriver_handlers::handle_get_property( |
| &documents, |
| pipeline_id, |
| node_id, |
| name, |
| reply, |
| can_gc, |
| ) |
| }, |
| WebDriverScriptCommand::GetElementCSS(node_id, name, reply) => { |
| webdriver_handlers::handle_get_css(&documents, pipeline_id, node_id, name, reply) |
| }, |
| WebDriverScriptCommand::GetElementRect(node_id, reply) => { |
| webdriver_handlers::handle_get_rect(&documents, pipeline_id, node_id, reply, can_gc) |
| }, |
| WebDriverScriptCommand::GetBoundingClientRect(node_id, reply) => { |
| webdriver_handlers::handle_get_bounding_client_rect( |
| &documents, |
| pipeline_id, |
| node_id, |
| reply, |
| can_gc, |
| ) |
| }, |
| WebDriverScriptCommand::GetElementText(node_id, reply) => { |
| webdriver_handlers::handle_get_text(&documents, pipeline_id, node_id, reply) |
| }, |
| WebDriverScriptCommand::GetElementInViewCenterPoint(node_id, reply) => { |
| webdriver_handlers::handle_get_element_in_view_center_point( |
| &documents, |
| pipeline_id, |
| node_id, |
| reply, |
| can_gc, |
| ) |
| }, |
| WebDriverScriptCommand::GetParentFrameId(reply) => { |
| webdriver_handlers::handle_get_parent_frame_id(&documents, pipeline_id, reply) |
| }, |
| WebDriverScriptCommand::GetBrowsingContextId(webdriver_frame_id, reply) => { |
| webdriver_handlers::handle_get_browsing_context_id( |
| &documents, |
| pipeline_id, |
| webdriver_frame_id, |
| reply, |
| ) |
| }, |
| WebDriverScriptCommand::GetUrl(reply) => { |
| webdriver_handlers::handle_get_url(&documents, pipeline_id, reply, can_gc) |
| }, |
| WebDriverScriptCommand::IsEnabled(element_id, reply) => { |
| webdriver_handlers::handle_is_enabled(&documents, pipeline_id, element_id, reply) |
| }, |
| WebDriverScriptCommand::IsSelected(element_id, reply) => { |
| webdriver_handlers::handle_is_selected(&documents, pipeline_id, element_id, reply) |
| }, |
| WebDriverScriptCommand::GetTitle(reply) => { |
| webdriver_handlers::handle_get_title(&documents, pipeline_id, reply) |
| }, |
| WebDriverScriptCommand::WillSendKeys( |
| element_id, |
| text, |
| strict_file_interactability, |
| reply, |
| ) => webdriver_handlers::handle_will_send_keys( |
| &documents, |
| pipeline_id, |
| element_id, |
| text, |
| strict_file_interactability, |
| reply, |
| can_gc, |
| ), |
| WebDriverScriptCommand::AddLoadStatusSender(_, response_sender) => { |
| webdriver_handlers::handle_add_load_status_sender( |
| &documents, |
| pipeline_id, |
| response_sender, |
| ) |
| }, |
| WebDriverScriptCommand::RemoveLoadStatusSender(_) => { |
| webdriver_handlers::handle_remove_load_status_sender(&documents, pipeline_id) |
| }, |
| _ => (), |
| } |
| } |
| |
| /// Batch window resize operations into a single "update the rendering" task, |
| /// or, if a load is in progress, set the window size directly. |
| pub(crate) fn handle_resize_message( |
| &self, |
| id: PipelineId, |
| viewport_details: ViewportDetails, |
| size_type: WindowSizeType, |
| ) { |
| self.profile_event(ScriptThreadEventCategory::Resize, Some(id), || { |
| let window = self.documents.borrow().find_window(id); |
| if let Some(ref window) = window { |
| window.add_resize_event(viewport_details, size_type); |
| return; |
| } |
| let mut loads = self.incomplete_loads.borrow_mut(); |
| if let Some(ref mut load) = loads.iter_mut().find(|load| load.pipeline_id == id) { |
| load.viewport_details = viewport_details; |
| } |
| }) |
| } |
| |
| /// Handle changes to the theme, triggering reflow if the theme actually changed. |
| fn handle_theme_change_msg(&self, theme: Theme) { |
| for (_, document) in self.documents.borrow().iter() { |
| document.window().handle_theme_change(theme); |
| } |
| } |
| |
| // exit_fullscreen creates a new JS promise object, so we need to have entered a realm |
| fn handle_exit_fullscreen(&self, id: PipelineId, can_gc: CanGc) { |
| let document = self.documents.borrow().find_document(id); |
| if let Some(document) = document { |
| let _ac = enter_realm(&*document); |
| document.exit_fullscreen(can_gc); |
| } |
| } |
| |
| fn handle_viewport(&self, id: PipelineId, rect: Rect<f32>) { |
| let document = self.documents.borrow().find_document(id); |
| if let Some(document) = document { |
| document.window().set_viewport_size(rect.size); |
| return; |
| } |
| let loads = self.incomplete_loads.borrow(); |
| if loads.iter().any(|load| load.pipeline_id == id) { |
| return; |
| } |
| warn!("Page rect message sent to nonexistent pipeline"); |
| } |
| |
| fn handle_new_layout(&self, new_layout_info: NewLayoutInfo, origin: MutableOrigin) { |
| let NewLayoutInfo { |
| parent_info, |
| new_pipeline_id, |
| browsing_context_id, |
| webview_id, |
| opener, |
| load_data, |
| viewport_details, |
| theme, |
| } = new_layout_info; |
| |
| // Kick off the fetch for the new resource. |
| let url = load_data.url.clone(); |
| let new_load = InProgressLoad::new( |
| new_pipeline_id, |
| browsing_context_id, |
| webview_id, |
| parent_info, |
| opener, |
| viewport_details, |
| theme, |
| origin, |
| load_data, |
| ); |
| if url.as_str() == "about:blank" { |
| self.start_page_load_about_blank(new_load); |
| } else if url.as_str() == "about:srcdoc" { |
| self.page_load_about_srcdoc(new_load); |
| } else { |
| self.pre_page_load(new_load); |
| } |
| } |
| |
| fn collect_reports(&self, reports_chan: ReportsChan) { |
| let documents = self.documents.borrow(); |
| let urls = itertools::join(documents.iter().map(|(_, d)| d.url().to_string()), ", "); |
| |
| let mut reports = vec![]; |
| perform_memory_report(|ops| { |
| for (_, document) in documents.iter() { |
| document |
| .window() |
| .layout() |
| .collect_reports(&mut reports, ops); |
| } |
| |
| let prefix = format!("url({urls})"); |
| reports.extend(self.get_cx().get_reports(prefix.clone(), ops)); |
| }); |
| |
| reports_chan.send(ProcessReports::new(reports)); |
| } |
| |
| /// Updates iframe element after a change in visibility |
| fn handle_set_throttled_in_containing_iframe_msg( |
| &self, |
| parent_pipeline_id: PipelineId, |
| browsing_context_id: BrowsingContextId, |
| throttled: bool, |
| ) { |
| let iframe = self |
| .documents |
| .borrow() |
| .find_iframe(parent_pipeline_id, browsing_context_id); |
| if let Some(iframe) = iframe { |
| iframe.set_throttled(throttled); |
| } |
| } |
| |
| fn handle_set_throttled_msg(&self, id: PipelineId, throttled: bool) { |
| // Separate message sent since parent script thread could be different (Iframe of different |
| // domain) |
| self.senders |
| .pipeline_to_constellation_sender |
| .send(( |
| id, |
| ScriptToConstellationMessage::SetThrottledComplete(throttled), |
| )) |
| .unwrap(); |
| |
| let window = self.documents.borrow().find_window(id); |
| match window { |
| Some(window) => { |
| window.set_throttled(throttled); |
| return; |
| }, |
| None => { |
| let mut loads = self.incomplete_loads.borrow_mut(); |
| if let Some(ref mut load) = loads.iter_mut().find(|load| load.pipeline_id == id) { |
| load.throttled = throttled; |
| return; |
| } |
| }, |
| } |
| |
| warn!("SetThrottled sent to nonexistent pipeline"); |
| } |
| |
| /// Handles activity change message |
| fn handle_set_document_activity_msg( |
| &self, |
| id: PipelineId, |
| activity: DocumentActivity, |
| can_gc: CanGc, |
| ) { |
| debug!( |
| "Setting activity of {} to be {:?} in {:?}.", |
| id, |
| activity, |
| thread::current().name() |
| ); |
| let document = self.documents.borrow().find_document(id); |
| if let Some(document) = document { |
| document.set_activity(activity, can_gc); |
| return; |
| } |
| let mut loads = self.incomplete_loads.borrow_mut(); |
| if let Some(ref mut load) = loads.iter_mut().find(|load| load.pipeline_id == id) { |
| load.activity = activity; |
| return; |
| } |
| warn!("change of activity sent to nonexistent pipeline"); |
| } |
| |
| fn handle_focus_iframe_msg( |
| &self, |
| parent_pipeline_id: PipelineId, |
| browsing_context_id: BrowsingContextId, |
| sequence: FocusSequenceNumber, |
| can_gc: CanGc, |
| ) { |
| let document = self |
| .documents |
| .borrow() |
| .find_document(parent_pipeline_id) |
| .unwrap(); |
| |
| let Some(iframe_element_root) = ({ |
| // Enclose `iframes()` call and create a new root to avoid retaining |
| // borrow. |
| let iframes = document.iframes(); |
| iframes |
| .get(browsing_context_id) |
| .map(|iframe| DomRoot::from_ref(iframe.element.upcast())) |
| }) else { |
| return; |
| }; |
| |
| if document.get_focus_sequence() > sequence { |
| debug!( |
| "Disregarding the FocusIFrame message because the contained sequence number is \ |
| too old ({:?} < {:?})", |
| sequence, |
| document.get_focus_sequence() |
| ); |
| return; |
| } |
| |
| document.request_focus(Some(&iframe_element_root), FocusInitiator::Remote, can_gc); |
| } |
| |
| fn handle_focus_document_msg( |
| &self, |
| pipeline_id: PipelineId, |
| sequence: FocusSequenceNumber, |
| can_gc: CanGc, |
| ) { |
| if let Some(doc) = self.documents.borrow().find_document(pipeline_id) { |
| if doc.get_focus_sequence() > sequence { |
| debug!( |
| "Disregarding the FocusDocument message because the contained sequence number is \ |
| too old ({:?} < {:?})", |
| sequence, |
| doc.get_focus_sequence() |
| ); |
| return; |
| } |
| doc.request_focus(None, FocusInitiator::Remote, can_gc); |
| } else { |
| warn!( |
| "Couldn't find document by pipleline_id:{pipeline_id:?} when handle_focus_document_msg." |
| ); |
| } |
| } |
| |
| fn handle_unfocus_msg( |
| &self, |
| pipeline_id: PipelineId, |
| sequence: FocusSequenceNumber, |
| can_gc: CanGc, |
| ) { |
| if let Some(doc) = self.documents.borrow().find_document(pipeline_id) { |
| if doc.get_focus_sequence() > sequence { |
| debug!( |
| "Disregarding the Unfocus message because the contained sequence number is \ |
| too old ({:?} < {:?})", |
| sequence, |
| doc.get_focus_sequence() |
| ); |
| return; |
| } |
| doc.handle_container_unfocus(can_gc); |
| } else { |
| warn!( |
| "Couldn't find document by pipleline_id:{pipeline_id:?} when handle_unfocus_msg." |
| ); |
| } |
| } |
| |
| fn handle_post_message_msg( |
| &self, |
| pipeline_id: PipelineId, |
| source_pipeline_id: PipelineId, |
| source_browsing_context: WebViewId, |
| origin: Option<ImmutableOrigin>, |
| source_origin: ImmutableOrigin, |
| data: StructuredSerializedData, |
| ) { |
| let window = self.documents.borrow().find_window(pipeline_id); |
| match window { |
| None => warn!("postMessage after target pipeline {} closed.", pipeline_id), |
| Some(window) => { |
| // FIXME: synchronously talks to constellation. |
| // send the required info as part of postmessage instead. |
| let source = match self.remote_window_proxy( |
| window.upcast::<GlobalScope>(), |
| source_browsing_context, |
| source_pipeline_id, |
| None, |
| ) { |
| None => { |
| return warn!( |
| "postMessage after source pipeline {} closed.", |
| source_pipeline_id, |
| ); |
| }, |
| Some(source) => source, |
| }; |
| // FIXME(#22512): enqueues a task; unnecessary delay. |
| window.post_message(origin, source_origin, &source, data) |
| }, |
| } |
| } |
| |
| fn handle_stop_delaying_load_events_mode(&self, pipeline_id: PipelineId) { |
| let window = self.documents.borrow().find_window(pipeline_id); |
| if let Some(window) = window { |
| match window.undiscarded_window_proxy() { |
| Some(window_proxy) => window_proxy.stop_delaying_load_events_mode(), |
| None => warn!( |
| "Attempted to take {} of 'delaying-load-events-mode' after having been discarded.", |
| pipeline_id |
| ), |
| }; |
| } |
| } |
| |
| fn handle_unload_document(&self, pipeline_id: PipelineId, can_gc: CanGc) { |
| let document = self.documents.borrow().find_document(pipeline_id); |
| if let Some(document) = document { |
| document.unload(false, can_gc); |
| } |
| } |
| |
| fn handle_update_pipeline_id( |
| &self, |
| parent_pipeline_id: PipelineId, |
| browsing_context_id: BrowsingContextId, |
| webview_id: WebViewId, |
| new_pipeline_id: PipelineId, |
| reason: UpdatePipelineIdReason, |
| can_gc: CanGc, |
| ) { |
| let frame_element = self |
| .documents |
| .borrow() |
| .find_iframe(parent_pipeline_id, browsing_context_id); |
| if let Some(frame_element) = frame_element { |
| frame_element.update_pipeline_id(new_pipeline_id, reason, can_gc); |
| } |
| |
| if let Some(window) = self.documents.borrow().find_window(new_pipeline_id) { |
| // Ensure that the state of any local window proxies accurately reflects |
| // the new pipeline. |
| let _ = self.local_window_proxy( |
| &window, |
| browsing_context_id, |
| webview_id, |
| Some(parent_pipeline_id), |
| // Any local window proxy has already been created, so there |
| // is no need to pass along existing opener information that |
| // will be discarded. |
| None, |
| ); |
| } |
| } |
| |
| fn handle_update_history_state_msg( |
| &self, |
| pipeline_id: PipelineId, |
| history_state_id: Option<HistoryStateId>, |
| url: ServoUrl, |
| can_gc: CanGc, |
| ) { |
| let window = self.documents.borrow().find_window(pipeline_id); |
| match window { |
| None => { |
| warn!( |
| "update history state after pipeline {} closed.", |
| pipeline_id |
| ); |
| }, |
| Some(window) => window |
| .History() |
| .activate_state(history_state_id, url, can_gc), |
| } |
| } |
| |
| fn handle_remove_history_states( |
| &self, |
| pipeline_id: PipelineId, |
| history_states: Vec<HistoryStateId>, |
| ) { |
| let window = self.documents.borrow().find_window(pipeline_id); |
| match window { |
| None => { |
| warn!( |
| "update history state after pipeline {} closed.", |
| pipeline_id |
| ); |
| }, |
| Some(window) => window.History().remove_states(history_states), |
| } |
| } |
| |
| /// Window was resized, but this script was not active, so don't reflow yet |
| fn handle_resize_inactive_msg(&self, id: PipelineId, new_viewport_details: ViewportDetails) { |
| let window = self.documents.borrow().find_window(id) |
| .expect("ScriptThread: received a resize msg for a pipeline not in this script thread. This is a bug."); |
| window.set_viewport_details(new_viewport_details); |
| } |
| |
| /// We have received notification that the response associated with a load has completed. |
| /// Kick off the document and frame tree creation process using the result. |
| fn handle_page_headers_available( |
| &self, |
| id: &PipelineId, |
| metadata: Option<Metadata>, |
| can_gc: CanGc, |
| ) -> Option<DomRoot<ServoParser>> { |
| if self.closed_pipelines.borrow().contains(id) { |
| // If the pipeline closed, do not process the headers. |
| return None; |
| } |
| |
| let Some(idx) = self |
| .incomplete_loads |
| .borrow() |
| .iter() |
| .position(|load| load.pipeline_id == *id) |
| else { |
| unreachable!("Pipeline shouldn't have finished loading."); |
| }; |
| |
| // https://html.spec.whatwg.org/multipage/#process-a-navigate-response |
| // 2. If response's status is 204 or 205, then abort these steps. |
| let is_204_205 = match metadata { |
| Some(ref metadata) => metadata.status.in_range(204..=205), |
| _ => false, |
| }; |
| |
| if is_204_205 { |
| // If we have an existing window that is being navigated: |
| if let Some(window) = self.documents.borrow().find_window(*id) { |
| let window_proxy = window.window_proxy(); |
| // https://html.spec.whatwg.org/multipage/ |
| // #navigating-across-documents:delaying-load-events-mode-2 |
| if window_proxy.parent().is_some() { |
| // The user agent must take this nested browsing context |
| // out of the delaying load events mode |
| // when this navigation algorithm later matures, |
| // or when it terminates (whether due to having run all the steps, |
| // or being canceled, or being aborted), whichever happens first. |
| window_proxy.stop_delaying_load_events_mode(); |
| } |
| } |
| self.senders |
| .pipeline_to_constellation_sender |
| .send((*id, ScriptToConstellationMessage::AbortLoadUrl)) |
| .unwrap(); |
| return None; |
| }; |
| |
| let load = self.incomplete_loads.borrow_mut().remove(idx); |
| metadata.map(|meta| self.load(meta, load, can_gc)) |
| } |
| |
| /// Handles a request for the window title. |
| fn handle_get_title_msg(&self, pipeline_id: PipelineId) { |
| let document = match self.documents.borrow().find_document(pipeline_id) { |
| Some(document) => document, |
| None => return warn!("Message sent to closed pipeline {}.", pipeline_id), |
| }; |
| document.send_title_to_embedder(); |
| } |
| |
| /// Handles a request to exit a pipeline and shut down layout. |
| fn handle_exit_pipeline_msg( |
| &self, |
| webview_id: WebViewId, |
| id: PipelineId, |
| discard_bc: DiscardBrowsingContext, |
| can_gc: CanGc, |
| ) { |
| debug!("{id}: Starting pipeline exit."); |
| |
| // Abort the parser, if any, |
| // to prevent any further incoming networking messages from being handled. |
| let document = self.documents.borrow_mut().remove(id); |
| if let Some(document) = document { |
| // We should never have a pipeline that's still an incomplete load, but also has a Document. |
| debug_assert!( |
| !self |
| .incomplete_loads |
| .borrow() |
| .iter() |
| .any(|load| load.pipeline_id == id) |
| ); |
| |
| if let Some(parser) = document.get_current_parser() { |
| parser.abort(can_gc); |
| } |
| |
| debug!("{id}: Shutting down layout"); |
| document.window().layout_mut().exit_now(); |
| |
| // Clear any active animations and unroot all of the associated DOM objects. |
| debug!("{id}: Clearing animations"); |
| document.animations().clear(); |
| |
| // We discard the browsing context after requesting layout shut down, |
| // to avoid running layout on detached iframes. |
| let window = document.window(); |
| if discard_bc == DiscardBrowsingContext::Yes { |
| window.discard_browsing_context(); |
| } |
| |
| debug!("{id}: Clearing JavaScript runtime"); |
| window.clear_js_runtime(); |
| } |
| |
| // Prevent any further work for this Pipeline. |
| self.closed_pipelines.borrow_mut().insert(id); |
| |
| debug!("{id}: Sending PipelineExited message to constellation"); |
| self.senders |
| .pipeline_to_constellation_sender |
| .send((id, ScriptToConstellationMessage::PipelineExited)) |
| .ok(); |
| |
| self.compositor_api |
| .pipeline_exited(webview_id, id, PipelineExitSource::Script); |
| |
| debug!("{id}: Finished pipeline exit"); |
| } |
| |
| /// Handles a request to exit the script thread and shut down layout. |
| fn handle_exit_script_thread_msg(&self, can_gc: CanGc) { |
| debug!("Exiting script thread."); |
| |
| let mut webview_and_pipeline_ids = Vec::new(); |
| webview_and_pipeline_ids.extend( |
| self.incomplete_loads |
| .borrow() |
| .iter() |
| .next() |
| .map(|load| (load.webview_id, load.pipeline_id)), |
| ); |
| webview_and_pipeline_ids.extend( |
| self.documents |
| .borrow() |
| .iter() |
| .next() |
| .map(|(pipeline_id, document)| (document.webview_id(), pipeline_id)), |
| ); |
| |
| for (webview_id, pipeline_id) in webview_and_pipeline_ids { |
| self.handle_exit_pipeline_msg( |
| webview_id, |
| pipeline_id, |
| DiscardBrowsingContext::Yes, |
| can_gc, |
| ); |
| } |
| |
| self.background_hang_monitor.unregister(); |
| |
| // If we're in multiprocess mode, shut-down the IPC router for this process. |
| if opts::get().multiprocess { |
| debug!("Exiting IPC router thread in script thread."); |
| ROUTER.shutdown(); |
| } |
| |
| debug!("Exited script thread."); |
| } |
| |
| /// Handles animation tick requested during testing. |
| pub(crate) fn handle_tick_all_animations_for_testing(id: PipelineId) { |
| with_script_thread(|script_thread| { |
| let Some(document) = script_thread.documents.borrow().find_document(id) else { |
| warn!("Animation tick for tests for closed pipeline {id}."); |
| return; |
| }; |
| document.maybe_mark_animating_nodes_as_dirty(); |
| }); |
| } |
| |
| /// Handles a Web font being loaded. Does nothing if the page no longer exists. |
| fn handle_web_font_loaded(&self, pipeline_id: PipelineId, _success: bool) { |
| let Some(document) = self.documents.borrow().find_document(pipeline_id) else { |
| warn!("Web font loaded in closed pipeline {}.", pipeline_id); |
| return; |
| }; |
| |
| // TODO: This should only dirty nodes that are waiting for a web font to finish loading! |
| document.dirty_all_nodes(); |
| } |
| |
| /// Handles a worklet being loaded by triggering a relayout of the page. Does nothing if the |
| /// page no longer exists. |
| fn handle_worklet_loaded(&self, pipeline_id: PipelineId) { |
| if let Some(document) = self.documents.borrow().find_document(pipeline_id) { |
| document.add_restyle_reason(RestyleReason::PaintWorkletLoaded); |
| } |
| } |
| |
| /// Notify a window of a storage event |
| fn handle_storage_event( |
| &self, |
| pipeline_id: PipelineId, |
| storage_type: StorageType, |
| url: ServoUrl, |
| key: Option<String>, |
| old_value: Option<String>, |
| new_value: Option<String>, |
| ) { |
| let window = match self.documents.borrow().find_window(pipeline_id) { |
| None => return warn!("Storage event sent to closed pipeline {}.", pipeline_id), |
| Some(window) => window, |
| }; |
| |
| let storage = match storage_type { |
| StorageType::Local => window.LocalStorage(), |
| StorageType::Session => window.SessionStorage(), |
| }; |
| |
| storage.queue_storage_event(url, key, old_value, new_value); |
| } |
| |
| /// Notify the containing document of a child iframe that has completed loading. |
| fn handle_iframe_load_event( |
| &self, |
| parent_id: PipelineId, |
| browsing_context_id: BrowsingContextId, |
| child_id: PipelineId, |
| can_gc: CanGc, |
| ) { |
| let iframe = self |
| .documents |
| .borrow() |
| .find_iframe(parent_id, browsing_context_id); |
| match iframe { |
| Some(iframe) => iframe.iframe_load_event_steps(child_id, can_gc), |
| None => warn!("Message sent to closed pipeline {}.", parent_id), |
| } |
| } |
| |
| fn ask_constellation_for_browsing_context_info( |
| &self, |
| pipeline_id: PipelineId, |
| ) -> Option<(BrowsingContextId, Option<PipelineId>)> { |
| let (result_sender, result_receiver) = ipc::channel().unwrap(); |
| let msg = ScriptToConstellationMessage::GetBrowsingContextInfo(pipeline_id, result_sender); |
| self.senders |
| .pipeline_to_constellation_sender |
| .send((pipeline_id, msg)) |
| .expect("Failed to send to constellation."); |
| result_receiver |
| .recv() |
| .expect("Failed to get browsing context info from constellation.") |
| } |
| |
| fn ask_constellation_for_top_level_info( |
| &self, |
| sender_pipeline: PipelineId, |
| browsing_context_id: BrowsingContextId, |
| ) -> Option<WebViewId> { |
| let (result_sender, result_receiver) = ipc::channel().unwrap(); |
| let msg = ScriptToConstellationMessage::GetTopForBrowsingContext( |
| browsing_context_id, |
| result_sender, |
| ); |
| self.senders |
| .pipeline_to_constellation_sender |
| .send((sender_pipeline, msg)) |
| .expect("Failed to send to constellation."); |
| result_receiver |
| .recv() |
| .expect("Failed to get top-level id from constellation.") |
| } |
| |
| // Get the browsing context for a pipeline that may exist in another |
| // script thread. If the browsing context already exists in the |
| // `window_proxies` map, we return it, otherwise we recursively |
| // get the browsing context for the parent if there is one, |
| // construct a new dissimilar-origin browsing context, add it |
| // to the `window_proxies` map, and return it. |
| fn remote_window_proxy( |
| &self, |
| global_to_clone: &GlobalScope, |
| webview_id: WebViewId, |
| pipeline_id: PipelineId, |
| opener: Option<BrowsingContextId>, |
| ) -> Option<DomRoot<WindowProxy>> { |
| let (browsing_context_id, parent_pipeline_id) = |
| self.ask_constellation_for_browsing_context_info(pipeline_id)?; |
| if let Some(window_proxy) = self.window_proxies.borrow().get(&browsing_context_id) { |
| return Some(DomRoot::from_ref(window_proxy)); |
| } |
| |
| let parent_browsing_context = parent_pipeline_id.and_then(|parent_id| { |
| self.remote_window_proxy(global_to_clone, webview_id, parent_id, opener) |
| }); |
| |
| let opener_browsing_context = opener.and_then(ScriptThread::find_window_proxy); |
| |
| let creator = CreatorBrowsingContextInfo::from( |
| parent_browsing_context.as_deref(), |
| opener_browsing_context.as_deref(), |
| ); |
| |
| let window_proxy = WindowProxy::new_dissimilar_origin( |
| global_to_clone, |
| browsing_context_id, |
| webview_id, |
| parent_browsing_context.as_deref(), |
| opener, |
| creator, |
| ); |
| self.window_proxies |
| .borrow_mut() |
| .insert(browsing_context_id, Dom::from_ref(&*window_proxy)); |
| Some(window_proxy) |
| } |
| |
| // Get the browsing context for a pipeline that exists in this |
| // script thread. If the browsing context already exists in the |
| // `window_proxies` map, we return it, otherwise we recursively |
| // get the browsing context for the parent if there is one, |
| // construct a new similar-origin browsing context, add it |
| // to the `window_proxies` map, and return it. |
| fn local_window_proxy( |
| &self, |
| window: &Window, |
| browsing_context_id: BrowsingContextId, |
| webview_id: WebViewId, |
| parent_info: Option<PipelineId>, |
| opener: Option<BrowsingContextId>, |
| ) -> DomRoot<WindowProxy> { |
| if let Some(window_proxy) = self.window_proxies.borrow().get(&browsing_context_id) { |
| // Note: we do not set the window to be the currently-active one, |
| // this will be done instead when the script-thread handles the `SetDocumentActivity` msg. |
| return DomRoot::from_ref(window_proxy); |
| } |
| let iframe = parent_info.and_then(|parent_id| { |
| self.documents |
| .borrow() |
| .find_iframe(parent_id, browsing_context_id) |
| }); |
| let parent_browsing_context = match (parent_info, iframe.as_ref()) { |
| (_, Some(iframe)) => Some(iframe.owner_window().window_proxy()), |
| (Some(parent_id), _) => { |
| self.remote_window_proxy(window.upcast(), webview_id, parent_id, opener) |
| }, |
| _ => None, |
| }; |
| |
| let opener_browsing_context = opener.and_then(ScriptThread::find_window_proxy); |
| |
| let creator = CreatorBrowsingContextInfo::from( |
| parent_browsing_context.as_deref(), |
| opener_browsing_context.as_deref(), |
| ); |
| |
| let window_proxy = WindowProxy::new( |
| window, |
| browsing_context_id, |
| webview_id, |
| iframe.as_deref().map(Castable::upcast), |
| parent_browsing_context.as_deref(), |
| opener, |
| creator, |
| ); |
| self.window_proxies |
| .borrow_mut() |
| .insert(browsing_context_id, Dom::from_ref(&*window_proxy)); |
| window_proxy |
| } |
| |
| /// The entry point to document loading. Defines bindings, sets up the window and document |
| /// objects, parses HTML and CSS, and kicks off initial layout. |
| fn load( |
| &self, |
| metadata: Metadata, |
| incomplete: InProgressLoad, |
| can_gc: CanGc, |
| ) -> DomRoot<ServoParser> { |
| let final_url = metadata.final_url.clone(); |
| { |
| self.senders |
| .pipeline_to_constellation_sender |
| .send(( |
| incomplete.pipeline_id, |
| ScriptToConstellationMessage::SetFinalUrl(final_url.clone()), |
| )) |
| .unwrap(); |
| } |
| debug!( |
| "ScriptThread: loading {} on pipeline {:?}", |
| incomplete.load_data.url, incomplete.pipeline_id |
| ); |
| |
| let origin = if final_url.as_str() == "about:blank" || final_url.as_str() == "about:srcdoc" |
| { |
| incomplete.origin.clone() |
| } else { |
| MutableOrigin::new(final_url.origin()) |
| }; |
| |
| let script_to_constellation_chan = ScriptToConstellationChan { |
| sender: self.senders.pipeline_to_constellation_sender.clone(), |
| pipeline_id: incomplete.pipeline_id, |
| }; |
| |
| let font_context = Arc::new(FontContext::new( |
| self.system_font_service.clone(), |
| self.compositor_api.clone(), |
| self.resource_threads.clone(), |
| )); |
| |
| let image_cache = self |
| .image_cache |
| .create_new_image_cache(Some(incomplete.pipeline_id), self.compositor_api.clone()); |
| |
| let layout_config = LayoutConfig { |
| id: incomplete.pipeline_id, |
| webview_id: incomplete.webview_id, |
| url: final_url.clone(), |
| is_iframe: incomplete.parent_info.is_some(), |
| script_chan: self.senders.constellation_sender.clone(), |
| image_cache: image_cache.clone(), |
| font_context: font_context.clone(), |
| time_profiler_chan: self.senders.time_profiler_sender.clone(), |
| compositor_api: self.compositor_api.clone(), |
| viewport_details: incomplete.viewport_details, |
| theme: incomplete.theme, |
| }; |
| |
| // Create the window and document objects. |
| let window = Window::new( |
| incomplete.webview_id, |
| self.js_runtime.clone(), |
| self.senders.self_sender.clone(), |
| self.layout_factory.create(layout_config), |
| font_context, |
| self.senders.image_cache_sender.clone(), |
| image_cache.clone(), |
| self.resource_threads.clone(), |
| #[cfg(feature = "bluetooth")] |
| self.senders.bluetooth_sender.clone(), |
| self.senders.memory_profiler_sender.clone(), |
| self.senders.time_profiler_sender.clone(), |
| self.senders.devtools_server_sender.clone(), |
| script_to_constellation_chan, |
| self.senders.pipeline_to_embedder_sender.clone(), |
| self.senders.constellation_sender.clone(), |
| incomplete.pipeline_id, |
| incomplete.parent_info, |
| incomplete.viewport_details, |
| origin.clone(), |
| final_url.clone(), |
| // TODO(37417): Set correct top-level URL here. Currently, we only specify the |
| // url of the current window. However, in case this is an iframe, we should |
| // pass in the URL from the frame that includes the iframe (which potentially |
| // is another nested iframe in a frame). |
| final_url.clone(), |
| incomplete.navigation_start, |
| self.webgl_chan.as_ref().map(|chan| chan.channel()), |
| #[cfg(feature = "webxr")] |
| self.webxr_registry.clone(), |
| self.microtask_queue.clone(), |
| self.compositor_api.clone(), |
| self.unminify_js, |
| self.unminify_css, |
| self.local_script_source.clone(), |
| self.user_content_manager.clone(), |
| self.player_context.clone(), |
| #[cfg(feature = "webgpu")] |
| self.gpu_id_hub.clone(), |
| incomplete.load_data.inherited_secure_context, |
| incomplete.theme, |
| ); |
| self.debugger_global.fire_add_debuggee( |
| can_gc, |
| window.upcast(), |
| incomplete.pipeline_id, |
| None, |
| ); |
| |
| let _realm = enter_realm(&*window); |
| |
| // Initialize the browsing context for the window. |
| let window_proxy = self.local_window_proxy( |
| &window, |
| incomplete.browsing_context_id, |
| incomplete.webview_id, |
| incomplete.parent_info, |
| incomplete.opener, |
| ); |
| if window_proxy.parent().is_some() { |
| // https://html.spec.whatwg.org/multipage/#navigating-across-documents:delaying-load-events-mode-2 |
| // The user agent must take this nested browsing context |
| // out of the delaying load events mode |
| // when this navigation algorithm later matures. |
| window_proxy.stop_delaying_load_events_mode(); |
| } |
| window.init_window_proxy(&window_proxy); |
| |
| let last_modified = metadata.headers.as_ref().and_then(|headers| { |
| headers.typed_get::<LastModified>().map(|tm| { |
| let tm: SystemTime = tm.into(); |
| let local_time: DateTime<Local> = tm.into(); |
| local_time.format("%m/%d/%Y %H:%M:%S").to_string() |
| }) |
| }); |
| |
| let loader = DocumentLoader::new_with_threads( |
| self.resource_threads.clone(), |
| Some(final_url.clone()), |
| ); |
| |
| let content_type: Option<Mime> = metadata |
| .content_type |
| .map(Serde::into_inner) |
| .map(Mime::from_ct); |
| |
| let is_html_document = match content_type { |
| Some(ref mime) if mime.type_ == APPLICATION && mime.has_suffix("xml") => { |
| IsHTMLDocument::NonHTMLDocument |
| }, |
| |
| Some(ref mime) if mime.matches(TEXT, XML) || mime.matches(APPLICATION, XML) => { |
| IsHTMLDocument::NonHTMLDocument |
| }, |
| _ => IsHTMLDocument::HTMLDocument, |
| }; |
| |
| let referrer = metadata |
| .referrer |
| .as_ref() |
| .map(|referrer| referrer.clone().into_string()); |
| |
| let is_initial_about_blank = final_url.as_str() == "about:blank"; |
| |
| let document = Document::new( |
| &window, |
| HasBrowsingContext::Yes, |
| Some(final_url.clone()), |
| origin, |
| is_html_document, |
| content_type, |
| last_modified, |
| incomplete.activity, |
| DocumentSource::FromParser, |
| loader, |
| referrer, |
| Some(metadata.status.raw_code()), |
| incomplete.canceller, |
| is_initial_about_blank, |
| true, |
| incomplete.load_data.inherited_insecure_requests_policy, |
| incomplete.load_data.has_trustworthy_ancestor_origin, |
| self.custom_element_reaction_stack.clone(), |
| can_gc, |
| ); |
| |
| let referrer_policy = metadata |
| .headers |
| .as_deref() |
| .and_then(|h| h.typed_get::<ReferrerPolicyHeader>()) |
| .into(); |
| document.set_referrer_policy(referrer_policy); |
| |
| let refresh_header = metadata.headers.as_deref().and_then(|h| h.get(REFRESH)); |
| if let Some(refresh_val) = refresh_header { |
| // There are tests that this header handles Unicode code points |
| document.shared_declarative_refresh_steps(refresh_val.as_bytes()); |
| } |
| |
| document.set_ready_state(DocumentReadyState::Loading, can_gc); |
| |
| self.documents |
| .borrow_mut() |
| .insert(incomplete.pipeline_id, &document); |
| |
| window.init_document(&document); |
| |
| // For any similar-origin iframe, ensure that the contentWindow/contentDocument |
| // APIs resolve to the new window/document as soon as parsing starts. |
| if let Some(frame) = window_proxy |
| .frame_element() |
| .and_then(|e| e.downcast::<HTMLIFrameElement>()) |
| { |
| let parent_pipeline = frame.global().pipeline_id(); |
| self.handle_update_pipeline_id( |
| parent_pipeline, |
| window_proxy.browsing_context_id(), |
| window_proxy.webview_id(), |
| incomplete.pipeline_id, |
| UpdatePipelineIdReason::Navigation, |
| can_gc, |
| ); |
| } |
| |
| self.senders |
| .pipeline_to_constellation_sender |
| .send(( |
| incomplete.pipeline_id, |
| ScriptToConstellationMessage::ActivateDocument, |
| )) |
| .unwrap(); |
| |
| // Notify devtools that a new script global exists. |
| let incomplete_browsing_context_id: BrowsingContextId = incomplete.webview_id.into(); |
| let is_top_level_global = incomplete_browsing_context_id == incomplete.browsing_context_id; |
| self.notify_devtools( |
| document.Title(), |
| final_url.clone(), |
| is_top_level_global, |
| ( |
| incomplete.browsing_context_id, |
| incomplete.pipeline_id, |
| None, |
| incomplete.webview_id, |
| ), |
| ); |
| |
| document.set_https_state(metadata.https_state); |
| document.set_navigation_start(incomplete.navigation_start); |
| |
| if is_html_document == IsHTMLDocument::NonHTMLDocument { |
| ServoParser::parse_xml_document(&document, None, final_url, can_gc); |
| } else { |
| ServoParser::parse_html_document(&document, None, final_url, can_gc); |
| } |
| |
| if incomplete.activity == DocumentActivity::FullyActive { |
| window.resume(can_gc); |
| } else { |
| window.suspend(can_gc); |
| } |
| |
| if incomplete.throttled { |
| window.set_throttled(true); |
| } |
| |
| document.get_current_parser().unwrap() |
| } |
| |
| fn notify_devtools( |
| &self, |
| title: DOMString, |
| url: ServoUrl, |
| is_top_level_global: bool, |
| (browsing_context_id, pipeline_id, worker_id, webview_id): ( |
| BrowsingContextId, |
| PipelineId, |
| Option<WorkerId>, |
| WebViewId, |
| ), |
| ) { |
| if let Some(ref chan) = self.senders.devtools_server_sender { |
| let page_info = DevtoolsPageInfo { |
| title: String::from(title), |
| url, |
| is_top_level_global, |
| }; |
| chan.send(ScriptToDevtoolsControlMsg::NewGlobal( |
| (browsing_context_id, pipeline_id, worker_id, webview_id), |
| self.senders.devtools_client_to_script_thread_sender.clone(), |
| page_info.clone(), |
| )) |
| .unwrap(); |
| |
| let state = NavigationState::Stop(pipeline_id, page_info); |
| let _ = chan.send(ScriptToDevtoolsControlMsg::Navigate( |
| browsing_context_id, |
| state, |
| )); |
| } |
| } |
| |
| /// Queue compositor events for later dispatching as part of a |
| /// `update_the_rendering` task. |
| fn handle_input_event(&self, pipeline_id: PipelineId, event: ConstellationInputEvent) { |
| let Some(document) = self.documents.borrow().find_document(pipeline_id) else { |
| warn!("Compositor event sent to closed pipeline {pipeline_id}."); |
| return; |
| }; |
| |
| // Also send a 'click' event with same hit-test result if this is release |
| |
| // MAYBE? TODO: https://developer.mozilla.org/en-US/docs/Web/API/Element/click_event |
| // If the button is pressed on one element and the pointer is moved outside the element |
| // before the button is released, the event is fired on the most specific ancestor element |
| // that contained both elements. |
| |
| // But spec doesn't specify this https://w3c.github.io/uievents/#event-type-click |
| // "The click event type MUST be dispatched on the topmost event target indicated by |
| // the pointer, when the user presses down and releases the primary pointer button" |
| |
| // Servo-specific: Trigger if within 10px of the down point |
| if let InputEvent::MouseButton(mouse_button_event) = &event.event { |
| if let MouseButton::Left = mouse_button_event.button { |
| match mouse_button_event.action { |
| MouseButtonAction::Up => { |
| let pixel_dist = |
| self.relative_mouse_down_point.get() - mouse_button_event.point; |
| let pixel_dist = |
| (pixel_dist.x * pixel_dist.x + pixel_dist.y * pixel_dist.y).sqrt(); |
| if pixel_dist < 10.0 * document.window().device_pixel_ratio().get() { |
| // Pass webdriver_id to the newly generated click event |
| document.event_handler().note_pending_input_event( |
| ConstellationInputEvent { |
| hit_test_result: event.hit_test_result.clone(), |
| pressed_mouse_buttons: event.pressed_mouse_buttons, |
| active_keyboard_modifiers: event.active_keyboard_modifiers, |
| event: event.event.clone().with_webdriver_message_id(None), |
| }, |
| ); |
| document.event_handler().note_pending_input_event( |
| ConstellationInputEvent { |
| hit_test_result: event.hit_test_result, |
| pressed_mouse_buttons: event.pressed_mouse_buttons, |
| active_keyboard_modifiers: event.active_keyboard_modifiers, |
| event: InputEvent::MouseButton(MouseButtonEvent::new( |
| MouseButtonAction::Click, |
| mouse_button_event.button, |
| mouse_button_event.point, |
| )) |
| .with_webdriver_message_id(event.event.webdriver_message_id()), |
| }, |
| ); |
| return; |
| } |
| }, |
| MouseButtonAction::Down => { |
| self.relative_mouse_down_point.set(mouse_button_event.point) |
| }, |
| MouseButtonAction::Click => {}, |
| } |
| } |
| } |
| |
| document.event_handler().note_pending_input_event(event); |
| } |
| |
| /// Handle a "navigate an iframe" message from the constellation. |
| fn handle_navigate_iframe( |
| &self, |
| parent_pipeline_id: PipelineId, |
| browsing_context_id: BrowsingContextId, |
| load_data: LoadData, |
| history_handling: NavigationHistoryBehavior, |
| can_gc: CanGc, |
| ) { |
| let iframe = self |
| .documents |
| .borrow() |
| .find_iframe(parent_pipeline_id, browsing_context_id); |
| if let Some(iframe) = iframe { |
| iframe.navigate_or_reload_child_browsing_context(load_data, history_handling, can_gc); |
| } |
| } |
| |
| /// Turn javascript: URL into JS code to eval, according to the steps in |
| /// <https://html.spec.whatwg.org/multipage/#javascript-protocol> |
| pub(crate) fn eval_js_url(global_scope: &GlobalScope, load_data: &mut LoadData, can_gc: CanGc) { |
| // This slice of the URL’s serialization is equivalent to (5.) to (7.): |
| // Start with the scheme data of the parsed URL; |
| // append question mark and query component, if any; |
| // append number sign and fragment component if any. |
| let encoded = &load_data.url[Position::AfterScheme..][1..]; |
| |
| // Percent-decode (8.) and UTF-8 decode (9.) |
| let script_source = percent_decode(encoded.as_bytes()).decode_utf8_lossy(); |
| |
| // Script source is ready to be evaluated (11.) |
| let _ac = enter_realm(global_scope); |
| rooted!(in(*GlobalScope::get_cx()) let mut jsval = UndefinedValue()); |
| _ = global_scope.evaluate_js_on_global_with_result( |
| &script_source, |
| jsval.handle_mut(), |
| ScriptFetchOptions::default_classic_script(global_scope), |
| global_scope.api_base_url(), |
| can_gc, |
| Some(IntroductionType::JAVASCRIPT_URL), |
| ); |
| |
| load_data.js_eval_result = if jsval.get().is_string() { |
| let strval = DOMString::safe_from_jsval( |
| GlobalScope::get_cx(), |
| jsval.handle(), |
| StringificationBehavior::Empty, |
| ); |
| match strval { |
| Ok(ConversionResult::Success(s)) => { |
| Some(JsEvalResult::Ok(String::from(s).as_bytes().to_vec())) |
| }, |
| _ => None, |
| } |
| } else { |
| Some(JsEvalResult::NoContent) |
| }; |
| |
| load_data.url = ServoUrl::parse("about:blank").unwrap(); |
| } |
| |
| /// Instructs the constellation to fetch the document that will be loaded. Stores the InProgressLoad |
| /// argument until a notification is received that the fetch is complete. |
| fn pre_page_load(&self, mut incomplete: InProgressLoad) { |
| let context = ParserContext::new(incomplete.pipeline_id, incomplete.load_data.url.clone()); |
| self.incomplete_parser_contexts |
| .0 |
| .borrow_mut() |
| .push((incomplete.pipeline_id, context)); |
| |
| let request_builder = incomplete.request_builder(); |
| incomplete.canceller = FetchCanceller::new( |
| request_builder.id, |
| self.resource_threads.core_thread.clone(), |
| ); |
| NavigationListener::new(request_builder, self.senders.self_sender.clone()) |
| .initiate_fetch(&self.resource_threads.core_thread, None); |
| self.incomplete_loads.borrow_mut().push(incomplete); |
| } |
| |
| fn handle_navigation_response(&self, pipeline_id: PipelineId, message: FetchResponseMsg) { |
| if let Some(metadata) = NavigationListener::http_redirect_metadata(&message) { |
| self.handle_navigation_redirect(pipeline_id, metadata); |
| return; |
| }; |
| |
| match message { |
| FetchResponseMsg::ProcessResponse(request_id, metadata) => { |
| self.handle_fetch_metadata(pipeline_id, request_id, metadata) |
| }, |
| FetchResponseMsg::ProcessResponseChunk(request_id, chunk) => { |
| self.handle_fetch_chunk(pipeline_id, request_id, chunk) |
| }, |
| FetchResponseMsg::ProcessResponseEOF(request_id, eof) => { |
| self.handle_fetch_eof(pipeline_id, request_id, eof) |
| }, |
| FetchResponseMsg::ProcessCspViolations(request_id, violations) => { |
| self.handle_csp_violations(pipeline_id, request_id, violations) |
| }, |
| FetchResponseMsg::ProcessRequestBody(..) | FetchResponseMsg::ProcessRequestEOF(..) => { |
| }, |
| } |
| } |
| |
| fn handle_fetch_metadata( |
| &self, |
| id: PipelineId, |
| request_id: RequestId, |
| fetch_metadata: Result<FetchMetadata, NetworkError>, |
| ) { |
| match fetch_metadata { |
| Ok(_) => (), |
| Err(NetworkError::Crash(..)) => (), |
| Err(ref e) => { |
| warn!("Network error: {:?}", e); |
| }, |
| }; |
| |
| let mut incomplete_parser_contexts = self.incomplete_parser_contexts.0.borrow_mut(); |
| let parser = incomplete_parser_contexts |
| .iter_mut() |
| .find(|&&mut (pipeline_id, _)| pipeline_id == id); |
| if let Some(&mut (_, ref mut ctxt)) = parser { |
| ctxt.process_response(request_id, fetch_metadata); |
| } |
| } |
| |
| fn handle_fetch_chunk(&self, pipeline_id: PipelineId, request_id: RequestId, chunk: Vec<u8>) { |
| let mut incomplete_parser_contexts = self.incomplete_parser_contexts.0.borrow_mut(); |
| let parser = incomplete_parser_contexts |
| .iter_mut() |
| .find(|&&mut (parser_pipeline_id, _)| parser_pipeline_id == pipeline_id); |
| if let Some(&mut (_, ref mut ctxt)) = parser { |
| ctxt.process_response_chunk(request_id, chunk); |
| } |
| } |
| |
| fn handle_fetch_eof( |
| &self, |
| id: PipelineId, |
| request_id: RequestId, |
| eof: Result<ResourceFetchTiming, NetworkError>, |
| ) { |
| let idx = self |
| .incomplete_parser_contexts |
| .0 |
| .borrow() |
| .iter() |
| .position(|&(pipeline_id, _)| pipeline_id == id); |
| |
| if let Some(idx) = idx { |
| let (_, mut ctxt) = self.incomplete_parser_contexts.0.borrow_mut().remove(idx); |
| ctxt.process_response_eof(request_id, eof); |
| } |
| } |
| |
| fn handle_csp_violations(&self, id: PipelineId, _: RequestId, violations: Vec<Violation>) { |
| if let Some(global) = self.documents.borrow().find_global(id) { |
| // TODO(https://github.com/w3c/webappsec-csp/issues/687): Update after spec is resolved |
| global.report_csp_violations(violations, None, None); |
| } |
| } |
| |
| fn handle_navigation_redirect(&self, id: PipelineId, metadata: &Metadata) { |
| // TODO(mrobinson): This tries to accomplish some steps from |
| // <https://html.spec.whatwg.org/multipage/#process-a-navigate-fetch>, but it's |
| // very out of sync with the specification. |
| assert!(metadata.location_url.is_some()); |
| |
| let mut incomplete_loads = self.incomplete_loads.borrow_mut(); |
| let Some(incomplete_load) = incomplete_loads |
| .iter_mut() |
| .find(|incomplete_load| incomplete_load.pipeline_id == id) |
| else { |
| return; |
| }; |
| |
| // Update the `url_list` of the incomplete load to track all redirects. This will be reflected |
| // in the new `RequestBuilder` as well. |
| incomplete_load.url_list.push(metadata.final_url.clone()); |
| |
| let mut request_builder = incomplete_load.request_builder(); |
| request_builder.referrer = metadata |
| .referrer |
| .clone() |
| .map(Referrer::ReferrerUrl) |
| .unwrap_or(Referrer::NoReferrer); |
| request_builder.referrer_policy = metadata.referrer_policy; |
| |
| let headers = metadata |
| .headers |
| .as_ref() |
| .map(|headers| headers.clone().into_inner()) |
| .unwrap_or_default(); |
| |
| let response_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), |
| }); |
| |
| incomplete_load.canceller = FetchCanceller::new( |
| request_builder.id, |
| self.resource_threads.core_thread.clone(), |
| ); |
| NavigationListener::new(request_builder, self.senders.self_sender.clone()) |
| .initiate_fetch(&self.resource_threads.core_thread, response_init); |
| } |
| |
| /// Synchronously fetch `about:blank`. Stores the `InProgressLoad` |
| /// argument until a notification is received that the fetch is complete. |
| fn start_page_load_about_blank(&self, mut incomplete: InProgressLoad) { |
| let id = incomplete.pipeline_id; |
| |
| let url = ServoUrl::parse("about:blank").unwrap(); |
| let mut context = ParserContext::new(id, url.clone()); |
| |
| let mut meta = Metadata::default(url); |
| meta.set_content_type(Some(&mime::TEXT_HTML)); |
| meta.set_referrer_policy(incomplete.load_data.referrer_policy); |
| |
| // If this page load is the result of a javascript scheme url, map |
| // the evaluation result into a response. |
| let chunk = match incomplete.load_data.js_eval_result { |
| Some(JsEvalResult::Ok(ref mut content)) => std::mem::take(content), |
| Some(JsEvalResult::NoContent) => { |
| meta.status = http::StatusCode::NO_CONTENT.into(); |
| vec![] |
| }, |
| None => vec![], |
| }; |
| |
| let policy_container = incomplete.load_data.policy_container.clone(); |
| self.incomplete_loads.borrow_mut().push(incomplete); |
| |
| let dummy_request_id = RequestId::default(); |
| context.process_response(dummy_request_id, Ok(FetchMetadata::Unfiltered(meta))); |
| context.set_policy_container(policy_container.as_ref()); |
| context.process_response_chunk(dummy_request_id, chunk); |
| context.process_response_eof( |
| dummy_request_id, |
| Ok(ResourceFetchTiming::new(ResourceTimingType::None)), |
| ); |
| } |
| |
| /// Synchronously parse a srcdoc document from a giving HTML string. |
| fn page_load_about_srcdoc(&self, mut incomplete: InProgressLoad) { |
| let id = incomplete.pipeline_id; |
| |
| let url = ServoUrl::parse("about:srcdoc").unwrap(); |
| let mut meta = Metadata::default(url.clone()); |
| meta.set_content_type(Some(&mime::TEXT_HTML)); |
| meta.set_referrer_policy(incomplete.load_data.referrer_policy); |
| |
| let srcdoc = std::mem::take(&mut incomplete.load_data.srcdoc); |
| let chunk = srcdoc.into_bytes(); |
| |
| let policy_container = incomplete.load_data.policy_container.clone(); |
| self.incomplete_loads.borrow_mut().push(incomplete); |
| |
| let mut context = ParserContext::new(id, url); |
| let dummy_request_id = RequestId::default(); |
| |
| context.process_response(dummy_request_id, Ok(FetchMetadata::Unfiltered(meta))); |
| context.set_policy_container(policy_container.as_ref()); |
| context.process_response_chunk(dummy_request_id, chunk); |
| context.process_response_eof( |
| dummy_request_id, |
| Ok(ResourceFetchTiming::new(ResourceTimingType::None)), |
| ); |
| } |
| |
| fn handle_css_error_reporting( |
| &self, |
| pipeline_id: PipelineId, |
| filename: String, |
| line: u32, |
| column: u32, |
| msg: String, |
| ) { |
| let sender = match self.senders.devtools_server_sender { |
| Some(ref sender) => sender, |
| None => return, |
| }; |
| |
| if let Some(global) = self.documents.borrow().find_global(pipeline_id) { |
| if global.live_devtools_updates() { |
| let css_error = CSSError { |
| filename, |
| line, |
| column, |
| msg, |
| }; |
| let message = ScriptToDevtoolsControlMsg::ReportCSSError(pipeline_id, css_error); |
| sender.send(message).unwrap(); |
| } |
| } |
| } |
| |
| fn handle_reload(&self, pipeline_id: PipelineId, can_gc: CanGc) { |
| let window = self.documents.borrow().find_window(pipeline_id); |
| if let Some(window) = window { |
| window.Location().reload_without_origin_check(can_gc); |
| } |
| } |
| |
| fn handle_paint_metric( |
| &self, |
| pipeline_id: PipelineId, |
| metric_type: ProgressiveWebMetricType, |
| metric_value: CrossProcessInstant, |
| first_reflow: bool, |
| can_gc: CanGc, |
| ) { |
| match self.documents.borrow().find_document(pipeline_id) { |
| Some(document) => { |
| document.handle_paint_metric(metric_type, metric_value, first_reflow, can_gc) |
| }, |
| None => warn!( |
| "Received paint metric ({metric_type:?}) for unknown document: {pipeline_id:?}" |
| ), |
| } |
| } |
| |
| fn handle_media_session_action( |
| &self, |
| pipeline_id: PipelineId, |
| action: MediaSessionActionType, |
| can_gc: CanGc, |
| ) { |
| if let Some(window) = self.documents.borrow().find_window(pipeline_id) { |
| let media_session = window.Navigator().MediaSession(); |
| media_session.handle_action(action, can_gc); |
| } else { |
| warn!("No MediaSession for this pipeline ID"); |
| }; |
| } |
| |
| pub(crate) fn enqueue_microtask(job: Microtask) { |
| with_script_thread(|script_thread| { |
| script_thread |
| .microtask_queue |
| .enqueue(job, script_thread.get_cx()); |
| }); |
| } |
| |
| fn perform_a_microtask_checkpoint(&self, can_gc: CanGc) { |
| // Only perform the checkpoint if we're not shutting down. |
| if self.can_continue_running_inner() { |
| let globals = self |
| .documents |
| .borrow() |
| .iter() |
| .map(|(_id, document)| DomRoot::from_ref(document.window().upcast())) |
| .collect(); |
| |
| self.microtask_queue.checkpoint( |
| self.get_cx(), |
| |id| self.documents.borrow().find_global(id), |
| globals, |
| can_gc, |
| ) |
| } |
| } |
| |
| fn handle_evaluate_javascript( |
| &self, |
| pipeline_id: PipelineId, |
| evaluation_id: JavaScriptEvaluationId, |
| script: String, |
| can_gc: CanGc, |
| ) { |
| let Some(window) = self.documents.borrow().find_window(pipeline_id) else { |
| let _ = self.senders.pipeline_to_constellation_sender.send(( |
| pipeline_id, |
| ScriptToConstellationMessage::FinishJavaScriptEvaluation( |
| evaluation_id, |
| Err(JavaScriptEvaluationError::WebViewNotReady), |
| ), |
| )); |
| return; |
| }; |
| |
| let global_scope = window.as_global_scope(); |
| let realm = enter_realm(global_scope); |
| let context = window.get_cx(); |
| |
| rooted!(in(*context) let mut return_value = UndefinedValue()); |
| if let Err(err) = global_scope.evaluate_js_on_global_with_result( |
| &script, |
| return_value.handle_mut(), |
| ScriptFetchOptions::default_classic_script(global_scope), |
| global_scope.api_base_url(), |
| can_gc, |
| None, // No known `introductionType` for JS code from embedder |
| ) { |
| _ = self.senders.pipeline_to_constellation_sender.send(( |
| pipeline_id, |
| ScriptToConstellationMessage::FinishJavaScriptEvaluation(evaluation_id, Err(err)), |
| )); |
| return; |
| }; |
| |
| let result = jsval_to_webdriver( |
| context, |
| global_scope, |
| return_value.handle(), |
| (&realm).into(), |
| can_gc, |
| ) |
| .map_err(|_| JavaScriptEvaluationError::SerializationError); |
| |
| let _ = self.senders.pipeline_to_constellation_sender.send(( |
| pipeline_id, |
| ScriptToConstellationMessage::FinishJavaScriptEvaluation(evaluation_id, result), |
| )); |
| } |
| |
| fn handle_refresh_cursor(&self, pipeline_id: PipelineId) { |
| let Some(document) = self.documents.borrow().find_document(pipeline_id) else { |
| return; |
| }; |
| document.event_handler().handle_refresh_cursor(); |
| } |
| |
| pub(crate) fn is_servo_privileged(url: ServoUrl) -> bool { |
| with_script_thread(|script_thread| script_thread.privileged_urls.contains(&url)) |
| } |
| } |
| |
| impl Drop for ScriptThread { |
| fn drop(&mut self) { |
| SCRIPT_THREAD_ROOT.with(|root| { |
| root.set(None); |
| }); |
| } |
| } |