Skip to content

Commit ae17230

Browse files
hdshawkw
andauthored
feat(console): add large future lints (#587)
In Tokio, the futures for tasks are stored on the stack unless they are explicitly boxed. Having very large futures can be problematic as it can cause a stack overflow. This change makes use of new instrumentation in Tokio (tokio-rs/tokio#6881) which includes the size of the future which drives a task. The size isn't given its own column (we're running out of horizontal space) and appears in the additional fields column. In the case that a future was auto-boxed by Tokio, the original size of the task will also be provided. Two new lints have been added for large futures. The first will detect auto-boxed futures and warn about them. The second will detect futures which are large (over 1024 bytes by default), but have not been auto-boxed by the runtime. Since the new lints depend on the new instrumentation in Tokio, they will only trigger if the instrumented application is running using `tokio` 1.41.0 or later. The version is as yet, unreleased. Both lints have been added to the default list. Co-authored-by: Eliza Weisman <[email protected]>
1 parent d2a9441 commit ae17230

File tree

10 files changed

+244
-7
lines changed

10 files changed

+244
-7
lines changed

README.md

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,8 +224,17 @@ Options:
224224
225225
* `never-yielded` -- Warns when a task has never yielded.
226226
227-
[default: self-wakes lost-waker never-yielded]
228-
[possible values: self-wakes, lost-waker, never-yielded]
227+
* `auto-boxed-future` -- Warnings when the future driving a
228+
task was automatically boxed by the runtime because it was
229+
large.
230+
231+
* `large-future` -- Warnings when the future driving a task
232+
occupies a large amount of stack space.
233+
234+
[default: self-wakes lost-waker never-yielded
235+
auto-boxed-future large-future]
236+
[possible values: self-wakes, lost-waker, never-yielded,
237+
auto-boxed-future, large-future]
229238
230239
-A, --allow <ALLOW_WARNINGS>...
231240
Allow lint warnings.
@@ -243,9 +252,17 @@ Options:
243252
244253
* `never-yielded` -- Warns when a task has never yielded.
245254
255+
* `auto-boxed-future` -- Warnings when the future driving a
256+
task was automatically boxed by the runtime because it was
257+
large.
258+
259+
* `large-future` -- Warnings when the future driving a task
260+
occupies a large amount of stack space.
261+
246262
If this is set to `all`, all warnings are allowed.
247263
248-
[possible values: all, self-wakes, lost-waker, never-yielded]
264+
[possible values: all, self-wakes, lost-waker, never-yielded,
265+
large-future, auto-boxed-future]
249266
250267
--log-dir <LOG_DIRECTORY>
251268
Path to a directory to write the console's internal logs to.

console-subscriber/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ Other instrumentation is added in later Tokio releases:
130130

131131
* [Tokio v1.21.0] or later is required to use newest `task::Builder::spawn*` APIs.
132132

133+
* [Tokio v1.41.0] (as yet unreleased) or later is required for task future sizes and the related
134+
tokio-console lints `auto-boxed-future` and `large-future`.
135+
133136
[Tokio v1.0.0]: https://github.com/tokio-rs/tokio/releases/tag/tokio-1.0.0
134137
[Tokio v1.7.0]: https://github.com/tokio-rs/tokio/releases/tag/tokio-1.7.0
135138
[Tokio v1.12.0]:https://github.com/tokio-rs/tokio/releases/tag/tokio-1.12.0
@@ -147,6 +150,7 @@ Other instrumentation is added in later Tokio releases:
147150
[init]: https://docs.rs/console-subscriber/latest/console_subscriber/fn.init.html
148151
[compile_time_filters]: https://docs.rs/tracing/latest/tracing/level_filters/index.html#compile-time-filters
149152
[Tokio v1.21.0]: https://github.com/tokio-rs/tokio/releases/tag/tokio-1.21.0
153+
[Tokio v1.41.0]: https://github.com/tokio-rs/tokio/releases/tag/tokio-1.41.0
150154

151155
### Adding the Console Subscriber
152156

console-subscriber/examples/app.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ OPTIONS:
1212
burn Includes a (misbehaving) task that spins CPU with self-wakes
1313
coma Includes a (misbehaving) task that forgets to register a waker
1414
noyield Includes a (misbehaving) task that spawns tasks that never yield
15+
blocking Includes a blocking task that (not misbehaving)
16+
large Includes tasks that are driven by futures that are larger than recommended
1517
"#;
1618

1719
#[tokio::main]
@@ -51,6 +53,19 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
5153
.spawn(spawn_blocking(5))
5254
.unwrap();
5355
}
56+
"large" => {
57+
tokio::task::Builder::new()
58+
.name("pretty-big")
59+
// Below debug mode auto-boxing limit
60+
.spawn(large_future::<1024>())
61+
.unwrap();
62+
tokio::task::Builder::new()
63+
.name("huge")
64+
// Larger than the release mode auto-boxing limit
65+
.spawn(large_future::<20_000>())
66+
.unwrap();
67+
large_blocking::<20_000>();
68+
}
5469
"help" | "-h" => {
5570
eprintln!("{}", HELP);
5671
return Ok(());
@@ -152,6 +167,42 @@ async fn spawn_blocking(seconds: u64) {
152167
}
153168
}
154169

170+
#[tracing::instrument]
171+
async fn large_future<const N: usize>() {
172+
let mut numbers = [0_u8; N];
173+
174+
loop {
175+
for idx in 0..N {
176+
numbers[idx] = (idx % 256) as u8;
177+
tokio::time::sleep(Duration::from_millis(100)).await;
178+
(0..=idx).for_each(|jdx| {
179+
assert_eq!(numbers[jdx], (jdx % 256) as u8);
180+
});
181+
}
182+
}
183+
}
184+
185+
fn large_blocking<const N: usize>() {
186+
let numbers = [0_u8; N];
187+
188+
tokio::task::Builder::new()
189+
.name("huge-blocking")
190+
.spawn_blocking(move || {
191+
let mut numbers = numbers;
192+
193+
loop {
194+
for idx in 0..N {
195+
numbers[idx] = (idx % 256) as u8;
196+
std::thread::sleep(Duration::from_millis(100));
197+
(0..=idx).for_each(|jdx| {
198+
assert_eq!(numbers[jdx], (jdx % 256) as u8);
199+
});
200+
}
201+
}
202+
})
203+
.unwrap();
204+
}
205+
155206
fn self_wake() -> impl Future<Output = ()> {
156207
struct SelfWake {
157208
yielded: bool,

tokio-console/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,4 @@ hyper-util = { version = "0.1.6", features = ["tokio"] }
6262

6363
[dev-dependencies]
6464
trycmd = "0.15.4"
65+

tokio-console/console.example.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ warnings = [
44
'self-wakes',
55
'lost-waker',
66
'never-yielded',
7+
'auto-boxed-future',
8+
'large-future',
79
]
810
log_directory = '/tmp/tokio-console/logs'
911
retention = '6s'

tokio-console/src/config.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ pub struct Config {
6363
/// * `lost-waker` -- Warns when a task is dropped without being woken.
6464
///
6565
/// * `never-yielded` -- Warns when a task has never yielded.
66+
///
67+
/// * `auto-boxed-future` -- Warnings when the future driving a task was automatically boxed by
68+
/// the runtime because it was large.
69+
///
70+
/// * `large-future` -- Warnings when the future driving a task occupies a large amount of
71+
/// stack space.
6672
#[clap(long = "warn", short = 'W', value_delimiter = ',', num_args = 1..)]
6773
#[clap(default_values_t = KnownWarnings::default_enabled_warnings())]
6874
pub(crate) warnings: Vec<KnownWarnings>,
@@ -80,9 +86,15 @@ pub struct Config {
8086
///
8187
/// * `never-yielded` -- Warns when a task has never yielded.
8288
///
89+
/// * `auto-boxed-future` -- Warnings when the future driving a task was automatically boxed by
90+
/// the runtime because it was large.
91+
///
92+
/// * `large-future` -- Warnings when the future driving a task occupies a large amount of
93+
/// stack space.
94+
///
8395
/// If this is set to `all`, all warnings are allowed.
8496
///
85-
/// [possible values: all, self-wakes, lost-waker, never-yielded]
97+
/// [possible values: all, self-wakes, lost-waker, never-yielded, large-future, auto-boxed-future]
8698
#[clap(long = "allow", short = 'A', num_args = 1..)]
8799
pub(crate) allow_warnings: Option<AllowedWarnings>,
88100

@@ -143,6 +155,8 @@ pub(crate) enum KnownWarnings {
143155
SelfWakes,
144156
LostWaker,
145157
NeverYielded,
158+
AutoBoxedFuture,
159+
LargeFuture,
146160
}
147161

148162
impl FromStr for KnownWarnings {
@@ -153,6 +167,8 @@ impl FromStr for KnownWarnings {
153167
"self-wakes" => Ok(KnownWarnings::SelfWakes),
154168
"lost-waker" => Ok(KnownWarnings::LostWaker),
155169
"never-yielded" => Ok(KnownWarnings::NeverYielded),
170+
"auto-boxed-future" => Ok(KnownWarnings::AutoBoxedFuture),
171+
"large-future" => Ok(KnownWarnings::LargeFuture),
156172
_ => Err(format!("unknown warning: {}", s)),
157173
}
158174
}
@@ -164,6 +180,8 @@ impl From<&KnownWarnings> for warnings::Linter<Task> {
164180
KnownWarnings::SelfWakes => warnings::Linter::new(warnings::SelfWakePercent::default()),
165181
KnownWarnings::LostWaker => warnings::Linter::new(warnings::LostWaker),
166182
KnownWarnings::NeverYielded => warnings::Linter::new(warnings::NeverYielded::default()),
183+
KnownWarnings::AutoBoxedFuture => warnings::Linter::new(warnings::AutoBoxedFuture),
184+
KnownWarnings::LargeFuture => warnings::Linter::new(warnings::LargeFuture::default()),
167185
}
168186
}
169187
}
@@ -174,6 +192,8 @@ impl fmt::Display for KnownWarnings {
174192
KnownWarnings::SelfWakes => write!(f, "self-wakes"),
175193
KnownWarnings::LostWaker => write!(f, "lost-waker"),
176194
KnownWarnings::NeverYielded => write!(f, "never-yielded"),
195+
KnownWarnings::AutoBoxedFuture => write!(f, "auto-boxed-future"),
196+
KnownWarnings::LargeFuture => write!(f, "large-future"),
177197
}
178198
}
179199
}
@@ -184,6 +204,8 @@ impl KnownWarnings {
184204
KnownWarnings::SelfWakes,
185205
KnownWarnings::LostWaker,
186206
KnownWarnings::NeverYielded,
207+
KnownWarnings::AutoBoxedFuture,
208+
KnownWarnings::LargeFuture,
187209
]
188210
}
189211
}

tokio-console/src/state/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,8 @@ impl Field {
277277
const KIND: &'static str = "kind";
278278
const NAME: &'static str = "task.name";
279279
const TASK_ID: &'static str = "task.id";
280+
const SIZE_BYTES: &'static str = "size.bytes";
281+
const ORIGINAL_SIZE_BYTES: &'static str = "original_size.bytes";
280282

281283
/// Creates a new Field with a pre-interned `name` and a `FieldValue`.
282284
fn new(name: InternedStr, value: FieldValue) -> Self {

tokio-console/src/state/tasks.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ pub(crate) struct Task {
104104
location: String,
105105
/// The kind of task, currently one of task, blocking, block_on, local
106106
kind: InternedStr,
107+
/// The size of the future driving the task
108+
size_bytes: Option<usize>,
109+
/// The original size of the future (before runtime auto-boxing)
110+
original_size_bytes: Option<usize>,
107111
}
108112

109113
#[derive(Debug)]
@@ -184,6 +188,8 @@ impl TasksState {
184188
let mut name = None;
185189
let mut task_id = None;
186190
let mut kind = strings.string(String::new());
191+
let mut size_bytes = None;
192+
let mut original_size_bytes = None;
187193
let target_field = Field::new(
188194
strings.string_ref("target"),
189195
FieldValue::Str(meta.target.to_string()),
@@ -210,6 +216,24 @@ impl TasksState {
210216
kind = strings.string(field.value.to_string());
211217
None
212218
}
219+
Field::SIZE_BYTES => {
220+
size_bytes = match field.value {
221+
FieldValue::U64(size_bytes) => Some(size_bytes as usize),
222+
_ => None,
223+
};
224+
// Include size in pre-formatted fields
225+
Some(field)
226+
}
227+
Field::ORIGINAL_SIZE_BYTES => {
228+
original_size_bytes = match field.value {
229+
FieldValue::U64(original_size_bytes) => {
230+
Some(original_size_bytes as usize)
231+
}
232+
_ => None,
233+
};
234+
// Include size in pre-formatted fields
235+
Some(field)
236+
}
213237
_ => Some(field),
214238
}
215239
})
@@ -245,6 +269,8 @@ impl TasksState {
245269
warnings: Vec::new(),
246270
location,
247271
kind,
272+
size_bytes,
273+
original_size_bytes,
248274
};
249275
if let TaskLintResult::RequiresRecheck = task.lint(linters) {
250276
next_pending_lint.insert(task.id);
@@ -506,6 +532,14 @@ impl Task {
506532
pub(crate) fn location(&self) -> &str {
507533
&self.location
508534
}
535+
536+
pub(crate) fn size_bytes(&self) -> Option<usize> {
537+
self.size_bytes
538+
}
539+
540+
pub(crate) fn original_size_bytes(&self) -> Option<usize> {
541+
self.original_size_bytes
542+
}
509543
}
510544

511545
enum TaskLintResult {

tokio-console/src/warnings.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,3 +258,90 @@ impl Warn<Task> for NeverYielded {
258258
)
259259
}
260260
}
261+
262+
/// Warning for if a task's driving future was auto-boxed by the runtime
263+
#[derive(Clone, Debug, Default)]
264+
pub(crate) struct AutoBoxedFuture;
265+
266+
impl Warn<Task> for AutoBoxedFuture {
267+
fn summary(&self) -> &str {
268+
"tasks have been boxed by the runtime due to their size"
269+
}
270+
271+
fn check(&self, task: &Task) -> Warning {
272+
let (Some(size_bytes), Some(original_size_bytes)) =
273+
(task.size_bytes(), task.original_size_bytes())
274+
else {
275+
return Warning::Ok;
276+
};
277+
278+
if original_size_bytes != size_bytes {
279+
Warning::Warn
280+
} else {
281+
Warning::Ok
282+
}
283+
}
284+
285+
fn format(&self, task: &Task) -> String {
286+
let original_size = task
287+
.original_size_bytes()
288+
.expect("warning should not trigger if original size is None");
289+
let boxed_size = task
290+
.size_bytes()
291+
.expect("warning should not trigger if size is None");
292+
format!(
293+
"This task's future was auto-boxed by the runtime when spawning, due to its size (originally \
294+
{original_size} bytes, boxed size {boxed_size} bytes)",
295+
)
296+
}
297+
}
298+
299+
/// Warning for if a task's driving future if large
300+
#[derive(Clone, Debug)]
301+
pub(crate) struct LargeFuture {
302+
min_size: usize,
303+
description: String,
304+
}
305+
impl LargeFuture {
306+
pub(crate) const DEFAULT_MIN_SIZE_BYTES: usize = 1024;
307+
pub(crate) fn new(min_size: usize) -> Self {
308+
Self {
309+
min_size,
310+
description: format!("tasks are {} bytes or larger", min_size),
311+
}
312+
}
313+
}
314+
315+
impl Default for LargeFuture {
316+
fn default() -> Self {
317+
Self::new(Self::DEFAULT_MIN_SIZE_BYTES)
318+
}
319+
}
320+
321+
impl Warn<Task> for LargeFuture {
322+
fn summary(&self) -> &str {
323+
self.description.as_str()
324+
}
325+
326+
fn check(&self, task: &Task) -> Warning {
327+
// Don't fire warning for tasks that are not async
328+
if task.is_blocking() {
329+
return Warning::Ok;
330+
}
331+
332+
if let Some(size_bytes) = task.size_bytes() {
333+
if size_bytes >= self.min_size {
334+
return Warning::Warn;
335+
}
336+
}
337+
Warning::Ok
338+
}
339+
340+
fn format(&self, task: &Task) -> String {
341+
format!(
342+
"This task occupies a large amount of stack space ({} bytes)",
343+
task.size_bytes()
344+
.expect("warning should not trigger if size is None"),
345+
)
346+
}
347+
}

0 commit comments

Comments
 (0)