|
1 | | -use std::{str::FromStr, sync::Arc}; |
2 | | - |
3 | | -use database::{ |
4 | | - metric::Metric, |
5 | | - selector::{BenchmarkQuery, CompileBenchmarkQuery}, |
6 | | - ArtifactId, Connection, |
| 1 | +use database::{metric::Metric, Commit, Connection, Index}; |
| 2 | +use ratatui::widgets::{List, ListState}; |
| 3 | +use ratatui::{ |
| 4 | + crossterm::event::{self, Event, KeyCode}, |
| 5 | + prelude::*, |
| 6 | + widgets::Block, |
7 | 7 | }; |
8 | | -use tabled::{Table, Tabled}; |
9 | 8 |
|
10 | 9 | static ALL_METRICS: &[Metric] = &[ |
11 | 10 | Metric::InstructionsUser, |
@@ -46,160 +45,183 @@ pub async fn compare_artifacts( |
46 | 45 | ) -> anyhow::Result<()> { |
47 | 46 | let index = database::Index::load(&mut *conn).await; |
48 | 47 |
|
49 | | - let metric = match metric { |
50 | | - Some(v) => v, |
51 | | - None => { |
52 | | - let metric_str = inquire::Select::new( |
53 | | - "Choose metric:", |
54 | | - ALL_METRICS.iter().map(|m| m.as_str()).collect::<Vec<_>>(), |
55 | | - ) |
56 | | - .prompt()?; |
57 | | - Metric::from_str(metric_str).map_err(|e| anyhow::anyhow!(e))? |
58 | | - } |
59 | | - }; |
| 48 | + let metric = metric.unwrap_or(Metric::InstructionsUser); |
60 | 49 |
|
61 | | - let mut aids = index.artifacts().map(str::to_string).collect::<Vec<_>>(); |
| 50 | + let mut aids = index.commits(); |
62 | 51 | if aids.len() < 2 { |
63 | 52 | return Err(anyhow::anyhow!( |
64 | 53 | "There are not enough artifacts to compare, at least two are needed" |
65 | 54 | )); |
66 | 55 | } |
67 | 56 |
|
68 | | - let select_artifact_id = |name: &str, aids: &Vec<String>| { |
69 | | - anyhow::Ok( |
70 | | - inquire::Select::new( |
71 | | - &format!("Choose {} artifact to compare:", name), |
72 | | - aids.clone(), |
73 | | - ) |
74 | | - .prompt()?, |
75 | | - ) |
76 | | - }; |
| 57 | + // Error if the selected base/modified commits were not found |
| 58 | + fn check_commit( |
| 59 | + aids: &[Commit], |
| 60 | + commit: Option<String>, |
| 61 | + label: &str, |
| 62 | + ) -> anyhow::Result<Option<Commit>> { |
| 63 | + Ok(commit |
| 64 | + .map(|commit| { |
| 65 | + aids.iter() |
| 66 | + .find(|c| c.sha == commit) |
| 67 | + .cloned() |
| 68 | + .ok_or_else(|| anyhow::anyhow!("{label} commit {commit} not found")) |
| 69 | + }) |
| 70 | + .transpose()?) |
| 71 | + } |
77 | 72 |
|
78 | | - let base = match base { |
79 | | - Some(v) => v, |
80 | | - None => select_artifact_id("base", &aids)?.to_string(), |
81 | | - }; |
82 | | - aids.retain(|id| id != &base); |
83 | | - let modified = if let [new_modified] = &aids[..] { |
84 | | - let new_modified = new_modified.clone(); |
85 | | - println!( |
86 | | - "Only 1 artifact remains, automatically selecting: {}", |
87 | | - new_modified |
88 | | - ); |
89 | | - |
90 | | - new_modified |
91 | | - } else { |
92 | | - match modified { |
93 | | - Some(v) => v, |
94 | | - None => select_artifact_id("modified", &aids)?.to_string(), |
| 73 | + let base: Option<Commit> = check_commit(&aids, base, "Base")?; |
| 74 | + if let Some(ref base) = base { |
| 75 | + aids.retain(|c| c.sha != base.sha); |
| 76 | + } |
| 77 | + let modified: Option<Commit> = check_commit(&aids, modified, "Modified")?; |
| 78 | + |
| 79 | + let mut terminal = ratatui::init(); |
| 80 | + |
| 81 | + let mut screen: Box<dyn Screen> = match (base, modified) { |
| 82 | + (Some(base), Some(modified)) => Box::new(CompareScreen { base, modified }), |
| 83 | + (Some(base), None) => next_selection_screen(base, aids), |
| 84 | + (None, None) => Box::new(SelectArtifactScreen::new(aids, SelectState::SelectingBase)), |
| 85 | + (None, Some(_)) => { |
| 86 | + return Err(anyhow::anyhow!( |
| 87 | + "If modified commit is pre-selected, base commit must also be pre-selected" |
| 88 | + )); |
95 | 89 | } |
96 | 90 | }; |
97 | | - |
98 | | - let query = CompileBenchmarkQuery::default().metric(database::selector::Selector::One(metric)); |
99 | | - let resp = query |
100 | | - .execute( |
101 | | - &mut *conn, |
102 | | - &index, |
103 | | - Arc::new(vec![ArtifactId::Tag(base), ArtifactId::Tag(modified)]), |
104 | | - ) |
105 | | - .await |
106 | | - .unwrap(); |
107 | | - |
108 | | - let tuple_pstats = resp |
109 | | - .into_iter() |
110 | | - .map(|resp| { |
111 | | - let points = resp.series.points.collect::<Vec<_>>(); |
112 | | - (points[0], points[1]) |
113 | | - }) |
114 | | - .collect::<Vec<_>>(); |
115 | | - |
116 | | - #[derive(Tabled)] |
117 | | - struct Regression { |
118 | | - count: usize, |
119 | | - #[tabled(display_with = "display_range")] |
120 | | - range: (Option<f64>, Option<f64>), |
121 | | - #[tabled(display_with = "display_mean")] |
122 | | - mean: Option<f64>, |
123 | | - } |
124 | | - |
125 | | - fn format_value(value: Option<f64>) -> String { |
126 | | - match value { |
127 | | - Some(value) => format!("{:+.2}%", value), |
128 | | - None => "-".to_string(), |
| 91 | + let mut state = State { metric, index }; |
| 92 | + |
| 93 | + loop { |
| 94 | + terminal.draw(|frame| { |
| 95 | + screen.draw(frame, &state); |
| 96 | + })?; |
| 97 | + match event::read()? { |
| 98 | + Event::Key(key_event) => match key_event.code { |
| 99 | + KeyCode::Char('q') | KeyCode::Esc => break, |
| 100 | + key => { |
| 101 | + if let Some(action) = screen.handle_key(key, &mut state) { |
| 102 | + match action { |
| 103 | + Action::ChangeScreen(next_screen) => { |
| 104 | + screen = next_screen; |
| 105 | + } |
| 106 | + } |
| 107 | + } |
| 108 | + } |
| 109 | + }, |
| 110 | + _ => {} |
129 | 111 | } |
130 | 112 | } |
| 113 | + ratatui::restore(); |
131 | 114 |
|
132 | | - fn display_range(&(min, max): &(Option<f64>, Option<f64>)) -> String { |
133 | | - format!("[{}, {}]", &format_value(min), &format_value(max)) |
134 | | - } |
| 115 | + Ok(()) |
| 116 | +} |
135 | 117 |
|
136 | | - fn display_mean(value: &Option<f64>) -> String { |
137 | | - match value { |
138 | | - Some(value) => format!("{:+.2}%", value), |
139 | | - None => "-".to_string(), |
140 | | - } |
141 | | - } |
| 118 | +struct State { |
| 119 | + metric: Metric, |
| 120 | + index: Index, |
| 121 | +} |
142 | 122 |
|
143 | | - impl From<&Vec<f64>> for Regression { |
144 | | - fn from(value: &Vec<f64>) -> Self { |
145 | | - let min = value.iter().copied().min_by(|a, b| a.total_cmp(b)); |
146 | | - let max = value.iter().copied().max_by(|a, b| a.total_cmp(b)); |
147 | | - let count = value.len(); |
148 | | - |
149 | | - Regression { |
150 | | - range: (min, max), |
151 | | - count, |
152 | | - mean: if count == 0 { |
153 | | - None |
154 | | - } else { |
155 | | - Some(value.iter().sum::<f64>() / count as f64) |
156 | | - }, |
157 | | - } |
| 123 | +enum Action { |
| 124 | + ChangeScreen(Box<dyn Screen>), |
| 125 | +} |
| 126 | + |
| 127 | +trait Screen { |
| 128 | + fn draw(&mut self, frame: &mut Frame, state: &State); |
| 129 | + fn handle_key(&mut self, key: KeyCode, state: &mut State) -> Option<Action>; |
| 130 | +} |
| 131 | + |
| 132 | +enum SelectState { |
| 133 | + SelectingBase, |
| 134 | + SelectingModified { base: Commit }, |
| 135 | +} |
| 136 | + |
| 137 | +struct SelectArtifactScreen { |
| 138 | + aids: Vec<Commit>, |
| 139 | + select_state: SelectState, |
| 140 | + list_state: ListState, |
| 141 | +} |
| 142 | + |
| 143 | +impl SelectArtifactScreen { |
| 144 | + fn new(aids: Vec<Commit>, select_state: SelectState) -> Self { |
| 145 | + Self { |
| 146 | + aids, |
| 147 | + select_state, |
| 148 | + list_state: ListState::default().with_selected(Some(0)), |
158 | 149 | } |
159 | 150 | } |
| 151 | +} |
160 | 152 |
|
161 | | - let change = tuple_pstats |
162 | | - .iter() |
163 | | - .filter_map(|&(a, b)| match (a, b) { |
164 | | - (Some(a), Some(b)) => { |
165 | | - if a == 0.0 { |
166 | | - None |
| 153 | +impl Screen for SelectArtifactScreen { |
| 154 | + fn draw(&mut self, frame: &mut Frame, _state: &State) { |
| 155 | + let items = self |
| 156 | + .aids |
| 157 | + .iter() |
| 158 | + .map(|commit| format!("{} ({})", commit.sha, commit.date)) |
| 159 | + .collect::<Vec<_>>(); |
| 160 | + let list = List::new(items) |
| 161 | + .block(Block::bordered().title(format!( |
| 162 | + "Select {} artifact", |
| 163 | + if matches!(self.select_state, SelectState::SelectingBase) { |
| 164 | + "base" |
167 | 165 | } else { |
168 | | - Some((b - a) / a * 100.0) |
| 166 | + "modified" |
169 | 167 | } |
| 168 | + ))) |
| 169 | + .style(Style::new().white()) |
| 170 | + .highlight_style(Style::new().bold()) |
| 171 | + .highlight_symbol("> "); |
| 172 | + frame.render_stateful_widget(list, frame.area(), &mut self.list_state); |
| 173 | + } |
| 174 | + |
| 175 | + fn handle_key(&mut self, key: KeyCode, _state: &mut State) -> Option<Action> { |
| 176 | + match key { |
| 177 | + KeyCode::Down => self.list_state.select_next(), |
| 178 | + KeyCode::Up => self.list_state.select_previous(), |
| 179 | + KeyCode::Enter => { |
| 180 | + let mut aids = self.aids.clone(); |
| 181 | + let index = self.list_state.selected().unwrap(); |
| 182 | + let selected = aids.remove(index); |
| 183 | + |
| 184 | + let next_screen: Box<dyn Screen> = match &self.select_state { |
| 185 | + SelectState::SelectingBase => next_selection_screen(selected, aids), |
| 186 | + SelectState::SelectingModified { base } => Box::new(CompareScreen { |
| 187 | + base: base.clone(), |
| 188 | + modified: selected, |
| 189 | + }), |
| 190 | + }; |
| 191 | + return Some(Action::ChangeScreen(next_screen)); |
170 | 192 | } |
171 | | - (_, _) => None, |
172 | | - }) |
173 | | - .collect::<Vec<_>>(); |
174 | | - let negative_change = change |
175 | | - .iter() |
176 | | - .copied() |
177 | | - .filter(|&c| c < 0.0) |
178 | | - .collect::<Vec<_>>(); |
179 | | - let positive_change = change |
180 | | - .iter() |
181 | | - .copied() |
182 | | - .filter(|&c| c > 0.0) |
183 | | - .collect::<Vec<_>>(); |
184 | | - |
185 | | - #[derive(Tabled)] |
186 | | - struct NamedRegression { |
187 | | - name: String, |
188 | | - #[tabled(inline)] |
189 | | - regression: Regression, |
| 193 | + _ => {} |
| 194 | + }; |
| 195 | + None |
190 | 196 | } |
| 197 | +} |
191 | 198 |
|
192 | | - let regressions = [negative_change, positive_change, change] |
193 | | - .into_iter() |
194 | | - .map(|c| Regression::from(&c)) |
195 | | - .zip(["❌", "✅", "✅, ❌"]) |
196 | | - .map(|(c, label)| NamedRegression { |
197 | | - name: label.to_string(), |
198 | | - regression: c, |
199 | | - }) |
200 | | - .collect::<Vec<_>>(); |
| 199 | +/// Directly goes to comparison if there is only a single artifact ID left, otherwise |
| 200 | +/// opens the selection screen for the modified artifact. |
| 201 | +fn next_selection_screen(base: Commit, aids: Vec<Commit>) -> Box<dyn Screen> { |
| 202 | + match aids.as_slice() { |
| 203 | + [commit] => Box::new(CompareScreen { |
| 204 | + base, |
| 205 | + modified: commit.clone(), |
| 206 | + }), |
| 207 | + _ => Box::new(SelectArtifactScreen::new( |
| 208 | + aids, |
| 209 | + SelectState::SelectingModified { base }, |
| 210 | + )), |
| 211 | + } |
| 212 | +} |
201 | 213 |
|
202 | | - println!("{}", Table::new(regressions)); |
| 214 | +struct CompareScreen { |
| 215 | + base: Commit, |
| 216 | + modified: Commit, |
| 217 | +} |
203 | 218 |
|
204 | | - Ok(()) |
| 219 | +impl Screen for CompareScreen { |
| 220 | + fn draw(&mut self, frame: &mut Frame, state: &State) { |
| 221 | + todo!() |
| 222 | + } |
| 223 | + |
| 224 | + fn handle_key(&mut self, key: KeyCode, state: &mut State) -> Option<Action> { |
| 225 | + todo!() |
| 226 | + } |
205 | 227 | } |
0 commit comments