diff --git a/servo/components/compositing/constellation.rs b/servo/components/compositing/constellation.rs index 5f6a1f6527f3..d0c5003f9086 100644 --- a/servo/components/compositing/constellation.rs +++ b/servo/components/compositing/constellation.rs @@ -41,6 +41,7 @@ use profile_traits::mem; use profile_traits::time; use script_traits::{CompositorEvent, ConstellationControlMsg, LayoutControlMsg}; use script_traits::{ScriptState, ScriptTaskFactory}; +use script_traits::{TimerEventRequest}; use std::borrow::ToOwned; use std::collections::HashMap; use std::io::{self, Write}; @@ -49,6 +50,7 @@ use std::mem::replace; use std::process; use std::sync::mpsc::{Receiver, Sender, channel}; use style_traits::viewport::ViewportConstraints; +use timer_scheduler::TimerScheduler; use url::Url; use util::cursor::Cursor; use util::geometry::PagePx; @@ -135,6 +137,8 @@ pub struct Constellation { /// A list of in-process senders to `WebGLPaintTask`s. webgl_paint_tasks: Vec>, + + scheduler_chan: Sender, } /// State needed to construct a constellation. @@ -280,6 +284,7 @@ impl Constellation { webdriver: WebDriverData::new(), canvas_paint_tasks: Vec::new(), webgl_paint_tasks: Vec::new(), + scheduler_chan: TimerScheduler::start(), }; let namespace_id = constellation.next_pipeline_namespace_id(); PipelineNamespace::install(namespace_id); @@ -317,6 +322,7 @@ impl Constellation { id: pipeline_id, parent_info: parent_info, constellation_chan: self.chan.clone(), + scheduler_chan: self.scheduler_chan.clone(), compositor_proxy: self.compositor_proxy.clone_compositor_proxy(), devtools_chan: self.devtools_chan.clone(), image_cache_task: self.image_cache_task.clone(), diff --git a/servo/components/compositing/lib.rs b/servo/components/compositing/lib.rs index b8d6355bee23..a38f1d8490fa 100644 --- a/servo/components/compositing/lib.rs +++ b/servo/components/compositing/lib.rs @@ -6,6 +6,7 @@ #![feature(iter_cmp)] #![feature(slice_bytes)] #![feature(vec_push_all)] +#![feature(mpsc_select)] #![feature(plugin)] #![plugin(plugins)] @@ -54,6 +55,7 @@ mod compositor_layer; mod headless; mod scrolling; mod surface_map; +mod timer_scheduler; pub mod compositor_task; pub mod constellation; pub mod pipeline; diff --git a/servo/components/compositing/pipeline.rs b/servo/components/compositing/pipeline.rs index 344907307082..ad425224d265 100644 --- a/servo/components/compositing/pipeline.rs +++ b/servo/components/compositing/pipeline.rs @@ -24,6 +24,7 @@ use profile_traits::mem as profile_mem; use profile_traits::time; use script_traits::{ConstellationControlMsg, InitialScriptState}; use script_traits::{LayoutControlMsg, NewLayoutInfo, ScriptTaskFactory}; +use script_traits::{TimerEventRequest}; use std::any::Any; use std::mem; use std::sync::mpsc::{Receiver, Sender, channel}; @@ -75,6 +76,8 @@ pub struct InitialPipelineState { pub parent_info: Option<(PipelineId, SubpageId)>, /// A channel to the associated constellation. pub constellation_chan: ConstellationChan, + /// A channel to schedule timer events. + pub scheduler_chan: Sender, /// A channel to the compositor. pub compositor_proxy: Box, /// A channel to the developer tools, if applicable. @@ -181,6 +184,7 @@ impl Pipeline { id: state.id, parent_info: state.parent_info, constellation_chan: state.constellation_chan, + scheduler_chan: state.scheduler_chan, compositor_proxy: state.compositor_proxy, devtools_chan: script_to_devtools_chan, image_cache_task: state.image_cache_task, @@ -316,6 +320,7 @@ pub struct PipelineContent { id: PipelineId, parent_info: Option<(PipelineId, SubpageId)>, constellation_chan: ConstellationChan, + scheduler_chan: Sender, compositor_proxy: Box, devtools_chan: Option>, image_cache_task: ImageCacheTask, @@ -361,6 +366,7 @@ impl PipelineContent { control_chan: self.script_chan.clone(), control_port: mem::replace(&mut self.script_port, None).unwrap(), constellation_chan: self.constellation_chan.clone(), + scheduler_chan: self.scheduler_chan.clone(), failure_info: self.failure.clone(), resource_task: self.resource_task, storage_task: self.storage_task.clone(), diff --git a/servo/components/compositing/timer_scheduler.rs b/servo/components/compositing/timer_scheduler.rs new file mode 100644 index 000000000000..d29137be7880 --- /dev/null +++ b/servo/components/compositing/timer_scheduler.rs @@ -0,0 +1,221 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +use euclid::length::Length; +use num::traits::Saturating; +use script_traits::{MsDuration, NsDuration, precise_time_ms, precise_time_ns}; +use script_traits::{TimerEvent, TimerEventRequest}; +use std::cell::RefCell; +use std::cmp::{self, Ord}; +use std::collections::BinaryHeap; +use std::sync::Arc; +use std::sync::atomic::{self, AtomicBool}; +use std::sync::mpsc::{channel, Receiver, Select, Sender}; +use std::thread::{self, spawn, Thread}; +use util::task::spawn_named; + +/// A quick hack to work around the removal of [`std::old_io::timer::Timer`]( +/// http://doc.rust-lang.org/1.0.0-beta/std/old_io/timer/struct.Timer.html ) +struct CancelableOneshotTimer { + thread: Thread, + canceled: Arc, + port: Receiver<()>, +} + +impl CancelableOneshotTimer { + fn new(duration: MsDuration) -> CancelableOneshotTimer { + let (tx, rx) = channel(); + let canceled = Arc::new(AtomicBool::new(false)); + let canceled_clone = canceled.clone(); + + let thread = spawn(move || { + let due_time = precise_time_ms() + duration; + + let mut park_time = duration; + + loop { + thread::park_timeout_ms(park_time.get() as u32); + + if canceled_clone.load(atomic::Ordering::Relaxed) { + return; + } + + // park_timeout_ms does not guarantee parking for the + // given amout. We might have woken up early. + let current_time = precise_time_ms(); + if current_time >= due_time { + let _ = tx.send(()); + return; + } + park_time = due_time - current_time; + } + }).thread().clone(); + + CancelableOneshotTimer { + thread: thread, + canceled: canceled, + port: rx, + } + } + + fn port(&self) -> &Receiver<()> { + &self.port + } + + fn cancel(&self) { + self.canceled.store(true, atomic::Ordering::Relaxed); + self.thread.unpark(); + } +} + +pub struct TimerScheduler { + port: Receiver, + + scheduled_events: RefCell>, + + timer: RefCell>, +} + +struct ScheduledEvent { + request: TimerEventRequest, + for_time: NsDuration, +} + +impl Ord for ScheduledEvent { + fn cmp(&self, other: &ScheduledEvent) -> cmp::Ordering { + self.for_time.cmp(&other.for_time).reverse() + } +} + +impl PartialOrd for ScheduledEvent { + fn partial_cmp(&self, other: &ScheduledEvent) -> Option { + Some(self.cmp(other)) + } +} + +impl Eq for ScheduledEvent {} +impl PartialEq for ScheduledEvent { + fn eq(&self, other: &ScheduledEvent) -> bool { + self as *const ScheduledEvent == other as *const ScheduledEvent + } +} + +enum Task { + HandleRequest(TimerEventRequest), + DispatchDueEvents, +} + +impl TimerScheduler { + pub fn start() -> Sender { + let (chan, port) = channel(); + + let timer_scheduler = TimerScheduler { + port: port, + + scheduled_events: RefCell::new(BinaryHeap::new()), + + timer: RefCell::new(None), + }; + + spawn_named("TimerScheduler".to_owned(), move || { + timer_scheduler.run_event_loop(); + }); + + chan + } + + fn run_event_loop(&self) { + loop { + match self.receive_next_task() { + Some(Task::HandleRequest(request)) => self.handle_request(request), + Some(Task::DispatchDueEvents) => self.dispatch_due_events(), + None => break, + } + } + } + + #[allow(unsafe_code)] + fn receive_next_task(&self) -> Option { + let port = &self.port; + let timer = self.timer.borrow(); + let timer_port = timer.as_ref().map(|timer| timer.port()); + + if let Some(ref timer_port) = timer_port { + let sel = Select::new(); + let mut scheduler_handle = sel.handle(port); + let mut timer_handle = sel.handle(timer_port); + + unsafe { + scheduler_handle.add(); + timer_handle.add(); + } + + let ret = sel.wait(); + if ret == scheduler_handle.id() { + port.recv().ok().map(|req| Task::HandleRequest(req)) + } else if ret == timer_handle.id() { + timer_port.recv().ok().map(|_| Task::DispatchDueEvents) + } else { + panic!("unexpected select result!") + } + } else { + port.recv().ok().map(|req| Task::HandleRequest(req)) + } + } + + fn handle_request(&self, request: TimerEventRequest) { + let TimerEventRequest(_, _, _, duration_ms) = request; + let duration_ns = Length::new(duration_ms.get() * 1000 * 1000); + let schedule_for = precise_time_ns() + duration_ns; + + let previously_earliest = self.scheduled_events.borrow().peek() + .map(|scheduled| scheduled.for_time) + .unwrap_or(Length::new(u64::max_value())); + + self.scheduled_events.borrow_mut().push(ScheduledEvent { + request: request, + for_time: schedule_for, + }); + + if schedule_for < previously_earliest { + self.start_timer_for_next_event(); + } + } + + fn dispatch_due_events(&self) { + let now = precise_time_ns(); + + { + let mut events = self.scheduled_events.borrow_mut(); + + while !events.is_empty() && events.peek().as_ref().unwrap().for_time <= now { + let event = events.pop().unwrap(); + let TimerEventRequest(chan, source, id, _) = event.request; + + let _ = chan.send(TimerEvent(source, id)); + } + } + + self.start_timer_for_next_event(); + } + + fn start_timer_for_next_event(&self) { + let events = self.scheduled_events.borrow(); + let next_event = events.peek(); + + let mut timer = self.timer.borrow_mut(); + + if let Some(ref mut timer) = *timer { + timer.cancel(); + } + + *timer = next_event.map(|next_event| { + let delay_ns = next_event.for_time.get().saturating_sub(precise_time_ns().get()); + // Round up, we'd rather be late than early… + let delay_ms = Length::new(delay_ns.saturating_add(999999) / (1000 * 1000)); + + CancelableOneshotTimer::new(delay_ms) + }); + } +} diff --git a/servo/components/profile/time.rs b/servo/components/profile/time.rs index 43afb37ae17b..21acdba83669 100644 --- a/servo/components/profile/time.rs +++ b/servo/components/profile/time.rs @@ -92,6 +92,7 @@ impl Formattable for ProfilerCategory { ProfilerCategory::ScriptEvent => "Script Event", ProfilerCategory::ScriptUpdateReplacedElement => "Script Update Replaced Element", ProfilerCategory::ScriptSetViewport => "Script Set Viewport", + ProfilerCategory::ScriptTimerEvent => "Script Timer Event", ProfilerCategory::ScriptWebSocketEvent => "Script Web Socket Event", ProfilerCategory::ScriptWorkerEvent => "Script Worker Event", ProfilerCategory::ScriptXhrEvent => "Script Xhr Event", diff --git a/servo/components/profile_traits/time.rs b/servo/components/profile_traits/time.rs index 4056ee295d97..8cf485348f7e 100644 --- a/servo/components/profile_traits/time.rs +++ b/servo/components/profile_traits/time.rs @@ -70,6 +70,7 @@ pub enum ProfilerCategory { ScriptEvent, ScriptUpdateReplacedElement, ScriptSetViewport, + ScriptTimerEvent, ScriptWebSocketEvent, ScriptWorkerEvent, ScriptXhrEvent, diff --git a/servo/components/script/dom/bindings/global.rs b/servo/components/script/dom/bindings/global.rs index d45d3b154b11..cfb1acb99e96 100644 --- a/servo/components/script/dom/bindings/global.rs +++ b/servo/components/script/dom/bindings/global.rs @@ -22,6 +22,8 @@ use msg::constellation_msg::{ConstellationChan, PipelineId, WorkerId}; use net_traits::ResourceTask; use profile_traits::mem; use script_task::{CommonScriptMsg, ScriptChan, ScriptPort, ScriptTask}; +use script_traits::TimerEventRequest; +use std::sync::mpsc::Sender; use url::Url; use util::mem::HeapSizeOf; @@ -96,6 +98,14 @@ impl<'a> GlobalRef<'a> { } } + /// Get the scheduler channel to request timer events. + pub fn scheduler_chan(&self) -> Sender { + match *self { + GlobalRef::Window(window) => window.scheduler_chan(), + GlobalRef::Worker(worker) => worker.scheduler_chan(), + } + } + /// Get an `IpcSender` to send messages to Devtools /// task when available. pub fn devtools_chan(&self) -> Option> { diff --git a/servo/components/script/dom/bindings/trace.rs b/servo/components/script/dom/bindings/trace.rs index 00a7f6c6c90c..3a9c256155a5 100644 --- a/servo/components/script/dom/bindings/trace.rs +++ b/servo/components/script/dom/bindings/trace.rs @@ -37,6 +37,7 @@ use dom::bindings::js::{JS, Root}; use dom::bindings::refcounted::Trusted; use dom::bindings::utils::{Reflectable, Reflector, WindowProxyHandler}; use encoding::types::EncodingRef; +use euclid::length::Length as EuclidLength; use euclid::matrix2d::Matrix2D; use euclid::rect::Rect; use euclid::size::Size2D; @@ -58,7 +59,7 @@ use net_traits::storage_task::StorageType; use profile_traits::mem::ProfilerChan as MemProfilerChan; use profile_traits::time::ProfilerChan as TimeProfilerChan; use script_task::ScriptChan; -use script_traits::UntrustedNodeAddress; +use script_traits::{TimerEventChan, TimerEventId, TimerSource, UntrustedNodeAddress}; use selectors::parser::PseudoElement; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; @@ -283,6 +284,7 @@ no_jsmanaged_fields!(HashSet); // These three are interdependent, if you plan to put jsmanaged data // in one of these make sure it is propagated properly to containing structs no_jsmanaged_fields!(SubpageId, WindowSizeData, PipelineId); +no_jsmanaged_fields!(TimerEventId, TimerSource); no_jsmanaged_fields!(WorkerId); no_jsmanaged_fields!(QuirksMode); no_jsmanaged_fields!(Runtime); @@ -293,6 +295,7 @@ no_jsmanaged_fields!(WindowProxyHandler); no_jsmanaged_fields!(UntrustedNodeAddress); no_jsmanaged_fields!(LengthOrPercentageOrAuto); no_jsmanaged_fields!(RGBA); +no_jsmanaged_fields!(EuclidLength); no_jsmanaged_fields!(Matrix2D); no_jsmanaged_fields!(StorageType); no_jsmanaged_fields!(CanvasGradientStop, LinearGradientStyle, RadialGradientStyle); @@ -311,6 +314,13 @@ impl JSTraceable for Box { } } +impl JSTraceable for Box { + #[inline] + fn trace(&self, _trc: *mut JSTracer) { + // Do nothing + } +} + impl JSTraceable for Box { #[inline] fn trace(&self, _trc: *mut JSTracer) { diff --git a/servo/components/script/dom/dedicatedworkerglobalscope.rs b/servo/components/script/dom/dedicatedworkerglobalscope.rs index 413a47bb5264..2c9a6f9f629c 100644 --- a/servo/components/script/dom/dedicatedworkerglobalscope.rs +++ b/servo/components/script/dom/dedicatedworkerglobalscope.rs @@ -29,7 +29,8 @@ use msg::constellation_msg::PipelineId; use net_traits::load_whole_resource; use rand::random; use script_task::ScriptTaskEventCategory::WorkerEvent; -use script_task::{CommonScriptMsg, ScriptChan, ScriptPort, ScriptTask, StackRootTLS, TimerSource}; +use script_task::{ScriptTask, ScriptChan, ScriptPort, StackRootTLS, CommonScriptMsg}; +use script_traits::{TimerEvent, TimerEventChan, TimerSource}; use std::mem::replace; use std::rc::Rc; use std::sync::mpsc::{Receiver, RecvError, Select, Sender, channel}; @@ -101,6 +102,29 @@ impl ScriptPort for Receiver<(TrustedWorkerAddress, WorkerScriptMsg)> { } } +/// A TimerEventChan that can be cloned freely and will silently send a TrustedWorkerAddress +/// with timer events. While this SendableWorkerScriptChan is alive, the associated Worker +/// object will remain alive. +struct WorkerThreadTimerEventChan { + sender: Sender<(TrustedWorkerAddress, TimerEvent)>, + worker: TrustedWorkerAddress, +} + +impl TimerEventChan for WorkerThreadTimerEventChan { + fn send(&self, event: TimerEvent) -> Result<(), ()> { + self.sender + .send((self.worker.clone(), event)) + .map_err(|_| ()) + } + + fn clone(&self) -> Box { + box WorkerThreadTimerEventChan { + sender: self.sender.clone(), + worker: self.worker.clone(), + } + } +} + /// Set the `worker` field of a related DedicatedWorkerGlobalScope object to a particular /// value for the duration of this object's lifetime. This ensures that the related Worker /// object only lives as long as necessary (ie. while events are being executed), while @@ -127,6 +151,7 @@ impl<'a> Drop for AutoWorkerReset<'a> { enum MixedMessage { FromWorker((TrustedWorkerAddress, WorkerScriptMsg)), + FromScheduler((TrustedWorkerAddress, TimerEvent)), FromDevtools(DevtoolScriptControlMsg), } @@ -139,6 +164,8 @@ pub struct DedicatedWorkerGlobalScope { receiver: Receiver<(TrustedWorkerAddress, WorkerScriptMsg)>, #[ignore_heap_size_of = "Defined in std"] own_sender: Sender<(TrustedWorkerAddress, WorkerScriptMsg)>, + #[ignore_heap_size_of = "Defined in std"] + timer_event_port: Receiver<(TrustedWorkerAddress, TimerEvent)>, #[ignore_heap_size_of = "Trusted has unclear ownership like JS"] worker: DOMRefCell>, #[ignore_heap_size_of = "Can't measure trait objects"] @@ -154,14 +181,18 @@ impl DedicatedWorkerGlobalScope { runtime: Rc, parent_sender: Box, own_sender: Sender<(TrustedWorkerAddress, WorkerScriptMsg)>, - receiver: Receiver<(TrustedWorkerAddress, WorkerScriptMsg)>) + receiver: Receiver<(TrustedWorkerAddress, WorkerScriptMsg)>, + timer_event_chan: Box, + timer_event_port: Receiver<(TrustedWorkerAddress, TimerEvent)>) -> DedicatedWorkerGlobalScope { + DedicatedWorkerGlobalScope { workerglobalscope: WorkerGlobalScope::new_inherited( - init, worker_url, runtime, from_devtools_receiver), + init, worker_url, runtime, from_devtools_receiver, timer_event_chan), id: id, receiver: receiver, own_sender: own_sender, + timer_event_port: timer_event_port, parent_sender: parent_sender, worker: DOMRefCell::new(None), } @@ -174,11 +205,13 @@ impl DedicatedWorkerGlobalScope { runtime: Rc, parent_sender: Box, own_sender: Sender<(TrustedWorkerAddress, WorkerScriptMsg)>, - receiver: Receiver<(TrustedWorkerAddress, WorkerScriptMsg)>) + receiver: Receiver<(TrustedWorkerAddress, WorkerScriptMsg)>, + timer_event_chan: Box, + timer_event_port: Receiver<(TrustedWorkerAddress, TimerEvent)>) -> Root { let scope = box DedicatedWorkerGlobalScope::new_inherited( init, worker_url, id, from_devtools_receiver, runtime.clone(), parent_sender, - own_sender, receiver); + own_sender, receiver, timer_event_chan, timer_event_port); DedicatedWorkerGlobalScopeBinding::Wrap(runtime.cx(), scope) } @@ -214,9 +247,16 @@ impl DedicatedWorkerGlobalScope { let (devtools_mpsc_chan, devtools_mpsc_port) = channel(); ROUTER.route_ipc_receiver_to_mpsc_sender(from_devtools_receiver, devtools_mpsc_chan); + let (timer_tx, timer_rx) = channel(); + let timer_event_chan = box WorkerThreadTimerEventChan { + sender: timer_tx, + worker: worker.clone(), + }; + let global = DedicatedWorkerGlobalScope::new( init, url, id, devtools_mpsc_port, runtime.clone(), - parent_sender.clone(), own_sender, receiver); + parent_sender.clone(), own_sender, receiver, + timer_event_chan, timer_rx); // FIXME(njn): workers currently don't have a unique ID suitable for using in reporter // registration (#6631), so we instead use a random number and cross our fingers. let scope = global.upcast::(); @@ -263,13 +303,16 @@ impl DedicatedWorkerGlobalScope { fn receive_event(&self) -> Result { let scope = self.upcast::(); let worker_port = &self.receiver; + let timer_event_port = &self.timer_event_port; let devtools_port = scope.from_devtools_receiver(); let sel = Select::new(); let mut worker_handle = sel.handle(worker_port); + let mut timer_event_handle = sel.handle(timer_event_port); let mut devtools_handle = sel.handle(devtools_port); unsafe { worker_handle.add(); + timer_event_handle.add(); if scope.from_devtools_sender().is_some() { devtools_handle.add(); } @@ -277,6 +320,8 @@ impl DedicatedWorkerGlobalScope { let ret = sel.wait(); if ret == worker_handle.id() { Ok(MixedMessage::FromWorker(try!(worker_port.recv()))) + } else if ret == timer_event_handle.id() { + Ok(MixedMessage::FromScheduler(try!(timer_event_port.recv()))) } else if ret == devtools_handle.id() { Ok(MixedMessage::FromDevtools(try!(devtools_port.recv()))) } else { @@ -301,11 +346,6 @@ impl DedicatedWorkerGlobalScope { WorkerScriptMsg::Common(CommonScriptMsg::RefcountCleanup(addr)) => { LiveDOMReferences::cleanup(addr); }, - WorkerScriptMsg::Common( - CommonScriptMsg::FireTimer(TimerSource::FromWorker, timer_id)) => { - let scope = self.upcast::(); - scope.handle_fire_timer(timer_id); - }, WorkerScriptMsg::Common(CommonScriptMsg::CollectReports(reports_chan)) => { let scope = self.upcast::(); let cx = scope.get_cx(); @@ -313,9 +353,6 @@ impl DedicatedWorkerGlobalScope { let reports = ScriptTask::get_reports(cx, path_seg); reports_chan.send(reports); }, - WorkerScriptMsg::Common(CommonScriptMsg::FireTimer(_, _)) => { - panic!("obtained a fire timeout from window for the worker!") - }, } } @@ -333,6 +370,18 @@ impl DedicatedWorkerGlobalScope { _ => debug!("got an unusable devtools control message inside the worker!"), } }, + MixedMessage::FromScheduler((linked_worker, timer_event)) => { + match timer_event { + TimerEvent(TimerSource::FromWorker, id) => { + let _ar = AutoWorkerReset::new(self, linked_worker); + let scope = self.upcast::(); + scope.handle_fire_timer(id); + }, + TimerEvent(_, _) => { + panic!("A worker received a TimerEvent from a window.") + } + } + } MixedMessage::FromWorker((linked_worker, msg)) => { let _ar = AutoWorkerReset::new(self, linked_worker); self.handle_script_event(msg); diff --git a/servo/components/script/dom/window.rs b/servo/components/script/dom/window.rs index 5a9233319c1a..5906a28cb994 100644 --- a/servo/components/script/dom/window.rs +++ b/servo/components/script/dom/window.rs @@ -52,9 +52,9 @@ use num::traits::ToPrimitive; use page::Page; use profile_traits::mem; use rustc_serialize::base64::{FromBase64, STANDARD, ToBase64}; -use script_task::{MainThreadScriptChan, SendableMainThreadScriptChan}; -use script_task::{MainThreadScriptMsg, ScriptChan, ScriptPort, TimerSource}; -use script_traits::ConstellationControlMsg; +use script_task::{ScriptChan, ScriptPort, MainThreadScriptMsg}; +use script_task::{SendableMainThreadScriptChan, MainThreadScriptChan, MainThreadTimerEventChan}; +use script_traits::{ConstellationControlMsg, TimerEventChan, TimerEventId, TimerEventRequest, TimerSource}; use selectors::parser::PseudoElement; use std::ascii::AsciiExt; use std::borrow::ToOwned; @@ -70,7 +70,7 @@ use std::sync::mpsc::TryRecvError::{Disconnected, Empty}; use std::sync::mpsc::{Receiver, Sender, channel}; use string_cache::Atom; use time; -use timers::{IsInterval, TimerCallback, TimerId, TimerManager}; +use timers::{ActiveTimers, IsInterval, TimerCallback}; use url::Url; use util::geometry::{self, MAX_RECT}; use util::str::{DOMString, HTML_SPACE_CHARACTERS}; @@ -129,7 +129,9 @@ pub struct Window { screen: MutNullableHeap>, session_storage: MutNullableHeap>, local_storage: MutNullableHeap>, - timers: TimerManager, + #[ignore_heap_size_of = "channels are hard"] + scheduler_chan: Sender, + timers: ActiveTimers, next_worker_id: Cell, @@ -425,8 +427,7 @@ impl WindowMethods for Window { args, timeout, IsInterval::NonInterval, - TimerSource::FromWindow(self.id.clone()), - self.script_chan.clone()) + TimerSource::FromWindow(self.id.clone())) } // https://html.spec.whatwg.org/multipage/#dom-windowtimers-settimeout @@ -435,8 +436,7 @@ impl WindowMethods for Window { args, timeout, IsInterval::NonInterval, - TimerSource::FromWindow(self.id.clone()), - self.script_chan.clone()) + TimerSource::FromWindow(self.id.clone())) } // https://html.spec.whatwg.org/multipage/#dom-windowtimers-cleartimeout @@ -450,8 +450,7 @@ impl WindowMethods for Window { args, timeout, IsInterval::Interval, - TimerSource::FromWindow(self.id.clone()), - self.script_chan.clone()) + TimerSource::FromWindow(self.id.clone())) } // https://html.spec.whatwg.org/multipage/#dom-windowtimers-setinterval @@ -460,8 +459,7 @@ impl WindowMethods for Window { args, timeout, IsInterval::Interval, - TimerSource::FromWindow(self.id.clone()), - self.script_chan.clone()) + TimerSource::FromWindow(self.id.clone())) } // https://html.spec.whatwg.org/multipage/#dom-windowtimers-clearinterval @@ -1076,7 +1074,7 @@ impl Window { MainThreadScriptMsg::Navigate(self.id, LoadData::new(url))).unwrap(); } - pub fn handle_fire_timer(&self, timer_id: TimerId) { + pub fn handle_fire_timer(&self, timer_id: TimerEventId) { self.timers.fire_timer(timer_id, self); self.reflow(ReflowGoal::ForDisplay, ReflowQueryType::NoQuery, ReflowReason::Timer); } @@ -1122,6 +1120,10 @@ impl Window { self.constellation_chan.clone() } + pub fn scheduler_chan(&self) -> Sender { + self.scheduler_chan.clone() + } + pub fn windowproxy_handler(&self) -> WindowProxyHandler { WindowProxyHandler(self.dom_static.windowproxy_handler.0) } @@ -1267,6 +1269,8 @@ impl Window { mem_profiler_chan: mem::ProfilerChan, devtools_chan: Option>, constellation_chan: ConstellationChan, + scheduler_chan: Sender, + timer_event_chan: MainThreadTimerEventChan, layout_chan: LayoutChan, id: PipelineId, parent_info: Option<(PipelineId, SubpageId)>, @@ -1299,7 +1303,8 @@ impl Window { screen: Default::default(), session_storage: Default::default(), local_storage: Default::default(), - timers: TimerManager::new(), + scheduler_chan: scheduler_chan.clone(), + timers: ActiveTimers::new(box timer_event_chan, scheduler_chan), next_worker_id: Cell::new(WorkerId(0)), id: id, parent_info: parent_info, diff --git a/servo/components/script/dom/worker.rs b/servo/components/script/dom/worker.rs index e3fc2a202e52..b80f89720fa1 100644 --- a/servo/components/script/dom/worker.rs +++ b/servo/components/script/dom/worker.rs @@ -72,6 +72,7 @@ impl Worker { let resource_task = global.resource_task(); let constellation_chan = global.constellation_chan(); + let scheduler_chan = global.scheduler_chan(); let (sender, receiver) = channel(); let worker = Worker::new(global, sender.clone()); @@ -101,6 +102,7 @@ impl Worker { to_devtools_sender: global.devtools_chan(), from_devtools_sender: optional_sender, constellation_chan: constellation_chan, + scheduler_chan: scheduler_chan, worker_id: worker_id, }; DedicatedWorkerGlobalScope::run_worker_scope( diff --git a/servo/components/script/dom/workerglobalscope.rs b/servo/components/script/dom/workerglobalscope.rs index 40692b99e7eb..abf7f9352401 100644 --- a/servo/components/script/dom/workerglobalscope.rs +++ b/servo/components/script/dom/workerglobalscope.rs @@ -23,12 +23,13 @@ use js::rust::Runtime; use msg::constellation_msg::{ConstellationChan, PipelineId, WorkerId}; use net_traits::{ResourceTask, load_whole_resource}; use profile_traits::mem; -use script_task::{CommonScriptMsg, ScriptChan, ScriptPort, TimerSource}; +use script_task::{CommonScriptMsg, ScriptChan, ScriptPort}; +use script_traits::{TimerEventChan, TimerEventId, TimerEventRequest, TimerSource}; use std::cell::Cell; use std::default::Default; use std::rc::Rc; -use std::sync::mpsc::Receiver; -use timers::{IsInterval, TimerCallback, TimerId, TimerManager}; +use std::sync::mpsc::{Receiver, Sender}; +use timers::{ActiveTimers, IsInterval, TimerCallback}; use url::{Url, UrlParser}; use util::str::DOMString; @@ -43,6 +44,7 @@ pub struct WorkerGlobalScopeInit { pub to_devtools_sender: Option>, pub from_devtools_sender: Option>, pub constellation_chan: ConstellationChan, + pub scheduler_chan: Sender, pub worker_id: WorkerId, } @@ -61,7 +63,7 @@ pub struct WorkerGlobalScope { navigator: MutNullableHeap>, console: MutNullableHeap>, crypto: MutNullableHeap>, - timers: TimerManager, + timers: ActiveTimers, #[ignore_heap_size_of = "Defined in std"] mem_profiler_chan: mem::ProfilerChan, #[ignore_heap_size_of = "Defined in ipc-channel"] @@ -83,13 +85,17 @@ pub struct WorkerGlobalScope { #[ignore_heap_size_of = "Defined in std"] constellation_chan: ConstellationChan, + + #[ignore_heap_size_of = "Defined in std"] + scheduler_chan: Sender, } impl WorkerGlobalScope { pub fn new_inherited(init: WorkerGlobalScopeInit, worker_url: Url, runtime: Rc, - from_devtools_receiver: Receiver) + from_devtools_receiver: Receiver, + timer_event_chan: Box) -> WorkerGlobalScope { WorkerGlobalScope { eventtarget: EventTarget::new_inherited(), @@ -102,13 +108,14 @@ impl WorkerGlobalScope { navigator: Default::default(), console: Default::default(), crypto: Default::default(), - timers: TimerManager::new(), + timers: ActiveTimers::new(timer_event_chan, init.scheduler_chan.clone()), mem_profiler_chan: init.mem_profiler_chan, to_devtools_sender: init.to_devtools_sender, from_devtools_sender: init.from_devtools_sender, from_devtools_receiver: from_devtools_receiver, devtools_wants_updates: Cell::new(false), constellation_chan: init.constellation_chan, + scheduler_chan: init.scheduler_chan, } } @@ -132,6 +139,10 @@ impl WorkerGlobalScope { self.constellation_chan.clone() } + pub fn scheduler_chan(&self) -> Sender { + self.scheduler_chan.clone() + } + pub fn get_cx(&self) -> *mut JSContext { self.runtime.cx() } @@ -233,8 +244,7 @@ impl WorkerGlobalScopeMethods for WorkerGlobalScope { args, timeout, IsInterval::NonInterval, - TimerSource::FromWorker, - self.script_chan()) + TimerSource::FromWorker) } // https://html.spec.whatwg.org/multipage/#dom-windowtimers-setinterval @@ -243,8 +253,7 @@ impl WorkerGlobalScopeMethods for WorkerGlobalScope { args, timeout, IsInterval::NonInterval, - TimerSource::FromWorker, - self.script_chan()) + TimerSource::FromWorker) } // https://html.spec.whatwg.org/multipage/#dom-windowtimers-clearinterval @@ -258,8 +267,7 @@ impl WorkerGlobalScopeMethods for WorkerGlobalScope { args, timeout, IsInterval::Interval, - TimerSource::FromWorker, - self.script_chan()) + TimerSource::FromWorker) } // https://html.spec.whatwg.org/multipage/#dom-windowtimers-setinterval @@ -268,8 +276,7 @@ impl WorkerGlobalScopeMethods for WorkerGlobalScope { args, timeout, IsInterval::Interval, - TimerSource::FromWorker, - self.script_chan()) + TimerSource::FromWorker) } // https://html.spec.whatwg.org/multipage/#dom-windowtimers-clearinterval @@ -330,7 +337,7 @@ impl WorkerGlobalScope { } } - pub fn handle_fire_timer(&self, timer_id: TimerId) { + pub fn handle_fire_timer(&self, timer_id: TimerEventId) { self.timers.fire_timer(timer_id, self); } diff --git a/servo/components/script/horribly_inefficient_timers.rs b/servo/components/script/horribly_inefficient_timers.rs deleted file mode 100644 index 2e1ca0f12c3d..000000000000 --- a/servo/components/script/horribly_inefficient_timers.rs +++ /dev/null @@ -1,31 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -/// A quick hack to work around the removal of [`std::old_io::timer::Timer`]( -/// http://doc.rust-lang.org/1.0.0-beta/std/old_io/timer/struct.Timer.html ) - -use std::sync::mpsc::{Receiver, channel}; -use std::thread::{sleep_ms, spawn}; - -pub fn oneshot(duration_ms: u32) -> Receiver<()> { - let (tx, rx) = channel(); - spawn(move || { - sleep_ms(duration_ms); - let _ = tx.send(()); - }); - rx -} - -pub fn periodic(duration_ms: u32) -> Receiver<()> { - let (tx, rx) = channel(); - spawn(move || { - loop { - sleep_ms(duration_ms); - if tx.send(()).is_err() { - break - } - } - }); - rx -} diff --git a/servo/components/script/lib.rs b/servo/components/script/lib.rs index 5ebaa787dfde..5b1705c52f34 100644 --- a/servo/components/script/lib.rs +++ b/servo/components/script/lib.rs @@ -86,7 +86,6 @@ mod devtools; pub mod document_loader; #[macro_use] pub mod dom; -mod horribly_inefficient_timers; pub mod layout_interface; mod mem; mod network_listener; diff --git a/servo/components/script/script_task.rs b/servo/components/script/script_task.rs index 79e749b82d6c..a090d1c91ef3 100644 --- a/servo/components/script/script_task.rs +++ b/servo/components/script/script_task.rs @@ -82,6 +82,7 @@ use script_traits::CompositorEvent::{MouseDownEvent, MouseUpEvent}; use script_traits::{CompositorEvent, ConstellationControlMsg}; use script_traits::{InitialScriptState, MouseButton, NewLayoutInfo}; use script_traits::{OpaqueScriptLayoutChannel, ScriptState, ScriptTaskFactory}; +use script_traits::{TimerEvent, TimerEventChan, TimerEventRequest, TimerSource}; use std::any::Any; use std::borrow::ToOwned; use std::cell::{Cell, RefCell}; @@ -96,7 +97,6 @@ use std::sync::mpsc::{Receiver, Select, Sender, channel}; use std::sync::{Arc, Mutex}; use string_cache::Atom; use time::{Tm, now}; -use timers::TimerId; use url::{Url, UrlParser}; use util::opts; use util::str::DOMString; @@ -156,12 +156,6 @@ impl InProgressLoad { } } -#[derive(Copy, Clone)] -pub enum TimerSource { - FromWindow(PipelineId), - FromWorker -} - pub trait Runnable { fn handler(self: Box); } @@ -175,6 +169,7 @@ enum MixedMessage { FromScript(MainThreadScriptMsg), FromDevtools(DevtoolScriptControlMsg), FromImageCache(ImageCacheResult), + FromScheduler(TimerEvent), } /// Common messages used to control the event loops in both the script and the worker @@ -182,10 +177,6 @@ pub enum CommonScriptMsg { /// Requests that the script task measure its memory usage. The results are sent back via the /// supplied channel. CollectReports(ReportsChan), - /// Fires a JavaScript timeout - /// TimerSource must be FromWindow when dispatched to ScriptTask and - /// must be FromWorker when dispatched to a DedicatedGlobalWorkerScope - FireTimer(TimerSource, TimerId), /// A DOM object's last pinned reference was removed (dispatched to all tasks). RefcountCleanup(TrustedReference), /// Generic message that encapsulates event handling. @@ -205,6 +196,7 @@ pub enum ScriptTaskEventCategory { NetworkEvent, Resize, ScriptEvent, + TimerEvent, UpdateReplacedElement, SetViewport, WebSocketEvent, @@ -327,6 +319,20 @@ impl MainThreadScriptChan { } } +pub struct MainThreadTimerEventChan(Sender); + +impl TimerEventChan for MainThreadTimerEventChan { + fn send(&self, event: TimerEvent) -> Result<(), ()> { + let MainThreadTimerEventChan(ref chan) = *self; + chan.send(event).map_err(|_| ()) + } + + fn clone(&self) -> Box { + let MainThreadTimerEventChan(ref chan) = *self; + box MainThreadTimerEventChan((*chan).clone()) + } +} + pub struct StackRootTLS; impl StackRootTLS { @@ -408,6 +414,10 @@ pub struct ScriptTask { /// List of pipelines that have been owned and closed by this script task. closed_pipelines: RefCell>, + + scheduler_chan: Sender, + timer_event_chan: Sender, + timer_event_port: Receiver, } /// In the event of task failure, all data on the stack runs its destructor. However, there @@ -604,6 +614,8 @@ impl ScriptTask { let image_cache_port = ROUTER.route_ipc_receiver_to_new_mpsc_receiver(ipc_image_cache_port); + let (timer_event_chan, timer_event_port) = channel(); + ScriptTask { page: DOMRefCell::new(None), incomplete_loads: DOMRefCell::new(vec!()), @@ -631,6 +643,10 @@ impl ScriptTask { js_runtime: Rc::new(runtime), mouse_over_targets: DOMRefCell::new(vec!()), closed_pipelines: RefCell::new(HashSet::new()), + + scheduler_chan: state.scheduler_chan, + timer_event_chan: timer_event_chan, + timer_event_port: timer_event_port, } } @@ -717,26 +733,30 @@ impl ScriptTask { // Receive at least one message so we don't spinloop. let mut event = { let sel = Select::new(); - let mut port1 = sel.handle(&self.port); - let mut port2 = sel.handle(&self.control_port); - let mut port3 = sel.handle(&self.devtools_port); - let mut port4 = sel.handle(&self.image_cache_port); + let mut script_port = sel.handle(&self.port); + let mut control_port = sel.handle(&self.control_port); + let mut timer_event_port = sel.handle(&self.timer_event_port); + let mut devtools_port = sel.handle(&self.devtools_port); + let mut image_cache_port = sel.handle(&self.image_cache_port); unsafe { - port1.add(); - port2.add(); + script_port.add(); + control_port.add(); + timer_event_port.add(); if self.devtools_chan.is_some() { - port3.add(); + devtools_port.add(); } - port4.add(); + image_cache_port.add(); } let ret = sel.wait(); - if ret == port1.id() { + if ret == script_port.id() { MixedMessage::FromScript(self.port.recv().unwrap()) - } else if ret == port2.id() { + } else if ret == control_port.id() { MixedMessage::FromConstellation(self.control_port.recv().unwrap()) - } else if ret == port3.id() { + } else if ret == timer_event_port.id() { + MixedMessage::FromScheduler(self.timer_event_port.recv().unwrap()) + } else if ret == devtools_port.id() { MixedMessage::FromDevtools(self.devtools_port.recv().unwrap()) - } else if ret == port4.id() { + } else if ret == image_cache_port.id() { MixedMessage::FromImageCache(self.image_cache_port.recv().unwrap()) } else { panic!("unexpected select result") @@ -797,12 +817,15 @@ impl ScriptTask { // on and execute the sequential non-resize events we've seen. match self.control_port.try_recv() { Err(_) => match self.port.try_recv() { - Err(_) => match self.devtools_port.try_recv() { - Err(_) => match self.image_cache_port.try_recv() { - Err(_) => break, - Ok(ev) => event = MixedMessage::FromImageCache(ev), + Err(_) => match self.timer_event_port.try_recv() { + Err(_) => match self.devtools_port.try_recv() { + Err(_) => match self.image_cache_port.try_recv() { + Err(_) => break, + Ok(ev) => event = MixedMessage::FromImageCache(ev), + }, + Ok(ev) => event = MixedMessage::FromDevtools(ev), }, - Ok(ev) => event = MixedMessage::FromDevtools(ev), + Ok(ev) => event = MixedMessage::FromScheduler(ev), }, Ok(ev) => event = MixedMessage::FromScript(ev), }, @@ -823,6 +846,7 @@ impl ScriptTask { }, MixedMessage::FromConstellation(inner_msg) => self.handle_msg_from_constellation(inner_msg), MixedMessage::FromScript(inner_msg) => self.handle_msg_from_script(inner_msg), + MixedMessage::FromScheduler(inner_msg) => self.handle_timer_event(inner_msg), MixedMessage::FromDevtools(inner_msg) => self.handle_msg_from_devtools(inner_msg), MixedMessage::FromImageCache(inner_msg) => self.handle_msg_from_image_cache(inner_msg), } @@ -871,7 +895,8 @@ impl ScriptTask { *category, _ => ScriptTaskEventCategory::ScriptEvent } - } + }, + MixedMessage::FromScheduler(_) => ScriptTaskEventCategory::TimerEvent, } } @@ -893,6 +918,7 @@ impl ScriptTask { ScriptTaskEventCategory::ScriptEvent => ProfilerCategory::ScriptEvent, ScriptTaskEventCategory::UpdateReplacedElement => ProfilerCategory::ScriptUpdateReplacedElement, ScriptTaskEventCategory::SetViewport => ProfilerCategory::ScriptSetViewport, + ScriptTaskEventCategory::TimerEvent => ProfilerCategory::ScriptTimerEvent, ScriptTaskEventCategory::WebSocketEvent => ProfilerCategory::ScriptWebSocketEvent, ScriptTaskEventCategory::WorkerEvent => ProfilerCategory::ScriptWorkerEvent, ScriptTaskEventCategory::XhrEvent => ProfilerCategory::ScriptXhrEvent, @@ -966,12 +992,6 @@ impl ScriptTask { runnable.handler(self), MainThreadScriptMsg::DocumentLoadsComplete(id) => self.handle_loads_complete(id), - MainThreadScriptMsg::Common( - CommonScriptMsg::FireTimer(TimerSource::FromWindow(id), timer_id)) => - self.handle_fire_timer_msg(id, timer_id), - MainThreadScriptMsg::Common( - CommonScriptMsg::FireTimer(TimerSource::FromWorker, _)) => - panic!("Worker timeouts must not be sent to script task"), MainThreadScriptMsg::Common(CommonScriptMsg::RunnableMsg(_, runnable)) => // The category of the runnable is ignored by the pattern, however // it is still respected by profiling (see categorize_msg). @@ -983,6 +1003,22 @@ impl ScriptTask { } } + fn handle_timer_event(&self, timer_event: TimerEvent) { + let TimerEvent(source, id) = timer_event; + + let pipeline_id = match source { + TimerSource::FromWindow(pipeline_id) => pipeline_id, + TimerSource::FromWorker => panic!("Worker timeouts must not be sent to script task"), + }; + + let page = self.root_page(); + let page = page.find(pipeline_id).expect("ScriptTask: received fire timer msg for a + pipeline ID not associated with this script task. This is a bug."); + let window = page.window(); + + window.r().handle_fire_timer(id); + } + fn handle_msg_from_devtools(&self, msg: DevtoolScriptControlMsg) { let page = self.root_page(); match msg { @@ -1272,15 +1308,6 @@ impl ScriptTask { reports_chan.send(reports); } - /// Handles a timer that fired. - fn handle_fire_timer_msg(&self, id: PipelineId, timer_id: TimerId) { - let page = self.root_page(); - let page = page.find(id).expect("ScriptTask: received fire timer msg for a - pipeline ID not associated with this script task. This is a bug."); - let window = page.window(); - window.r().handle_fire_timer(timer_id); - } - /// Handles freeze message fn handle_freeze_msg(&self, id: PipelineId) { // Workaround for a race condition when navigating before the initial page has @@ -1587,6 +1614,8 @@ impl ScriptTask { self.mem_profiler_chan.clone(), self.devtools_chan.clone(), self.constellation_chan.clone(), + self.scheduler_chan.clone(), + MainThreadTimerEventChan(self.timer_event_chan.clone()), incomplete.layout_chan, incomplete.pipeline_id, incomplete.parent_info, diff --git a/servo/components/script/timers.rs b/servo/components/script/timers.rs index 1704ba3cbbfa..6c982cb1ad44 100644 --- a/servo/components/script/timers.rs +++ b/servo/components/script/timers.rs @@ -8,81 +8,63 @@ use dom::bindings::codegen::Bindings::FunctionBinding::Function; use dom::bindings::global::global_object_for_js_object; use dom::bindings::utils::Reflectable; use dom::window::ScriptHelpers; -use horribly_inefficient_timers; +use euclid::length::Length; use js::jsapi::{HandleValue, Heap, RootedValue}; use js::jsval::{JSVal, UndefinedValue}; -use script_task::{CommonScriptMsg, ScriptChan, TimerSource}; -use std::borrow::ToOwned; +use num::traits::Saturating; +use script_traits::{MsDuration, precise_time_ms}; +use script_traits::{TimerEventChan, TimerEventId, TimerEventRequest, TimerSource}; use std::cell::Cell; -use std::cmp; -use std::collections::HashMap; +use std::cmp::{self, Ord, Ordering}; use std::default::Default; -use std::hash::{Hash, Hasher}; use std::rc::Rc; -use std::sync::mpsc::Select; -use std::sync::mpsc::{Sender, channel}; +use std::sync::mpsc::Sender; use util::mem::HeapSizeOf; use util::str::DOMString; -use util::task::spawn_named; -#[derive(JSTraceable, PartialEq, Eq, Copy, Clone, HeapSizeOf)] -pub struct TimerId(i32); +#[derive(JSTraceable, PartialEq, Eq, Copy, Clone, HeapSizeOf, Hash, PartialOrd, Ord)] +pub struct TimerHandle(i32); #[derive(JSTraceable, HeapSizeOf)] #[privatize] -struct TimerHandle { - handle: TimerId, - data: TimerData, - #[ignore_heap_size_of = "channels are hard"] - control_chan: Option>, -} - -#[derive(JSTraceable, Clone)] -pub enum TimerCallback { - StringTimerCallback(DOMString), - FunctionTimerCallback(Rc) -} - -impl HeapSizeOf for TimerCallback { - fn heap_size_of_children(&self) -> usize { - // FIXME: Rc isn't HeapSizeOf and we can't ignore it due to #6870 and #6871 - 0 - } -} - -impl Hash for TimerId { - fn hash(&self, state: &mut H) { - let TimerId(id) = *self; - id.hash(state); - } -} - -impl TimerHandle { - fn cancel(&mut self) { - self.control_chan.as_ref().map(|chan| chan.send(TimerControlMsg::Cancel).ok()); - } - fn suspend(&mut self) { - self.control_chan.as_ref().map(|chan| chan.send(TimerControlMsg::Suspend).ok()); - } - fn resume(&mut self) { - self.control_chan.as_ref().map(|chan| chan.send(TimerControlMsg::Resume).ok()); - } +pub struct ActiveTimers { + #[ignore_heap_size_of = "Defined in std"] + timer_event_chan: Box, + #[ignore_heap_size_of = "Defined in std"] + scheduler_chan: Sender, + next_timer_handle: Cell, + timers: DOMRefCell>, + suspended_since: Cell>, + /// Initially 0, increased whenever the associated document is reactivated + /// by the amount of ms the document was inactive. The current time can be + /// offset back by this amount for a coherent time across document + /// activations. + suspension_offset: Cell, + /// Calls to `fire_timer` with a different argument than this get ignored. + /// They were previously scheduled and got invalidated when + /// - timers were suspended, + /// - the timer it was scheduled for got canceled or + /// - a timer was added with an earlier callback time. In this case the + /// original timer is rescheduled when it is the next one to get called. + expected_event_id: Cell, + /// The nesting level of the currently executing timer task or 0. + nesting_level: Cell, } +// Holder for the various JS values associated with setTimeout +// (ie. function value to invoke and all arguments to pass +// to the function when calling it) +// TODO: Handle rooting during fire_timer when movable GC is turned on #[derive(JSTraceable, HeapSizeOf)] #[privatize] -pub struct TimerManager { - active_timers: DOMRefCell>, - next_timer_handle: Cell, -} - - -impl Drop for TimerManager { - fn drop(&mut self) { - for (_, timer_handle) in &mut *self.active_timers.borrow_mut() { - timer_handle.cancel(); - } - } +struct Timer { + handle: TimerHandle, + source: TimerSource, + callback: InternalTimerCallback, + is_interval: IsInterval, + nesting_level: u32, + duration: MsDuration, + next_call: MsDuration, } // Enum allowing more descriptive values for the is_interval field @@ -92,175 +74,269 @@ pub enum IsInterval { NonInterval, } -// Messages sent control timers from script task -#[derive(JSTraceable, PartialEq, Copy, Clone, Debug)] -pub enum TimerControlMsg { - Cancel, - Suspend, - Resume +impl Ord for Timer { + fn cmp(&self, other: &Timer) -> Ordering { + match self.next_call.cmp(&other.next_call).reverse() { + Ordering::Equal => self.handle.cmp(&other.handle).reverse(), + res @ _ => res + } + } } -// Holder for the various JS values associated with setTimeout -// (ie. function value to invoke and all arguments to pass -// to the function when calling it) -// TODO: Handle rooting during fire_timer when movable GC is turned on -#[derive(JSTraceable, HeapSizeOf)] -#[privatize] -struct TimerData { - is_interval: IsInterval, - callback: TimerCallback, - args: Vec> +impl PartialOrd for Timer { + fn partial_cmp(&self, other: &Timer) -> Option { + Some(self.cmp(other)) + } } -impl TimerManager { - pub fn new() -> TimerManager { - TimerManager { - active_timers: DOMRefCell::new(HashMap::new()), - next_timer_handle: Cell::new(0) +impl Eq for Timer {} +impl PartialEq for Timer { + fn eq(&self, other: &Timer) -> bool { + self as *const Timer == other as *const Timer + } +} + +#[derive(Clone)] +pub enum TimerCallback { + StringTimerCallback(DOMString), + FunctionTimerCallback(Rc), +} + +#[derive(JSTraceable, Clone)] +enum InternalTimerCallback { + StringTimerCallback(DOMString), + FunctionTimerCallback(Rc, Rc>>), +} + +impl HeapSizeOf for InternalTimerCallback { + fn heap_size_of_children(&self) -> usize { + // FIXME: Rc isn't HeapSizeOf and we can't ignore it due to #6870 and #6871 + 0 + } +} + +impl ActiveTimers { + pub fn new(timer_event_chan: Box, + scheduler_chan: Sender) + -> ActiveTimers { + ActiveTimers { + timer_event_chan: timer_event_chan, + scheduler_chan: scheduler_chan, + next_timer_handle: Cell::new(TimerHandle(1)), + timers: DOMRefCell::new(Vec::new()), + suspended_since: Cell::new(None), + suspension_offset: Cell::new(Length::new(0)), + expected_event_id: Cell::new(TimerEventId(0)), + nesting_level: Cell::new(0), + } + } + + // see https://html.spec.whatwg.org/multipage/#timer-initialisation-steps + pub fn set_timeout_or_interval(&self, + callback: TimerCallback, + arguments: Vec, + timeout: i32, + is_interval: IsInterval, + source: TimerSource) + -> i32 { + assert!(self.suspended_since.get().is_none()); + + // step 3 + let TimerHandle(new_handle) = self.next_timer_handle.get(); + self.next_timer_handle.set(TimerHandle(new_handle + 1)); + + let timeout = cmp::max(0, timeout); + // step 7 + let duration = self.clamp_duration(Length::new(timeout as u64)); + let next_call = self.base_time() + duration; + + let callback = match callback { + TimerCallback::StringTimerCallback(code_str) => + InternalTimerCallback::StringTimerCallback(code_str), + TimerCallback::FunctionTimerCallback(function) => { + // This is a bit complicated, but this ensures that the vector's + // buffer isn't reallocated (and moved) after setting the Heap values + let mut args = Vec::with_capacity(arguments.len()); + for _ in 0..arguments.len() { + args.push(Heap::default()); + } + for (i, item) in arguments.iter().enumerate() { + args.get_mut(i).unwrap().set(item.get()); + } + InternalTimerCallback::FunctionTimerCallback(function, Rc::new(args)) + } + }; + + let timer = Timer { + handle: TimerHandle(new_handle), + source: source, + callback: callback, + is_interval: is_interval, + duration: duration, + // step 6 + nesting_level: self.nesting_level.get() + 1, + next_call: next_call, + }; + + self.insert_timer(timer); + + let TimerHandle(max_handle) = self.timers.borrow().last().unwrap().handle; + if max_handle == new_handle { + self.schedule_timer_call(); + } + + // step 10 + new_handle + } + + pub fn clear_timeout_or_interval(&self, handle: i32) { + let handle = TimerHandle(handle); + let was_next = self.is_next_timer(handle); + + self.timers.borrow_mut().retain(|t| t.handle != handle); + + if was_next { + self.invalidate_expected_event_id(); + self.schedule_timer_call(); + } + } + + // see https://html.spec.whatwg.org/multipage/#timer-initialisation-steps + #[allow(unsafe_code)] + pub fn fire_timer(&self, id: TimerEventId, this: &T) { + let expected_id = self.expected_event_id.get(); + if expected_id != id { + debug!("ignoring timer fire event {:?} (expected {:?}", id, expected_id); + return; + } + + assert!(self.suspended_since.get().is_none()); + + let base_time = self.base_time(); + + // Since the event id was the expected one, at least one timer should be due. + assert!(base_time >= self.timers.borrow().last().unwrap().next_call); + + loop { + let timer = { + let mut timers = self.timers.borrow_mut(); + + if timers.is_empty() || timers.last().unwrap().next_call > base_time { + break; + } + + timers.pop().unwrap() + }; + let callback = timer.callback.clone(); + + // prep for step 6 in nested set_timeout_or_interval calls + self.nesting_level.set(timer.nesting_level); + + // step 4.3 + if timer.is_interval == IsInterval::Interval { + let mut timer = timer; + + // step 7 + timer.duration = self.clamp_duration(timer.duration); + // step 8, 9 + timer.nesting_level += 1; + timer.next_call = base_time + timer.duration; + self.insert_timer(timer); + } + + // step 14 + match callback { + InternalTimerCallback::StringTimerCallback(code_str) => { + let proxy = this.reflector().get_jsobject(); + let cx = global_object_for_js_object(proxy.get()).r().get_cx(); + let mut rval = RootedValue::new(cx, UndefinedValue()); + + this.evaluate_js_on_global_with_result(&code_str, rval.handle_mut()); + }, + InternalTimerCallback::FunctionTimerCallback(function, arguments) => { + let arguments: Vec = arguments.iter().map(|arg| arg.get()).collect(); + let arguments = arguments.iter().by_ref().map(|arg| unsafe { + HandleValue::from_marked_location(arg) + }).collect(); + + let _ = function.Call_(this, arguments, Report); + } + }; + + self.nesting_level.set(0); + } + + self.schedule_timer_call(); + } + + fn insert_timer(&self, timer: Timer) { + let mut timers = self.timers.borrow_mut(); + let insertion_index = timers.binary_search(&timer).err().unwrap(); + timers.insert(insertion_index, timer); + } + + fn is_next_timer(&self, handle: TimerHandle) -> bool { + match self.timers.borrow().last() { + None => false, + Some(ref max_timer) => max_timer.handle == handle + } + } + + fn schedule_timer_call(&self) { + assert!(self.suspended_since.get().is_none()); + + let timers = self.timers.borrow(); + + if let Some(timer) = timers.last() { + let expected_event_id = self.invalidate_expected_event_id(); + + let delay = Length::new(timer.next_call.get().saturating_sub(precise_time_ms().get())); + let request = TimerEventRequest(self.timer_event_chan.clone(), timer.source, + expected_event_id, delay); + self.scheduler_chan.send(request).unwrap(); } } pub fn suspend(&self) { - for (_, timer_handle) in &mut *self.active_timers.borrow_mut() { - timer_handle.suspend(); - } + assert!(self.suspended_since.get().is_none()); + + self.suspended_since.set(Some(precise_time_ms())); + self.invalidate_expected_event_id(); } + pub fn resume(&self) { - for (_, timer_handle) in &mut *self.active_timers.borrow_mut() { - timer_handle.resume(); - } - } + assert!(self.suspended_since.get().is_some()); - #[allow(unsafe_code)] - pub fn set_timeout_or_interval(&self, - callback: TimerCallback, - arguments: Vec, - timeout: i32, - is_interval: IsInterval, - source: TimerSource, - script_chan: Box) - -> i32 { - let duration_ms = cmp::max(0, timeout) as u32; - let handle = self.next_timer_handle.get(); - self.next_timer_handle.set(handle + 1); - - // Spawn a new timer task; it will dispatch the `CommonScriptMsg::FireTimer` - // to the relevant script handler that will deal with it. - let (control_chan, control_port) = channel(); - let spawn_name = match source { - TimerSource::FromWindow(_) if is_interval == IsInterval::Interval => "Window:SetInterval", - TimerSource::FromWorker if is_interval == IsInterval::Interval => "Worker:SetInterval", - TimerSource::FromWindow(_) => "Window:SetTimeout", - TimerSource::FromWorker => "Worker:SetTimeout", - }.to_owned(); - spawn_named(spawn_name, move || { - let timeout_port = if is_interval == IsInterval::Interval { - horribly_inefficient_timers::periodic(duration_ms) - } else { - horribly_inefficient_timers::oneshot(duration_ms) - }; - let control_port = control_port; - - let select = Select::new(); - let mut timeout_handle = select.handle(&timeout_port); - unsafe { timeout_handle.add() }; - let mut control_handle = select.handle(&control_port); - unsafe { control_handle.add() }; - - loop { - let id = select.wait(); - - if id == timeout_handle.id() { - timeout_port.recv().unwrap(); - if script_chan.send(CommonScriptMsg::FireTimer(source, TimerId(handle))).is_err() { - break; - } - - if is_interval == IsInterval::NonInterval { - break; - } - } else if id == control_handle.id() { - match control_port.recv().unwrap() { - TimerControlMsg::Suspend => { - let msg = control_port.recv().unwrap(); - match msg { - TimerControlMsg::Suspend => panic!("Nothing to suspend!"), - TimerControlMsg::Resume => {}, - TimerControlMsg::Cancel => { - break; - }, - } - }, - TimerControlMsg::Resume => panic!("Nothing to resume!"), - TimerControlMsg::Cancel => { - break; - } - } - } - } - }); - let timer_id = TimerId(handle); - let timer = TimerHandle { - handle: timer_id, - control_chan: Some(control_chan), - data: TimerData { - is_interval: is_interval, - callback: callback, - args: Vec::with_capacity(arguments.len()) - } + let additional_offset = match self.suspended_since.get() { + Some(suspended_since) => precise_time_ms() - suspended_since, + None => panic!("Timers are not suspended.") }; - self.active_timers.borrow_mut().insert(timer_id, timer); - // This is a bit complicated, but this ensures that the vector's - // buffer isn't reallocated (and moved) after setting the Heap values - let mut timers = self.active_timers.borrow_mut(); - let mut timer = timers.get_mut(&timer_id).unwrap(); - for _ in 0..arguments.len() { - timer.data.args.push(Heap::default()); - } - for (i, item) in arguments.iter().enumerate() { - timer.data.args.get_mut(i).unwrap().set(item.get()); - } - handle + self.suspension_offset.set(self.suspension_offset.get() + additional_offset); + + self.schedule_timer_call(); } - pub fn clear_timeout_or_interval(&self, handle: i32) { - let mut timer_handle = self.active_timers.borrow_mut().remove(&TimerId(handle)); - match timer_handle { - Some(ref mut handle) => handle.cancel(), - None => {} - } + fn base_time(&self) -> MsDuration { + precise_time_ms() - self.suspension_offset.get() } - #[allow(unsafe_code)] - pub fn fire_timer(&self, timer_id: TimerId, this: &T) { + // see step 7 of https://html.spec.whatwg.org/multipage/#timer-initialisation-steps + fn clamp_duration(&self, unclamped: MsDuration) -> MsDuration { + let ms = if self.nesting_level.get() > 5 { + 4 + } else { + 0 + }; - let (is_interval, callback, args): (IsInterval, TimerCallback, Vec) = - match self.active_timers.borrow().get(&timer_id) { - Some(timer_handle) => - (timer_handle.data.is_interval, - timer_handle.data.callback.clone(), - timer_handle.data.args.iter().map(|arg| arg.get()).collect()), - None => return, - }; + cmp::max(Length::new(ms), unclamped) + } - match callback { - TimerCallback::FunctionTimerCallback(function) => { - let arg_handles = args.iter().by_ref().map(|arg| unsafe { - HandleValue::from_marked_location(arg) - }).collect(); - let _ = function.Call_(this, arg_handles, Report); - } - TimerCallback::StringTimerCallback(code_str) => { - let proxy = this.reflector().get_jsobject(); - let cx = global_object_for_js_object(proxy.get()).r().get_cx(); - let mut rval = RootedValue::new(cx, UndefinedValue()); - this.evaluate_js_on_global_with_result(&code_str, rval.handle_mut()); - } - } - - if is_interval == IsInterval::NonInterval { - self.active_timers.borrow_mut().remove(&timer_id); - } + fn invalidate_expected_event_id(&self) -> TimerEventId { + let TimerEventId(currently_expected) = self.expected_event_id.get(); + let next_id = TimerEventId(currently_expected + 1); + debug!("invalidating expected timer (was {:?}, now {:?}", currently_expected, next_id); + self.expected_event_id.set(next_id); + next_id } } diff --git a/servo/components/script_traits/Cargo.toml b/servo/components/script_traits/Cargo.toml index d038ce36f397..11e3b077096f 100644 --- a/servo/components/script_traits/Cargo.toml +++ b/servo/components/script_traits/Cargo.toml @@ -38,3 +38,4 @@ libc = "0.1" euclid = "0.2" serde = "0.6" serde_macros = "0.5" +time = "0.1.12" diff --git a/servo/components/script_traits/lib.rs b/servo/components/script_traits/lib.rs index 7693529d4b19..3c04fa36dda6 100644 --- a/servo/components/script_traits/lib.rs +++ b/servo/components/script_traits/lib.rs @@ -19,11 +19,13 @@ extern crate msg; extern crate net_traits; extern crate profile_traits; extern crate serde; +extern crate time; extern crate url; extern crate util; use app_units::Au; use devtools_traits::ScriptToDevtoolsControlMsg; +use euclid::length::Length; use euclid::point::Point2D; use euclid::rect::Rect; use ipc_channel::ipc::{IpcReceiver, IpcSender}; @@ -36,10 +38,11 @@ use msg::webdriver_msg::WebDriverScriptCommand; use net_traits::ResourceTask; use net_traits::image_cache_task::ImageCacheTask; use net_traits::storage_task::StorageTask; -use profile_traits::{mem, time}; +use profile_traits::mem; use std::any::Any; use std::sync::mpsc::{Receiver, Sender}; use url::Url; +use util::mem::HeapSizeOf; /// The address of a node. Layout sends these back. They must be validated via /// `from_untrusted_node_address` before they can be used, because we do not trust layout. @@ -177,6 +180,56 @@ pub enum CompositorEvent { /// crates that don't need to know about them. pub struct OpaqueScriptLayoutChannel(pub (Box, Box)); +/// Requests a TimerEvent-Message be sent after the given duration. +pub struct TimerEventRequest(pub Box, pub TimerSource, pub TimerEventId, pub MsDuration); + +/// Notifies the script task to fire due timers. +/// TimerSource must be FromWindow when dispatched to ScriptTask and +/// must be FromWorker when dispatched to a DedicatedGlobalWorkerScope +pub struct TimerEvent(pub TimerSource, pub TimerEventId); + +/// A cloneable interface for sending timer events. +pub trait TimerEventChan { + /// Send a timer event to the associated event loop. + fn send(&self, msg: TimerEvent) -> Result<(), ()>; + /// Clone this handle. + fn clone(&self) -> Box; +} + +/// Describes the task that requested the TimerEvent. +#[derive(Copy, Clone, HeapSizeOf)] +pub enum TimerSource { + /// The event was requested from a window (ScriptTask). + FromWindow(PipelineId), + /// The event was requested from a worker (DedicatedGlobalWorkerScope). + FromWorker +} + +/// The id to be used for a TimerEvent is defined by the corresponding TimerEventRequest. +#[derive(PartialEq, Eq, Copy, Clone, Debug, HeapSizeOf)] +pub struct TimerEventId(pub u32); + +/// Unit of measurement. +#[derive(Clone, Copy, HeapSizeOf)] +pub enum Milliseconds {} +/// Unit of measurement. +#[derive(Clone, Copy, HeapSizeOf)] +pub enum Nanoseconds {} + +/// Amount of milliseconds. +pub type MsDuration = Length; +/// Amount of nanoseconds. +pub type NsDuration = Length; + +/// Returns the duration since an unspecified epoch measured in ms. +pub fn precise_time_ms() -> MsDuration { + Length::new(time::precise_time_ns() / (1000 * 1000)) +} +/// Returns the duration since an unspecified epoch measured in ns. +pub fn precise_time_ns() -> NsDuration { + Length::new(time::precise_time_ns()) +} + /// Data needed to construct a script thread. pub struct InitialScriptState { /// The ID of the pipeline with which this script thread is associated. @@ -192,6 +245,8 @@ pub struct InitialScriptState { pub control_port: Receiver, /// A channel on which messages can be sent to the constellation from script. pub constellation_chan: ConstellationChan, + /// A channel to schedule timer events. + pub scheduler_chan: Sender, /// Information that script sends out when it panics. pub failure_info: Failure, /// A channel to the resource manager task. @@ -201,7 +256,7 @@ pub struct InitialScriptState { /// A channel to the image cache task. pub image_cache_task: ImageCacheTask, /// A channel to the time profiler thread. - pub time_profiler_chan: time::ProfilerChan, + pub time_profiler_chan: profile_traits::time::ProfilerChan, /// A channel to the memory profiler thread. pub mem_profiler_chan: mem::ProfilerChan, /// A channel to the developer tools, if applicable. diff --git a/servo/components/servo/Cargo.lock b/servo/components/servo/Cargo.lock index bf251e5fc025..513cbcaa38f4 100644 --- a/servo/components/servo/Cargo.lock +++ b/servo/components/servo/Cargo.lock @@ -1567,6 +1567,7 @@ dependencies = [ "profile_traits 0.0.1", "serde 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde_macros 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", "url 0.2.37 (registry+https://github.com/rust-lang/crates.io-index)", "util 0.0.1", ] diff --git a/servo/ports/cef/Cargo.lock b/servo/ports/cef/Cargo.lock index 12d7e6d39de8..0d49024f8f89 100644 --- a/servo/ports/cef/Cargo.lock +++ b/servo/ports/cef/Cargo.lock @@ -1491,6 +1491,7 @@ dependencies = [ "profile_traits 0.0.1", "serde 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde_macros 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", "url 0.2.37 (registry+https://github.com/rust-lang/crates.io-index)", "util 0.0.1", ] diff --git a/servo/ports/gonk/Cargo.lock b/servo/ports/gonk/Cargo.lock index deeeaf1a3d55..c3d7ac2ade3c 100644 --- a/servo/ports/gonk/Cargo.lock +++ b/servo/ports/gonk/Cargo.lock @@ -1353,6 +1353,7 @@ dependencies = [ "profile_traits 0.0.1", "serde 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde_macros 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", "url 0.2.37 (registry+https://github.com/rust-lang/crates.io-index)", "util 0.0.1", ]