Skip to content

Commit de34605

Browse files
authored
feat(console): add self-wake percentage and warning (console-rs#113)
Depends on console-rs#112 This branch builds on console-rs#112 by calculating the *percentage* of a task's wakeups that were self-wakes, and displaying them in the tasks view. It also adds the beginnings of a rudimentary "Warnings" system, and displays warnings for any tasks who have woken themselves for more than 50% of their wakeups. There is a new `Warn` trait for generating warnings that can be used to add new warnings in the future. The warnings functionality can almost certainly be expanded --- for example, it would be nice to be able to quickly jump to the details view for a task that has warnings. In the future, I could imagine generating web documentation with details about a particular warning and how to fix it, like [clippy has][1], and linking to them from the console. But, this is a start, at least! [1]: https://rust-lang.github.io/rust-clippy/v0.0.174/ ![image](https://user-images.githubusercontent.com/2796466/132146062-9599ba12-cb17-48f8-b15c-4ba91947e282.png) ![image](https://user-images.githubusercontent.com/2796466/132146081-6dac231c-e929-4f93-b6ef-ac16f1dd166d.png) Signed-off-by: Eliza Weisman <[email protected]>
1 parent 05b9f5b commit de34605

File tree

6 files changed

+171
-36
lines changed

6 files changed

+171
-36
lines changed

console/src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ mod conn;
2020
mod input;
2121
mod tasks;
2222
mod term;
23+
mod util;
2324
mod view;
25+
mod warnings;
2426

2527
#[tokio::main]
2628
async fn main() -> color_eyre::Result<()> {

console/src/tasks.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::view;
1+
use crate::{util::Percentage, view};
22
use console_api as proto;
33
use hdrhistogram::Histogram;
44
use std::{
@@ -379,6 +379,11 @@ impl Task {
379379
self.stats.self_wakes
380380
}
381381

382+
/// Returns the percentage of this task's total wakeups that were self-wakes.
383+
pub(crate) fn self_wake_percent(&self) -> u64 {
384+
self.self_wakes().percent_of(self.wakes())
385+
}
386+
382387
fn update(&mut self) {
383388
let completed = self.stats.total.is_some() && self.completed_for == 0;
384389
if completed {

console/src/util.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
pub(crate) trait Percentage {
2+
// Using an extension trait for this is maybe a bit excessive, but making it
3+
// a method has the nice advantage of making it *really* obvious which is
4+
// the total and which is the amount.
5+
fn percent_of(self, total: Self) -> Self;
6+
}
7+
8+
impl Percentage for usize {
9+
fn percent_of(self, total: Self) -> Self {
10+
percentage(total as f64, self as f64) as Self
11+
}
12+
}
13+
14+
impl Percentage for u64 {
15+
fn percent_of(self, total: Self) -> Self {
16+
percentage(total as f64, self as f64) as Self
17+
}
18+
}
19+
20+
pub(crate) fn percentage(total: f64, amount: f64) -> f64 {
21+
debug_assert!(
22+
total >= amount,
23+
"assertion failed: total >= amount; total={}, amount={}",
24+
total,
25+
amount
26+
);
27+
(amount / total) * 100.0
28+
}

console/src/view/task.rs

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -133,34 +133,40 @@ impl TaskView {
133133
dur(styles, task.idle(now)),
134134
]));
135135

136-
let wakers = Spans::from(vec![
136+
let mut waker_stats = vec![Spans::from(vec![
137137
bold("Current wakers: "),
138138
Span::from(format!("{} (", task.waker_count())),
139139
bold("clones: "),
140140
Span::from(format!("{}, ", task.waker_clones())),
141141
bold("drops: "),
142142
Span::from(format!("{})", task.waker_drops())),
143-
]);
143+
])];
144144

145145
let mut wakeups = vec![
146146
bold("Woken: "),
147147
Span::from(format!("{} times", task.wakes())),
148148
];
149149

150-
if task.self_wakes() > 0 {
151-
wakeups.push(Span::raw(", "));
152-
wakeups.push(bold("Self Wakes: "));
153-
wakeups.push(Span::from(format!("{} times", task.self_wakes())));
154-
}
155-
156150
// If the task has been woken, add the time since wake to its stats as well.
157151
if let Some(since) = task.since_wake(now) {
152+
wakeups.reserve(3);
158153
wakeups.push(Span::raw(", "));
159154
wakeups.push(bold("last woken:"));
160155
wakeups.push(Span::from(format!(" {:?} ago", since)));
161156
}
162157

163-
let wakeups = Spans::from(wakeups);
158+
waker_stats.push(Spans::from(wakeups));
159+
160+
if task.self_wakes() > 0 {
161+
waker_stats.push(Spans::from(vec![
162+
bold("Self Wakes: "),
163+
Span::from(format!(
164+
"{} times ({}%)",
165+
task.self_wakes(),
166+
task.self_wake_percent()
167+
)),
168+
]));
169+
}
164170

165171
let mut fields = Text::default();
166172
fields.extend(task.formatted_fields().iter().cloned().map(Spans::from));
@@ -190,8 +196,7 @@ impl TaskView {
190196
}
191197

192198
let task_widget = Paragraph::new(metrics).block(styles.border_block().title("Task"));
193-
let wakers_widget =
194-
Paragraph::new(vec![wakers, wakeups]).block(styles.border_block().title("Waker"));
199+
let wakers_widget = Paragraph::new(waker_stats).block(styles.border_block().title("Waker"));
195200
let fields_widget = Paragraph::new(fields).block(styles.border_block().title("Fields"));
196201
let percentiles_widget = Paragraph::new(
197202
details

console/src/view/tasks.rs

Lines changed: 82 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use crate::{
22
input,
3-
tasks::{self, TaskRef, TaskState},
3+
tasks::{self, Task, TaskRef, TaskState},
44
view::{self, bold},
5+
warnings,
56
};
67
use std::convert::TryFrom;
78
use tui::{
@@ -10,8 +11,12 @@ use tui::{
1011
text::{self, Span, Spans},
1112
widgets::{Cell, Paragraph, Row, Table, TableState},
1213
};
13-
#[derive(Clone, Debug, Default)]
14+
15+
#[derive(Debug)]
1416
pub(crate) struct List {
17+
/// A list of linters (implementing the [`warnings::Warn`] trait) used to generate
18+
/// warnings.
19+
linters: Vec<Box<dyn warnings::Warn<Task>>>,
1520
sorted_tasks: Vec<TaskRef>,
1621
sort_by: tasks::SortBy,
1722
table_state: TableState,
@@ -76,27 +81,6 @@ impl List {
7681
area: layout::Rect,
7782
state: &mut tasks::State,
7883
) {
79-
let chunks = layout::Layout::default()
80-
.direction(layout::Direction::Vertical)
81-
.margin(0)
82-
.constraints(
83-
[
84-
layout::Constraint::Length(1),
85-
layout::Constraint::Min(area.height - 1),
86-
]
87-
.as_ref(),
88-
)
89-
.split(area);
90-
let controls_area = chunks[0];
91-
let tasks_area = chunks[1];
92-
93-
let now = if let Some(now) = state.last_updated_at() {
94-
now
95-
} else {
96-
// If we have never gotten an update yet, skip...
97-
return;
98-
};
99-
10084
const STATE_LEN: u16 = List::HEADER[1].len() as u16;
10185
const DUR_LEN: usize = 10;
10286
// This data is only updated every second, so it doesn't make a ton of
@@ -105,6 +89,13 @@ impl List {
10589
const DUR_PRECISION: usize = 4;
10690
const POLLS_LEN: usize = 5;
10791

92+
let now = if let Some(now) = state.last_updated_at() {
93+
now
94+
} else {
95+
// If we have never gotten an update yet, skip...
96+
return;
97+
};
98+
10899
self.sorted_tasks.extend(state.take_new_tasks());
109100
self.sort_by.sort(now, &mut self.sorted_tasks);
110101

@@ -123,17 +114,36 @@ impl List {
123114
let mut target_width = view::Width::new(Self::HEADER[7].len() as u16);
124115
let mut num_idle = 0;
125116
let mut num_running = 0;
117+
let mut warnings = Vec::new();
126118
let rows = {
127119
let id_width = &mut id_width;
128120
let target_width = &mut target_width;
129121
let name_width = &mut name_width;
130122
let num_running = &mut num_running;
131123
let num_idle = &mut num_idle;
124+
let warnings = &mut warnings;
125+
126+
let linters = &self.linters;
132127
self.sorted_tasks.iter().filter_map(move |task| {
133128
let task = task.upgrade()?;
134129
let task = task.borrow();
135130
let state = task.state();
136-
131+
warnings.extend(linters.iter().filter_map(|warning| {
132+
let warning = warning.check(&*task)?;
133+
let task = if let Some(name) = task.name() {
134+
Span::from(format!("Task '{}' (ID {}) ", name, task.id()))
135+
} else {
136+
Span::from(format!("Task ID {} ", task.id()))
137+
};
138+
Some(Spans::from(vec![
139+
Span::styled(
140+
styles.if_utf8("\u{26A0} ", "/!\\ "),
141+
styles.fg(Color::LightYellow),
142+
),
143+
task,
144+
Span::from(warning),
145+
]))
146+
}));
137147
// Count task states
138148
match state {
139149
TaskState::Running => *num_running += 1,
@@ -215,6 +225,33 @@ impl List {
215225
+ POLLS_LEN as u16
216226
+ target_width.chars();
217227
*/
228+
let layout = layout::Layout::default()
229+
.direction(layout::Direction::Vertical)
230+
.margin(0);
231+
let (controls_area, tasks_area, warnings_area) = if warnings.is_empty() {
232+
let chunks = layout
233+
.constraints(
234+
[
235+
layout::Constraint::Length(1),
236+
layout::Constraint::Min(area.height - 1),
237+
]
238+
.as_ref(),
239+
)
240+
.split(area);
241+
(chunks[0], chunks[1], None)
242+
} else {
243+
let chunks = layout
244+
.constraints(
245+
[
246+
layout::Constraint::Length(1),
247+
layout::Constraint::Length(warnings.len() as u16 + 2),
248+
layout::Constraint::Min(area.height - 1),
249+
]
250+
.as_ref(),
251+
)
252+
.split(area);
253+
(chunks[0], chunks[2], Some(chunks[1]))
254+
};
218255

219256
// Fill all remaining characters in the frame with the task's fields.
220257
//
@@ -260,6 +297,14 @@ impl List {
260297

261298
frame.render_widget(Paragraph::new(controls), controls_area);
262299

300+
if let Some(area) = warnings_area {
301+
let block = styles.border_block().title(Spans::from(vec![
302+
bold("Warnings"),
303+
Span::from(format!(" ({})", warnings.len())),
304+
]));
305+
frame.render_widget(Paragraph::new(warnings).block(block), area);
306+
}
307+
263308
self.sorted_tasks.retain(|t| t.upgrade().is_some());
264309
}
265310

@@ -317,3 +362,16 @@ impl List {
317362
.unwrap_or_default()
318363
}
319364
}
365+
366+
impl Default for List {
367+
fn default() -> Self {
368+
Self {
369+
linters: vec![Box::new(warnings::SelfWakePercent::default())],
370+
sorted_tasks: Vec::new(),
371+
sort_by: tasks::SortBy::default(),
372+
table_state: TableState::default(),
373+
selected_column: 0,
374+
sort_descending: false,
375+
}
376+
}
377+
}

console/src/warnings.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
use crate::tasks::Task;
2+
3+
pub trait Warn<T>: std::fmt::Debug {
4+
fn check(&self, val: &T) -> Option<String>;
5+
}
6+
7+
#[derive(Clone, Debug)]
8+
pub(crate) struct SelfWakePercent {
9+
min_percent: u64,
10+
}
11+
12+
impl SelfWakePercent {
13+
pub(crate) const DEFAULT_PERCENT: u64 = 50;
14+
pub(crate) fn new(min_percent: u64) -> Self {
15+
Self { min_percent }
16+
}
17+
}
18+
19+
impl Default for SelfWakePercent {
20+
fn default() -> Self {
21+
Self::new(Self::DEFAULT_PERCENT)
22+
}
23+
}
24+
25+
impl Warn<Task> for SelfWakePercent {
26+
fn check(&self, task: &Task) -> Option<String> {
27+
let self_wakes = task.self_wake_percent();
28+
if self_wakes > self.min_percent {
29+
return Some(format!(
30+
"has woken itself for more than {}% of its total wakeups ({}%)",
31+
self.min_percent, self_wakes
32+
));
33+
}
34+
35+
None
36+
}
37+
}

0 commit comments

Comments
 (0)