git_policy_cli/process/quiet_ledger.rs
1//! What:
2//! The longest quiet gap of every Git step and every tool this process ran, kept until a timing record takes it.
3//! A quiet gap is a stretch in which the step showed no sign of work: no bytes moved, no processor time, no task running or
4//! waiting for the disk (the signs of `push_progress.rs`).
5//! Why:
6//! A step ends only when it has been quiet for a window, so the window must be set above the longest gap a healthy step
7//! shows (`doc/planning/cli-git-shipped-limits-audit.md`, "Time limits on Git child steps of a commit").
8//! Nothing logged a gap before this, only whole times, and a whole time is an upper bound of the gap, never the gap.
9//! The timing records (`commit_timing.rs`, `add_timing.rs`) take the figures when they are written, so a measurement
10//! comes from the runs the person's own work makes. A step that was ended for being quiet is kept apart from the steps
11//! that ended by themselves, because only the steps that ended by themselves show how quiet a healthy step gets.
12//!
13//! In TS you'd write (pseudocode):
14//! ```ts
15//! const gaps = new Map<string, number>();
16//! export const note = (step: string, gapMs: number, ended: boolean) => gaps.set(key(step, ended), Math.max(gaps.get(key(step, ended)) ?? 0, gapMs));
17//! export const drain = () => { const all = [...gaps]; gaps.clear(); return all; };
18//! ```
19
20/// Escaping of a step name inside a JSON string.
21use crate::json_record::quote;
22/// A lock around the shared list.
23use std::sync::Mutex;
24/// A span of time.
25use std::time::Duration;
26
27/// The suffix of a step name whose run was ended for being quiet, so it never mixes with the steps that ended by themselves.
28pub const ENDED_SUFFIX: &str = " [ended for being quiet]";
29
30/// The longest gap of each step name so far; the names are the Git subcommands and the tool names, a small fixed set.
31static GAPS: Mutex<Vec<(String, Duration)>> = Mutex::new(Vec::new());
32
33/// What:
34/// Keep `gap` as the longest quiet gap of `step` when it is longer than the one kept.
35/// Why:
36/// A step runs many times in one command; the longest of them is the figure an interval has to clear.
37///
38/// In TS you'd write (pseudocode):
39/// ```ts
40/// note('git rev-parse', 1200, false);
41/// ```
42pub fn note(step: &str, gap: Duration, ended_for_being_quiet: bool) {
43 let name: String = if ended_for_being_quiet { format!("{step}{ENDED_SUFFIX}") } else { step.to_owned() };
44 // A poisoned lock means another thread panicked while holding it; the list is still a list of gaps.
45 let mut gaps = GAPS.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
46 match gaps.iter_mut().find(|(known, _)| { return *known == name; }) {
47 Some((_, longest)) => *longest = (*longest).max(gap),
48 None => gaps.push((name, gap)),
49 }
50}
51
52/// What:
53/// Take every figure kept and forget them, in the order of the step names.
54/// Why:
55/// One record takes the figures of the command it describes; the next command starts from nothing.
56///
57/// In TS you'd write (pseudocode):
58/// ```ts
59/// const figures = drain();
60/// ```
61pub fn drain() -> Vec<(String, Duration)> {
62 let mut gaps = GAPS.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
63 let mut taken: Vec<(String, Duration)> = std::mem::take(&mut *gaps);
64 taken.sort();
65 return taken;
66}
67
68/// The milliseconds of a gap with three decimals, the way the timing records write times.
69fn milliseconds(gap: Duration) -> String {
70 return format!("{:.3}", gap.as_micros() as f64 / 1000.0);
71}
72
73/// What:
74/// The figures as a JSON object from step name to milliseconds.
75/// Why:
76/// Both timing records carry it under one key, so a reader asks one question of both files.
77///
78/// In TS you'd write (pseudocode):
79/// ```ts
80/// JSON.stringify(Object.fromEntries(figures.map(([step, gap]) => [step, gap])))
81/// ```
82pub fn object_json(figures: &[(String, Duration)]) -> String {
83 let members: Vec<String> = figures.iter().map(|(step, gap)| { return format!("{}:{}", quote(step.as_str()), milliseconds(*gap)); }).collect();
84 return format!("{{{}}}", members.join(","));
85}
86
87/// What:
88/// The longest gap among the steps that ended by themselves, in milliseconds, or zero when none ran.
89/// Why:
90/// One number answers "how quiet did a healthy step get in this command" without reading the object.
91///
92/// In TS you'd write (pseudocode):
93/// ```ts
94/// Math.max(0, ...figures.filter(([step]) => !step.endsWith(ENDED_SUFFIX)).map(([, gap]) => gap))
95/// ```
96pub fn longest_healthy(figures: &[(String, Duration)]) -> String {
97 let longest: Duration = figures
98 .iter()
99 .filter(|(step, _)| { return !step.ends_with(ENDED_SUFFIX); })
100 .map(|(_, gap)| { return *gap; })
101 .max()
102 .unwrap_or(Duration::ZERO);
103 return milliseconds(longest);
104}
105
106/// The figures are kept, drained and written as controls in the test module.
107#[cfg(test)]
108#[path = "quiet_ledger_tests.rs"]
109mod tests;