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;