use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Condvar, Mutex};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
#[derive(Debug)]
pub struct WorkerHeartbeat {
epoch: Instant,
last: AtomicU64,
busy: AtomicBool,
hardware: AtomicBool,
opening_hardware: AtomicBool,
activity: Mutex<Option<Activity>>,
abandoned: AtomicBool,
#[cfg(test)]
pub stall: Option<InjectedStall>,
#[cfg(test)]
operations: std::sync::atomic::AtomicUsize,
#[cfg(test)]
stalled: AtomicBool,
}
#[cfg(test)]
#[derive(Clone, Copy, Debug)]
pub enum InjectedStall {
AfterOperations(usize),
OnClose,
PanicAfterOperations(usize),
#[cfg_attr(not(feature = "extensions"), allow(dead_code))]
SlowProgress(Duration),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Activity {
pub what: String,
pub provider: bool,
}
pub struct HeartbeatWork(Arc<WorkerHeartbeat>);
#[cfg_attr(not(feature = "hwdecode"), allow(dead_code))]
pub struct HardwareOpen<'a>(&'a WorkerHeartbeat);
#[cfg_attr(not(feature = "hwdecode"), allow(dead_code))]
impl HardwareOpen<'_> {
pub fn opened(self) {
self.0.opened_hardware();
}
}
impl Drop for HardwareOpen<'_> {
fn drop(&mut self) {
self.0.opening_hardware.store(false, Ordering::Release);
}
}
pub struct HeartbeatIdle {
beat: Arc<WorkerHeartbeat>,
was_busy: bool,
}
impl Drop for HeartbeatIdle {
fn drop(&mut self) {
if self.was_busy {
self.beat.mark();
self.beat.busy.store(true, Ordering::Release);
}
}
}
impl Drop for HeartbeatWork {
fn drop(&mut self) {
self.0.busy.store(false, Ordering::Release);
}
}
impl Default for WorkerHeartbeat {
fn default() -> Self {
Self {
epoch: Instant::now(),
last: AtomicU64::new(0),
busy: AtomicBool::new(false),
hardware: AtomicBool::new(false),
opening_hardware: AtomicBool::new(false),
activity: Mutex::new(None),
abandoned: AtomicBool::new(false),
#[cfg(test)]
stall: None,
#[cfg(test)]
operations: std::sync::atomic::AtomicUsize::new(0),
#[cfg(test)]
stalled: AtomicBool::new(false),
}
}
}
impl WorkerHeartbeat {
#[cfg(test)]
pub fn stalling(stall: Option<InjectedStall>) -> Self {
Self {
stall,
..Self::default()
}
}
#[cfg(test)]
pub fn abandoned(&self) -> bool {
self.abandoned.load(Ordering::Acquire)
}
pub fn beat(&self) {
self.mark();
self.inject();
}
pub fn progressed(&self) {
self.mark();
#[cfg(test)]
if let Some(InjectedStall::SlowProgress(step)) = self.stall {
thread::sleep(step);
}
}
fn mark(&self) {
self.last.store(
u64::try_from(self.epoch.elapsed().as_nanos()).unwrap_or(u64::MAX),
Ordering::Release,
);
}
#[cfg(not(test))]
#[allow(clippy::unused_self)]
const fn inject(&self) {}
#[cfg(test)]
fn inject(&self) {
match self.stall {
Some(InjectedStall::AfterOperations(limit))
if self.operations.fetch_add(1, Ordering::Relaxed) >= limit =>
{
self.stall_forever();
}
Some(InjectedStall::PanicAfterOperations(limit))
if self.operations.fetch_add(1, Ordering::Relaxed) >= limit =>
{
panic!("injected decoder panic");
}
_ => {}
}
}
#[cfg(test)]
fn stall_forever(&self) -> ! {
self.stalled.store(true, Ordering::Release);
loop {
thread::park();
}
}
#[cfg(test)]
pub fn is_stalled(&self) -> bool {
self.stalled.load(Ordering::Acquire)
}
pub fn begin_work(self: &Arc<Self>) -> HeartbeatWork {
let work = HeartbeatWork(Arc::clone(self));
self.mark();
self.busy.store(true, Ordering::Release);
self.inject();
work
}
pub fn begin_close(self: &Arc<Self>) -> HeartbeatWork {
let work = self.begin_work();
#[cfg(test)]
if matches!(self.stall, Some(InjectedStall::OnClose)) {
self.stall_forever();
}
work
}
#[cfg_attr(not(feature = "hwdecode"), allow(dead_code))]
pub fn begin_hardware_open(&self) -> HardwareOpen<'_> {
self.opening_hardware.store(true, Ordering::Release);
HardwareOpen(self)
}
pub fn opened_hardware(&self) {
self.hardware.store(true, Ordering::Release);
}
pub fn holds_hardware(&self) -> bool {
self.hardware.load(Ordering::Acquire) || self.opening_hardware.load(Ordering::Acquire)
}
pub fn set_activity(&self, activity: Option<Activity>) {
*self
.activity
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = activity;
}
pub fn activity(&self) -> Option<Activity> {
self.activity
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
pub fn idle(self: &Arc<Self>) -> HeartbeatIdle {
let was_busy = self.busy.swap(false, Ordering::AcqRel);
HeartbeatIdle {
beat: Arc::clone(self),
was_busy,
}
}
pub fn stalled_for(&self) -> Option<Duration> {
if !self.busy.load(Ordering::Acquire) {
return None;
}
let started = Duration::from_nanos(self.last.load(Ordering::Acquire));
Some(self.epoch.elapsed().saturating_sub(started))
}
pub fn stuck_past(&self, limit: Duration) -> Option<Duration> {
self.stalled_for()
.filter(|stalled_for| *stalled_for > limit)
}
}
#[derive(Default)]
pub struct CommandProgress {
taken: Mutex<u64>,
changed: Condvar,
}
impl CommandProgress {
pub fn current(&self) -> u64 {
*self
.taken
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
pub fn took_one(&self) {
let mut taken = self
.taken
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*taken = taken.wrapping_add(1);
self.changed.notify_all();
}
pub fn wait_past(&self, seen: u64, timeout: Duration) {
let taken = self
.taken
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
drop(
self.changed
.wait_timeout_while(taken, timeout, |taken| *taken == seen)
.unwrap_or_else(std::sync::PoisonError::into_inner),
);
}
}
pub const STALL_POLL: Duration = Duration::from_millis(250);
const JOIN_POLL: Duration = Duration::from_millis(5);
static THREADS_ABANDONED: AtomicBool = AtomicBool::new(false);
pub fn worker_threads_abandoned() -> bool {
THREADS_ABANDONED.load(Ordering::Acquire)
}
pub enum Joined<T> {
Finished(T),
Panicked,
Left,
}
pub fn join_or_leave<T>(
join: JoinHandle<T>,
heartbeats: &[Arc<WorkerHeartbeat>],
limit: Duration,
deadline: Option<Duration>,
) -> Joined<T> {
let started = Instant::now();
while !join.is_finished()
&& deadline.is_none_or(|deadline| started.elapsed() < deadline)
&& heartbeats
.iter()
.all(|beat| beat.stuck_past(limit).is_none())
{
thread::sleep(JOIN_POLL);
}
if !join.is_finished() {
for beat in heartbeats {
beat.abandoned.store(true, Ordering::Release);
}
return Joined::Left;
}
join.join().map_or(Joined::Panicked, Joined::Finished)
}
pub fn join_or_abandon(
join: JoinHandle<()>,
heartbeats: &[Arc<WorkerHeartbeat>],
limit: Duration,
what: &str,
) -> bool {
match join_or_leave(join, heartbeats, limit, Some(limit)) {
Joined::Finished(()) => true,
Joined::Left => {
THREADS_ABANDONED.store(true, Ordering::Release);
let inside = if heartbeats.iter().any(|beat| {
beat.stalled_for().is_some()
&& beat.activity().is_some_and(|activity| activity.provider)
}) {
"an extension provider"
} else {
"FFmpeg"
};
alpha2_os::diagnostic!(
"alpha2 native {what} left running: it is stuck inside {inside}"
);
false
}
Joined::Panicked => {
alpha2_os::diagnostic!("alpha2 native {what} panicked during cleanup");
false
}
}
}
enum WatchedEvent<T> {
Progress(u64),
Done(T),
}
pub struct Watched<'a> {
pub what: &'static str,
pub heartbeat: Arc<WorkerHeartbeat>,
pub limit: Duration,
pub cancel: &'a alpha2_media::CancelToken,
}
pub fn run_watched<T: Send + 'static>(
watched: &Watched<'_>,
body: impl FnOnce(&dyn Fn(u64)) -> T + Send + 'static,
progress: &mut dyn FnMut(u64) -> Result<(), String>,
stalled: &dyn Fn(Duration) -> String,
) -> Result<T, String> {
let (events, received) = std::sync::mpsc::channel();
let beat = Arc::clone(&watched.heartbeat);
let join = thread::Builder::new()
.name(format!("alpha2-{}", watched.what.replace(' ', "-")))
.spawn(move || {
let report = events.clone();
let work = beat.begin_work();
let result = body(&move |reached| {
let _ = report.send(WatchedEvent::Progress(reached));
});
drop(work);
let _ = events.send(WatchedEvent::Done(result));
})
.map_err(|error| format!("failed to start the {}: {error}", watched.what))?;
let mut failed = None;
loop {
match received.recv_timeout(STALL_POLL) {
Ok(WatchedEvent::Progress(reached)) => {
if failed.is_none()
&& let Err(error) = progress(reached)
{
failed = Some(error);
watched.cancel.cancel();
}
}
Ok(WatchedEvent::Done(result)) => {
join_or_abandon(
join,
std::slice::from_ref(&watched.heartbeat),
watched.limit,
watched.what,
);
return failed.map_or(Ok(result), Err);
}
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
if let Some(stalled_for) = watched.heartbeat.stuck_past(watched.limit) {
let error = failed.unwrap_or_else(|| stalled(stalled_for));
watched.cancel.cancel();
join_or_abandon(
join,
std::slice::from_ref(&watched.heartbeat),
Duration::ZERO,
watched.what,
);
return Err(error);
}
}
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
let _ = join.join();
return Err(format!(
"{}the {} panicked",
crate::protocol::internal_tag(),
watched.what
));
}
}
}
}