|
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