1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
//! What:
//! The record of where one wrapped `git worktree add` spent its time, step by step, appended to a size-capped file in the
//! common Git directory beside the commit timing record.
//! Why:
//! An add that took thirteen seconds could not be split afterwards without a `strace` that distorted it. Each step is a
//! stretch of the command (a walk of the source, a Git run, the snapshot call, a durable write), timed on the monotonic
//! clock; a step inside another is taken out of the outer one, so the steps are disjoint and what none covers is reported
//! as `otherMs`. The record goes to the file only, in one append when the command ends, with no flush to disk, and it
//! never changes what the add does.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! begin(commonDir);
//! const value = step('walkSource', () => walk());
//! finish(exitCode); // appends JSON.stringify(record) + '\n' to join(commonDir, 'cli-git-worktree-add-timing.jsonl')
//! ```
use super::diagnostic_log::debug;
use super::discard_audit::append_line;
use super::iso_time::now_iso;
use super::json_record::quote;
use super::quiet_ledger::{drain, object_json};
use super::service_protocol::RELEASE;
use std::cell::RefCell;
use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::time::{Duration, Instant};
/// The file inside the common Git directory that collects the add timing records of every worktree of the repository.
pub const ADD_TIMING_FILE: &str = "cli-git-worktree-add-timing.jsonl";
/// The version of the record's fields; it changes only when a field's meaning changes, never when a field is added.
pub const ADD_TIMING_SCHEMA_VERSION: u32 = 1;
/// Module tag for diagnostics.
const TAG: &str = "addTiming";
/// A step that has been entered and not yet left.
#[derive(Debug)]
struct Frame {
/// The step's name.
name: String,
/// When the step was entered, since the recorder began.
entered: Duration,
/// The time of the steps entered and left inside this one.
inside: Duration,
}
/// What one step name has spent so far.
#[derive(Debug)]
struct Spent {
/// The step's name.
name: String,
/// The time of the step itself, the steps inside it taken out.
own: Duration,
/// How many times the step was entered.
calls: usize,
}
/// What:
/// The steps of one add, filled as the add runs and written when it ends.
/// Why:
/// The accounting is separate from the clock so that the controls can drive it with chosen times.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type Recorder = { commonDir: string; open: Frame[]; spent: Spent[]; notes: [string, string][] };
/// ```
#[derive(Debug)]
pub struct Recorder {
/// The common Git directory that holds the file.
common_dir: PathBuf,
/// The steps entered and not left, the innermost last.
open: Vec<Frame>,
/// The time per step name, in the order the names first appeared.
spent: Vec<Spent>,
/// Facts about the add that are words, not times (the route taken, why not the snapshot), in the order they were noted.
notes: Vec<(String, String)>,
/// Milliseconds since the Unix epoch at the start, for finding the add among other logs.
start_millis: u128,
/// Time spent on worker threads by name, summed over the threads; threads overlap, so it is not part of the sum rule.
work: Vec<Spent>,
/// The longest quiet gap of every Git step and tool the add ran, by name, taken from the ledger when the record is written.
quiet: Vec<(String, Duration)>,
}
impl Recorder {
/// A recorder for an add in the repository whose common Git directory is `common_dir`.
pub fn new(common_dir: PathBuf) -> Self {
let start_millis: u128 = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|elapsed: Duration| { return elapsed.as_millis(); }).unwrap_or_default();
return Self { common_dir, open: Vec::new(), spent: Vec::new(), notes: Vec::new(), start_millis, work: Vec::new(), quiet: Vec::new() };
}
/// Enter the step `name` at `at`, a time since the recorder began.
pub fn enter(&mut self, name: &str, at: Duration) {
self.open.push(Frame { name: name.to_string(), entered: at, inside: Duration::ZERO });
}
/// Leave the innermost step at `at`; its own time is what the steps inside it left, and the outer step loses the whole.
pub fn leave(&mut self, at: Duration) {
let Some(frame) = self.open.pop() else { return };
let whole: Duration = at.saturating_sub(frame.entered);
let own: Duration = whole.saturating_sub(frame.inside);
if let Some(outer) = self.open.last_mut() {
outer.inside += whole;
}
match self.spent.iter_mut().find(|known: &&mut Spent| { return known.name == frame.name; }) {
Some(known) => {
known.own += own;
known.calls += 1;
}
None => self.spent.push(Spent { name: frame.name, own, calls: 1 }),
}
}
/// Note a fact about the add; a later note of the same key replaces the earlier one.
pub fn note(&mut self, key: &str, value: &str) {
match self.notes.iter_mut().find(|known: &&mut (String, String)| { return known.0 == key; }) {
Some(known) => known.1 = value.to_string(),
None => self.notes.push((key.to_string(), value.to_string())),
}
}
/// Add `own` to the work named `name`, as one call.
pub fn add_work(&mut self, name: &str, own: Duration) {
match self.work.iter_mut().find(|known: &&mut Spent| { return known.name == name; }) {
Some(known) => {
known.own += own;
known.calls += 1;
}
None => self.work.push(Spent { name: name.to_string(), own, calls: 1 }),
}
}
/// The record as one line of JSON ending in a newline; `total` is the add's whole time.
///
/// One `write` per line keeps the lines of concurrent adds from interleaving; a word with a quote or a newline is escaped
/// by `quote`, so the line stays one line.
pub fn line(&self, total: Duration, exit_code: i32) -> String {
let micros_to_ms = |time: Duration| -> String { return format!("{:.3}", time.as_micros() as f64 / 1000.0); };
let covered: Duration = self.spent.iter().map(|each: &Spent| { return each.own; }).sum();
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();
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();
let notes: Vec<String> = self.notes.iter().map(|(key, value): &(String, String)| { return format!("{}:{}", quote(key.as_str()), quote(value.as_str())); }).collect();
return format!(
"{{\"schemaVersion\":{ADD_TIMING_SCHEMA_VERSION},\"time\":{},\"startMillis\":{},\"pid\":{},\"release\":{},\"exitCode\":{exit_code},\"totalMs\":{},\"steps\":{{{}}},\"otherMs\":{},\"work\":{{{}}},\"notes\":{{{}}},\"quiet\":{}}}\n",
quote(now_iso().as_str()),
self.start_millis,
std::process::id(),
quote(RELEASE),
micros_to_ms(total),
steps.join(","),
micros_to_ms(total.saturating_sub(covered)),
work.join(","),
notes.join(","),
object_json(self.quiet.as_slice()),
);
}
/// The directory that holds the record's file.
pub fn common_dir(&self) -> &Path {
return self.common_dir.as_path();
}
}
/// The work of worker threads while an add is recorded: none outside a recording, so a stray call keeps nothing.
static WORK: Mutex<Option<Vec<(String, Duration)>>> = Mutex::new(None);
thread_local! {
/// The recorder of the add this thread is running, and when it began; none outside an add.
static ACTIVE: RefCell<Option<(Instant, Recorder)>> = const { RefCell::new(None) };
}
/// Begin recording the add of the repository whose common Git directory is `common_dir`, replacing any earlier recording
/// of this thread.
pub fn begin(common_dir: PathBuf) {
ACTIVE.with(|active: &RefCell<Option<(Instant, Recorder)>>| { *active.borrow_mut() = Some((Instant::now(), Recorder::new(common_dir))); });
if let Ok(mut work) = WORK.lock() {
*work = Some(Vec::new());
}
}
/// Run `body` as the step `name`; with no recording begun, `body` runs alone.
pub fn step<T>(name: &str, body: impl FnOnce() -> T) -> T {
let entered: bool = ACTIVE.with(|active: &RefCell<Option<(Instant, Recorder)>>| {
return match active.borrow_mut().as_mut() {
Some((began, recorder)) => {
recorder.enter(name, began.elapsed());
true
}
None => false,
};
});
let value: T = body();
if entered {
ACTIVE.with(|active: &RefCell<Option<(Instant, Recorder)>>| {
if let Some((began, recorder)) = active.borrow_mut().as_mut() {
recorder.leave(began.elapsed());
}
});
}
return value;
}
/// What:
/// The step name of one Git run: `git ` and the subcommand, found past the global options that take a value (`-C`, `-c`,
/// `--git-dir`, `--work-tree`) and the flags that do not.
/// Why:
/// The record says how many Git processes an add started and how long each kind took, which names the avoidable ones.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// git_step_label(['-C', dir, '--no-optional-locks', 'status']) // 'git status'
/// ```
pub fn git_step_label(arguments: &[OsString]) -> String {
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); }); };
let mut words = arguments.iter().map(OsString::as_os_str);
while let Some(word) = words.next() {
if takes_value(word) {
words.next();
continue;
}
if word.to_string_lossy().starts_with('-') {
continue;
}
return format!("git {}", word.to_string_lossy());
}
return String::from("git");
}
/// What:
/// Run `body` as work named `name` on any thread, adding its time to the name's sum; with no recording begun, `body` runs alone.
/// Why:
/// The repair of package managers runs its tools on worker threads, which the thread's own steps cannot see; the sum over
/// threads says how much work a step held, and the step that waited for it says how long the add waited.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const value = work('clone', () => cloneTree());
/// ```
pub fn work<T>(name: &str, body: impl FnOnce() -> T) -> T {
let began: Instant = Instant::now();
let value: T = body();
let spent: Duration = began.elapsed();
if let Ok(mut work) = WORK.lock()
&& let Some(list) = work.as_mut()
{
list.push((name.to_string(), spent));
}
return value;
}
/// Note a fact about the add being recorded; nothing outside a recording.
pub fn note(key: &str, value: &str) {
ACTIVE.with(|active: &RefCell<Option<(Instant, Recorder)>>| {
if let Some((_began, recorder)) = active.borrow_mut().as_mut() {
recorder.note(key, value);
}
});
}
/// What:
/// End the recording and append its record to the repository's add timing file, best effort.
/// Why:
/// The command's result never depends on the record: a file that cannot be written is only a debug line. The directory is
/// never created and nothing is flushed to disk, so the record costs one `write`.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// finish(exitCode);
/// ```
pub fn finish(exit_code: i32) {
let taken: Option<(Instant, Recorder)> = ACTIVE.with(|active: &RefCell<Option<(Instant, Recorder)>>| { return active.borrow_mut().take(); });
let drained: Option<Vec<(String, Duration)>> = WORK.lock().ok().and_then(|mut work: std::sync::MutexGuard<'_, Option<Vec<(String, Duration)>>>| { return work.take(); });
let Some((began, mut recorder)) = taken else { return };
for (name, spent) in drained.unwrap_or_default() {
recorder.add_work(name.as_str(), spent);
}
recorder.quiet = drain();
let target: PathBuf = recorder.common_dir().join(ADD_TIMING_FILE);
let line: String = recorder.line(began.elapsed(), exit_code);
if let Err(error) = append_line(target.as_path(), line.as_bytes()) {
debug(TAG, &format!("the add timing record could not be written to {}: {error}", target.display()));
}
}
/// The accounting, the line and the writer.
#[cfg(test)]
#[path = "add_timing_tests.rs"]
mod tests;