servo/components/shared/background_hang_monitor/lib.rs
Gregory Terzian 815ed10b5f
background_hang_monitor: ensure workers run until monitored components do (#38322)
Shut-down of the background hang monitor(bhm) is currently  problematic:

- it does not always run until the monitored script-thread does(see
"BackgroundHangMonitor has gone away" mentioned in
https://github.com/servo/servo/issues/34158).
- it shuts-down before the constellation(good, so actually
https://github.com/servo/servo/issues/24850 was "fixed" but in a way
that introduced a new problem), but using a mechanism that allows it to
shutdown before script(the problem above).
- there are various mechanism(see the doc comments removed by this PR)
in place which are meant to ensure a clean shutdown despite the above
problems; those are complicated, and become unnecessary once those
problems are fixed.

All of the above is fixed by the changes in this PR, which ensure the
bhm does not shut-down before script, and also maintains the invariant
that it must shut-down before the constellation(in single-process mode)
or before the main thread(in multi-process mode), but using a mechanism
which allows it to keep running until script shuts-down.

An unnecessary option around the exit signal is also removed.

As a positive side-effect, it also ensures that any script-thread is
shut-down before the constellation(because for the bhm worker to exit,
the monitored script must have exited first), so this should also fix a
host of other problems noted in
https://github.com/servo/servo/issues/30849, but each should be
confirmed independently(and various other improvements seem possible in
their specific contexts, such as joining on script threads, and removing
the `ScriptThreadMessage::ExitScriptThread`).

Fixes: https://github.com/servo/servo/issues/24850 and part of
https://github.com/servo/servo/issues/34158

Testing: Unit tests in `component/background_hang_monitor/tests`. Also
manually tested loading "about-blank" in single- and multi-process mode.

---------

Signed-off-by: gterzian <2792687+gterzian@users.noreply.github.com>
2025-07-30 13:03:28 +00:00

212 lines
6.4 KiB
Rust

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
#![deny(unsafe_code)]
//! An API interface to the BackgroundHangMonitor.
use std::time::Duration;
use std::{fmt, mem};
use base::id::PipelineId;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
/// The equivalent of script::script_runtime::ScriptEventCategory
pub enum ScriptHangAnnotation {
AttachLayout,
ConstellationMsg,
DatabaseAccessEvent,
DevtoolsMsg,
DocumentEvent,
FileRead,
FontLoading,
FormPlannedNavigation,
ImageCacheMsg,
InputEvent,
HistoryEvent,
NetworkEvent,
Rendering,
Resize,
ScriptEvent,
SetScrollState,
SetViewport,
StylesheetLoad,
TimerEvent,
UpdateReplacedElement,
WebSocketEvent,
WorkerEvent,
WorkletEvent,
ServiceWorkerEvent,
EnterFullscreen,
ExitFullscreen,
WebVREvent,
PerformanceTimelineTask,
PortMessage,
WebGPUMsg,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
pub enum HangAnnotation {
Script(ScriptHangAnnotation),
}
/// Hang-alerts are sent by the monitor to the constellation.
#[derive(Deserialize, Serialize)]
pub enum HangMonitorAlert {
/// A component hang has been detected.
Hang(HangAlert),
/// Report a completed sampled profile.
Profile(Vec<u8>),
}
impl fmt::Debug for HangMonitorAlert {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match *self {
HangMonitorAlert::Hang(..) => write!(fmt, "Hang"),
HangMonitorAlert::Profile(..) => write!(fmt, "Profile"),
}
}
}
/// 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<HangProfile>),
}
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, None)
},
HangAlert::Permanent(component_id, annotation, profile) => {
write!(
fmt,
"\n The following component is experiencing a permanent hang: \n {:?}",
component_id
)?;
(*annotation, 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<String>,
pub filename: Option<String>,
pub lineno: Option<u32>,
}
#[derive(Clone, Deserialize, Serialize)]
/// Info related to the activity of an hanging component.
pub struct HangProfile {
pub backtrace: Vec<HangProfileSymbol>,
}
impl fmt::Debug for HangProfile {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
let hex_width = mem::size_of::<usize>() * 2 + 2;
write!(fmt, "HangProfile backtrace:")?;
if self.backtrace.is_empty() {
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, " - <unknown>")?;
}
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 {
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,
exit_signal: Box<dyn BackgroundHangMonitorExitSignal>,
) -> Box<dyn BackgroundHangMonitor>;
}
impl Clone for Box<dyn BackgroundHangMonitorRegister> {
fn clone(&self) -> Box<dyn BackgroundHangMonitorRegister> {
self.clone_box()
}
}
pub trait BackgroundHangMonitorClone {
fn clone_box(&self) -> Box<dyn BackgroundHangMonitorRegister>;
}
/// 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);
/// Unregister the component from monitor.
fn unregister(&self);
}
/// A means for the BHM to signal a monitored component to exit.
/// Useful when the component is hanging, and cannot be notified via the usual way.
/// The component should implement this in a way allowing for the signal to be received when hanging,
/// if at all.
pub trait BackgroundHangMonitorExitSignal: Send {
/// Called by the BHM, to notify the monitored component to exit.
fn signal_to_exit(&self);
}
/// Messages to control the sampling profiler.
#[derive(Deserialize, Serialize)]
pub enum BackgroundHangMonitorControlMsg {
/// Toggle the sampler, with a given sampling rate and max total sampling duration.
ToggleSampler(Duration, Duration),
/// Propagate exit signal to monitored components, and shutdown when they have.
Exit,
}