use crate::bounded_push::{Patience, StopCause, StopReport};
use crate::process_tree::{TreeMember, descendants_of, member_of};
use crate::wait_ceilings::CONNECTED_PUSH_QUIET_WINDOW;
use crate::tcp_sockets::{ESTABLISHED, established_sockets, socket_inodes_of};
use std::collections::HashMap;
use std::time::{Duration, Instant};
pub const SAMPLE_INTERVAL: Duration = Duration::from_millis(500);
#[cfg(target_os = "linux")]
fn bytes_moved_by(pid: i32) -> Option<u64> {
let text: String = std::fs::read_to_string(format!("/proc/{pid}/io")).ok()?;
let mut read: Option<u64> = None;
let mut written: Option<u64> = None;
for line in text.lines() {
if let Some(value) = line.strip_prefix("rchar: ") {
read = value.trim().parse::<u64>().ok();
} else if let Some(value) = line.strip_prefix("wchar: ") {
written = value.trim().parse::<u64>().ok();
}
}
return Some(read?.saturating_add(written?));
}
#[cfg(not(target_os = "linux"))]
fn bytes_moved_by(_pid: i32) -> Option<u64> {
return None;
}
pub(crate) fn parse_activity(stat: &str) -> Option<(char, u64)> {
let after_name: &str = stat.get(stat.rfind(')')? + 2..)?;
let fields: Vec<&str> = after_name.split(' ').collect();
let state: char = fields.first()?.chars().next()?;
let user: u64 = fields.get(11)?.parse().ok()?;
let system: u64 = fields.get(12)?.parse().ok()?;
return Some((state, user.saturating_add(system)));
}
pub(crate) fn is_working(state: char) -> bool {
return state == 'R' || state == 'D';
}
#[cfg(target_os = "linux")]
fn activity_of(pid: i32) -> Option<(u64, bool)> {
let text: String = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
let (state, ticks): (char, u64) = parse_activity(text.as_str())?;
let mut working: bool = is_working(state);
if !working && let Ok(tasks) = std::fs::read_dir(format!("/proc/{pid}/task")) {
for task in tasks.flatten() {
if let Ok(line) = std::fs::read_to_string(task.path().join("stat"))
&& let Some((task_state, _)) = parse_activity(line.as_str())
&& is_working(task_state)
{
working = true;
break;
}
}
}
return Some((ticks, working));
}
#[cfg(not(target_os = "linux"))]
fn activity_of(_pid: i32) -> Option<(u64, bool)> {
return None;
}
fn system_measures() -> bool {
return bytes_moved_by(i32::try_from(std::process::id()).unwrap_or(1)).is_some();
}
#[derive(Debug, Default)]
pub struct Meter {
seen: HashMap<(i32, u64), (u64, u64)>,
moved: u64,
sockets: HashMap<u64, u64>,
socket_moved: u64,
connected: bool,
scans: u64,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct WaitReport {
pub silent: Duration,
pub elapsed: Duration,
}
impl Meter {
pub fn new() -> Self {
return Self::default();
}
pub fn moved(&self) -> u64 {
return self.moved.saturating_add(self.socket_moved);
}
pub fn scans(&self) -> u64 {
return self.scans;
}
pub fn socket_bytes(&self) -> u64 {
return self.socket_moved;
}
pub fn connected(&self) -> bool {
return self.connected;
}
fn observe_sockets(&mut self, members: &[TreeMember]) -> bool {
let mut held: Vec<u64> = Vec::new();
for member in members {
held.extend(socket_inodes_of(member.pid));
}
self.connected = false;
if held.is_empty() {
return false;
}
let Some(connected) = established_sockets() else {
return false;
};
let mut progressed: bool = false;
for socket in connected {
if socket.state != ESTABLISHED || !held.contains(&socket.inode) {
continue;
}
self.connected = true;
let total: u64 = socket.bytes_acked.saturating_add(socket.bytes_received);
let before: u64 = self.sockets.insert(socket.inode, total).unwrap_or(0);
let carried: u64 = total.saturating_sub(before);
self.socket_moved = self.socket_moved.saturating_add(carried);
if carried > 0 {
progressed = true;
}
}
return progressed;
}
fn observe(&mut self, key: (i32, u64), bytes: u64, ticks: u64, working: bool) -> bool {
let (bytes_before, ticks_before): (u64, u64) = self.seen.insert(key, (bytes, ticks)).unwrap_or((0, 0));
let moved_now: u64 = bytes.saturating_sub(bytes_before);
self.moved = self.moved.saturating_add(moved_now);
return moved_now > 0 || ticks > ticks_before || working;
}
pub fn sample(&mut self, root: i32) -> bool {
return self.look(root, true);
}
pub fn sample_root(&mut self, root: i32) -> bool {
return self.look(root, false);
}
fn look(&mut self, root: i32, whole_tree: bool) -> bool {
let mut members: Vec<TreeMember> = member_of(root).into_iter().collect();
if whole_tree {
self.scans += 1;
members.extend(descendants_of(root));
}
let mut progressed: bool = false;
for member in &members {
let bytes: Option<u64> = bytes_moved_by(member.pid);
let activity: Option<(u64, bool)> = activity_of(member.pid);
if bytes.is_none() && activity.is_none() {
continue;
}
let (ticks, working): (u64, bool) = activity.unwrap_or((0, false));
if self.observe((member.pid, member.started), bytes.unwrap_or(0), ticks, working) {
progressed = true;
}
}
if self.observe_sockets(members.as_slice()) {
progressed = true;
}
return progressed;
}
}
pub struct Clock {
root: i32,
patience: Patience,
started: Instant,
last_progress: Instant,
next_sample: Instant,
meter: Option<Meter>,
waiting_since: Option<Instant>,
waited: Option<WaitReport>,
wait_noted: bool,
longest_quiet: Duration,
looked: bool,
wall_fallback: bool,
connected_window: Duration,
}
impl Clock {
pub fn start(pid: u32, patience: Patience) -> Self {
let now: Instant = Instant::now();
let measured: bool = matches!(patience, Patience::Progress { .. }) && system_measures();
return Self {
root: i32::try_from(pid).unwrap_or(i32::MAX),
patience,
started: now,
last_progress: now,
next_sample: now,
looked: false,
meter: if measured { Some(Meter::new()) } else { None },
waiting_since: None,
waited: None,
wait_noted: false,
longest_quiet: Duration::ZERO,
wall_fallback: true,
connected_window: CONNECTED_PUSH_QUIET_WINDOW,
};
}
pub fn without_wall_fallback(mut self) -> Self {
self.wall_fallback = false;
return self;
}
pub fn with_connected_window(mut self, window: Duration) -> Self {
self.connected_window = window;
return self;
}
pub fn measures(&self) -> bool {
return self.meter.is_some();
}
pub fn longest_quiet(&self) -> Duration {
return self.longest_quiet.max(Instant::now().saturating_duration_since(self.last_progress));
}
#[cfg(test)]
pub fn unmeasured(mut self) -> Self {
self.meter = None;
return self;
}
pub fn wait_report(&self) -> Option<WaitReport> {
return self.waited;
}
pub fn take_wait_note(&mut self) -> Option<WaitReport> {
if self.wait_noted {
return None;
}
let report: WaitReport = self.waited?;
self.wait_noted = true;
return Some(report);
}
fn report(&self, limit: Duration, now: Instant) -> StopReport {
return StopReport { limit, moved: self.meter.as_ref().map_or(0, Meter::moved), elapsed: now.duration_since(self.started) };
}
pub fn check(&mut self) -> Option<StopCause> {
let now: Instant = Instant::now();
let elapsed: Duration = now.duration_since(self.started);
let stall: Duration = match self.patience {
Patience::Fixed(limit) => return (elapsed >= limit).then_some(StopCause::Deadline),
Patience::Progress { stall } => stall,
};
let Some(meter) = self.meter.as_mut() else {
return (self.wall_fallback && elapsed >= stall).then_some(StopCause::Deadline);
};
if now >= self.next_sample {
let progressed: bool = if self.looked { meter.sample(self.root) } else { meter.sample_root(self.root) };
self.looked = true;
if progressed {
self.longest_quiet = self.longest_quiet.max(now.saturating_duration_since(self.last_progress));
self.last_progress = now;
self.waiting_since = None;
} else if meter.connected() {
let since: Instant = *self.waiting_since.get_or_insert(now);
self.last_progress = now;
if self.waited.is_none() && now.duration_since(since) >= stall {
self.waited = Some(WaitReport { silent: now.duration_since(since), elapsed });
}
if now.duration_since(since) >= self.connected_window {
return Some(StopCause::Stalled(self.report(self.connected_window, now)));
}
} else {
self.waiting_since = None;
}
self.next_sample = now + SAMPLE_INTERVAL;
}
if now.duration_since(self.last_progress) >= stall {
return Some(StopCause::Stalled(self.report(stall, now)));
}
return None;
}
}
#[cfg(test)]
#[path = "push_progress_tests.rs"]
pub(crate) mod tests;