Skip to main content

git_policy_cli/process/
thread_quiet.rs

1//! What:
2//!  Whether one thread of this process is at work, judged with the signs the push meter reads for a child process, and the
3//!  clock that says when the thread has shown none of them for a window.
4//! Why:
5//!  The Markdown linter runs on a thread of this process and the gateway call of the standards review blocks one, so neither
6//!  has a child process whose tree `push_progress.rs` could sample. Linux keeps the same counters for each thread under
7//!  `/proc/self/task/<tid>/`: the bytes it read and wrote (`io`), and its processor ticks and state (`stat`). The thread being
8//!  judged is never the thread that reads these files, so the reading does not count as the thread's work.
9//!  Where `/proc/self/task/<tid>/io` or `stat` cannot be read nothing is measured and the thread is never judged quiet.
10//!
11//! In TS you'd write (pseudocode):
12//! ```ts
13//! const clock = new ThreadClock(tid, windowMs); while (!done) { if (clock.quiet()) abandon(); await sleep(SAMPLE_MS); }
14//! ```
15
16/// The parsing of a `stat` line and the state letters that count as work.
17use crate::push_progress::{SAMPLE_INTERVAL, is_working, parse_activity};
18/// The connected sockets of the machine, and those a process holds.
19use crate::tcp_sockets::{ESTABLISHED, established_sockets, socket_inodes_of};
20/// What each socket had carried when it was last seen.
21use std::collections::HashMap;
22/// Shared flags between the thread that judges and the one that waits.
23use std::sync::Arc;
24use std::sync::atomic::{AtomicBool, Ordering};
25/// Time spans and the monotonic clock.
26use std::time::{Duration, Instant};
27
28/// What:
29///  The kernel's number for the calling thread, which names its files under `/proc/self/task/`.
30/// Why:
31///   `std` does not expose it; the system call takes no arguments and cannot fail.
32///
33/// In TS you'd write (pseudocode):
34/// ```ts
35/// const tid = threadId();
36/// ```
37#[cfg(target_os = "linux")]
38pub fn current_thread_id() -> Option<i32> {
39    // SAFETY: `gettid` takes no arguments, touches no memory and cannot fail.
40    let raw: libc::c_long = unsafe { libc::syscall(libc::SYS_gettid) };
41    return i32::try_from(raw).ok();
42}
43
44/// Only Linux names a thread by a number that `/proc` understands; elsewhere nothing is measured.
45// UNTESTED ON WINDOWS: no gate compiles or runs this branch (the crate does not build for Windows; README, Requirements).
46#[cfg(not(target_os = "linux"))]
47pub fn current_thread_id() -> Option<i32> {
48    return None;
49}
50
51/// The bytes a thread has read and written, from its `io` file, or nothing where the file cannot be read.
52#[cfg(target_os = "linux")]
53fn thread_bytes(tid: i32) -> Option<u64> {
54    let text: String = std::fs::read_to_string(format!("/proc/self/task/{tid}/io")).ok()?;
55    let mut read: Option<u64> = None;
56    let mut written: Option<u64> = None;
57    for line in text.lines() {
58        if let Some(value) = line.strip_prefix("rchar: ") {
59            read = value.trim().parse::<u64>().ok();
60        } else if let Some(value) = line.strip_prefix("wchar: ") {
61            written = value.trim().parse::<u64>().ok();
62        }
63    }
64    return Some(read?.saturating_add(written?));
65}
66
67/// Only Linux counts a thread's bytes; elsewhere nothing is measured.
68// UNTESTED ON WINDOWS: no gate compiles or runs this branch (the crate does not build for Windows; README, Requirements).
69#[cfg(not(target_os = "linux"))]
70fn thread_bytes(_tid: i32) -> Option<u64> {
71    return None;
72}
73
74/// The processor ticks of a thread and whether it is able to run or waiting for the disk, or nothing where `stat` cannot be read.
75#[cfg(target_os = "linux")]
76fn thread_activity(tid: i32) -> Option<(u64, bool)> {
77    let text: String = std::fs::read_to_string(format!("/proc/self/task/{tid}/stat")).ok()?;
78    let (state, ticks): (char, u64) = parse_activity(text.as_str())?;
79    return Some((ticks, is_working(state)));
80}
81
82/// Only Linux reads what a thread is doing; elsewhere nothing is.
83// UNTESTED ON WINDOWS: no gate compiles or runs this branch (the crate does not build for Windows; README, Requirements).
84#[cfg(not(target_os = "linux"))]
85fn thread_activity(_tid: i32) -> Option<(u64, bool)> {
86    return None;
87}
88
89/// What:
90///  What one thread had done when it was last looked at, and the look that says whether it did more since.
91/// Why:
92///   Bytes and ticks only grow, so a change since the last look is a sign of work; a thread in a state able to run or waiting
93///  for the disk is at work now. The connected sockets of the process count too, when asked: an HTTP client moves its bytes
94///  with `send` and `recv`, which the thread's `rchar` and `wchar` do not see.
95///
96/// In TS you'd write (pseudocode):
97/// ```ts
98/// class ThreadSigns { tid: number; look(): boolean }
99/// ```
100#[derive(Debug)]
101pub struct ThreadSigns {
102    /// The thread's number.
103    tid: i32,
104    /// The bytes and processor ticks at the last look.
105    before: Option<(u64, u64)>,
106    /// Whether the process's connected sockets count as the thread's work.
107    count_sockets: bool,
108    /// The bytes each connected socket had carried at the last look, by inode.
109    sockets: HashMap<u64, u64>,
110    /// Whether anything about the thread could be read at the last look.
111    measured: bool,
112}
113
114impl ThreadSigns {
115    /// Signs of the thread `tid`; `count_sockets` adds the process's connected sockets.
116    pub fn new(tid: i32, count_sockets: bool) -> Self {
117        return Self { tid, before: None, count_sockets, sockets: HashMap::new(), measured: false };
118    }
119
120    /// Whether the last look could read the thread at all, so a quiet thread is quiet and not unreadable.
121    pub fn measured(&self) -> bool {
122        return self.measured;
123    }
124
125    /// The bytes the process's connected sockets carried since the last look, or zero where they cannot be listed.
126    fn socket_bytes_moved(&mut self) -> u64 {
127        let held: Vec<u64> = socket_inodes_of(i32::try_from(std::process::id()).unwrap_or(i32::MAX));
128        let Some(connected) = established_sockets() else {
129            return 0;
130        };
131        let mut moved: u64 = 0;
132        for socket in connected {
133            if socket.state != ESTABLISHED || !held.contains(&socket.inode) {
134                continue;
135            }
136            let total: u64 = socket.bytes_acked.saturating_add(socket.bytes_received);
137            let seen: u64 = self.sockets.insert(socket.inode, total).unwrap_or(total);
138            moved = moved.saturating_add(total.saturating_sub(seen));
139        }
140        return moved;
141    }
142
143    /// Look at the thread once: true when it moved bytes or used processor time since the last look, or is at work now.
144    ///  The first look is a sign of work, because there is nothing to compare with.
145    pub fn look(&mut self) -> bool {
146        let bytes: Option<u64> = thread_bytes(self.tid);
147        let activity: Option<(u64, bool)> = thread_activity(self.tid);
148        self.measured = bytes.is_some() || activity.is_some();
149        if !self.measured {
150            return false;
151        }
152        let now: (u64, u64) = (bytes.unwrap_or(0), activity.map_or(0, |(ticks, _)| { return ticks; }));
153        let working: bool = activity.is_some_and(|(_, at_work)| { return at_work; });
154        let changed: bool = self.before.is_none_or(|(old_bytes, old_ticks)| { return now.0 > old_bytes || now.1 > old_ticks; });
155        self.before = Some(now);
156        let carried: bool = self.count_sockets && self.socket_bytes_moved() > 0;
157        return changed || working || carried;
158    }
159}
160
161/// What:
162///  The clock of one thread: it looks at the thread's signs at the sampling interval and says when none has shown for a window.
163/// Why:
164///   The waiting thread asks every few milliseconds; the counters are read only every `SAMPLE_INTERVAL`, as for a child process.
165///
166/// In TS you'd write (pseudocode):
167/// ```ts
168/// class ThreadClock { quiet(): boolean; longestQuiet(): number }
169/// ```
170#[derive(Debug)]
171pub struct ThreadClock {
172    /// What the thread has done.
173    signs: ThreadSigns,
174    /// How long the thread may show no sign of work.
175    window: Duration,
176    /// When a sign of work was last seen (the start, before the first).
177    last_progress: Instant,
178    /// When the counters are next read.
179    next_sample: Instant,
180    /// The longest stretch between two looks that found a sign of work.
181    longest_quiet: Duration,
182}
183
184impl ThreadClock {
185    /// Start the clock of the thread `tid` now; `count_sockets` adds the process's connected sockets to its signs.
186    pub fn start(tid: i32, window: Duration, count_sockets: bool) -> Self {
187        let now: Instant = Instant::now();
188        return Self { signs: ThreadSigns::new(tid, count_sockets), window, last_progress: now, next_sample: now, longest_quiet: Duration::ZERO };
189    }
190
191    /// Read the signs when a sample is due, and say whether the thread has now been quiet for the whole window.
192    ///  A thread that cannot be read is never judged quiet.
193    pub fn quiet(&mut self) -> bool {
194        let now: Instant = Instant::now();
195        if now >= self.next_sample {
196            if self.signs.look() {
197                self.longest_quiet = self.longest_quiet.max(now.saturating_duration_since(self.last_progress));
198                self.last_progress = now;
199            }
200            self.next_sample = now + SAMPLE_INTERVAL;
201        }
202        return self.signs.measured() && now.saturating_duration_since(self.last_progress) >= self.window;
203    }
204
205    /// The longest stretch with no sign of work so far, the one still running included.
206    pub fn longest_quiet(&self) -> Duration {
207        return self.longest_quiet.max(Instant::now().saturating_duration_since(self.last_progress));
208    }
209}
210
211/// What:
212///  Run `job` on this thread while another thread measures how quiet this thread gets, and keep the longest gap in the ledger.
213/// Why:
214///   A call that ends itself (the gateway call ends on its transport's own idle timeouts) still needs its quiet gap recorded.
215///  Nothing here ends the job.
216///
217/// In TS you'd write (pseudocode):
218/// ```ts
219/// const value = await measureQuiet('gateway call', () => post(body));
220/// ```
221pub fn measured_quiet<T>(step: &str, job: impl FnOnce() -> T) -> T {
222    let Some(tid) = current_thread_id() else {
223        return job();
224    };
225    let done: Arc<AtomicBool> = Arc::new(AtomicBool::new(false));
226    let finished: Arc<AtomicBool> = Arc::clone(&done);
227    let sampler: Option<std::thread::JoinHandle<Duration>> = std::thread::Builder::new()
228        .name(String::from("cli-git-quiet-measure"))
229        .spawn(move || {
230            let mut clock: ThreadClock = ThreadClock::start(tid, Duration::MAX, true);
231            while !finished.load(Ordering::Acquire) {
232                // The result is unused: this clock only measures, and its window is never reached.
233                let _quiet = clock.quiet();
234                std::thread::sleep(MEASURE_PAUSE);
235            }
236            return clock.longest_quiet();
237        })
238        .ok();
239    let value: T = job();
240    done.store(true, Ordering::Release);
241    if let Some(handle) = sampler
242        && let Ok(gap) = handle.join()
243    {
244        crate::quiet_ledger::note(step, gap, false);
245    }
246    return value;
247}
248
249/// How long the measuring thread sleeps between two looks at its clock.
250const MEASURE_PAUSE: Duration = Duration::from_millis(20);
251
252/// The thread's signs are controlled in the test module.
253#[cfg(test)]
254#[path = "thread_quiet_tests.rs"]
255mod tests;