Skip to content

Commit d6d5074

Browse files
authored
feat(console): display warnings in task details *and* list (console-rs#123)
The current method of displaying warnings is as list of all individual tasks with warnings on the task list screen. As discussed in the comments on PR console-rs#113, this may not be the ideal approach when there are a very large number of tasks with warnings. This branch changes this behavior so that the details of a particular warning for a given task is shown only on the task details screen. On the task list screen, we instead list the different _categories_ of warnings that were detected, along with the number of tasks with that warning. This means that when a large number of tasks have warnings, we will only use a number of lines equal to the number of different categories of warning that were detected, rather than the number of individual tasks that have that warning. Each individual task that has warnings shows a warning icon and count in a new column in the task list table. This makes it easy for the user to find the tasks that have warnings and get details on them, including sorting by the number of warnings. Implementing this required some refactoring of how warnings are implemented. This includes: * Changing the `Warn` trait to have separate methods for detecting a warning, formatting the warning for an individual task instance, and summarizing the warning for the list of detected warning types * Introducing a new `Linter` struct that wraps a `dyn Warning` in an `Rc` and clones it into tasks that have lints. This allows the task details screen to determine how to format the lint when it is needed. It also allows tracking the total number of tasks that have a given warning, by using the `Rc`'s reference count. ## Screenshots To demonstrate how this design saves screen real estate when there are many tasks with warnings, I modified the `burn` example to spawn several burn tasks rather than just one. Before, we spent several lines on warnings (one for each task): ![warn_old](https://user-images.githubusercontent.com/2796466/132567589-862d1f82-1b8a-481e-9ce0-fc0798319c8a.png) After, we only need one line: ![warnings1](https://user-images.githubusercontent.com/2796466/132567656-2c1473fb-22a2-45bb-99b1-c23cce4d86dd.png) The detailed warning text for the individual tasks are displayed on the task details view: ![warnings1_details](https://user-images.githubusercontent.com/2796466/132567713-8e1162f4-f989-488b-b347-f8837ba67d65.png) And, it still looks okay in ASCII-only mode: ![warnings1_ascii-only](https://user-images.githubusercontent.com/2796466/132567772-9d6ed35d-254d-4b9e-bf6e-eef1819c211c.png) ![warnings1_details_ascii-only](https://user-images.githubusercontent.com/2796466/132567783-a88e4730-0a0d-4240-a285-a99bc2ff1047.png) Signed-off-by: Eliza Weisman <[email protected]>
1 parent de34605 commit d6d5074

File tree

7 files changed

+285
-90
lines changed

7 files changed

+285
-90
lines changed

console/src/main.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,12 @@ async fn main() -> color_eyre::Result<()> {
4444
// A channel to send the task details update stream (no need to keep outdated details in the memory)
4545
let (details_tx, mut details_rx) = mpsc::channel::<TaskDetails>(2);
4646

47-
let mut tasks = State::default();
47+
let mut tasks = State::default()
48+
// TODO(eliza): allow configuring the list of linters via the
49+
// CLI/possibly a config file?
50+
.with_linters(vec![warnings::Linter::new(
51+
warnings::SelfWakePercent::default(),
52+
)]);
4853
let mut input = input::EventStream::new();
4954
let mut view = view::View::new(styles);
5055

console/src/tasks.rs

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{util::Percentage, view};
1+
use crate::{util::Percentage, view, warnings::Linter};
22
use console_api as proto;
33
use hdrhistogram::Histogram;
44
use std::{
@@ -20,6 +20,7 @@ use tui::{
2020
pub(crate) struct State {
2121
tasks: HashMap<u64, Rc<RefCell<Task>>>,
2222
metas: HashMap<u64, Metadata>,
23+
linters: Vec<Linter<Task>>,
2324
last_updated_at: Option<SystemTime>,
2425
new_tasks: Vec<TaskRef>,
2526
current_task_details: DetailsRef,
@@ -35,13 +36,14 @@ enum Temporality {
3536
#[derive(Debug, Copy, Clone)]
3637
#[repr(usize)]
3738
pub(crate) enum SortBy {
38-
Tid = 0,
39-
State = 1,
40-
Name = 2,
41-
Total = 3,
42-
Busy = 4,
43-
Idle = 5,
44-
Polls = 6,
39+
Warns = 0,
40+
Tid = 1,
41+
State = 2,
42+
Name = 3,
43+
Total = 4,
44+
Busy = 5,
45+
Idle = 6,
46+
Polls = 7,
4547
}
4648

4749
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
@@ -63,6 +65,8 @@ pub(crate) struct Task {
6365
completed_for: usize,
6466
target: Arc<str>,
6567
name: Option<Arc<str>>,
68+
/// Currently active warnings for this task.
69+
warnings: Vec<Linter<Task>>,
6670
}
6771

6872
#[derive(Debug, Default)]
@@ -123,6 +127,11 @@ pub(crate) enum FieldValue {
123127
impl State {
124128
const RETAIN_COMPLETED_FOR: usize = 6;
125129

130+
pub(crate) fn with_linters(mut self, linters: impl IntoIterator<Item = Linter<Task>>) -> Self {
131+
self.linters.extend(linters.into_iter());
132+
self
133+
}
134+
126135
pub(crate) fn last_updated_at(&self) -> Option<SystemTime> {
127136
self.last_updated_at
128137
}
@@ -202,19 +211,29 @@ impl State {
202211
stats,
203212
completed_for: 0,
204213
target: meta.target.clone(),
214+
warnings: Vec::new(),
205215
};
206216
task.update();
207217
let task = Rc::new(RefCell::new(task));
208218
new_list.push(Rc::downgrade(&task));
209219
Some((id, task))
210220
});
211221
self.tasks.extend(new_tasks);
212-
222+
let linters = &self.linters;
213223
for (id, stats) in stats_update {
214224
if let Some(task) = self.tasks.get_mut(&id) {
215-
let mut t = task.borrow_mut();
216-
t.stats = stats.into();
217-
t.update();
225+
let mut task = task.borrow_mut();
226+
tracing::trace!(?task, "processing stats update for");
227+
task.warnings.clear();
228+
for lint in linters {
229+
tracing::debug!(?lint, ?task, "checking...");
230+
if let Some(lint) = lint.check(&*task) {
231+
tracing::info!(?lint, ?task, "found a warning!");
232+
task.warnings.push(lint)
233+
}
234+
}
235+
task.stats = stats.into();
236+
task.update();
218237
}
219238
}
220239
}
@@ -272,6 +291,10 @@ impl State {
272291
pub(crate) fn is_paused(&self) -> bool {
273292
matches!(self.temporality, Temporality::Paused)
274293
}
294+
295+
pub(crate) fn warnings(&self) -> impl Iterator<Item = &Linter<Task>> {
296+
self.linters.iter().filter(|linter| linter.count() > 0)
297+
}
275298
}
276299

277300
impl Task {
@@ -384,6 +407,10 @@ impl Task {
384407
self.self_wakes().percent_of(self.wakes())
385408
}
386409

410+
pub(crate) fn warnings(&self) -> &[Linter<Task>] {
411+
&self.warnings[..]
412+
}
413+
387414
fn update(&mut self) {
388415
let completed = self.stats.total.is_some() && self.completed_for == 0;
389416
if completed {
@@ -481,6 +508,8 @@ impl SortBy {
481508
Self::State => {
482509
tasks.sort_unstable_by_key(|task| task.upgrade().map(|t| t.borrow().state()))
483510
}
511+
Self::Warns => tasks
512+
.sort_unstable_by_key(|task| task.upgrade().map(|t| t.borrow().warnings().len())),
484513
Self::Total => {
485514
tasks.sort_unstable_by_key(|task| task.upgrade().map(|t| t.borrow().total(now)))
486515
}
@@ -503,6 +532,7 @@ impl TryFrom<usize> for SortBy {
503532
match idx {
504533
idx if idx == Self::Tid as usize => Ok(Self::Tid),
505534
idx if idx == Self::State as usize => Ok(Self::State),
535+
idx if idx == Self::Warns as usize => Ok(Self::Warns),
506536
idx if idx == Self::Name as usize => Ok(Self::Name),
507537
idx if idx == Self::Total as usize => Ok(Self::Total),
508538
idx if idx == Self::Busy as usize => Ok(Self::Busy),

console/src/view/mod.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,12 +137,14 @@ impl Width {
137137
}
138138

139139
pub(crate) fn update_str<S: AsRef<str>>(&mut self, s: S) -> S {
140-
let len = s.as_ref().len();
140+
self.update_len(s.as_ref().len());
141+
s
142+
}
143+
pub(crate) fn update_len(&mut self, len: usize) {
141144
let max = cmp::max(self.curr as usize, len);
142145
// Cap since a string could be stupid-long and not fit in a u16.
143146
// 100 is arbitrarily chosen, to keep the UI sane.
144147
self.curr = cmp::min(max, 100) as u16;
145-
s
146148
}
147149

148150
pub(crate) fn constraint(&self) -> layout::Constraint {

console/src/view/styles.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,20 @@ impl Styles {
113113
}
114114
}
115115

116+
pub fn warning_wide(&self) -> Span<'static> {
117+
Span::styled(
118+
self.if_utf8("\u{26A0} ", "/!\\ "),
119+
self.fg(Color::LightYellow).add_modifier(Modifier::BOLD),
120+
)
121+
}
122+
123+
pub fn warning_narrow(&self) -> Span<'static> {
124+
Span::styled(
125+
self.if_utf8("\u{26A0} ", "! "),
126+
self.fg(Color::LightYellow).add_modifier(Modifier::BOLD),
127+
)
128+
}
129+
116130
pub fn color(&self, color: Color) -> Option<Color> {
117131
use Palette::*;
118132
match (self.palette, color) {

console/src/view/task.rs

Lines changed: 65 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use std::{
1414
use tui::{
1515
layout::{self, Layout},
1616
text::{Span, Spans, Text},
17-
widgets::{Block, Paragraph},
17+
widgets::{Block, List, ListItem, Paragraph},
1818
};
1919

2020
pub(crate) struct TaskView {
@@ -50,20 +50,60 @@ impl TaskView {
5050
.as_ref()
5151
.filter(|details| details.task_id() == task.id());
5252

53-
let chunks = Layout::default()
54-
.direction(layout::Direction::Vertical)
55-
.constraints(
56-
[
57-
layout::Constraint::Length(1),
58-
layout::Constraint::Length(8),
59-
layout::Constraint::Length(9),
60-
layout::Constraint::Percentage(60),
61-
]
62-
.as_ref(),
63-
)
64-
.split(area);
53+
let warnings: Vec<_> = task
54+
.warnings()
55+
.iter()
56+
.map(|linter| {
57+
ListItem::new(Text::from(Spans::from(vec![
58+
styles.warning_wide(),
59+
// TODO(eliza): it would be nice to handle singular vs plural...
60+
Span::from(linter.format(task)),
61+
])))
62+
})
63+
.collect();
64+
65+
let (controls_area, stats_area, poll_dur_area, fields_area, warnings_area) =
66+
if warnings.is_empty() {
67+
let chunks = Layout::default()
68+
.direction(layout::Direction::Vertical)
69+
.constraints(
70+
[
71+
// controls
72+
layout::Constraint::Length(1),
73+
// task stats
74+
layout::Constraint::Length(8),
75+
// poll duration
76+
layout::Constraint::Length(9),
77+
// fields
78+
layout::Constraint::Percentage(60),
79+
]
80+
.as_ref(),
81+
)
82+
.split(area);
83+
(chunks[0], chunks[1], chunks[2], chunks[3], None)
84+
} else {
85+
let chunks = Layout::default()
86+
.direction(layout::Direction::Vertical)
87+
.constraints(
88+
[
89+
// controls
90+
layout::Constraint::Length(1),
91+
// warnings (add 2 for top and bottom borders)
92+
layout::Constraint::Length(warnings.len() as u16 + 2),
93+
// task stats
94+
layout::Constraint::Length(8),
95+
// poll duration
96+
layout::Constraint::Length(9),
97+
// fields
98+
layout::Constraint::Percentage(60),
99+
]
100+
.as_ref(),
101+
)
102+
.split(area);
103+
104+
(chunks[0], chunks[2], chunks[3], chunks[4], Some(chunks[1]))
105+
};
65106

66-
let controls_area = chunks[0];
67107
let stats_area = Layout::default()
68108
.direction(layout::Direction::Horizontal)
69109
.constraints(
@@ -73,11 +113,11 @@ impl TaskView {
73113
]
74114
.as_ref(),
75115
)
76-
.split(chunks[1]);
116+
.split(stats_area);
77117

78118
// Only split the histogram area in half if we're also drawing a
79119
// sparkline (which requires UTF-8 characters).
80-
let histogram_area = if styles.utf8 {
120+
let poll_dur_area = if styles.utf8 {
81121
Layout::default()
82122
.direction(layout::Direction::Horizontal)
83123
.constraints(
@@ -88,14 +128,12 @@ impl TaskView {
88128
]
89129
.as_ref(),
90130
)
91-
.split(chunks[2])
131+
.split(poll_dur_area)
92132
} else {
93-
vec![chunks[2]]
133+
vec![poll_dur_area]
94134
};
95135

96-
let percentiles_area = histogram_area[0];
97-
98-
let fields_area = chunks[3];
136+
let percentiles_area = poll_dur_area[0];
99137

100138
let controls = Spans::from(vec![
101139
Span::raw("controls: "),
@@ -173,7 +211,7 @@ impl TaskView {
173211

174212
// If UTF-8 is disabled we can't draw the histogram sparklne.
175213
if styles.utf8 {
176-
let sparkline_area = histogram_area[1];
214+
let sparkline_area = poll_dur_area[1];
177215

178216
// Bit of a deadlock: We cannot know the highest bucket value without determining the number of buckets,
179217
// and we cannot determine the number of buckets without knowing the width of the chart area which depends on
@@ -195,6 +233,11 @@ impl TaskView {
195233
frame.render_widget(histogram_sparkline, sparkline_area);
196234
}
197235

236+
if let Some(warnings_area) = warnings_area {
237+
let warnings = List::new(warnings).block(styles.border_block().title("Warnings"));
238+
frame.render_widget(warnings, warnings_area);
239+
}
240+
198241
let task_widget = Paragraph::new(metrics).block(styles.border_block().title("Task"));
199242
let wakers_widget = Paragraph::new(waker_stats).block(styles.border_block().title("Waker"));
200243
let fields_widget = Paragraph::new(fields).block(styles.border_block().title("Fields"));

0 commit comments

Comments
 (0)