1use std::collections::BTreeMap;
4use std::fs::File;
5use std::io::Write;
6use std::path::Path;
7use std::sync::Mutex;
8use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
9
10pub struct Logger {
12 file: Option<Mutex<File>>,
13 echo: bool,
14 tz_offset_s: i64,
15}
16
17impl Logger {
18 pub fn new(path: Option<&Path>, echo: bool) -> std::io::Result<Self> {
20 let file = match path {
21 Some(p) => Some(Mutex::new(File::create(p)?)),
22 None => None,
23 };
24 Ok(Self { file, echo, tz_offset_s: local_tz_offset() })
25 }
26
27 pub fn line(&self, s: &str) {
29 if let Some(f) = &self.file {
30 if let Ok(mut f) = f.lock() {
31 let _ = writeln!(f, "{s}");
32 let _ = f.flush();
33 }
34 }
35 if self.echo {
36 println!("{s}");
37 }
38 }
39
40 pub fn stamped(&self, s: &str) {
42 self.line(&format!("[{}] {s}", self.clock()));
43 }
44
45 pub fn stamped_at(&self, at: SystemTime, s: &str) {
47 self.line(&format!("[{}] {s}", self.clock_at(at)));
48 }
49
50 pub fn clock(&self) -> String {
52 self.clock_at(SystemTime::now())
53 }
54
55 pub fn clock_at(&self, at: SystemTime) -> String {
57 let t = at.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs() as i64 + self.tz_offset_s;
58 let d = t.rem_euclid(86400);
59 format!("{:02}:{:02}:{:02}", d / 3600, (d / 60) % 60, d % 60)
60 }
61}
62
63fn local_tz_offset() -> i64 {
65 let out = std::process::Command::new("date").arg("+%z").output();
66 let Ok(out) = out else { return 0 };
67 let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
68 if s.len() != 5 {
69 return 0;
70 }
71 let sign = if s.starts_with('-') { -1 } else { 1 };
72 let h: i64 = s[1..3].parse().unwrap_or(0);
73 let m: i64 = s[3..5].parse().unwrap_or(0);
74 sign * (h * 3600 + m * 60)
75}
76
77pub struct Timeline {
79 file: Mutex<File>,
80 events: Mutex<Vec<(f64, String)>>,
81}
82
83impl Timeline {
84 pub fn new(path: &Path) -> std::io::Result<Self> {
86 Ok(Self { file: Mutex::new(File::create(path)?), events: Mutex::new(Vec::new()) })
87 }
88
89 pub fn ev(&self, log: &Logger, s: &str) {
91 let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default();
92 let line = format!("{}.{:09} {s}", now.as_secs(), now.subsec_nanos());
93 if let Ok(mut f) = self.file.lock() {
94 let _ = writeln!(f, "{line}");
95 let _ = f.flush();
96 }
97 if let Ok(mut e) = self.events.lock() {
98 e.push((now.as_secs_f64(), s.to_string()));
99 }
100 log.stamped(s);
101 }
102
103 pub fn relative(&self) -> Vec<(f64, String)> {
105 let e = self.events.lock().map(|e| e.clone()).unwrap_or_default();
106 let t0 = e.first().map(|x| x.0).unwrap_or(0.0);
107 e.into_iter().map(|(t, s)| (t - t0, s)).collect()
108 }
109}
110
111#[derive(Default)]
113pub struct StageTimes {
114 m: Mutex<BTreeMap<String, (Duration, usize)>>,
115}
116
117impl StageTimes {
118 pub fn add(&self, stage: &str, d: Duration) {
120 if let Ok(mut m) = self.m.lock() {
121 let e = m.entry(stage.to_string()).or_default();
122 e.0 += d;
123 e.1 += 1;
124 }
125 }
126
127 pub fn snapshot(&self) -> BTreeMap<String, (Duration, usize)> {
129 self.m.lock().map(|m| m.clone()).unwrap_or_default()
130 }
131}
132
133pub fn timed<T>(log: &Logger, times: &StageTimes, stage: &str, what: &str, f: impl FnOnce() -> T) -> T {
135 let t = Instant::now();
136 let r = f();
137 let d = t.elapsed();
138 times.add(stage, d);
139 log.stamped(&format!("[time] {stage} {what}: {:.3}s", d.as_secs_f64()));
140 r
141}
142
143pub fn py_float(x: f64, digits: i32) -> String {
145 let p = 10f64.powi(digits);
146 let r = (x * p).round() / p;
147 let r = if r == 0.0 { 0.0 } else { r };
148 if !r.is_finite() {
149 return if r.is_nan() { "NaN".into() } else if r > 0.0 { "Infinity".into() } else { "-Infinity".into() };
150 }
151 let s = format!("{r}");
152 if s.contains('.') || s.contains('e') {
153 s
154 } else {
155 format!("{s}.0")
156 }
157}
158
159#[derive(Clone, Debug)]
161pub enum Json {
162 Null,
164 Bool(bool),
166 Int(i64),
168 Num(String),
170 Str(String),
172 Arr(Vec<Json>),
174 Obj(Vec<(String, Json)>),
176}
177
178impl Json {
179 pub fn obj(items: Vec<(&str, Json)>) -> Json {
181 Json::Obj(items.into_iter().map(|(k, v)| (k.to_string(), v)).collect())
182 }
183 pub fn f(x: f64, digits: i32) -> Json {
185 Json::Num(py_float(x, digits))
186 }
187 pub fn of(x: Option<f64>, digits: i32) -> Json {
189 x.map_or(Json::Null, |v| Json::f(v, digits))
190 }
191 pub fn ints(v: &[usize]) -> Json {
193 Json::Arr(v.iter().map(|x| Json::Int(*x as i64)).collect())
194 }
195
196 pub fn dump(&self) -> String {
198 let mut s = String::new();
199 self.write(&mut s, 0);
200 s
201 }
202
203 fn write(&self, s: &mut String, ind: usize) {
204 match self {
205 Json::Null => s.push_str("null"),
206 Json::Bool(b) => s.push_str(if *b { "true" } else { "false" }),
207 Json::Int(i) => s.push_str(&i.to_string()),
208 Json::Num(n) => s.push_str(n),
209 Json::Str(t) => push_str_lit(s, t),
210 Json::Arr(v) => {
211 if v.is_empty() {
212 s.push_str("[]");
213 return;
214 }
215 s.push_str("[\n");
216 for (i, x) in v.iter().enumerate() {
217 s.push_str(&" ".repeat(ind + 1));
218 x.write(s, ind + 1);
219 if i + 1 < v.len() {
220 s.push(',');
221 }
222 s.push('\n');
223 }
224 s.push_str(&" ".repeat(ind));
225 s.push(']');
226 }
227 Json::Obj(v) => {
228 if v.is_empty() {
229 s.push_str("{}");
230 return;
231 }
232 s.push_str("{\n");
233 for (i, (k, x)) in v.iter().enumerate() {
234 s.push_str(&" ".repeat(ind + 1));
235 push_str_lit(s, k);
236 s.push_str(": ");
237 x.write(s, ind + 1);
238 if i + 1 < v.len() {
239 s.push(',');
240 }
241 s.push('\n');
242 }
243 s.push_str(&" ".repeat(ind));
244 s.push('}');
245 }
246 }
247 }
248
249 pub fn py_repr(&self) -> String {
251 match self {
252 Json::Null => "None".into(),
253 Json::Bool(b) => (if *b { "True" } else { "False" }).into(),
254 Json::Int(i) => i.to_string(),
255 Json::Num(n) => n.clone(),
256 Json::Str(t) => format!("'{}'", t.replace('\\', "\\\\").replace('\'', "\\'")),
257 Json::Arr(v) => format!("[{}]", v.iter().map(|x| x.py_repr()).collect::<Vec<_>>().join(", ")),
258 Json::Obj(v) => {
259 format!("{{{}}}", v.iter().map(|(k, x)| format!("'{k}': {}", x.py_repr())).collect::<Vec<_>>().join(", "))
260 }
261 }
262 }
263
264 pub fn get(&self, key: &str) -> Option<&Json> {
266 match self {
267 Json::Obj(v) => v.iter().find(|(k, _)| k == key).map(|(_, x)| x),
268 _ => None,
269 }
270 }
271}
272
273fn push_str_lit(s: &mut String, t: &str) {
274 s.push('"');
275 for c in t.chars() {
276 match c {
277 '"' => s.push_str("\\\""),
278 '\\' => s.push_str("\\\\"),
279 '\n' => s.push_str("\\n"),
280 '\t' => s.push_str("\\t"),
281 c if (c as u32) < 0x20 => s.push_str(&format!("\\u{:04x}", c as u32)),
282 c => s.push(c),
283 }
284 }
285 s.push('"');
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291
292 #[test]
293 fn py_float_matches_python_repr() {
294 assert_eq!(py_float(1.0, 3), "1.0");
295 assert_eq!(py_float(12.345, 1), "12.3");
296 assert_eq!(py_float(-0.0001, 2), "0.0");
297 assert_eq!(py_float(0.987654, 3), "0.988");
298 }
299
300 #[test]
301 fn json_dump_indent1() {
302 let j = Json::obj(vec![("a", Json::Int(1)), ("b", Json::Arr(vec![])), ("c", Json::Arr(vec![Json::Null]))]);
303 assert_eq!(j.dump(), "{\n \"a\": 1,\n \"b\": [],\n \"c\": [\n null\n ]\n}");
304 assert_eq!(j.py_repr(), "{'a': 1, 'b': [], 'c': [None]}");
305 }
306}