Supervision Layer
Overview
Section titled “Overview”PCP runs in-process with the compositor, but a bug in PCP must not crash the user’s desktop. The supervision layer wraps the PCP subsystem to provide fault isolation, hang detection, and graceful recovery without restarting the full compositor.
The supervisor monitors PCP Core through three mechanisms: a watchdog timer that detects hangs, a memory budget tracker that prevents resource exhaustion, and a panic handler that catches unwinding panics before they reach the compositor.
Architecture
Section titled “Architecture”+-------------------------------------------------------------+| COMPOSITOR PROCESS || || +-------------------------------------------------------+ || | PCP SUPERVISOR | || | | || | +---------------+ +------------------------------+ | || | | Watchdog | | Memory Budget Tracker | | || | | Timer | | (allocation counter) | | || | +-------+-------+ +--------------+---------------+ | || | | | | || | +-------v------------------------v---------------+ | || | | Panic Handler (catch_unwind) | | || | +----------------------+--------------------------+ | || | | | || | +----------------------v--------------------------+ | || | | PCP CORE | | || | | (Registry / Events / Execution / Permission) | | || | +-------------------------------------------------+ | || +-------------------------------------------------------+ || || Compositor core (Wayfire/wlroots) continues unaffected || if PCP needs restart. |+-------------------------------------------------------------+The compositor’s own event loop and rendering pipeline sit outside PCP entirely. If the supervisor tears down and restarts PCP Core, the desktop remains interactive throughout.
PcpSupervisor
Section titled “PcpSupervisor”The top-level supervisor struct owns the PCP Core instance and coordinates all fault detection:
/// PCP Supervisor — wraps PCP Core with fault detection and recovery.pub struct PcpSupervisor { /// The supervised PCP Core instance. pcp_core: Option<PcpCore>, /// Watchdog timer — resets on every successful PCP heartbeat. watchdog: WatchdogTimer, /// Memory budget tracker. memory_tracker: MemoryBudgetTracker, /// Supervisor state machine. state: SupervisorState, /// Event channel to notify compositor of PCP state changes. notifier: mpsc::Sender<SupervisorEvent>,}The pcp_core field is Option so the supervisor can drop and recreate the core instance during a restart without moving the supervisor itself.
SupervisorState
Section titled “SupervisorState”The supervisor tracks four states:
#[derive(Debug, Clone, Copy, PartialEq)]pub enum SupervisorState { /// PCP Core is running normally. Running, /// PCP Core has not responded within the watchdog timeout. SuspectedHang, /// PCP Core is being restarted. Restarting, /// PCP Core failed to restart after max attempts. Degraded,}State transitions flow in one direction during recovery. Degraded is a terminal state that persists until the next compositor wake or idle cycle, at which point the supervisor retries.
Watchdog Timer
Section titled “Watchdog Timer”PCP operations have defined timeout budgets. PCP Core must call heartbeat() periodically to prove it is alive. If the watchdog detects too many missed heartbeats, it declares a hang and triggers a restart.
/// Watchdog configuration.pub struct WatchdogConfig { /// Heartbeat interval — PCP must tick within this period. pub heartbeat_interval: Duration, /// Maximum consecutive missed heartbeats before declaring hang. pub max_missed_heartbeats: u32, /// Default: 2s heartbeat, 3 misses = 6s hang detection.}With the defaults, PCP has 6 seconds of unresponsiveness before the supervisor intervenes. This is long enough to tolerate brief GC pauses or slow adapter probes, but short enough that the user notices nothing worse than a brief stutter.
/// PCP Core must call this periodically to prove liveness.impl PcpCore { pub fn heartbeat(&self) { self.supervisor.heartbeat_received(); }}
impl PcpSupervisor { fn check_watchdog(&mut self) { if self.watchdog.missed_heartbeats() > self.config.max_missed_heartbeats { self.state = SupervisorState::SuspectedHang; log::error!( "PCP Core hang detected: {} missed heartbeats", self.watchdog.missed_heartbeats() ); self.notify(SupervisorEvent::HangDetected { missed: self.watchdog.missed_heartbeats(), }); self.initiate_restart(); } }}Memory Budget Tracker
Section titled “Memory Budget Tracker”PCP operations are budgeted to prevent memory exhaustion from taking down the compositor. Every PCP component registers its allocations with the tracker, and the tracker rejects allocations that would exceed the shared budget.
/// Memory budget for PCP subsystem.pub struct MemoryBudgetTracker { /// Total allocation budget for all of PCP. total_budget: usize, /// Current tracked allocations. current_usage: AtomicUsize, /// Per-component budgets. component_budgets: HashMap<PcpComponent, usize>, /// Alert threshold (e.g., 80% of budget triggers warning). alert_threshold: f64,}
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]pub enum PcpComponent { Registry, EventBus, ExecutionPipeline, AdapterAtspi2, AdapterWine, Simulator, AuditLog,}Each allocation goes through track_allocation. If the new total would exceed the budget, the call fails with MemoryBudgetExceeded and the component must handle the error gracefully, usually by falling back to a lighter code path.
impl MemoryBudgetTracker { pub fn track_allocation( &self, component: PcpComponent, size: usize, ) -> Result<(), MemoryBudgetExceeded> { let new_usage = self.current_usage.fetch_add(size, Ordering::Relaxed) + size; if new_usage > self.total_budget { self.current_usage.fetch_sub(size, Ordering::Relaxed); return Err(MemoryBudgetExceeded { component, requested: size, budget: self.total_budget, current: new_usage - size, }); } if (new_usage as f64 / self.total_budget as f64) > self.alert_threshold { log::warn!( "PCP memory usage at {:.0}% of budget ({} / {} bytes)", (new_usage as f64 / self.total_budget as f64) * 100.0, new_usage, self.total_budget, ); } Ok(()) }
pub fn track_deallocation(&self, size: usize) { self.current_usage.fetch_sub(size, Ordering::Relaxed); }}
pub struct MemoryBudgetExceeded { pub component: PcpComponent, pub requested: usize, pub budget: usize, pub current: usize,}The AtomicUsize counter allows lock-free tracking from multiple threads. The relaxed ordering is acceptable here because exact byte precision is not required; approximate budget enforcement is sufficient to prevent runaway allocation.
Panic Recovery
Section titled “Panic Recovery”Critical PCP operations are wrapped in catch_unwind to prevent panics from propagating to the compositor. This is the last line of defense against a PCP bug killing the desktop.
use std::panic::catch_unwind;use std::panic::AssertUnwindSafe;
impl PcpSupervisor { /// Execute a PCP operation with panic protection. /// If the operation panics, the supervisor catches it, logs the state, /// and returns an error instead of crashing the compositor. pub fn supervised_execute<F, T>( &self, operation_name: &str, f: F, ) -> Result<T, SupervisedError> where F: FnOnce() -> T + std::panic::UnwindSafe, { match catch_unwind(f) { Ok(result) => Ok(result), Err(panic_payload) => { log::error!( "PCP panic in '{}': {:?}", operation_name, panic_payload ); self.notify(SupervisorEvent::PanicCaught { operation: operation_name.to_string(), }); Err(SupervisedError::PanicCaught { operation: operation_name.to_string(), }) } } }}The UnwindSafe bound restricts which closures can be passed. Closures that capture &mut T references or Rc pointers are rejected at compile time, which prevents undefined behavior from unwinding through a borrowed reference.
Recovery Procedure
Section titled “Recovery Procedure”When the supervisor detects a hang, panic, or memory breach, it follows a four-step recovery sequence:
Supervisor detects PCP failure | v(1) LOG — Record full PCP state dump: +- Registry contents (app count, capability count) +- Pending event subscriptions +- Active transactions +- Memory usage per component +- Watchdog state (missed heartbeats) +- Last N audit log entries | v(2) NOTIFY — User-facing notification: +- Compositor overlay: "System Intelligence is restarting. | Your desktop is unaffected." +- Severity indicator (minor = transient, major = degraded mode) | v(3) RESTART — Hot-restart PCP Core: +- Drop PCP Core instance (free all PCP memory) +- Create new PcpCore instance +- Replay audit log to reconstruct registry state +- Re-subscribe to compositor events +- Re-probe running apps (lightweight: validate existing entries) +- Resume normal operation | v(4) VERIFY — Confirm PCP is healthy: +- Watchdog receives heartbeat +- Registry responds to queries +- Event bus delivers events +- Execution pipeline accepts intents | +- SUCCESS -> SupervisorState::Running | +- Notify user: "Back online. Everything looks good." | +- FAILURE (3 consecutive restart failures) +- SupervisorState::Degraded +- Compositor continues without System Intelligence +- User operates desktop normally +- Retry restart on next compositor wake/idle cycleThe key property of this procedure is that the compositor never stops. The overlay notification appears and disappears within the restart window. If the restart fails repeatedly, the compositor enters degraded mode and retries later. The user never has to manually intervene.
Configuration Defaults
Section titled “Configuration Defaults”pub struct SupervisorConfig { /// Watchdog heartbeat interval. pub heartbeat_interval: Duration, // default: 2s /// Max missed heartbeats before hang declared. pub max_missed_heartbeats: u32, // default: 3 /// Max restart attempts before entering degraded mode. pub max_restart_attempts: u32, // default: 3 /// Delay between restart attempts (exponential backoff). pub restart_backoff_base: Duration, // default: 1s /// Total PCP memory budget. pub memory_budget_bytes: usize, // default: 128 MiB /// Memory alert threshold (fraction). pub memory_alert_threshold: f64, // default: 0.80}The 128 MiB default budget is generous for PCP’s typical workload (registry lookups, event dispatch, adapter probing) but far below what would threaten compositor stability. The 80% alert threshold fires a warning at ~100 MiB, giving the system headroom before the hard cap is reached. Restart backoff uses exponential delays (1s, 2s, 4s) to avoid tight restart loops that would thrash the system.