1use crate::diagnostic_log::debug;
19use crate::discard_audit::append_line;
20use crate::iso_time::now_iso;
21use crate::json_record::quote;
22use crate::quiet_ledger::{drain, object_json};
23use crate::service_protocol::RELEASE;
24use std::cell::RefCell;
25use std::ffi::{OsStr, OsString};
26use std::path::{Path, PathBuf};
27use std::sync::Mutex;
28use std::time::{Duration, Instant};
29
30pub const ADD_TIMING_FILE: &str = "cli-git-worktree-add-timing.jsonl";
32
33pub const ADD_TIMING_SCHEMA_VERSION: u32 = 1;
35
36const TAG: &str = "addTiming";
38
39#[derive(Debug)]
41struct Frame {
42 name: String,
44 entered: Duration,
46 inside: Duration,
48}
49
50#[derive(Debug)]
52struct Spent {
53 name: String,
55 own: Duration,
57 calls: usize,
59}
60
61#[derive(Debug)]
71pub struct Recorder {
72 common_dir: PathBuf,
74 open: Vec<Frame>,
76 spent: Vec<Spent>,
78 notes: Vec<(String, String)>,
80 start_millis: u128,
82 work: Vec<Spent>,
84 quiet: Vec<(String, Duration)>,
86}
87
88impl Recorder {
89 pub fn new(common_dir: PathBuf) -> Self {
91 let start_millis: u128 = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|elapsed: Duration| { return elapsed.as_millis(); }).unwrap_or_default();
92 return Self { common_dir, open: Vec::new(), spent: Vec::new(), notes: Vec::new(), start_millis, work: Vec::new(), quiet: Vec::new() };
93 }
94
95 pub fn enter(&mut self, name: &str, at: Duration) {
97 self.open.push(Frame { name: name.to_string(), entered: at, inside: Duration::ZERO });
98 }
99
100 pub fn leave(&mut self, at: Duration) {
102 let Some(frame) = self.open.pop() else { return };
103 let whole: Duration = at.saturating_sub(frame.entered);
104 let own: Duration = whole.saturating_sub(frame.inside);
105 if let Some(outer) = self.open.last_mut() {
106 outer.inside += whole;
107 }
108 match self.spent.iter_mut().find(|known: &&mut Spent| { return known.name == frame.name; }) {
109 Some(known) => {
110 known.own += own;
111 known.calls += 1;
112 }
113 None => self.spent.push(Spent { name: frame.name, own, calls: 1 }),
114 }
115 }
116
117 pub fn note(&mut self, key: &str, value: &str) {
119 match self.notes.iter_mut().find(|known: &&mut (String, String)| { return known.0 == key; }) {
120 Some(known) => known.1 = value.to_string(),
121 None => self.notes.push((key.to_string(), value.to_string())),
122 }
123 }
124
125 pub fn add_work(&mut self, name: &str, own: Duration) {
127 match self.work.iter_mut().find(|known: &&mut Spent| { return known.name == name; }) {
128 Some(known) => {
129 known.own += own;
130 known.calls += 1;
131 }
132 None => self.work.push(Spent { name: name.to_string(), own, calls: 1 }),
133 }
134 }
135
136 pub fn line(&self, total: Duration, exit_code: i32) -> String {
141 let micros_to_ms = |time: Duration| -> String { return format!("{:.3}", time.as_micros() as f64 / 1000.0); };
142 let covered: Duration = self.spent.iter().map(|each: &Spent| { return each.own; }).sum();
143 let steps: Vec<String> = self.spent.iter().map(|each: &Spent| { return format!("{}:{{\"ms\":{},\"calls\":{}}}", quote(each.name.as_str()), micros_to_ms(each.own), each.calls); }).collect();
144 let work: Vec<String> = self.work.iter().map(|each: &Spent| { return format!("{}:{{\"ms\":{},\"calls\":{}}}", quote(each.name.as_str()), micros_to_ms(each.own), each.calls); }).collect();
145 let notes: Vec<String> = self.notes.iter().map(|(key, value): &(String, String)| { return format!("{}:{}", quote(key.as_str()), quote(value.as_str())); }).collect();
146 return format!(
147 "{{\"schemaVersion\":{ADD_TIMING_SCHEMA_VERSION},\"time\":{},\"startMillis\":{},\"pid\":{},\"release\":{},\"exitCode\":{exit_code},\"totalMs\":{},\"steps\":{{{}}},\"otherMs\":{},\"work\":{{{}}},\"notes\":{{{}}},\"quiet\":{}}}\n",
148 quote(now_iso().as_str()),
149 self.start_millis,
150 std::process::id(),
151 quote(RELEASE),
152 micros_to_ms(total),
153 steps.join(","),
154 micros_to_ms(total.saturating_sub(covered)),
155 work.join(","),
156 notes.join(","),
157 object_json(self.quiet.as_slice()),
158 );
159 }
160
161 pub fn common_dir(&self) -> &Path {
163 return self.common_dir.as_path();
164 }
165}
166
167static WORK: Mutex<Option<Vec<(String, Duration)>>> = Mutex::new(None);
169
170thread_local! {
171 static ACTIVE: RefCell<Option<(Instant, Recorder)>> = const { RefCell::new(None) };
173}
174
175pub fn begin(common_dir: PathBuf) {
178 ACTIVE.with(|active: &RefCell<Option<(Instant, Recorder)>>| { *active.borrow_mut() = Some((Instant::now(), Recorder::new(common_dir))); });
179 if let Ok(mut work) = WORK.lock() {
180 *work = Some(Vec::new());
181 }
182}
183
184pub fn step<T>(name: &str, body: impl FnOnce() -> T) -> T {
186 let entered: bool = ACTIVE.with(|active: &RefCell<Option<(Instant, Recorder)>>| {
187 return match active.borrow_mut().as_mut() {
188 Some((began, recorder)) => {
189 recorder.enter(name, began.elapsed());
190 true
191 }
192 None => false,
193 };
194 });
195 let value: T = body();
196 if entered {
197 ACTIVE.with(|active: &RefCell<Option<(Instant, Recorder)>>| {
198 if let Some((began, recorder)) = active.borrow_mut().as_mut() {
199 recorder.leave(began.elapsed());
200 }
201 });
202 }
203 return value;
204}
205
206pub fn git_step_label(arguments: &[OsString]) -> String {
217 let takes_value = |word: &OsStr| -> bool { return ["-C", "-c", "--git-dir", "--work-tree", "--namespace", "--exec-path"].iter().any(|known: &&str| { return word == OsStr::new(known); }); };
218 let mut words = arguments.iter().map(OsString::as_os_str);
219 while let Some(word) = words.next() {
220 if takes_value(word) {
221 words.next();
222 continue;
223 }
224 if word.to_string_lossy().starts_with('-') {
225 continue;
226 }
227 return format!("git {}", word.to_string_lossy());
228 }
229 return String::from("git");
230}
231
232pub fn work<T>(name: &str, body: impl FnOnce() -> T) -> T {
243 let began: Instant = Instant::now();
244 let value: T = body();
245 let spent: Duration = began.elapsed();
246 if let Ok(mut work) = WORK.lock()
247 && let Some(list) = work.as_mut()
248 {
249 list.push((name.to_string(), spent));
250 }
251 return value;
252}
253
254pub fn note(key: &str, value: &str) {
256 ACTIVE.with(|active: &RefCell<Option<(Instant, Recorder)>>| {
257 if let Some((_began, recorder)) = active.borrow_mut().as_mut() {
258 recorder.note(key, value);
259 }
260 });
261}
262
263pub fn finish(exit_code: i32) {
274 let taken: Option<(Instant, Recorder)> = ACTIVE.with(|active: &RefCell<Option<(Instant, Recorder)>>| { return active.borrow_mut().take(); });
275 let drained: Option<Vec<(String, Duration)>> = WORK.lock().ok().and_then(|mut work: std::sync::MutexGuard<'_, Option<Vec<(String, Duration)>>>| { return work.take(); });
276 let Some((began, mut recorder)) = taken else { return };
277 for (name, spent) in drained.unwrap_or_default() {
278 recorder.add_work(name.as_str(), spent);
279 }
280 recorder.quiet = drain();
281 let target: PathBuf = recorder.common_dir().join(ADD_TIMING_FILE);
282 let line: String = recorder.line(began.elapsed(), exit_code);
283 if let Err(error) = append_line(target.as_path(), line.as_bytes()) {
284 debug(TAG, &format!("the add timing record could not be written to {}: {error}", target.display()));
285 }
286}
287
288#[cfg(test)]
290#[path = "add_timing_tests.rs"]
291mod tests;