diff --git a/Cargo.lock b/Cargo.lock index 555d81f5236..2f1ac73911e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -135,6 +135,22 @@ dependencies = [ "servo-skia 0.30000020.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "background_hang_monitor" +version = "0.0.1" +dependencies = [ + "backtrace 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "crossbeam-channel 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "ipc-channel 0.11.2 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", + "mach 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "msg 0.0.1", + "serde 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "backtrace" version = "0.3.9" @@ -561,6 +577,7 @@ dependencies = [ name = "constellation" version = "0.0.1" dependencies = [ + "background_hang_monitor 0.0.1", "backtrace 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", "bluetooth_traits 0.0.1", "canvas 0.0.1", @@ -2095,6 +2112,7 @@ dependencies = [ name = "libservo" version = "0.0.1" dependencies = [ + "background_hang_monitor 0.0.1", "bluetooth 0.0.1", "bluetooth_traits 0.0.1", "canvas 0.0.1", @@ -2217,6 +2235,14 @@ name = "mac" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "mach" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -4976,6 +5002,7 @@ dependencies = [ "checksum log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "d4fcce5fa49cc693c312001daf1d13411c4a5283796bac1084299ea3e567113f" "checksum lzw 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7d947cbb889ed21c2a84be6ffbaebf5b4e0f4340638cba0444907e38b56be084" "checksum mac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +"checksum mach 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "86dd2487cdfea56def77b88438a2c915fb45113c5319bfe7e14306ca4cd0b0e1" "checksum malloc_buf 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" "checksum markup5ever 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c220b3a3d75543b76e5c1fcab6635a8430ab5f9bfa011d003c3787ae0abf4ffa" "checksum matches 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "efd7622e3022e1a6eaa602c4cea8912254e5582c9c692e9167714182244801b1" diff --git a/components/background_hang_monitor/Cargo.toml b/components/background_hang_monitor/Cargo.toml new file mode 100644 index 00000000000..35795a87951 --- /dev/null +++ b/components/background_hang_monitor/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "background_hang_monitor" +version = "0.0.1" +authors = ["The Servo Project Developers"] +license = "MPL-2.0" +publish = false +edition = "2018" + +[lib] +name = "background_hang_monitor" +path = "lib.rs" +test = false +doctest = false + +[dependencies] +backtrace = "0.3" +bitflags = "1.0" +ipc-channel = "0.11" +lazy_static = "1" +libc = "0.2" +log = "0.4" +msg = {path = "../msg"} +serde = "1.0.60" +crossbeam-channel = "0.3" + +[target.'cfg(target_os = "macos")'.dependencies] +mach = "0.2.3" diff --git a/components/background_hang_monitor/background_hang_monitor.rs b/components/background_hang_monitor/background_hang_monitor.rs new file mode 100644 index 00000000000..e703dac173f --- /dev/null +++ b/components/background_hang_monitor/background_hang_monitor.rs @@ -0,0 +1,260 @@ +/* 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 crate::sampler::Sampler; +use crossbeam_channel::{after, unbounded, Receiver, Sender}; +use ipc_channel::ipc::IpcSender; +use msg::constellation_msg::MonitoredComponentId; +use msg::constellation_msg::{ + BackgroundHangMonitor, BackgroundHangMonitorClone, BackgroundHangMonitorRegister, +}; +use msg::constellation_msg::{HangAlert, HangAnnotation}; +use std::cell::Cell; +use std::collections::HashMap; +use std::thread; +use std::time::{Duration, Instant}; + +#[derive(Clone)] +pub struct HangMonitorRegister { + sender: Sender<(MonitoredComponentId, MonitoredComponentMsg)>, +} + +impl HangMonitorRegister { + /// Start a new hang monitor worker, and return a handle to register components for monitoring. + pub fn init(constellation_chan: IpcSender) -> Box { + let (sender, port) = unbounded(); + let _ = thread::Builder::new().spawn(move || { + let mut monitor = { BackgroundHangMonitorWorker::new(constellation_chan, port) }; + while monitor.run() { + // Monitoring until all senders have been dropped... + } + }); + Box::new(HangMonitorRegister { sender }) + } +} + +impl BackgroundHangMonitorRegister for HangMonitorRegister { + /// Register a component for monitoring. + /// Returns a dedicated wrapper around a sender + /// to be used for communication with the hang monitor worker. + fn register_component( + &self, + component_id: MonitoredComponentId, + transient_hang_timeout: Duration, + permanent_hang_timeout: Duration, + ) -> Box { + let bhm_chan = BackgroundHangMonitorChan::new(self.sender.clone(), component_id); + + #[cfg(target_os = "windows")] + let sampler = crate::sampler_windows::WindowsSampler::new(); + #[cfg(target_os = "macos")] + let sampler = crate::sampler_mac::MacOsSampler::new(); + #[cfg(any(target_os = "android", target_os = "linux"))] + let sampler = crate::sampler_linux::LinuxSampler::new(); + + bhm_chan.send(MonitoredComponentMsg::Register( + sampler, + transient_hang_timeout, + permanent_hang_timeout, + )); + Box::new(bhm_chan) + } +} + +impl BackgroundHangMonitorClone for HangMonitorRegister { + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + +/// Messages sent from monitored components to the monitor. +pub enum MonitoredComponentMsg { + /// Register component for monitoring, + Register(Box, Duration, Duration), + /// Notify start of new activity for a given component, + NotifyActivity(HangAnnotation), + /// Notify start of waiting for a new task to come-in for processing. + NotifyWait, +} + +/// A wrapper around a sender to the monitor, +/// which will send the Id of the monitored component along with each message, +/// and keep track of whether the monitor is still listening on the other end. +pub struct BackgroundHangMonitorChan { + sender: Sender<(MonitoredComponentId, MonitoredComponentMsg)>, + component_id: MonitoredComponentId, + disconnected: Cell, +} + +impl BackgroundHangMonitorChan { + pub fn new( + sender: Sender<(MonitoredComponentId, MonitoredComponentMsg)>, + component_id: MonitoredComponentId, + ) -> Self { + BackgroundHangMonitorChan { + sender, + component_id: component_id, + disconnected: Default::default(), + } + } + + pub fn send(&self, msg: MonitoredComponentMsg) { + if self.disconnected.get() { + return; + } + if let Err(_) = self.sender.send((self.component_id.clone(), msg)) { + warn!("BackgroundHangMonitor has gone away"); + self.disconnected.set(true); + } + } +} + +impl BackgroundHangMonitor for BackgroundHangMonitorChan { + fn notify_activity(&self, annotation: HangAnnotation) { + let msg = MonitoredComponentMsg::NotifyActivity(annotation); + self.send(msg); + } + fn notify_wait(&self) { + let msg = MonitoredComponentMsg::NotifyWait; + self.send(msg); + } +} + +struct MonitoredComponent { + sampler: Box, + last_activity: Instant, + last_annotation: Option, + transient_hang_timeout: Duration, + permanent_hang_timeout: Duration, + sent_transient_alert: bool, + sent_permanent_alert: bool, + is_waiting: bool, +} + +pub struct BackgroundHangMonitorWorker { + monitored_components: HashMap, + constellation_chan: IpcSender, + port: Receiver<(MonitoredComponentId, MonitoredComponentMsg)>, +} + +impl BackgroundHangMonitorWorker { + pub fn new( + constellation_chan: IpcSender, + port: Receiver<(MonitoredComponentId, MonitoredComponentMsg)>, + ) -> Self { + Self { + monitored_components: Default::default(), + constellation_chan, + port, + } + } + + pub fn run(&mut self) -> bool { + let received = select! { + recv(self.port) -> event => { + match event { + Ok(msg) => Some(msg), + // Our sender has been dropped, quit. + Err(_) => return false, + } + }, + recv(after(Duration::from_millis(100))) -> _ => None, + }; + if let Some(msg) = received { + self.handle_msg(msg); + while let Ok(another_msg) = self.port.try_recv() { + // Handle any other incoming messages, + // before performing a hang checkpoint. + self.handle_msg(another_msg); + } + } + self.perform_a_hang_monitor_checkpoint(); + true + } + + fn handle_msg(&mut self, msg: (MonitoredComponentId, MonitoredComponentMsg)) { + match msg { + ( + component_id, + MonitoredComponentMsg::Register( + sampler, + transient_hang_timeout, + permanent_hang_timeout, + ), + ) => { + let component = MonitoredComponent { + sampler, + last_activity: Instant::now(), + last_annotation: None, + transient_hang_timeout, + permanent_hang_timeout, + sent_transient_alert: false, + sent_permanent_alert: false, + is_waiting: true, + }; + assert!( + self.monitored_components + .insert(component_id, component) + .is_none(), + "This component was already registered for monitoring." + ); + }, + (component_id, MonitoredComponentMsg::NotifyActivity(annotation)) => { + let component = self + .monitored_components + .get_mut(&component_id) + .expect("Received NotifyActivity for an unknown component"); + component.last_activity = Instant::now(); + component.last_annotation = Some(annotation); + component.sent_transient_alert = false; + component.sent_permanent_alert = false; + component.is_waiting = false; + }, + (component_id, MonitoredComponentMsg::NotifyWait) => { + let component = self + .monitored_components + .get_mut(&component_id) + .expect("Received NotifyWait for an unknown component"); + component.last_activity = Instant::now(); + component.sent_transient_alert = false; + component.sent_permanent_alert = false; + component.is_waiting = true; + }, + } + } + + fn perform_a_hang_monitor_checkpoint(&mut self) { + for (component_id, monitored) in self.monitored_components.iter_mut() { + if monitored.is_waiting { + continue; + } + let last_annotation = monitored.last_annotation.unwrap(); + if monitored.last_activity.elapsed() > monitored.permanent_hang_timeout { + if monitored.sent_permanent_alert { + continue; + } + let profile = match monitored.sampler.suspend_and_sample_thread() { + Ok(native_stack) => Some(native_stack.to_hangprofile()), + Err(()) => None, + }; + let _ = self.constellation_chan.send(HangAlert::Permanent( + component_id.clone(), + last_annotation, + profile, + )); + monitored.sent_permanent_alert = true; + continue; + } + if monitored.last_activity.elapsed() > monitored.transient_hang_timeout { + if monitored.sent_transient_alert { + continue; + } + let _ = self + .constellation_chan + .send(HangAlert::Transient(component_id.clone(), last_annotation)); + monitored.sent_transient_alert = true; + } + } + } +} diff --git a/components/background_hang_monitor/lib.rs b/components/background_hang_monitor/lib.rs new file mode 100644 index 00000000000..d76bcf6d4b9 --- /dev/null +++ b/components/background_hang_monitor/lib.rs @@ -0,0 +1,21 @@ +/* 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/. */ + +#![deny(unsafe_code)] + +#[macro_use] +extern crate crossbeam_channel; +#[macro_use] +extern crate log; + +pub mod background_hang_monitor; +mod sampler; +#[cfg(any(target_os = "android", target_os = "linux"))] +mod sampler_linux; +#[cfg(target_os = "macos")] +mod sampler_mac; +#[cfg(target_os = "windows")] +mod sampler_windows; + +pub use self::background_hang_monitor::*; diff --git a/components/background_hang_monitor/sampler.rs b/components/background_hang_monitor/sampler.rs new file mode 100644 index 00000000000..f83fb0f84a9 --- /dev/null +++ b/components/background_hang_monitor/sampler.rs @@ -0,0 +1,83 @@ +/* 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 backtrace; +use msg::constellation_msg::{HangProfile, HangProfileSymbol}; +use std::ptr; + +const MAX_NATIVE_FRAMES: usize = 1024; + +pub trait Sampler: Send { + fn suspend_and_sample_thread(&self) -> Result; +} + +// Several types in this file are currently not used in a Linux or Windows build. +#[allow(dead_code)] +pub type Address = *const libc::uint8_t; + +/// The registers used for stack unwinding +#[allow(dead_code)] +pub struct Registers { + /// Instruction pointer. + pub instruction_ptr: Address, + /// Stack pointer. + pub stack_ptr: Address, + /// Frame pointer. + pub frame_ptr: Address, +} + +pub struct NativeStack { + instruction_ptrs: [*mut std::ffi::c_void; MAX_NATIVE_FRAMES], + stack_ptrs: [*mut std::ffi::c_void; MAX_NATIVE_FRAMES], + count: usize, +} + +impl NativeStack { + pub fn new() -> Self { + NativeStack { + instruction_ptrs: [ptr::null_mut(); MAX_NATIVE_FRAMES], + stack_ptrs: [ptr::null_mut(); MAX_NATIVE_FRAMES], + count: 0, + } + } + + pub fn process_register( + &mut self, + instruction_ptr: *mut std::ffi::c_void, + stack_ptr: *mut std::ffi::c_void, + ) -> Result<(), ()> { + if !(self.count < MAX_NATIVE_FRAMES) { + return Err(()); + } + self.instruction_ptrs[self.count] = instruction_ptr; + self.stack_ptrs[self.count] = stack_ptr; + self.count = self.count + 1; + Ok(()) + } + + pub fn to_hangprofile(&self) -> HangProfile { + let mut profile = HangProfile { + backtrace: Vec::new(), + }; + for ip in self.instruction_ptrs.iter().rev() { + if ip.is_null() { + continue; + } + backtrace::resolve(*ip, |symbol| { + // TODO: use the demangled or C++ demangled symbols if available. + let name = symbol + .name() + .map(|n| String::from_utf8_lossy(&n.as_bytes()).to_string()); + let filename = symbol.filename().map(|n| n.to_string_lossy().to_string()); + let lineno = symbol.lineno(); + profile.backtrace.push(HangProfileSymbol { + name, + filename, + lineno, + }); + }); + } + profile + } +} diff --git a/components/background_hang_monitor/sampler_linux.rs b/components/background_hang_monitor/sampler_linux.rs new file mode 100644 index 00000000000..4d7ebbc6153 --- /dev/null +++ b/components/background_hang_monitor/sampler_linux.rs @@ -0,0 +1,35 @@ +/* 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 crate::sampler::{NativeStack, Sampler}; +use libc; + +type MonitoredThreadId = libc::pthread_t; + +#[allow(dead_code)] +pub struct LinuxSampler { + thread_id: MonitoredThreadId, +} + +impl LinuxSampler { + #[allow(unsafe_code, dead_code)] + pub fn new() -> Box { + let thread_id = unsafe { libc::pthread_self() }; + Box::new(LinuxSampler { thread_id }) + } +} + +impl Sampler for LinuxSampler { + #[allow(unsafe_code)] + fn suspend_and_sample_thread(&self) -> Result { + // Warning: The "critical section" begins here. + // In the critical section: + // we must not do any dynamic memory allocation, + // nor try to acquire any lock + // or any other unshareable resource. + + // NOTE: End of "critical section". + Err(()) + } +} diff --git a/components/background_hang_monitor/sampler_mac.rs b/components/background_hang_monitor/sampler_mac.rs new file mode 100644 index 00000000000..d4669bb2b75 --- /dev/null +++ b/components/background_hang_monitor/sampler_mac.rs @@ -0,0 +1,118 @@ +/* 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 crate::sampler::{Address, NativeStack, Registers, Sampler}; +use libc; +use mach; +use std::panic; +use std::process; + +type MonitoredThreadId = mach::mach_types::thread_act_t; + +pub struct MacOsSampler { + thread_id: MonitoredThreadId, +} + +impl MacOsSampler { + #[allow(unsafe_code)] + pub fn new() -> Box { + let thread_id = unsafe { mach::mach_init::mach_thread_self() }; + Box::new(MacOsSampler { thread_id }) + } +} + +impl Sampler for MacOsSampler { + #[allow(unsafe_code)] + fn suspend_and_sample_thread(&self) -> Result { + // Warning: The "critical section" begins here. + // In the critical section: + // we must not do any dynamic memory allocation, + // nor try to acquire any lock + // or any other unshareable resource. + let current_hook = panic::take_hook(); + panic::set_hook(Box::new(|_| { + // Avoiding any allocation or locking as part of standard panicking. + process::abort(); + })); + let native_stack = unsafe { + if let Err(()) = suspend_thread(self.thread_id) { + panic::set_hook(current_hook); + return Err(()); + }; + let native_stack = match get_registers(self.thread_id) { + Ok(regs) => Ok(frame_pointer_stack_walk(regs)), + Err(()) => Err(()), + }; + if let Err(()) = resume_thread(self.thread_id) { + process::abort(); + } + native_stack + }; + panic::set_hook(current_hook); + // NOTE: End of "critical section". + native_stack + } +} + +fn check_kern_return(kret: mach::kern_return::kern_return_t) -> Result<(), ()> { + if kret != mach::kern_return::KERN_SUCCESS { + return Err(()); + } + Ok(()) +} + +#[allow(unsafe_code)] +unsafe fn suspend_thread(thread_id: MonitoredThreadId) -> Result<(), ()> { + check_kern_return(mach::thread_act::thread_suspend(thread_id)) +} + +#[allow(unsafe_code)] +unsafe fn get_registers(thread_id: MonitoredThreadId) -> Result { + let mut state = mach::structs::x86_thread_state64_t::new(); + let mut state_count = mach::structs::x86_thread_state64_t::count(); + let kret = mach::thread_act::thread_get_state( + thread_id, + mach::thread_status::x86_THREAD_STATE64, + (&mut state) as *mut _ as *mut _, + &mut state_count, + ); + check_kern_return(kret)?; + Ok(Registers { + instruction_ptr: state.__rip as Address, + stack_ptr: state.__rsp as Address, + frame_ptr: state.__rbp as Address, + }) +} + +#[allow(unsafe_code)] +unsafe fn resume_thread(thread_id: MonitoredThreadId) -> Result<(), ()> { + check_kern_return(mach::thread_act::thread_resume(thread_id)) +} + +#[allow(unsafe_code)] +unsafe fn frame_pointer_stack_walk(regs: Registers) -> NativeStack { + // Note: this function will only work with code build with: + // --dev, + // or --with-frame-pointer. + + let stackaddr = libc::pthread_get_stackaddr_np(libc::pthread_self()); + let mut native_stack = NativeStack::new(); + let pc = regs.instruction_ptr as *mut std::ffi::c_void; + let stack = regs.stack_ptr as *mut std::ffi::c_void; + let _ = native_stack.process_register(pc, stack); + let mut current = regs.frame_ptr as *mut *mut std::ffi::c_void; + while !current.is_null() { + if (current as usize) < stackaddr as usize { + break; + } + let next = *current as *mut *mut std::ffi::c_void; + let pc = current.add(1); + let stack = current.add(2); + if let Err(()) = native_stack.process_register(*pc, *stack) { + break; + } + current = next; + } + native_stack +} diff --git a/components/background_hang_monitor/sampler_windows.rs b/components/background_hang_monitor/sampler_windows.rs new file mode 100644 index 00000000000..1a235ed2513 --- /dev/null +++ b/components/background_hang_monitor/sampler_windows.rs @@ -0,0 +1,40 @@ +/* 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 crate::sampler::{NativeStack, Sampler}; + +type MonitoredThreadId = usize; // TODO: use winapi + +#[allow(dead_code)] +pub struct WindowsSampler { + thread_id: MonitoredThreadId, +} + +impl WindowsSampler { + #[allow(unsafe_code, dead_code)] + pub fn new() -> Box { + let thread_id = 0; // TODO: use winapi::um::processthreadsapi::GetThreadId + Box::new(WindowsSampler { thread_id }) + } +} + +impl Sampler for WindowsSampler { + fn suspend_and_sample_thread(&self) -> Result { + // Warning: The "critical section" begins here. + // In the critical section: + // we must not do any dynamic memory allocation, + // nor try to acquire any lock + // or any other unshareable resource. + + // TODO: + // 1: use winapi::um::processthreadsapi::SuspendThread + // 2: use winapi::um::processthreadsapi::GetThreadContext + // 3: populate registers using the context, see + // https://dxr.mozilla.org/mozilla-central/source/tools/profiler/core/platform-win32.cpp#129 + // 4: use winapi::um::processthreadsapi::ResumeThread + + // NOTE: End of "critical section". + Err(()) + } +} diff --git a/components/background_hang_monitor/tests/hang_monitor_tests.rs b/components/background_hang_monitor/tests/hang_monitor_tests.rs new file mode 100644 index 00000000000..f6904e5ee83 --- /dev/null +++ b/components/background_hang_monitor/tests/hang_monitor_tests.rs @@ -0,0 +1,107 @@ +/* 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 background_hang_monitor::HangMonitorRegister; +use ipc_channel::ipc; +use msg::constellation_msg::ScriptHangAnnotation; +use msg::constellation_msg::TEST_PIPELINE_ID; +use msg::constellation_msg::{HangAlert, HangAnnotation}; +use msg::constellation_msg::{MonitoredComponentId, MonitoredComponentType}; +use std::thread; +use std::time::Duration; + +#[test] +fn test_hang_monitoring() { + let (background_hang_monitor_ipc_sender, background_hang_monitor_receiver) = + ipc::channel().expect("ipc channel failure"); + + let background_hang_monitor_register = + HangMonitorRegister::init(background_hang_monitor_ipc_sender.clone()); + let background_hang_monitor = background_hang_monitor_register.register_component( + MonitoredComponentId(TEST_PIPELINE_ID, MonitoredComponentType::Script), + Duration::from_millis(10), + Duration::from_millis(1000), + ); + + // Start an activity. + let hang_annotation = HangAnnotation::Script(ScriptHangAnnotation::AttachLayout); + background_hang_monitor.notify_activity(hang_annotation); + + // Sleep until the "transient" timeout has been reached. + thread::sleep(Duration::from_millis(10)); + + // Check for a transient hang alert. + match background_hang_monitor_receiver.recv().unwrap() { + HangAlert::Transient(component_id, _annotation) => { + let expected = MonitoredComponentId(TEST_PIPELINE_ID, MonitoredComponentType::Script); + assert_eq!(expected, component_id); + }, + HangAlert::Permanent(..) => unreachable!(), + } + + // Sleep until the "permanent" timeout has been reached. + thread::sleep(Duration::from_millis(1000)); + + // Check for a permanent hang alert. + match background_hang_monitor_receiver.recv().unwrap() { + HangAlert::Permanent(component_id, _annotation, _profile) => { + let expected = MonitoredComponentId(TEST_PIPELINE_ID, MonitoredComponentType::Script); + assert_eq!(expected, component_id); + }, + HangAlert::Transient(..) => unreachable!(), + } + + // Now the component is not hanging anymore. + background_hang_monitor.notify_activity(hang_annotation); + assert!(background_hang_monitor_receiver.try_recv().is_err()); + + // Sleep for a while. + thread::sleep(Duration::from_millis(10)); + + // Check for a transient hang alert. + match background_hang_monitor_receiver.recv().unwrap() { + HangAlert::Transient(component_id, _annotation) => { + let expected = MonitoredComponentId(TEST_PIPELINE_ID, MonitoredComponentType::Script); + assert_eq!(expected, component_id); + }, + HangAlert::Permanent(..) => unreachable!(), + } + + // Now the component is waiting for a new task. + background_hang_monitor.notify_wait(); + + // Sleep for a while. + thread::sleep(Duration::from_millis(100)); + + // The component is still waiting, but not hanging. + assert!(background_hang_monitor_receiver.try_recv().is_err()); + + // New task handling starts. + background_hang_monitor.notify_activity(hang_annotation); + + // Sleep for a while. + thread::sleep(Duration::from_millis(10)); + + // We're getting new hang alerts for the latest task. + match background_hang_monitor_receiver.recv().unwrap() { + HangAlert::Transient(component_id, _annotation) => { + let expected = MonitoredComponentId(TEST_PIPELINE_ID, MonitoredComponentType::Script); + assert_eq!(expected, component_id); + }, + HangAlert::Permanent(..) => unreachable!(), + } + + // No new alert yet + assert!(background_hang_monitor_receiver.try_recv().is_err()); + + // Shut-down the hang monitor + drop(background_hang_monitor_register); + drop(background_hang_monitor); + + // Sleep until the "max-timeout" has been reached. + thread::sleep(Duration::from_millis(1000)); + + // Still no new alerts because the hang monitor has shut-down already. + assert!(background_hang_monitor_receiver.try_recv().is_err()); +} diff --git a/components/constellation/Cargo.toml b/components/constellation/Cargo.toml index 5ef39c5bbae..f9e9b351180 100644 --- a/components/constellation/Cargo.toml +++ b/components/constellation/Cargo.toml @@ -11,6 +11,7 @@ name = "constellation" path = "lib.rs" [dependencies] +background_hang_monitor = { path = "../background_hang_monitor"} backtrace = "0.3" bluetooth_traits = { path = "../bluetooth_traits" } canvas = {path = "../canvas"} diff --git a/components/constellation/constellation.rs b/components/constellation/constellation.rs index 2641c946967..62ebebf9ebe 100644 --- a/components/constellation/constellation.rs +++ b/components/constellation/constellation.rs @@ -89,6 +89,7 @@ //! //! See https://github.com/servo/servo/issues/14704 +use background_hang_monitor::HangMonitorRegister; use backtrace::Backtrace; use bluetooth_traits::BluetoothRequest; use canvas::canvas_paint_thread::CanvasPaintThread; @@ -123,6 +124,7 @@ use keyboard_types::webdriver::Event as WebDriverInputEvent; use keyboard_types::KeyboardEvent; use layout_traits::LayoutThreadFactory; use log::{Level, LevelFilter, Log, Metadata, Record}; +use msg::constellation_msg::{BackgroundHangMonitorRegister, HangAlert}; use msg::constellation_msg::{ BrowsingContextId, HistoryStateId, PipelineId, TopLevelBrowsingContextId, }; @@ -200,6 +202,18 @@ pub struct Constellation { /// This is the constellation's view of `script_sender`. script_receiver: Receiver>, + /// A handle to register components for hang monitoring. + /// None when in multiprocess mode. + background_monitor_register: Option>, + + /// A channel for the background hang monitor to send messages + /// to the constellation. + background_hang_monitor_sender: IpcSender, + + /// A channel for the constellation to receiver messages + /// from the background hang monitor. + background_hang_monitor_receiver: Receiver>, + /// An IPC channel for layout threads to send messages to the constellation. /// This is the layout threads' view of `layout_receiver`. layout_sender: IpcSender, @@ -587,6 +601,21 @@ where let script_receiver = route_ipc_receiver_to_new_mpsc_receiver_preserving_errors(ipc_script_receiver); + let (background_hang_monitor_sender, ipc_bhm_receiver) = + ipc::channel().expect("ipc channel failure"); + let background_hang_monitor_receiver = + route_ipc_receiver_to_new_mpsc_receiver_preserving_errors(ipc_bhm_receiver); + + // If we are in multiprocess mode, + // a dedicated per-process hang monitor will be initialized later inside the content process. + // See run_content_process in servo/lib.rs + let background_monitor_register = match opts::multiprocess() { + true => None, + false => Some(HangMonitorRegister::init( + background_hang_monitor_sender.clone(), + )), + }; + let (ipc_layout_sender, ipc_layout_receiver) = ipc::channel().expect("ipc channel failure"); let layout_receiver = @@ -602,6 +631,9 @@ where let mut constellation: Constellation = Constellation { script_sender: ipc_script_sender, + background_hang_monitor_sender, + background_hang_monitor_receiver, + background_monitor_register, layout_sender: ipc_layout_sender, script_receiver: script_receiver, compositor_receiver: compositor_receiver, @@ -768,6 +800,10 @@ where sender: self.script_sender.clone(), pipeline_id: pipeline_id, }, + background_monitor_register: self.background_monitor_register.clone(), + background_hang_monitor_to_constellation_chan: self + .background_hang_monitor_sender + .clone(), layout_to_constellation_chan: self.layout_sender.clone(), scheduler_chan: self.scheduler_chan.clone(), compositor_proxy: self.compositor_proxy.clone(), @@ -889,6 +925,7 @@ where #[derive(Debug)] enum Request { Script((PipelineId, FromScriptMsg)), + BackgroundHangMonitor(HangAlert), Compositor(FromCompositorMsg), Layout(FromLayoutMsg), NetworkListener((PipelineId, FetchResponseMsg)), @@ -910,6 +947,9 @@ where recv(self.script_receiver) -> msg => { msg.expect("Unexpected script channel panic in constellation").map(Request::Script) } + recv(self.background_hang_monitor_receiver) -> msg => { + msg.expect("Unexpected BHM channel panic in constellation").map(Request::BackgroundHangMonitor) + } recv(self.compositor_receiver) -> msg => { Ok(Request::Compositor(msg.expect("Unexpected compositor channel panic in constellation"))) } @@ -936,6 +976,9 @@ where Request::Script(message) => { self.handle_request_from_script(message); }, + Request::BackgroundHangMonitor(message) => { + self.handle_request_from_background_hang_monitor(message); + }, Request::Layout(message) => { self.handle_request_from_layout(message); }, @@ -948,6 +991,12 @@ where } } + fn handle_request_from_background_hang_monitor(&self, message: HangAlert) { + // TODO: In case of a permanent hang being reported, add a "kill script" workflow, + // via the embedder? + warn!("Component hang alert: {:?}", message); + } + fn handle_request_from_network_listener(&mut self, message: (PipelineId, FetchResponseMsg)) { let (id, message_) = message; let result = match self.pipelines.get(&id) { diff --git a/components/constellation/pipeline.rs b/components/constellation/pipeline.rs index b0df616cbdf..63376315101 100644 --- a/components/constellation/pipeline.rs +++ b/components/constellation/pipeline.rs @@ -18,6 +18,7 @@ use ipc_channel::Error; use layout_traits::LayoutThreadFactory; use metrics::PaintTimeMetrics; use msg::constellation_msg::TopLevelBrowsingContextId; +use msg::constellation_msg::{BackgroundHangMonitorRegister, HangAlert}; use msg::constellation_msg::{BrowsingContextId, HistoryStateId, PipelineId, PipelineNamespaceId}; use net::image_cache::ImageCacheImpl; use net_traits::image_cache::ImageCache; @@ -113,6 +114,13 @@ pub struct InitialPipelineState { /// A channel to the associated constellation. pub script_to_constellation_chan: ScriptToConstellationChan, + /// A handle to register components for hang monitoring. + /// None when in multiprocess mode. + pub background_monitor_register: Option>, + + /// A channel for the background hang monitor to send messages to the constellation. + pub background_hang_monitor_to_constellation_chan: IpcSender, + /// A channel for the layout thread to send messages to the constellation. pub layout_to_constellation_chan: IpcSender, @@ -262,6 +270,9 @@ impl Pipeline { parent_pipeline_id: state.parent_pipeline_id, opener: state.opener, script_to_constellation_chan: state.script_to_constellation_chan.clone(), + background_hang_monitor_to_constellation_chan: state + .background_hang_monitor_to_constellation_chan + .clone(), scheduler_chan: state.scheduler_chan, devtools_chan: script_to_devtools_chan, bluetooth_thread: state.bluetooth_thread, @@ -295,7 +306,11 @@ impl Pipeline { if opts::multiprocess() { let _ = unprivileged_pipeline_content.spawn_multiprocess()?; } else { - unprivileged_pipeline_content.start_all::(false); + // Should not be None in single-process mode. + let register = state + .background_monitor_register + .expect("Couldn't start content, no background monitor has been initiated"); + unprivileged_pipeline_content.start_all::(false, register); } EventLoop::new(script_chan) @@ -452,6 +467,7 @@ pub struct UnprivilegedPipelineContent { parent_pipeline_id: Option, opener: Option, script_to_constellation_chan: ScriptToConstellationChan, + background_hang_monitor_to_constellation_chan: IpcSender, layout_to_constellation_chan: IpcSender, scheduler_chan: IpcSender, devtools_chan: Option>, @@ -480,8 +496,11 @@ pub struct UnprivilegedPipelineContent { } impl UnprivilegedPipelineContent { - pub fn start_all(self, wait_for_completion: bool) - where + pub fn start_all( + self, + wait_for_completion: bool, + background_hang_monitor_register: Box, + ) where LTF: LayoutThreadFactory, STF: ScriptThreadFactory, { @@ -503,6 +522,7 @@ impl UnprivilegedPipelineContent { control_chan: self.script_chan.clone(), control_port: self.script_port, script_to_constellation_chan: self.script_to_constellation_chan.clone(), + background_hang_monitor_register: background_hang_monitor_register.clone(), layout_to_constellation_chan: self.layout_to_constellation_chan.clone(), scheduler_chan: self.scheduler_chan, bluetooth_thread: self.bluetooth_thread, @@ -529,6 +549,7 @@ impl UnprivilegedPipelineContent { self.parent_pipeline_id.is_some(), layout_pair, self.pipeline_port, + background_hang_monitor_register, self.layout_to_constellation_chan, self.script_chan, image_cache.clone(), @@ -626,6 +647,10 @@ impl UnprivilegedPipelineContent { } } + pub fn background_hang_monitor_to_constellation_chan(&self) -> &IpcSender { + &self.background_hang_monitor_to_constellation_chan + } + pub fn script_to_constellation_chan(&self) -> &ScriptToConstellationChan { &self.script_to_constellation_chan } diff --git a/components/layout_thread/lib.rs b/components/layout_thread/lib.rs index b5f9c8faf61..611bbbfac00 100644 --- a/components/layout_thread/lib.rs +++ b/components/layout_thread/lib.rs @@ -65,8 +65,11 @@ use layout_traits::LayoutThreadFactory; use libc::c_void; use malloc_size_of::{MallocSizeOf, MallocSizeOfOps}; use metrics::{PaintTimeMetrics, ProfilerMetadataFactory, ProgressiveWebMetric}; -use msg::constellation_msg::PipelineId; -use msg::constellation_msg::TopLevelBrowsingContextId; +use msg::constellation_msg::{ + BackgroundHangMonitor, BackgroundHangMonitorRegister, HangAnnotation, +}; +use msg::constellation_msg::{LayoutHangAnnotation, MonitoredComponentType, PipelineId}; +use msg::constellation_msg::{MonitoredComponentId, TopLevelBrowsingContextId}; use net_traits::image_cache::{ImageCache, UsePlaceholder}; use parking_lot::RwLock; use profile_traits::mem::{self as profile_mem, Report, ReportKind, ReportsChan}; @@ -96,6 +99,7 @@ use std::process; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex, MutexGuard}; use std::thread; +use std::time::Duration; use style::animation::Animation; use style::context::{QuirksMode, RegisteredSpeculativePainter, RegisteredSpeculativePainters}; use style::context::{SharedStyleContext, StyleSystemOptions, ThreadLocalStyleContextCreationInfo}; @@ -150,6 +154,9 @@ pub struct LayoutThread { /// The channel on which the font cache can send messages to us. font_cache_sender: IpcSender<()>, + /// A means of communication with the background hang monitor. + background_hang_monitor: Box, + /// The channel on which messages can be sent to the constellation. constellation_chan: IpcSender, @@ -253,6 +260,7 @@ impl LayoutThreadFactory for LayoutThread { is_iframe: bool, chan: (Sender, Receiver), pipeline_port: IpcReceiver, + background_hang_monitor_register: Box, constellation_chan: IpcSender, script_chan: IpcSender, image_cache: Arc, @@ -276,6 +284,14 @@ impl LayoutThreadFactory for LayoutThread { { // Ensures layout thread is destroyed before we send shutdown message let sender = chan.0; + + let background_hang_monitor = background_hang_monitor_register + .register_component( + MonitoredComponentId(id, MonitoredComponentType::Layout), + Duration::from_millis(1000), + Duration::from_millis(5000), + ); + let layout = LayoutThread::new( id, top_level_browsing_context_id, @@ -283,6 +299,7 @@ impl LayoutThreadFactory for LayoutThread { is_iframe, chan.1, pipeline_port, + background_hang_monitor, constellation_chan, script_chan, image_cache.clone(), @@ -444,6 +461,7 @@ impl LayoutThread { is_iframe: bool, port: Receiver, pipeline_port: IpcReceiver, + background_hang_monitor: Box, constellation_chan: IpcSender, script_chan: IpcSender, image_cache: Arc, @@ -493,6 +511,7 @@ impl LayoutThread { port: port, pipeline_port: pipeline_receiver, script_chan: script_chan.clone(), + background_hang_monitor, constellation_chan: constellation_chan.clone(), time_profiler_chan: time_profiler_chan, mem_profiler_chan: mem_profiler_chan, @@ -605,6 +624,34 @@ impl LayoutThread { } } + fn notify_activity_to_hang_monitor(&self, request: &Msg) { + let hang_annotation = match request { + Msg::AddStylesheet(..) => LayoutHangAnnotation::AddStylesheet, + Msg::RemoveStylesheet(..) => LayoutHangAnnotation::RemoveStylesheet, + Msg::SetQuirksMode(..) => LayoutHangAnnotation::SetQuirksMode, + Msg::Reflow(..) => LayoutHangAnnotation::Reflow, + Msg::GetRPC(..) => LayoutHangAnnotation::GetRPC, + Msg::TickAnimations => LayoutHangAnnotation::TickAnimations, + Msg::AdvanceClockMs(..) => LayoutHangAnnotation::AdvanceClockMs, + Msg::ReapStyleAndLayoutData(..) => LayoutHangAnnotation::ReapStyleAndLayoutData, + Msg::CollectReports(..) => LayoutHangAnnotation::CollectReports, + Msg::PrepareToExit(..) => LayoutHangAnnotation::PrepareToExit, + Msg::ExitNow => LayoutHangAnnotation::ExitNow, + Msg::GetCurrentEpoch(..) => LayoutHangAnnotation::GetCurrentEpoch, + Msg::GetWebFontLoadState(..) => LayoutHangAnnotation::GetWebFontLoadState, + Msg::CreateLayoutThread(..) => LayoutHangAnnotation::CreateLayoutThread, + Msg::SetFinalUrl(..) => LayoutHangAnnotation::SetFinalUrl, + Msg::SetScrollStates(..) => LayoutHangAnnotation::SetScrollStates, + Msg::UpdateScrollStateFromScript(..) => { + LayoutHangAnnotation::UpdateScrollStateFromScript + }, + Msg::RegisterPaint(..) => LayoutHangAnnotation::RegisterPaint, + Msg::SetNavigationStart(..) => LayoutHangAnnotation::SetNavigationStart, + }; + self.background_hang_monitor + .notify_activity(HangAnnotation::Layout(hang_annotation)); + } + /// Receives and dispatches messages from the script and constellation threads fn handle_request<'a, 'b>(&mut self, possibly_locked_rw_data: &mut RwData<'a, 'b>) -> bool { enum Request { @@ -613,6 +660,9 @@ impl LayoutThread { FromFontCache, } + // Notify the background-hang-monitor we are waiting for an event. + self.background_hang_monitor.notify_wait(); + let request = select! { recv(self.pipeline_port) -> msg => Request::FromPipeline(msg.unwrap()), recv(self.port) -> msg => Request::FromScript(msg.unwrap()), @@ -659,6 +709,8 @@ impl LayoutThread { request: Msg, possibly_locked_rw_data: &mut RwData<'a, 'b>, ) -> bool { + self.notify_activity_to_hang_monitor(&request); + match request { Msg::AddStylesheet(stylesheet, before_stylesheet) => { let guard = stylesheet.shared_lock.read(); @@ -815,6 +867,7 @@ impl LayoutThread { info.is_parent, info.layout_pair, info.pipeline_port, + info.background_hang_monitor_register, info.constellation_chan, info.script_chan.clone(), info.image_cache.clone(), diff --git a/components/layout_traits/lib.rs b/components/layout_traits/lib.rs index 9a7788051eb..5e58938a277 100644 --- a/components/layout_traits/lib.rs +++ b/components/layout_traits/lib.rs @@ -13,8 +13,8 @@ use crossbeam_channel::{Receiver, Sender}; use gfx::font_cache_thread::FontCacheThread; use ipc_channel::ipc::{IpcReceiver, IpcSender}; use metrics::PaintTimeMetrics; -use msg::constellation_msg::PipelineId; use msg::constellation_msg::TopLevelBrowsingContextId; +use msg::constellation_msg::{BackgroundHangMonitorRegister, PipelineId}; use net_traits::image_cache::ImageCache; use profile_traits::{mem, time}; use script_traits::LayoutMsg as ConstellationMsg; @@ -33,6 +33,7 @@ pub trait LayoutThreadFactory { is_iframe: bool, chan: (Sender, Receiver), pipeline_port: IpcReceiver, + background_hang_monitor: Box, constellation_chan: IpcSender, script_chan: IpcSender, image_cache: Arc, diff --git a/components/msg/constellation_msg.rs b/components/msg/constellation_msg.rs index af8b591b21e..5eba4767c75 100644 --- a/components/msg/constellation_msg.rs +++ b/components/msg/constellation_msg.rs @@ -7,7 +7,9 @@ use std::cell::Cell; use std::fmt; +use std::mem; use std::num::NonZeroU32; +use std::time::Duration; #[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub enum TraversalDirection { @@ -285,3 +287,185 @@ pub enum InputMethodType { Url, Week, } + +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +/// The equivalent of script_layout_interface::message::Msg +pub enum LayoutHangAnnotation { + AddStylesheet, + RemoveStylesheet, + SetQuirksMode, + Reflow, + GetRPC, + TickAnimations, + AdvanceClockMs, + ReapStyleAndLayoutData, + CollectReports, + PrepareToExit, + ExitNow, + GetCurrentEpoch, + GetWebFontLoadState, + CreateLayoutThread, + SetFinalUrl, + SetScrollStates, + UpdateScrollStateFromScript, + RegisterPaint, + SetNavigationStart, +} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +/// The equivalent of script::script_runtime::ScriptEventCategory +pub enum ScriptHangAnnotation { + AttachLayout, + ConstellationMsg, + DevtoolsMsg, + DocumentEvent, + DomEvent, + FileRead, + FormPlannedNavigation, + ImageCacheMsg, + InputEvent, + HistoryEvent, + NetworkEvent, + Resize, + ScriptEvent, + SetScrollState, + SetViewport, + StylesheetLoad, + TimerEvent, + UpdateReplacedElement, + WebSocketEvent, + WorkerEvent, + WorkletEvent, + ServiceWorkerEvent, + EnterFullscreen, + ExitFullscreen, + WebVREvent, + PerformanceTimelineTask, +} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +pub enum HangAnnotation { + Layout(LayoutHangAnnotation), + Script(ScriptHangAnnotation), +} + +/// Hang-alerts are sent by the monitor to the constellation. +#[derive(Deserialize, Serialize)] +pub enum HangAlert { + /// Report a transient hang. + Transient(MonitoredComponentId, HangAnnotation), + /// Report a permanent hang. + Permanent(MonitoredComponentId, HangAnnotation, Option), +} + +impl fmt::Debug for HangAlert { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + let (annotation, profile) = match self { + HangAlert::Transient(component_id, annotation) => { + write!( + fmt, + "\n The following component is experiencing a transient hang: \n {:?}", + component_id + )?; + (annotation.clone(), None) + }, + HangAlert::Permanent(component_id, annotation, profile) => { + write!( + fmt, + "\n The following component is experiencing a permanent hang: \n {:?}", + component_id + )?; + (annotation.clone(), profile.clone()) + }, + }; + + write!(fmt, "\n Annotation for the hang:\n{:?}", annotation)?; + if let Some(profile) = profile { + write!(fmt, "\n {:?}", profile)?; + } + + Ok(()) + } +} + +#[derive(Clone, Deserialize, Serialize)] +pub struct HangProfileSymbol { + pub name: Option, + pub filename: Option, + pub lineno: Option, +} + +#[derive(Clone, Deserialize, Serialize)] +/// Info related to the activity of an hanging component. +pub struct HangProfile { + pub backtrace: Vec, +} + +impl fmt::Debug for HangProfile { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + let hex_width = mem::size_of::() * 2 + 2; + + write!(fmt, "HangProfile backtrace:")?; + + if self.backtrace.len() == 0 { + write!(fmt, "backtrace failed to resolve")?; + return Ok(()); + } + + for symbol in self.backtrace.iter() { + write!(fmt, "\n {:1$}", "", hex_width)?; + + if let Some(ref name) = symbol.name { + write!(fmt, " - {}", name)?; + } else { + write!(fmt, " - ")?; + } + + if let (Some(ref file), Some(ref line)) = (symbol.filename.as_ref(), symbol.lineno) { + write!(fmt, "\n {:3$}at {}:{}", "", file, line, hex_width)?; + } + } + + Ok(()) + } +} + +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub enum MonitoredComponentType { + Layout, + Script, +} + +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct MonitoredComponentId(pub PipelineId, pub MonitoredComponentType); + +/// A handle to register components for hang monitoring, +/// and to receive a means to communicate with the underlying hang monitor worker. +pub trait BackgroundHangMonitorRegister: BackgroundHangMonitorClone + Send { + /// Register a component for hang monitoring: + /// to be called from within the thread to be monitored for hangs. + fn register_component( + &self, + component: MonitoredComponentId, + transient_hang_timeout: Duration, + permanent_hang_timeout: Duration, + ) -> Box; +} + +impl Clone for Box { + fn clone(&self) -> Box { + self.clone_box() + } +} + +pub trait BackgroundHangMonitorClone { + fn clone_box(&self) -> Box; +} + +/// Proxy methods to communicate with the background hang monitor +pub trait BackgroundHangMonitor { + /// Notify the start of handling an event. + fn notify_activity(&self, annotation: HangAnnotation); + /// Notify the start of waiting for a new event to come in. + fn notify_wait(&self); +} diff --git a/components/script/script_thread.rs b/components/script/script_thread.rs index 21c87d772e9..ee06f672bc3 100644 --- a/components/script/script_thread.rs +++ b/components/script/script_thread.rs @@ -101,7 +101,11 @@ use js::jsapi::{JSTracer, SetWindowProxyClass}; use js::jsval::UndefinedValue; use metrics::{PaintTimeMetrics, MAX_TASK_NS}; use mime::{self, Mime}; +use msg::constellation_msg::{ + BackgroundHangMonitor, BackgroundHangMonitorRegister, ScriptHangAnnotation, +}; use msg::constellation_msg::{BrowsingContextId, HistoryStateId, PipelineId}; +use msg::constellation_msg::{HangAnnotation, MonitoredComponentId, MonitoredComponentType}; use msg::constellation_msg::{PipelineNamespace, TopLevelBrowsingContextId}; use net_traits::image_cache::{ImageCache, PendingImageResponse}; use net_traits::request::{CredentialsMode, Destination, RedirectMode, RequestInit}; @@ -140,7 +144,7 @@ use std::rc::Rc; use std::result::Result; use std::sync::Arc; use std::thread; -use std::time::SystemTime; +use std::time::{Duration, SystemTime}; use style::thread_state::{self, ThreadState}; use time::{at_utc, get_time, precise_time_ns, Timespec}; use url::percent_encoding::percent_decode; @@ -483,6 +487,9 @@ unsafe_no_jsmanaged_fields!(RefCell); unsafe_no_jsmanaged_fields!(TaskQueue); +unsafe_no_jsmanaged_fields!(BackgroundHangMonitorRegister); +unsafe_no_jsmanaged_fields!(BackgroundHangMonitor); + #[derive(JSTraceable)] // ScriptThread instances are rooted on creation, so this is okay #[allow(unrooted_must_root)] @@ -511,6 +518,11 @@ pub struct ScriptThread { /// A queue of tasks to be executed in this script-thread. task_queue: TaskQueue, + /// A handle to register associated layout threads for hang-monitoring. + background_hang_monitor_register: Box, + /// The dedicated means of communication with the background-hang-monitor for this script-thread. + background_hang_monitor: Box, + /// A channel to hand out to script thread-based entities that need to be able to enqueue /// events in the event queue. chan: MainThreadScriptChan, @@ -668,6 +680,7 @@ impl ScriptThreadFactory for ScriptThread { let opener = state.opener; let mem_profiler_chan = state.mem_profiler_chan.clone(); let window_size = state.window_size; + let script_thread = ScriptThread::new(state, script_port, script_chan.clone()); SCRIPT_THREAD_ROOT.with(|root| { @@ -1019,6 +1032,12 @@ impl ScriptThread { let task_queue = TaskQueue::new(port, chan.clone()); + let background_hang_monitor = state.background_hang_monitor_register.register_component( + MonitoredComponentId(state.id, MonitoredComponentType::Script), + Duration::from_millis(1000), + Duration::from_millis(5000), + ); + ScriptThread { documents: DomRefCell::new(Documents::new()), window_proxies: DomRefCell::new(HashMap::new()), @@ -1036,6 +1055,9 @@ impl ScriptThread { task_queue, + background_hang_monitor_register: state.background_hang_monitor_register, + background_hang_monitor, + chan: MainThreadScriptChan(chan.clone()), dom_manipulation_task_sender: boxed_script_sender.clone(), media_element_task_sender: chan.clone(), @@ -1129,6 +1151,9 @@ impl ScriptThread { // Store new resizes, and gather all other events. let mut sequential = vec![]; + // Notify the background-hang-monitor we are waiting for an event. + self.background_hang_monitor.notify_wait(); + // Receive at least one message so we don't spinloop. debug!("Waiting for event."); let mut event = select! { @@ -1325,6 +1350,47 @@ impl ScriptThread { } } + fn notify_activity_to_hang_monitor(&self, category: &ScriptThreadEventCategory) { + let hang_annotation = match category { + ScriptThreadEventCategory::AttachLayout => ScriptHangAnnotation::AttachLayout, + ScriptThreadEventCategory::ConstellationMsg => ScriptHangAnnotation::ConstellationMsg, + ScriptThreadEventCategory::DevtoolsMsg => ScriptHangAnnotation::DevtoolsMsg, + ScriptThreadEventCategory::DocumentEvent => ScriptHangAnnotation::DocumentEvent, + ScriptThreadEventCategory::DomEvent => ScriptHangAnnotation::DomEvent, + ScriptThreadEventCategory::FileRead => ScriptHangAnnotation::FileRead, + ScriptThreadEventCategory::FormPlannedNavigation => { + ScriptHangAnnotation::FormPlannedNavigation + }, + ScriptThreadEventCategory::HistoryEvent => ScriptHangAnnotation::HistoryEvent, + ScriptThreadEventCategory::ImageCacheMsg => ScriptHangAnnotation::ImageCacheMsg, + ScriptThreadEventCategory::InputEvent => ScriptHangAnnotation::InputEvent, + ScriptThreadEventCategory::NetworkEvent => ScriptHangAnnotation::NetworkEvent, + ScriptThreadEventCategory::Resize => ScriptHangAnnotation::Resize, + ScriptThreadEventCategory::ScriptEvent => ScriptHangAnnotation::ScriptEvent, + ScriptThreadEventCategory::SetScrollState => ScriptHangAnnotation::SetScrollState, + ScriptThreadEventCategory::SetViewport => ScriptHangAnnotation::SetViewport, + ScriptThreadEventCategory::StylesheetLoad => ScriptHangAnnotation::StylesheetLoad, + ScriptThreadEventCategory::TimerEvent => ScriptHangAnnotation::TimerEvent, + ScriptThreadEventCategory::UpdateReplacedElement => { + ScriptHangAnnotation::UpdateReplacedElement + }, + ScriptThreadEventCategory::WebSocketEvent => ScriptHangAnnotation::WebSocketEvent, + ScriptThreadEventCategory::WorkerEvent => ScriptHangAnnotation::WorkerEvent, + ScriptThreadEventCategory::WorkletEvent => ScriptHangAnnotation::WorkletEvent, + ScriptThreadEventCategory::ServiceWorkerEvent => { + ScriptHangAnnotation::ServiceWorkerEvent + }, + ScriptThreadEventCategory::EnterFullscreen => ScriptHangAnnotation::EnterFullscreen, + ScriptThreadEventCategory::ExitFullscreen => ScriptHangAnnotation::ExitFullscreen, + ScriptThreadEventCategory::WebVREvent => ScriptHangAnnotation::WebVREvent, + ScriptThreadEventCategory::PerformanceTimelineTask => { + ScriptHangAnnotation::PerformanceTimelineTask + }, + }; + self.background_hang_monitor + .notify_activity(HangAnnotation::Script(hang_annotation)); + } + fn message_to_pipeline(&self, msg: &MixedMessage) -> Option { use script_traits::ConstellationControlMsg::*; match *msg { @@ -1399,6 +1465,7 @@ impl ScriptThread { where F: FnOnce() -> R, { + self.notify_activity_to_hang_monitor(&category); let start = precise_time_ns(); let value = if opts::get().profile_script_events { let profiler_cat = match category { @@ -1855,6 +1922,7 @@ impl ScriptThread { is_parent: false, layout_pair: layout_pair, pipeline_port: pipeline_port, + background_hang_monitor_register: self.background_hang_monitor_register.clone(), constellation_chan: self.layout_to_constellation_chan.clone(), script_chan: self.control_chan.clone(), image_cache: self.image_cache.clone(), diff --git a/components/script_layout_interface/message.rs b/components/script_layout_interface/message.rs index 7eb42e9d487..b159afb3a1c 100644 --- a/components/script_layout_interface/message.rs +++ b/components/script_layout_interface/message.rs @@ -10,7 +10,7 @@ use euclid::{Point2D, Rect}; use gfx_traits::Epoch; use ipc_channel::ipc::{IpcReceiver, IpcSender}; use metrics::PaintTimeMetrics; -use msg::constellation_msg::PipelineId; +use msg::constellation_msg::{BackgroundHangMonitorRegister, PipelineId}; use net_traits::image_cache::ImageCache; use profile_traits::mem::ReportsChan; use script_traits::Painter; @@ -212,6 +212,7 @@ pub struct NewLayoutThreadInfo { pub is_parent: bool, pub layout_pair: (Sender, Receiver), pub pipeline_port: IpcReceiver, + pub background_hang_monitor_register: Box, pub constellation_chan: IpcSender, pub script_chan: IpcSender, pub image_cache: Arc, diff --git a/components/script_traits/lib.rs b/components/script_traits/lib.rs index 580e17b48e4..57ed4e77b1c 100644 --- a/components/script_traits/lib.rs +++ b/components/script_traits/lib.rs @@ -33,6 +33,7 @@ use ipc_channel::Error as IpcError; use keyboard_types::webdriver::Event as WebDriverInputEvent; use keyboard_types::{CompositionEvent, KeyboardEvent}; use libc::c_void; +use msg::constellation_msg::BackgroundHangMonitorRegister; use msg::constellation_msg::{BrowsingContextId, HistoryStateId, PipelineId}; use msg::constellation_msg::{PipelineNamespaceId, TopLevelBrowsingContextId, TraversalDirection}; use net_traits::image::base::Image; @@ -546,6 +547,8 @@ pub struct InitialScriptState { pub control_port: IpcReceiver, /// A channel on which messages can be sent to the constellation from script. pub script_to_constellation_chan: ScriptToConstellationChan, + /// A handle to register script-(and associated layout-)threads for hang monitoring. + pub background_hang_monitor_register: Box, /// A sender for the layout thread to communicate to the constellation. pub layout_to_constellation_chan: IpcSender, /// A channel to schedule timer events. diff --git a/components/servo/Cargo.toml b/components/servo/Cargo.toml index 3978634743f..d513534d7d4 100644 --- a/components/servo/Cargo.toml +++ b/components/servo/Cargo.toml @@ -34,6 +34,7 @@ webgl_backtrace = [ ] [dependencies] +background_hang_monitor = {path = "../background_hang_monitor"} bluetooth_traits = {path = "../bluetooth_traits"} bluetooth = {path = "../bluetooth"} canvas = {path = "../canvas"} diff --git a/components/servo/lib.rs b/components/servo/lib.rs index d44289e6126..93cda15c419 100644 --- a/components/servo/lib.rs +++ b/components/servo/lib.rs @@ -20,6 +20,7 @@ #[macro_use] extern crate log; +pub use background_hang_monitor; pub use bluetooth; pub use bluetooth_traits; pub use canvas; @@ -59,6 +60,7 @@ fn webdriver(port: u16, constellation: Sender) { #[cfg(not(feature = "webdriver"))] fn webdriver(_port: u16, _constellation: Sender) {} +use background_hang_monitor::HangMonitorRegister; use bluetooth::BluetoothThreadFactory; use bluetooth_traits::BluetoothRequest; use canvas::gl_context::GLContextFactory; @@ -640,6 +642,12 @@ pub fn run_content_process(token: String) { create_sandbox(); } + let background_hang_monitor_register = HangMonitorRegister::init( + unprivileged_content + .background_hang_monitor_to_constellation_chan() + .clone(), + ); + // send the required channels to the service worker manager let sw_senders = unprivileged_content.swmanager_senders(); script::init(); @@ -647,7 +655,10 @@ pub fn run_content_process(token: String) { unprivileged_content.start_all::(true); + script::script_thread::ScriptThread>( + true, + background_hang_monitor_register + ); } #[cfg(all(not(target_os = "windows"), not(target_os = "ios")))] diff --git a/etc/ci/check_dynamic_symbols.py b/etc/ci/check_dynamic_symbols.py index 5f13e7d347a..cbc1b8ea376 100644 --- a/etc/ci/check_dynamic_symbols.py +++ b/etc/ci/check_dynamic_symbols.py @@ -15,7 +15,7 @@ import subprocess import sys symbol_regex = re.compile(b"D \*UND\*\t(.*) (.*)$") -allowed_symbols = frozenset([b'unshare', b'malloc_usable_size', b'__cxa_type_match']) +allowed_symbols = frozenset([b'unshare', b'malloc_usable_size', b'__cxa_type_match', b'signal']) actual_symbols = set() objdump_output = subprocess.check_output([ diff --git a/python/servo/build_commands.py b/python/servo/build_commands.py index b6b0a413026..f486515560f 100644 --- a/python/servo/build_commands.py +++ b/python/servo/build_commands.py @@ -192,10 +192,14 @@ class MachCommands(CommandBase): default=None, action='store_true', help='Build the libsimpleservo library instead of the servo executable') + @CommandArgument('--with-frame-pointer', + default=None, + action='store_true', + help='Build with frame pointer enabled, used by the background hang monitor.') def build(self, target=None, release=False, dev=False, jobs=None, features=None, android=None, magicleap=None, no_package=False, verbose=False, very_verbose=False, debug_mozjs=False, params=None, with_debug_assertions=False, - libsimpleservo=False): + libsimpleservo=False, with_frame_pointer=False): opts = params or [] @@ -288,6 +292,9 @@ class MachCommands(CommandBase): if with_debug_assertions: env['RUSTFLAGS'] = env.get('RUSTFLAGS', "") + " -C debug_assertions" + if with_frame_pointer: + env['RUSTFLAGS'] = env.get('RUSTFLAGS', "") + " -C force-frame-pointers=yes" + if android: if "ANDROID_NDK" not in env: print("Please set the ANDROID_NDK environment variable.") diff --git a/python/servo/testing_commands.py b/python/servo/testing_commands.py index 93a2564dda3..90bded199a6 100644 --- a/python/servo/testing_commands.py +++ b/python/servo/testing_commands.py @@ -245,6 +245,7 @@ class MachCommands(CommandBase): test_patterns.append(test) self_contained_tests = [ + "background_hang_monitor", "gfx", "layout", "msg",