servo: Merge #10225 - Implement non-visible pipeline and iframe visibility methods (from jmr0:visibility_api); r=jdm
authorjmr0 <jrosello720@gmail.com>
Thu, 16 Jun 2016 08:53:56 -0500
changeset 339082 34b7a143e50cf58712d0a5e067d794fce4eb397f
parent 339081 73a10e84aaa215aaaa839cd11c685113964db709
child 339083 22f916cd1fa7df071afc8a603d44dad470ae9158
push id31307
push usergszorc@mozilla.com
push dateSat, 04 Feb 2017 00:59:06 +0000
treeherdermozilla-central@94079d43835f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjdm
servo: Merge #10225 - Implement non-visible pipeline and iframe visibility methods (from jmr0:visibility_api); r=jdm This addresses #9566 and a good part of #9751, specifically: * Pipeline has a notion of visibility * IFrame setVisible/getVisible interface with IFrame's pipeline visibility * IFrame mozbrowservisibilitychange responds to changes in visibility * Pipeline visibility is used to limit animations (requestAnimationFrame does not tick animations when hidden) and to increase timer intervals (currently set to a minimum of 1 second while hidden) Absent for now are any changes to the Document API and general implementation of the Page Visibility API, since the more interesting parts require knowledge of whether the user agent is minimized, OS screen locked, etc. cc @paulrouget @jdm Source-Repo: https://github.com/servo/servo Source-Revision: d620ab71c41431c3fb040162f554faefb9abfbd7
servo/components/compositing/compositor.rs
servo/components/compositing/compositor_thread.rs
servo/components/constellation/constellation.rs
servo/components/constellation/pipeline.rs
servo/components/script/dom/htmliframeelement.rs
servo/components/script/dom/webidls/BrowserElement.webidl
servo/components/script/dom/window.rs
servo/components/script/script_thread.rs
servo/components/script/timers.rs
servo/components/script_traits/lib.rs
servo/components/script_traits/script_msg.rs
--- a/servo/components/compositing/compositor.rs
+++ b/servo/components/compositing/compositor.rs
@@ -285,25 +285,29 @@ struct PipelineDetails {
     /// The current layout epoch that this pipeline wants to draw.
     current_epoch: Epoch,
 
     /// Whether animations are running
     animations_running: bool,
 
     /// Whether there are animation callbacks
     animation_callbacks_running: bool,
+
+    /// Whether this pipeline is visible
+    visible: bool,
 }
 
 impl PipelineDetails {
     fn new() -> PipelineDetails {
         PipelineDetails {
             pipeline: None,
             current_epoch: Epoch(0),
             animations_running: false,
             animation_callbacks_running: false,
+            visible: true,
         }
     }
 }
 
 #[derive(Clone, Copy, PartialEq, Debug)]
 enum CompositeTarget {
     /// Normal composition to a window
     Window,
@@ -755,16 +759,23 @@ impl<Window: WindowMethods> IOCompositor
                 }, mem::Report {
                     path: path![name, "layer-tree"],
                     kind: ReportKind::ExplicitUnknownLocationSize,
                     size: self.scene.get_memory_usage(),
                 }];
                 reports_chan.send(reports);
             }
 
+            (Msg::PipelineVisibilityChanged(pipeline_id, visible), ShutdownState::NotShuttingDown) => {
+                self.pipeline_details(pipeline_id).visible = visible;
+                if visible {
+                    self.process_animations();
+                }
+            }
+
             (Msg::PipelineExited(pipeline_id, sender), _) => {
                 debug!("Compositor got pipeline exited: {:?}", pipeline_id);
                 self.pending_subpages.remove(&pipeline_id);
                 self.remove_pipeline_root_layer(pipeline_id);
                 let _ = sender.send(());
             }
 
             (Msg::GetScrollOffset(pipeline_id, layer_id, sender), ShutdownState::NotShuttingDown) => {
@@ -790,23 +801,26 @@ impl<Window: WindowMethods> IOCompositor
 
     /// Sets or unsets the animations-running flag for the given pipeline, and schedules a
     /// recomposite if necessary.
     fn change_running_animations_state(&mut self,
                                        pipeline_id: PipelineId,
                                        animation_state: AnimationState) {
         match animation_state {
             AnimationState::AnimationsPresent => {
+                let visible = self.pipeline_details(pipeline_id).visible;
                 self.pipeline_details(pipeline_id).animations_running = true;
-                self.composite_if_necessary(CompositingReason::Animation);
+                if visible {
+                    self.composite_if_necessary(CompositingReason::Animation);
+                }
             }
             AnimationState::AnimationCallbacksPresent => {
-                if !self.pipeline_details(pipeline_id).animation_callbacks_running {
-                    self.pipeline_details(pipeline_id).animation_callbacks_running =
-                        true;
+                let visible = self.pipeline_details(pipeline_id).visible;
+                self.pipeline_details(pipeline_id).animation_callbacks_running = true;
+                if visible {
                     self.tick_animations_for_pipeline(pipeline_id);
                 }
             }
             AnimationState::NoAnimationsPresent => {
                 self.pipeline_details(pipeline_id).animations_running = false;
             }
             AnimationState::NoAnimationCallbacksPresent => {
                 self.pipeline_details(pipeline_id).animation_callbacks_running = false;
@@ -1707,19 +1721,20 @@ impl<Window: WindowMethods> IOCompositor
             self.channel_to_self.send(Msg::Recomposite(CompositingReason::ContinueScroll));
         }
     }
 
     /// If there are any animations running, dispatches appropriate messages to the constellation.
     fn process_animations(&mut self) {
         let mut pipeline_ids = vec![];
         for (pipeline_id, pipeline_details) in &self.pipeline_details {
-            if pipeline_details.animations_running ||
-               pipeline_details.animation_callbacks_running {
-                pipeline_ids.push(*pipeline_id);
+            if (pipeline_details.animations_running ||
+                pipeline_details.animation_callbacks_running) &&
+               pipeline_details.visible {
+                   pipeline_ids.push(*pipeline_id);
             }
         }
         for pipeline_id in &pipeline_ids {
             self.tick_animations_for_pipeline(*pipeline_id)
         }
     }
 
     fn tick_animations_for_pipeline(&mut self, pipeline_id: PipelineId) {
--- a/servo/components/compositing/compositor_thread.rs
+++ b/servo/components/compositing/compositor_thread.rs
@@ -178,16 +178,18 @@ pub enum Msg {
     /// Get Window Informations size and position
     GetClientWindow(IpcSender<(Size2D<u32>, Point2D<i32>)>),
     /// Move the window to a point
     MoveTo(Point2D<i32>),
     /// Resize the window to size
     ResizeTo(Size2D<u32>),
     /// Get scroll offset of a layer
     GetScrollOffset(PipelineId, LayerId, IpcSender<Point2D<f32>>),
+    /// Pipeline visibility changed
+    PipelineVisibilityChanged(PipelineId, bool),
     /// A pipeline was shut down.
     // This message acts as a synchronization point between the constellation,
     // when it shuts down a pipeline, to the compositor; when the compositor
     // sends a reply on the IpcSender, the constellation knows it's safe to
     // tear down the other threads associated with this pipeline.
     PipelineExited(PipelineId, IpcSender<()>),
 }
 
@@ -218,16 +220,17 @@ impl Debug for Msg {
             Msg::NewFavicon(..) => write!(f, "NewFavicon"),
             Msg::HeadParsed => write!(f, "HeadParsed"),
             Msg::ReturnUnusedNativeSurfaces(..) => write!(f, "ReturnUnusedNativeSurfaces"),
             Msg::CollectMemoryReports(..) => write!(f, "CollectMemoryReports"),
             Msg::Status(..) => write!(f, "Status"),
             Msg::GetClientWindow(..) => write!(f, "GetClientWindow"),
             Msg::MoveTo(..) => write!(f, "MoveTo"),
             Msg::ResizeTo(..) => write!(f, "ResizeTo"),
+            Msg::PipelineVisibilityChanged(..) => write!(f, "PipelineVisibilityChanged"),
             Msg::PipelineExited(..) => write!(f, "PipelineExited"),
             Msg::GetScrollOffset(..) => write!(f, "GetScrollOffset"),
         }
     }
 }
 
 /// Data used to construct a compositor.
 pub struct InitialCompositorState {
--- a/servo/components/constellation/constellation.rs
+++ b/servo/components/constellation/constellation.rs
@@ -402,16 +402,22 @@ impl<Message, LTF, STF> Constellation<Me
     fn new_pipeline(&mut self,
                     pipeline_id: PipelineId,
                     parent_info: Option<(PipelineId, SubpageId, FrameType)>,
                     initial_window_size: Option<TypedSize2D<PagePx, f32>>,
                     script_channel: Option<IpcSender<ConstellationControlMsg>>,
                     load_data: LoadData) {
         if self.shutting_down { return; }
 
+        let parent_visibility = if let Some((parent_pipeline_id, _, _)) = parent_info {
+            self.pipelines.get(&parent_pipeline_id).map(|pipeline| pipeline.visible)
+        } else {
+            None
+        };
+
         let result = Pipeline::spawn::<Message, LTF, STF>(InitialPipelineState {
             id: pipeline_id,
             parent_info: parent_info,
             constellation_chan: self.script_sender.clone(),
             layout_to_constellation_chan: self.layout_sender.clone(),
             panic_chan: self.panic_sender.clone(),
             scheduler_chan: self.scheduler_chan.clone(),
             compositor_proxy: self.compositor_proxy.clone_compositor_proxy(),
@@ -422,16 +428,17 @@ impl<Message, LTF, STF> Constellation<Me
             resource_threads: self.resource_threads.clone(),
             time_profiler_chan: self.time_profiler_chan.clone(),
             mem_profiler_chan: self.mem_profiler_chan.clone(),
             window_size: initial_window_size,
             script_chan: script_channel,
             load_data: load_data,
             device_pixel_ratio: self.window_size.device_pixel_ratio,
             pipeline_namespace_id: self.next_pipeline_namespace_id(),
+            parent_visibility: parent_visibility,
             webrender_api_sender: self.webrender_api_sender.clone(),
         });
 
         let (pipeline, child_process) = match result {
             Ok(result) => result,
             Err(e) => return self.handle_send_error(pipeline_id, e),
         };
 
@@ -705,16 +712,24 @@ impl<Message, LTF, STF> Constellation<Me
             }
             FromScriptMsg::SetClipboardContents(s) => {
                 if let Some(ref mut ctx) = self.clipboard_ctx {
                     if let Err(e) = ctx.set_contents(s) {
                         warn!("Error setting clipboard contents ({})", e);
                     }
                 }
             }
+            FromScriptMsg::SetVisible(pipeline_id, visible) => {
+                debug!("constellation got set visible messsage");
+                self.handle_set_visible_msg(pipeline_id, visible);
+            }
+            FromScriptMsg::VisibilityChangeComplete(pipeline_id, visible) => {
+                debug!("constellation got set visibility change complete message");
+                self.handle_visibility_change_complete(pipeline_id, visible);
+            }
             FromScriptMsg::RemoveIFrame(pipeline_id, sender) => {
                 debug!("constellation got remove iframe message");
                 self.handle_remove_iframe_msg(pipeline_id);
                 if let Some(sender) = sender {
                     if let Err(e) = sender.send(()) {
                         warn!("Error replying to remove iframe ({})", e);
                     }
                 }
@@ -944,17 +959,18 @@ impl<Message, LTF, STF> Constellation<Me
 
         self.handled_panic = true;
     }
 
     fn handle_init_load(&mut self, url: Url) {
         let window_size = self.window_size.visible_viewport;
         let root_pipeline_id = PipelineId::new();
         debug_assert!(PipelineId::fake_root_pipeline_id() == root_pipeline_id);
-        self.new_pipeline(root_pipeline_id, None, Some(window_size), None, LoadData::new(url.clone(), None, None));
+        self.new_pipeline(root_pipeline_id, None, Some(window_size), None,
+                          LoadData::new(url.clone(), None, None));
         self.handle_load_start_msg(&root_pipeline_id);
         self.push_pending_frame(root_pipeline_id, None);
         self.compositor_proxy.send(ToCompositorMsg::ChangePageUrl(root_pipeline_id, url));
     }
 
     fn handle_frame_size_msg(&mut self,
                              pipeline_id: PipelineId,
                              size: &TypedSize2D<PagePx, f32>) {
@@ -1483,16 +1499,45 @@ impl<Message, LTF, STF> Constellation<Me
                 // This iframe is currently loading / painting for the first time.
                 // In this case, it doesn't exist in the frame tree, but the pipeline
                 // still needs to be shut down.
                 self.close_pipeline(pipeline_id, ExitPipelineMode::Normal);
             }
         }
     }
 
+    fn handle_set_visible_msg(&mut self, pipeline_id: PipelineId, visible: bool) {
+        let frame_id = self.pipeline_to_frame_map.get(&pipeline_id).map(|frame_id| *frame_id);
+        let child_pipeline_ids: Vec<PipelineId> = self.current_frame_tree_iter(frame_id)
+                                                      .map(|frame| frame.current)
+                                                      .collect();
+        for id in child_pipeline_ids {
+            if let Some(pipeline) = self.pipelines.get_mut(&id) {
+                pipeline.change_visibility(visible);
+            }
+        }
+    }
+
+    fn handle_visibility_change_complete(&mut self, pipeline_id: PipelineId, visibility: bool) {
+        let parent_pipeline_info = self.pipelines.get(&pipeline_id).and_then(|source| source.parent_info);
+        if let Some((parent_pipeline_id, _, _)) = parent_pipeline_info {
+            let visibility_msg = ConstellationControlMsg::NotifyVisibilityChange(parent_pipeline_id,
+                                                                                 pipeline_id,
+                                                                                 visibility);
+            let  result = match self.pipelines.get(&parent_pipeline_id) {
+                None => return warn!("Parent pipeline {:?} closed", parent_pipeline_id),
+                Some(parent_pipeline) => parent_pipeline.script_chan.send(visibility_msg),
+            };
+
+            if let Err(e) = result {
+                self.handle_send_error(parent_pipeline_id, e);
+            }
+        }
+    }
+
     fn handle_create_canvas_paint_thread_msg(
             &mut self,
             size: &Size2D<i32>,
             response_sender: IpcSender<IpcSender<CanvasMsg>>) {
         let webrender_api = self.webrender_api_sender.clone();
         let sender = CanvasPaintThread::start(*size, webrender_api);
         if let Err(e) = response_sender.send(sender) {
             warn!("Create canvas paint thread response failed ({})", e);
--- a/servo/components/constellation/pipeline.rs
+++ b/servo/components/constellation/pipeline.rs
@@ -60,16 +60,19 @@ pub struct Pipeline {
     /// The title of the most recently-loaded page.
     pub title: Option<String>,
     pub size: Option<TypedSize2D<PagePx, f32>>,
     /// Whether this pipeline is currently running animations. Pipelines that are running
     /// animations cause composites to be continually scheduled.
     pub running_animations: bool,
     pub children: Vec<FrameId>,
     pub is_private: bool,
+    /// Whether this pipeline should be treated as visible for the purposes of scheduling and
+    /// resource management.
+    pub visible: bool,
 }
 
 /// Initial setup data needed to construct a pipeline.
 ///
 /// *DO NOT* add any Senders to this unless you absolutely know what you're doing, or pcwalton will
 /// have to rewrite your code. Use IPC senders instead.
 pub struct InitialPipelineState {
     /// The ID of the pipeline to create.
@@ -107,16 +110,18 @@ pub struct InitialPipelineState {
     pub device_pixel_ratio: ScaleFactor<ViewportPx, DevicePixel, f32>,
     /// A channel to the script thread, if applicable. If this is `Some`,
     /// then `parent_info` must also be `Some`.
     pub script_chan: Option<IpcSender<ConstellationControlMsg>>,
     /// Information about the page to load.
     pub load_data: LoadData,
     /// The ID of the pipeline namespace for this script thread.
     pub pipeline_namespace_id: PipelineNamespaceId,
+    /// Pipeline visibility is inherited from parent
+    pub parent_visibility: Option<bool>,
     /// Optional webrender api (if enabled).
     pub webrender_api_sender: Option<webrender_traits::RenderApiSender>,
 }
 
 impl Pipeline {
     /// Starts a paint thread, layout thread, and possibly a script thread, in
     /// a new process if requested.
     pub fn spawn<Message, LTF, STF>(state: InitialPipelineState)
@@ -245,43 +250,48 @@ impl Pipeline {
 
         let pipeline = Pipeline::new(state.id,
                                      state.parent_info,
                                      script_chan,
                                      pipeline_chan,
                                      state.compositor_proxy,
                                      chrome_to_paint_chan,
                                      state.load_data.url,
-                                     state.window_size);
+                                     state.window_size,
+                                     state.parent_visibility.unwrap_or(true));
+
+        pipeline.notify_visibility();
 
         Ok((pipeline, child_process))
     }
 
     fn new(id: PipelineId,
            parent_info: Option<(PipelineId, SubpageId, FrameType)>,
            script_chan: IpcSender<ConstellationControlMsg>,
            layout_chan: IpcSender<LayoutControlMsg>,
            compositor_proxy: Box<CompositorProxy + 'static + Send>,
            chrome_to_paint_chan: Sender<ChromeToPaintMsg>,
            url: Url,
-           size: Option<TypedSize2D<PagePx, f32>>)
+           size: Option<TypedSize2D<PagePx, f32>>,
+           visible: bool)
            -> Pipeline {
         Pipeline {
             id: id,
             parent_info: parent_info,
             script_chan: script_chan,
             layout_chan: layout_chan,
             compositor_proxy: compositor_proxy,
             chrome_to_paint_chan: chrome_to_paint_chan,
             url: url,
             title: None,
             children: vec!(),
             size: size,
             running_animations: false,
             is_private: false,
+            visible: visible,
         }
     }
 
     pub fn grant_paint_permission(&self) {
         let _ = self.chrome_to_paint_chan.send(ChromeToPaintMsg::PaintPermissionGranted);
     }
 
     pub fn revoke_paint_permission(&self) {
@@ -362,16 +372,32 @@ impl Pipeline {
 
         let event = ConstellationControlMsg::MozBrowserEvent(self.id,
                                                              subpage_id,
                                                              event);
         if let Err(e) = self.script_chan.send(event) {
             warn!("Sending mozbrowser event to script failed ({}).", e);
         }
     }
+
+    fn notify_visibility(&self) {
+        self.script_chan.send(ConstellationControlMsg::ChangeFrameVisibilityStatus(self.id, self.visible))
+                        .expect("Pipeline script chan");
+
+        self.compositor_proxy.send(CompositorMsg::PipelineVisibilityChanged(self.id, self.visible));
+    }
+
+    pub fn change_visibility(&mut self, visible: bool) {
+        if visible == self.visible {
+            return;
+        }
+        self.visible = visible;
+        self.notify_visibility();
+    }
+
 }
 
 #[derive(Deserialize, Serialize)]
 pub struct UnprivilegedPipelineContent {
     id: PipelineId,
     parent_info: Option<(PipelineId, SubpageId, FrameType)>,
     constellation_chan: IpcSender<ScriptMsg>,
     layout_to_constellation_chan: IpcSender<LayoutMsg>,
--- a/servo/components/script/dom/htmliframeelement.rs
+++ b/servo/components/script/dom/htmliframeelement.rs
@@ -6,22 +6,23 @@ use document_loader::{LoadType, LoadBloc
 use dom::attr::Attr;
 use dom::bindings::cell::DOMRefCell;
 use dom::bindings::codegen::Bindings::BrowserElementBinding::BrowserElementErrorEventDetail;
 use dom::bindings::codegen::Bindings::BrowserElementBinding::BrowserElementIconChangeEventDetail;
 use dom::bindings::codegen::Bindings::BrowserElementBinding::BrowserElementLocationChangeEventDetail;
 use dom::bindings::codegen::Bindings::BrowserElementBinding::BrowserElementOpenTabEventDetail;
 use dom::bindings::codegen::Bindings::BrowserElementBinding::BrowserElementOpenWindowEventDetail;
 use dom::bindings::codegen::Bindings::BrowserElementBinding::BrowserElementSecurityChangeDetail;
+use dom::bindings::codegen::Bindings::BrowserElementBinding::BrowserElementVisibilityChangeEventDetail;
 use dom::bindings::codegen::Bindings::BrowserElementBinding::BrowserShowModalPromptEventDetail;
 use dom::bindings::codegen::Bindings::HTMLIFrameElementBinding;
 use dom::bindings::codegen::Bindings::HTMLIFrameElementBinding::HTMLIFrameElementMethods;
 use dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
 use dom::bindings::conversions::ToJSValConvertible;
-use dom::bindings::error::{Error, ErrorResult};
+use dom::bindings::error::{Error, ErrorResult, Fallible};
 use dom::bindings::global::GlobalRef;
 use dom::bindings::inheritance::Castable;
 use dom::bindings::js::{JS, MutNullableHeap, Root, LayoutJS};
 use dom::bindings::reflector::Reflectable;
 use dom::bindings::str::DOMString;
 use dom::browsingcontext::BrowsingContext;
 use dom::customevent::CustomEvent;
 use dom::document::Document;
@@ -63,16 +64,17 @@ enum SandboxAllowance {
 #[dom_struct]
 pub struct HTMLIFrameElement {
     htmlelement: HTMLElement,
     pipeline_id: Cell<Option<PipelineId>>,
     subpage_id: Cell<Option<SubpageId>>,
     sandbox: MutNullableHeap<JS<DOMTokenList>>,
     sandbox_allowance: Cell<Option<u8>>,
     load_blocker: DOMRefCell<Option<LoadBlocker>>,
+    visibility: Cell<bool>,
 }
 
 impl HTMLIFrameElement {
     pub fn is_sandboxed(&self) -> bool {
         self.sandbox_allowance.get().is_some()
     }
 
     /// <https://html.spec.whatwg.org/multipage/#otherwise-steps-for-iframe-or-frame-elements>,
@@ -194,16 +196,17 @@ impl HTMLIFrameElement {
                      document: &Document) -> HTMLIFrameElement {
         HTMLIFrameElement {
             htmlelement: HTMLElement::new_inherited(localName, prefix, document),
             pipeline_id: Cell::new(None),
             subpage_id: Cell::new(None),
             sandbox: Default::default(),
             sandbox_allowance: Cell::new(None),
             load_blocker: DOMRefCell::new(None),
+            visibility: Cell::new(true),
         }
     }
 
     #[allow(unrooted_must_root)]
     pub fn new(localName: Atom,
                prefix: Option<DOMString>,
                document: &Document) -> Root<HTMLIFrameElement> {
         let element = HTMLIFrameElement::new_inherited(localName, prefix, document);
@@ -219,16 +222,36 @@ impl HTMLIFrameElement {
     pub fn subpage_id(&self) -> Option<SubpageId> {
         self.subpage_id.get()
     }
 
     pub fn pipeline(&self) -> Option<PipelineId> {
         self.pipeline_id.get()
     }
 
+    pub fn change_visibility_status(&self, visibility: bool) {
+        if self.visibility.get() != visibility {
+            self.visibility.set(visibility);
+
+            // Visibility changes are only exposed to Mozbrowser iframes
+            if self.Mozbrowser() {
+                self.dispatch_mozbrowser_event(MozBrowserEvent::VisibilityChange(visibility));
+            }
+        }
+    }
+
+    pub fn set_visible(&self, visible: bool) {
+        if let Some(pipeline_id) = self.pipeline_id.get() {
+            let window = window_from_node(self);
+            let window = window.r();
+            let msg = ConstellationMsg::SetVisible(pipeline_id, visible);
+            window.constellation_chan().send(msg).unwrap();
+        }
+    }
+
     /// https://html.spec.whatwg.org/multipage/#iframe-load-event-steps steps 1-4
     pub fn iframe_load_event_steps(&self, loaded_pipeline: PipelineId) {
         // TODO(#9592): assert that the load blocker is present at all times when we
         //              can guarantee that it's created for the case of iframe.reload().
         assert_eq!(loaded_pipeline, self.pipeline().unwrap());
 
         // TODO A cross-origin child document would not be easily accessible
         //      from this script thread. It's unclear how to implement
@@ -385,16 +408,21 @@ impl MozBrowserEventDetailBuilder for HT
             MozBrowserEvent::ShowModalPrompt(prompt_type, title, message, return_value) => {
                 BrowserShowModalPromptEventDetail {
                     promptType: Some(DOMString::from(prompt_type)),
                     title: Some(DOMString::from(title)),
                     message: Some(DOMString::from(message)),
                     returnValue: Some(DOMString::from(return_value)),
                 }.to_jsval(cx, rval)
             }
+            MozBrowserEvent::VisibilityChange(visibility) => {
+                BrowserElementVisibilityChangeEventDetail {
+                    visible: Some(visibility),
+                }.to_jsval(cx, rval);
+            }
         }
     }
 }
 
 pub fn Navigate(iframe: &HTMLIFrameElement, direction: NavigationDirection) -> ErrorResult {
     if iframe.Mozbrowser() {
         if iframe.upcast::<Node>().is_in_doc() {
             let window = window_from_node(iframe);
@@ -491,16 +519,40 @@ impl HTMLIFrameElementMethods for HTMLIF
             Ok(())
         } else {
             debug!("this frame is not mozbrowser: mozbrowser attribute missing, or not a top
                 level window, or mozbrowser preference not set (use --pref dom.mozbrowser.enabled)");
             Err(Error::NotSupported)
         }
     }
 
+    // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/setVisible
+    fn SetVisible(&self, visible: bool) -> ErrorResult {
+        if self.Mozbrowser() {
+            self.set_visible(visible);
+            Ok(())
+        } else {
+            debug!("this frame is not mozbrowser: mozbrowser attribute missing, or not a top
+                level window, or mozbrowser preference not set (use --pref dom.mozbrowser.enabled)");
+            Err(Error::NotSupported)
+        }
+    }
+
+    // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/getVisible
+    fn GetVisible(&self) -> Fallible<bool> {
+        if self.Mozbrowser() {
+            Ok(self.visibility.get())
+        } else {
+            debug!("this frame is not mozbrowser: mozbrowser attribute missing, or not a top
+                level window, or mozbrowser preference not set (use --pref dom.mozbrowser.enabled)");
+            Err(Error::NotSupported)
+        }
+    }
+
+
     // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/stop
     fn Stop(&self) -> ErrorResult {
         Err(Error::NotSupported)
     }
 
     // https://html.spec.whatwg.org/multipage/#dom-dim-width
     make_getter!(Width, "width");
     // https://html.spec.whatwg.org/multipage/#dom-dim-width
--- a/servo/components/script/dom/webidls/BrowserElement.webidl
+++ b/servo/components/script/dom/webidls/BrowserElement.webidl
@@ -91,30 +91,34 @@ dictionary BrowserElementOpenTabEventDet
 dictionary BrowserElementOpenWindowEventDetail {
   // https://developer.mozilla.org/en-US/docs/Web/Events/mozbrowseropenwindow
   DOMString url;
   DOMString target;
   DOMString features;
   // Element frameElement;
 };
 
+dictionary BrowserElementVisibilityChangeEventDetail {
+  boolean visible;
+};
+
 BrowserElement implements BrowserElementCommon;
 BrowserElement implements BrowserElementPrivileged;
 
 [NoInterfaceObject]
 interface BrowserElementCommon {
-  //[Throws,
-  // Pref="dom.mozBrowserFramesEnabled",
-  // CheckAnyPermissions="browser embed-widgets"]
-  //void setVisible(boolean visible);
+  [Throws,
+   Pref="dom.mozbrowser.enabled",
+   CheckAnyPermissions="browser embed-widgets"]
+  void setVisible(boolean visible);
 
-  //[Throws,
-  // Pref="dom.mozBrowserFramesEnabled",
-  // CheckAnyPermissions="browser embed-widgets"]
-  //DOMRequest getVisible();
+  [Throws,
+   Pref="dom.mozbrowser.enabled",
+   CheckAnyPermissions="browser embed-widgets"]
+  boolean getVisible();
 
   //[Throws,
   // Pref="dom.mozBrowserFramesEnabled",
   // CheckAnyPermissions="browser embed-widgets"]
   //void setActive(boolean active);
 
   //[Throws,
   // Pref="dom.mozBrowserFramesEnabled",
--- a/servo/components/script/dom/window.rs
+++ b/servo/components/script/dom/window.rs
@@ -1504,16 +1504,24 @@ impl Window {
         // activating this document due to a navigation.
         self.Document().title_changed();
     }
 
     pub fn freeze(&self) {
         self.timers.suspend();
     }
 
+    pub fn slow_down_timers(&self) {
+        self.timers.slow_down();
+    }
+
+    pub fn speed_up_timers(&self) {
+        self.timers.speed_up();
+    }
+
     pub fn need_emit_timeline_marker(&self, timeline_type: TimelineMarkerType) -> bool {
         let markers = self.devtools_markers.borrow();
         markers.contains(&timeline_type)
     }
 
     pub fn emit_timeline_marker(&self, marker: TimelineMarker) {
         let sender = self.devtools_marker_sender.borrow();
         let sender = sender.as_ref().expect("There is no marker sender");
--- a/servo/components/script/script_thread.rs
+++ b/servo/components/script/script_thread.rs
@@ -135,16 +135,18 @@ struct InProgressLoad {
     /// The current window size associated with this pipeline.
     window_size: Option<WindowSizeData>,
     /// Channel to the layout thread associated with this pipeline.
     layout_chan: Sender<layout_interface::Msg>,
     /// The current viewport clipping rectangle applying to this pipeline, if any.
     clip_rect: Option<Rect<f32>>,
     /// Window is frozen (navigated away while loading for example).
     is_frozen: bool,
+    /// Window is visible.
+    is_visible: bool,
     /// The requested URL of the load.
     url: Url,
 }
 
 impl InProgressLoad {
     /// Create a new InProgressLoad object.
     fn new(id: PipelineId,
            parent_info: Option<(PipelineId, SubpageId, FrameType)>,
@@ -153,16 +155,17 @@ impl InProgressLoad {
            url: Url) -> InProgressLoad {
         InProgressLoad {
             pipeline_id: id,
             parent_info: parent_info,
             layout_chan: layout_chan,
             window_size: window_size,
             clip_rect: None,
             is_frozen: false,
+            is_visible: true,
             url: url,
         }
     }
 }
 
 /// Encapsulated state required to create cancellable runnables from non-script threads.
 pub struct RunnableWrapper {
     pub cancelled: Arc<AtomicBool>,
@@ -914,16 +917,20 @@ impl ScriptThread {
             ConstellationControlMsg::ExitPipeline(..) =>
                 panic!("should have handled ExitPipeline already"),
             ConstellationControlMsg::GetTitle(pipeline_id) =>
                 self.handle_get_title_msg(pipeline_id),
             ConstellationControlMsg::Freeze(pipeline_id) =>
                 self.handle_freeze_msg(pipeline_id),
             ConstellationControlMsg::Thaw(pipeline_id) =>
                 self.handle_thaw_msg(pipeline_id),
+            ConstellationControlMsg::ChangeFrameVisibilityStatus(pipeline_id, visible) =>
+                self.handle_visibility_change_msg(pipeline_id, visible),
+            ConstellationControlMsg::NotifyVisibilityChange(containing_id, pipeline_id, visible) =>
+                self.handle_visibility_change_complete_msg(containing_id, pipeline_id, visible),
             ConstellationControlMsg::MozBrowserEvent(parent_pipeline_id,
                                                      subpage_id,
                                                      event) =>
                 self.handle_mozbrowser_event_msg(parent_pipeline_id,
                                                  subpage_id,
                                                  event),
             ConstellationControlMsg::UpdateSubpageId(containing_pipeline_id,
                                                      old_subpage_id,
@@ -1227,16 +1234,65 @@ impl ScriptThread {
                 urls.push(current_url);
             }
         }
         let path_seg = format!("url({})", urls.join(", "));
         reports.extend(get_reports(self.get_cx(), path_seg));
         reports_chan.send(reports);
     }
 
+    /// To slow/speed up timers and manage any other script thread resource based on visibility.
+    /// Returns true if successful.
+    fn alter_resource_utilization(&self, id: PipelineId, visible: bool) -> bool {
+        if let Some(root_context) = self.browsing_context.get() {
+            if let Some(ref inner_context) = root_context.find(id) {
+                let window = inner_context.active_window();
+                if visible {
+                    window.speed_up_timers();
+                } else {
+                    window.slow_down_timers();
+                }
+                return true;
+            }
+        }
+        false
+    }
+
+    /// Updates iframe element after a change in visibility
+    fn handle_visibility_change_complete_msg(&self, containing_id: PipelineId, id: PipelineId, visible: bool) {
+        if let Some(root_context) = self.browsing_context.get() {
+            if let Some(ref inner_context) = root_context.find(containing_id) {
+                if let Some(iframe) = inner_context.active_document().find_iframe_by_pipeline(id) {
+                    iframe.change_visibility_status(visible);
+                }
+            }
+        }
+    }
+
+    /// Handle visibility change message
+    fn handle_visibility_change_msg(&self, id: PipelineId, visible: bool) {
+        let resources_altered = self.alter_resource_utilization(id, visible);
+
+        // Separate message sent since parent script thread could be different (Iframe of different
+        // domain)
+        self.constellation_chan.send(ConstellationMsg::VisibilityChangeComplete(id, visible)).unwrap();
+
+        if !resources_altered {
+            let mut loads = self.incomplete_loads.borrow_mut();
+            if let Some(ref mut load) = loads.iter_mut().find(|load| load.pipeline_id == id) {
+                load.is_visible = visible;
+                return;
+            }
+        } else {
+            return;
+        }
+
+        warn!("change visibility message sent to nonexistent pipeline");
+    }
+
     /// Handles freeze message
     fn handle_freeze_msg(&self, id: PipelineId) {
         if let Some(root_context) = self.browsing_context.get() {
             if let Some(ref inner_context) = root_context.find(id) {
                 let window = inner_context.active_window();
                 window.freeze();
                 return;
             }
@@ -1702,16 +1758,20 @@ impl ScriptThread {
                        final_url,
                        ParseContext::Owner(Some(incomplete.pipeline_id)));
         }
 
         if incomplete.is_frozen {
             window.freeze();
         }
 
+        if !incomplete.is_visible {
+            self.alter_resource_utilization(browsing_context.pipeline(), false);
+        }
+
         context_remover.neuter();
 
         document.get_current_parser().unwrap()
     }
 
     fn notify_devtools(&self, title: DOMString, url: Url, ids: (PipelineId, Option<WorkerId>)) {
         if let Some(ref chan) = self.devtools_chan {
             let page_info = DevtoolsPageInfo {
--- a/servo/components/script/timers.rs
+++ b/servo/components/script/timers.rs
@@ -17,16 +17,17 @@ use js::jsapi::{HandleValue, Heap, Roote
 use js::jsval::{JSVal, UndefinedValue};
 use script_traits::{MsDuration, precise_time_ms};
 use script_traits::{TimerEvent, TimerEventId, TimerEventRequest, TimerSource};
 use std::cell::Cell;
 use std::cmp::{self, Ord, Ordering};
 use std::collections::HashMap;
 use std::default::Default;
 use std::rc::Rc;
+use util::prefs::get_pref;
 
 #[derive(JSTraceable, PartialEq, Eq, Copy, Clone, HeapSizeOf, Hash, PartialOrd, Ord, Debug)]
 pub struct OneshotTimerHandle(i32);
 
 #[derive(JSTraceable, HeapSizeOf)]
 #[privatize]
 pub struct OneshotTimers {
     js_timers: JsTimers,
@@ -207,16 +208,25 @@ impl OneshotTimers {
         let offset = self.suspension_offset.get();
 
         match self.suspended_since.get() {
             Some(time) => time - offset,
             None => precise_time_ms() - offset,
         }
     }
 
+    pub fn slow_down(&self) {
+        let duration = get_pref("js.timers.minimum_duration").as_u64().unwrap_or(1000);
+        self.js_timers.set_min_duration(MsDuration::new(duration));
+    }
+
+    pub fn speed_up(&self) {
+        self.js_timers.remove_min_duration();
+    }
+
     pub fn suspend(&self) {
         assert!(self.suspended_since.get().is_none());
 
         self.suspended_since.set(Some(precise_time_ms()));
         self.invalidate_expected_event_id();
     }
 
     pub fn resume(&self) {
@@ -285,16 +295,18 @@ pub struct JsTimerHandle(i32);
 
 #[derive(JSTraceable, HeapSizeOf)]
 #[privatize]
 pub struct JsTimers {
     next_timer_handle: Cell<JsTimerHandle>,
     active_timers: DOMRefCell<HashMap<JsTimerHandle, JsTimerEntry>>,
     /// The nesting level of the currently executing timer task or 0.
     nesting_level: Cell<u32>,
+    /// Used to introduce a minimum delay in event intervals
+    min_duration: Cell<Option<MsDuration>>,
 }
 
 #[derive(JSTraceable, HeapSizeOf)]
 struct JsTimerEntry {
     oneshot_handle: OneshotTimerHandle,
 }
 
 // Holder for the various JS values associated with setTimeout
@@ -339,16 +351,17 @@ impl HeapSizeOf for InternalTimerCallbac
 }
 
 impl JsTimers {
     pub fn new() -> JsTimers {
         JsTimers {
             next_timer_handle: Cell::new(JsTimerHandle(1)),
             active_timers: DOMRefCell::new(HashMap::new()),
             nesting_level: Cell::new(0),
+            min_duration: Cell::new(None),
         }
     }
 
     // see https://html.spec.whatwg.org/multipage/#timer-initialisation-steps
     pub fn set_timeout_or_interval(&self,
                                global: GlobalRef,
                                callback: TimerCallback,
                                arguments: Vec<HandleValue>,
@@ -402,31 +415,48 @@ impl JsTimers {
     pub fn clear_timeout_or_interval(&self, global: GlobalRef, handle: i32) {
         let mut active_timers = self.active_timers.borrow_mut();
 
         if let Some(entry) = active_timers.remove(&JsTimerHandle(handle)) {
             global.unschedule_callback(entry.oneshot_handle);
         }
     }
 
+    pub fn set_min_duration(&self, duration: MsDuration) {
+        self.min_duration.set(Some(duration));
+    }
+
+    pub fn remove_min_duration(&self) {
+        self.min_duration.set(None);
+    }
+
+    // see step 13 of https://html.spec.whatwg.org/multipage/#timer-initialisation-steps
+    fn user_agent_pad(&self, current_duration: MsDuration) -> MsDuration {
+        match self.min_duration.get() {
+            Some(min_duration) => {
+                cmp::max(min_duration, current_duration)
+            },
+            None => current_duration
+        }
+    }
+
     // see https://html.spec.whatwg.org/multipage/#timer-initialisation-steps
     fn initialize_and_schedule(&self, global: GlobalRef, mut task: JsTimerTask) {
         let handle = task.handle;
         let mut active_timers = self.active_timers.borrow_mut();
 
         // step 6
         let nesting_level = self.nesting_level.get();
 
-        // step 7
-        let duration = clamp_duration(nesting_level, task.duration);
-
+        // step 7, 13
+        let duration = self.user_agent_pad(clamp_duration(nesting_level, task.duration));
         // step 8, 9
         task.nesting_level = nesting_level + 1;
 
-        // essentially step 11-14
+        // essentially step 11, 12, and 14
         let callback = OneshotTimerCallback::JsTimer(task);
         let oneshot_handle = global.schedule_callback(callback, duration);
 
         // step 3
         let entry = active_timers.entry(handle).or_insert(JsTimerEntry {
             oneshot_handle: oneshot_handle,
         });
         entry.oneshot_handle = oneshot_handle;
--- a/servo/components/script_traits/lib.rs
+++ b/servo/components/script_traits/lib.rs
@@ -158,16 +158,20 @@ pub enum ConstellationControlMsg {
     /// Notifies script of a new set of scroll offsets.
     SetScrollState(PipelineId, Vec<(UntrustedNodeAddress, Point2D<f32>)>),
     /// Requests that the script thread immediately send the constellation the title of a pipeline.
     GetTitle(PipelineId),
     /// Notifies script thread to suspend all its timers
     Freeze(PipelineId),
     /// Notifies script thread to resume all its timers
     Thaw(PipelineId),
+    /// Notifies script thread whether frame is visible
+    ChangeFrameVisibilityStatus(PipelineId, bool),
+    /// Notifies script thread that frame visibility change is complete
+    NotifyVisibilityChange(PipelineId, PipelineId, bool),
     /// Notifies script thread that a url should be loaded in this iframe.
     Navigate(PipelineId, SubpageId, LoadData),
     /// Requests the script thread forward a mozbrowser event to an iframe it owns
     MozBrowserEvent(PipelineId, SubpageId, MozBrowserEvent),
     /// Updates the current subpage and pipeline IDs of a given iframe
     UpdateSubpageId(PipelineId, SubpageId, SubpageId, PipelineId),
     /// Set an iframe to be focused. Used when an element in an iframe gains focus.
     FocusIFrame(PipelineId, SubpageId),
@@ -446,16 +450,18 @@ pub enum MozBrowserEvent {
     /// Sent when alert(), confirm(), or prompt() is called within a browser `<iframe>`.
     ShowModalPrompt(String, String, String, String), // TODO(simartin): Handle unblock()
     /// Sent when the document.title changes within a browser `<iframe>`.
     TitleChange(String),
     /// Sent when an HTTP authentification is requested.
     UsernameAndPasswordRequired,
     /// Sent when a link to a search engine is found.
     OpenSearch,
+    /// Sent when visibility state changes.
+    VisibilityChange(bool),
 }
 
 impl MozBrowserEvent {
     /// Get the name of the event as a `& str`
     pub fn name(&self) -> &'static str {
         match *self {
             MozBrowserEvent::AsyncScroll => "mozbrowserasyncscroll",
             MozBrowserEvent::Close => "mozbrowserclose",
@@ -467,17 +473,18 @@ impl MozBrowserEvent {
             MozBrowserEvent::LoadStart => "mozbrowserloadstart",
             MozBrowserEvent::LocationChange(_, _, _) => "mozbrowserlocationchange",
             MozBrowserEvent::OpenTab(_) => "mozbrowseropentab",
             MozBrowserEvent::OpenWindow(_, _, _) => "mozbrowseropenwindow",
             MozBrowserEvent::SecurityChange(_) => "mozbrowsersecuritychange",
             MozBrowserEvent::ShowModalPrompt(_, _, _, _) => "mozbrowsershowmodalprompt",
             MozBrowserEvent::TitleChange(_) => "mozbrowsertitlechange",
             MozBrowserEvent::UsernameAndPasswordRequired => "mozbrowserusernameandpasswordrequired",
-            MozBrowserEvent::OpenSearch => "mozbrowseropensearch"
+            MozBrowserEvent::OpenSearch => "mozbrowseropensearch",
+            MozBrowserEvent::VisibilityChange(_) => "mozbrowservisibilitychange",
         }
     }
 }
 
 // https://developer.mozilla.org/en-US/docs/Web/Events/mozbrowsererror
 /// The different types of Browser error events
 #[derive(Deserialize, Serialize)]
 pub enum MozBrowserErrorType {
--- a/servo/components/script_traits/script_msg.rs
+++ b/servo/components/script_traits/script_msg.rs
@@ -76,16 +76,20 @@ pub enum ScriptMsg {
     /// HTMLIFrameElement Forward or Back navigation.
     Navigate(Option<(PipelineId, SubpageId)>, NavigationDirection),
     /// Favicon detected
     NewFavicon(Url),
     /// Status message to be displayed in the chrome, eg. a link URL on mouseover.
     NodeStatus(Option<String>),
     /// Notification that this iframe should be removed.
     RemoveIFrame(PipelineId, Option<IpcSender<()>>),
+    /// Change pipeline visibility
+    SetVisible(PipelineId, bool),
+    /// Notifies constellation that an iframe's visibility has been changed.
+    VisibilityChangeComplete(PipelineId, bool),
     /// A load has been requested in an IFrame.
     ScriptLoadedURLInIFrame(IFrameLoadInfo),
     /// Requests that the constellation set the contents of the clipboard
     SetClipboardContents(String),
     /// Mark a new document as active
     ActivateDocument(PipelineId),
     /// Set the document state for a pipeline (used by screenshot / reftests)
     SetDocumentState(PipelineId, DocumentState),