1use std::collections::BTreeMap;
13use std::path::Path;
14
15use braidz_parser::braidz_parse_path;
16
17#[derive(Debug, Clone, PartialEq)]
19pub struct TrackStats {
20 pub num_objects: usize,
22 pub total_rows: usize,
24 pub longest_span: u64,
27 pub total_span: u64,
29 pub frame_range: Option<(u64, u64)>,
31}
32
33pub fn track_stats(braidz_path: &Path) -> eyre::Result<TrackStats> {
35 let archive = braidz_parse_path(braidz_path)
36 .map_err(|e| eyre::eyre!("opening braidz {}: {e}", braidz_path.display()))?;
37
38 let rows = archive
39 .kalman_estimates_table
40 .as_ref()
41 .ok_or_else(|| eyre::eyre!("braidz {} has no kalman_estimates", braidz_path.display()))?;
42
43 let mut per_obj: BTreeMap<u32, (u64, u64, usize)> = BTreeMap::new();
45 let mut global_min = u64::MAX;
46 let mut global_max = 0u64;
47 for row in rows {
48 let f = row.frame.0;
49 global_min = global_min.min(f);
50 global_max = global_max.max(f);
51 let e = per_obj.entry(row.obj_id).or_insert((f, f, 0));
52 e.0 = e.0.min(f);
53 e.1 = e.1.max(f);
54 e.2 += 1;
55 }
56
57 let mut longest_span = 0u64;
58 let mut total_span = 0u64;
59 for (lo, hi, _n) in per_obj.values() {
60 let span = hi - lo + 1;
61 longest_span = longest_span.max(span);
62 total_span += span;
63 }
64
65 Ok(TrackStats {
66 num_objects: per_obj.len(),
67 total_rows: rows.len(),
68 longest_span,
69 total_span,
70 frame_range: if rows.is_empty() {
71 None
72 } else {
73 Some((global_min, global_max))
74 },
75 })
76}
77
78#[derive(Debug, Clone)]
80pub struct Differential {
81 pub live: TrackStats,
83 pub retrack: TrackStats,
85}
86
87impl Differential {
88 pub fn live_is_shortened(&self, span_frac: f64) -> bool {
93 let live = self.live.longest_span as f64;
94 let retrack = self.retrack.longest_span as f64;
95 let shorter = retrack > 0.0 && live < span_frac * retrack;
96 let more_fragmented = self.live.num_objects > self.retrack.num_objects;
97 shorter || more_fragmented
98 }
99
100 pub fn report(&self) -> String {
102 format!(
103 " {:>12} {:>12}\n\
104 objects (frags) {:>12} {:>12}\n\
105 longest span {:>12} {:>12}\n\
106 total span {:>12} {:>12}\n\
107 total rows {:>12} {:>12}\n\
108 frame range {:>12} {:>12}",
109 "live",
110 "retrack",
111 self.live.num_objects,
112 self.retrack.num_objects,
113 self.live.longest_span,
114 self.retrack.longest_span,
115 self.live.total_span,
116 self.retrack.total_span,
117 self.live.total_rows,
118 self.retrack.total_rows,
119 fmt_range(self.live.frame_range),
120 fmt_range(self.retrack.frame_range),
121 )
122 }
123}
124
125fn fmt_range(r: Option<(u64, u64)>) -> String {
126 match r {
127 Some((a, b)) => format!("{a}-{b}"),
128 None => "(none)".to_string(),
129 }
130}
131
132pub fn differential(
137 retrack_exe: &Path,
138 live_braidz: &Path,
139 out_braidz: &Path,
140) -> eyre::Result<Differential> {
141 if out_braidz.exists() {
142 std::fs::remove_file(out_braidz)?;
143 }
144 let status = std::process::Command::new(retrack_exe)
145 .arg("--data-src")
146 .arg(live_braidz)
147 .arg("--output")
148 .arg(out_braidz)
149 .status()
150 .map_err(|e| eyre::eyre!("running {}: {e}", retrack_exe.display()))?;
151 if !status.success() {
152 eyre::bail!("braid-offline-retrack failed with status {status}");
153 }
154
155 Ok(Differential {
156 live: track_stats(live_braidz)?,
157 retrack: track_stats(out_braidz)?,
158 })
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 fn stats(num_objects: usize, longest_span: u64) -> TrackStats {
166 TrackStats {
167 num_objects,
168 total_rows: longest_span as usize,
169 longest_span,
170 total_span: longest_span,
171 frame_range: Some((0, longest_span.saturating_sub(1))),
172 }
173 }
174
175 #[test]
176 fn agreeing_recordings_are_not_flagged() {
177 let diff = Differential {
178 live: stats(1, 800),
179 retrack: stats(1, 798),
180 };
181 assert!(!diff.live_is_shortened(0.9));
182 }
183
184 #[test]
185 fn much_shorter_live_span_is_flagged() {
186 let diff = Differential {
187 live: stats(1, 300),
188 retrack: stats(1, 800),
189 };
190 assert!(diff.live_is_shortened(0.9));
191 }
192
193 #[test]
194 fn more_live_fragments_is_flagged() {
195 let diff = Differential {
197 live: stats(4, 800),
198 retrack: stats(1, 800),
199 };
200 assert!(diff.live_is_shortened(0.9));
201 }
202}