use crate::push_progress::{SAMPLE_INTERVAL, is_working, parse_activity};
use crate::tcp_sockets::{ESTABLISHED, established_sockets, socket_inodes_of};
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
#[cfg(target_os = "linux")]
pub fn current_thread_id() -> Option<i32> {
let raw: libc::c_long = unsafe { libc::syscall(libc::SYS_gettid) };
return i32::try_from(raw).ok();
}
#[cfg(not(target_os = "linux"))]
pub fn current_thread_id() -> Option<i32> {
return None;
}
#[cfg(target_os = "linux")]
fn thread_bytes(tid: i32) -> Option<u64> {
let text: String = std::fs::read_to_string(format!("/proc/self/task/{tid}/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 thread_bytes(_tid: i32) -> Option<u64> {
return None;
}
#[cfg(target_os = "linux")]
fn thread_activity(tid: i32) -> Option<(u64, bool)> {
let text: String = std::fs::read_to_string(format!("/proc/self/task/{tid}/stat")).ok()?;
let (state, ticks): (char, u64) = parse_activity(text.as_str())?;
return Some((ticks, is_working(state)));
}
#[cfg(not(target_os = "linux"))]
fn thread_activity(_tid: i32) -> Option<(u64, bool)> {
return None;
}
#[derive(Debug)]
pub struct ThreadSigns {
tid: i32,
before: Option<(u64, u64)>,
count_sockets: bool,
sockets: HashMap<u64, u64>,
measured: bool,
}
impl ThreadSigns {
pub fn new(tid: i32, count_sockets: bool) -> Self {
return Self { tid, before: None, count_sockets, sockets: HashMap::new(), measured: false };
}
pub fn measured(&self) -> bool {
return self.measured;
}
fn socket_bytes_moved(&mut self) -> u64 {
let held: Vec<u64> = socket_inodes_of(i32::try_from(std::process::id()).unwrap_or(i32::MAX));
let Some(connected) = established_sockets() else {
return 0;
};
let mut moved: u64 = 0;
for socket in connected {
if socket.state != ESTABLISHED || !held.contains(&socket.inode) {
continue;
}
let total: u64 = socket.bytes_acked.saturating_add(socket.bytes_received);
let seen: u64 = self.sockets.insert(socket.inode, total).unwrap_or(total);
moved = moved.saturating_add(total.saturating_sub(seen));
}
return moved;
}
pub fn look(&mut self) -> bool {
let bytes: Option<u64> = thread_bytes(self.tid);
let activity: Option<(u64, bool)> = thread_activity(self.tid);
self.measured = bytes.is_some() || activity.is_some();
if !self.measured {
return false;
}
let now: (u64, u64) = (bytes.unwrap_or(0), activity.map_or(0, |(ticks, _)| { return ticks; }));
let working: bool = activity.is_some_and(|(_, at_work)| { return at_work; });
let changed: bool = self.before.is_none_or(|(old_bytes, old_ticks)| { return now.0 > old_bytes || now.1 > old_ticks; });
self.before = Some(now);
let carried: bool = self.count_sockets && self.socket_bytes_moved() > 0;
return changed || working || carried;
}
}
#[derive(Debug)]
pub struct ThreadClock {
signs: ThreadSigns,
window: Duration,
last_progress: Instant,
next_sample: Instant,
longest_quiet: Duration,
}
impl ThreadClock {
pub fn start(tid: i32, window: Duration, count_sockets: bool) -> Self {
let now: Instant = Instant::now();
return Self { signs: ThreadSigns::new(tid, count_sockets), window, last_progress: now, next_sample: now, longest_quiet: Duration::ZERO };
}
pub fn quiet(&mut self) -> bool {
let now: Instant = Instant::now();
if now >= self.next_sample {
if self.signs.look() {
self.longest_quiet = self.longest_quiet.max(now.saturating_duration_since(self.last_progress));
self.last_progress = now;
}
self.next_sample = now + SAMPLE_INTERVAL;
}
return self.signs.measured() && now.saturating_duration_since(self.last_progress) >= self.window;
}
pub fn longest_quiet(&self) -> Duration {
return self.longest_quiet.max(Instant::now().saturating_duration_since(self.last_progress));
}
}
pub fn measured_quiet<T>(step: &str, job: impl FnOnce() -> T) -> T {
let Some(tid) = current_thread_id() else {
return job();
};
let done: Arc<AtomicBool> = Arc::new(AtomicBool::new(false));
let finished: Arc<AtomicBool> = Arc::clone(&done);
let sampler: Option<std::thread::JoinHandle<Duration>> = std::thread::Builder::new()
.name(String::from("cli-git-quiet-measure"))
.spawn(move || {
let mut clock: ThreadClock = ThreadClock::start(tid, Duration::MAX, true);
while !finished.load(Ordering::Acquire) {
let _quiet = clock.quiet();
std::thread::sleep(MEASURE_PAUSE);
}
return clock.longest_quiet();
})
.ok();
let value: T = job();
done.store(true, Ordering::Release);
if let Some(handle) = sampler
&& let Ok(gap) = handle.join()
{
crate::quiet_ledger::note(step, gap, false);
}
return value;
}
const MEASURE_PAUSE: Duration = Duration::from_millis(20);
#[cfg(test)]
#[path = "thread_quiet_tests.rs"]
mod tests;