Skip to content

Commit 88ec58c

Browse files
authored
[nextest-runner] use indicatif's println to print statuses (#2740)
The `println` function causes notably less flicker than `suspend` -- we arrange things such that message updates and newlines are part of the same terminal flush. (Spent some time in gdb walking over this code.) Also bump up the default tick rate to 20/sec, which feels much nicer than 10/sec. Make this configurable via a semi-hidden environment variable.
1 parent 51fc6c7 commit 88ec58c

File tree

6 files changed

+287
-70
lines changed

6 files changed

+287
-70
lines changed

nextest-runner/src/reporter/displayer/imp.rs

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,6 @@ use std::{
4646
time::Duration,
4747
};
4848

49-
/// The duration after which ticks are generated.
50-
pub const TICK_INTERVAL: Duration = Duration::from_millis(100);
51-
52-
/// The refresh rate for the progress bar, set to match the tick interval in hz.
53-
pub const PROGRESS_REFRESH_RATE_HZ: u8 = 10;
54-
5549
pub(crate) struct DisplayReporterBuilder {
5650
pub(crate) default_filter: CompiledDefaultFilter,
5751
pub(crate) status_levels: StatusLevels,
@@ -224,6 +218,10 @@ impl<'a> DisplayReporter<'a> {
224218
progress_bar,
225219
term_progress,
226220
} => {
221+
if let Some(term_progress) = term_progress {
222+
term_progress.update_progress(event);
223+
}
224+
227225
if let Some(state) = progress_bar {
228226
// Write to a string that will be printed as a log line.
229227
let mut buf = String::new();
@@ -232,11 +230,6 @@ impl<'a> DisplayReporter<'a> {
232230
.map_err(WriteEventError::Io)?;
233231

234232
state.update_progress_bar(event, &self.inner.styles);
235-
if let Some(term_progress) = term_progress {
236-
term_progress
237-
.update_progress(event, &mut buf)
238-
.map_err(WriteEventError::Io)?;
239-
}
240233
state.write_buf(&buf);
241234
Ok(())
242235
} else {
@@ -245,11 +238,6 @@ impl<'a> DisplayReporter<'a> {
245238
self.inner
246239
.write_event_impl(event, &mut writer)
247240
.map_err(WriteEventError::Io)?;
248-
if let Some(state) = term_progress {
249-
state
250-
.update_progress(event, &mut writer)
251-
.map_err(WriteEventError::Io)?;
252-
}
253241
writer.flush().map_err(WriteEventError::Io)
254242
}
255243
}
@@ -280,24 +268,41 @@ impl ReporterStderrImpl<'_> {
280268
fn tick(&mut self, styles: &Styles) {
281269
match self {
282270
ReporterStderrImpl::Terminal {
283-
progress_bar: Some(state),
284-
..
271+
progress_bar,
272+
term_progress,
285273
} => {
286-
state.tick(styles);
274+
if let Some(state) = progress_bar {
275+
state.tick(styles);
276+
}
277+
if let Some(term_progress) = term_progress {
278+
// In this case, write the last value directly to stderr.
279+
// This is a very small amount of data so buffering is not
280+
// required. It also doesn't have newlines or any visible
281+
// text, so it can be directly written out to stderr without
282+
// going through the progress bar (which screws up
283+
// indicatif's calculations).
284+
eprint!("{}", term_progress.last_value())
285+
}
287286
}
288-
ReporterStderrImpl::Terminal { .. } | ReporterStderrImpl::Buffer(_) => {}
287+
ReporterStderrImpl::Buffer(_) => {}
289288
}
290289
}
291290

292291
fn finish_and_clear_bar(&self) {
293292
match self {
294293
ReporterStderrImpl::Terminal {
295-
progress_bar: Some(state),
296-
..
294+
progress_bar,
295+
term_progress,
297296
} => {
298-
state.finish_and_clear();
297+
if let Some(state) = progress_bar {
298+
state.finish_and_clear();
299+
}
300+
if let Some(term_progress) = term_progress {
301+
// The last value is expected to be Remove.
302+
eprint!("{}", term_progress.last_value())
303+
}
299304
}
300-
ReporterStderrImpl::Terminal { .. } | ReporterStderrImpl::Buffer(_) => {}
305+
ReporterStderrImpl::Buffer(_) => {}
301306
}
302307
}
303308

nextest-runner/src/reporter/displayer/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ mod progress;
99
mod status_level;
1010
mod unit_output;
1111

12-
pub use imp::*;
12+
pub(crate) use imp::*;
1313
pub use progress::{MaxProgressRunning, ShowProgress};
1414
pub use status_level::*;
1515
pub use unit_output::*;

nextest-runner/src/reporter/displayer/progress.rs

Lines changed: 76 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,38 @@ use crate::{
66
helpers::{DisplayTestInstance, plural},
77
list::TestInstanceId,
88
reporter::{
9-
PROGRESS_REFRESH_RATE_HZ, displayer::formatters::DisplayBracketedHhMmSs, events::*,
10-
helpers::Styles,
9+
displayer::formatters::DisplayBracketedHhMmSs,
10+
events::*,
11+
helpers::{Styles, print_lines_in_chunks},
1112
},
12-
write_str::WriteStr,
1313
};
1414
use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
1515
use nextest_metadata::RustBinaryId;
1616
use owo_colors::OwoColorize;
1717
use std::{
1818
cmp::{max, min},
1919
env, fmt,
20-
io::{self, IsTerminal, Write},
20+
io::IsTerminal,
2121
num::NonZero,
2222
str::FromStr,
2323
time::{Duration, Instant},
2424
};
2525
use swrite::{SWrite, swrite};
2626
use tracing::debug;
2727

28+
/// The refresh rate for the progress bar, set to a minimal value.
29+
///
30+
/// For progress, during each tick, two things happen:
31+
///
32+
/// - We update the message, calling self.bar.set_message.
33+
/// - We print any buffered output.
34+
///
35+
/// We want both of these updates to be combined into one terminal flush, so we
36+
/// set *this* to a minimal value (so self.bar.set_message doesn't do a redraw),
37+
/// and rely on ProgressBar::print_and_flush_buffer to always flush the
38+
/// terminal.
39+
const PROGRESS_REFRESH_RATE_HZ: u8 = 1;
40+
2841
/// The maximum number of running tests to display with
2942
/// `--show-progress=running` or `only`.
3043
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -159,7 +172,10 @@ pub(super) struct ProgressBarState {
159172
max_running_displayed: usize,
160173
// None when the running tests are not displayed
161174
running_tests: Option<Vec<RunningTest>>,
162-
buffer: Vec<u8>,
175+
buffer: String,
176+
// Size in bytes for chunking println calls. Configurable via the
177+
// undocumented __NEXTEST_PROGRESS_PRINTLN_CHUNK_SIZE env var.
178+
println_chunk_size: usize,
163179
// Reasons for hiding the progress bar. We show the progress bar if none of
164180
// these are set and hide it if any of them are set.
165181
//
@@ -201,14 +217,23 @@ impl ProgressBarState {
201217

202218
let running_tests = show_running.then(Vec::new);
203219

220+
// The println chunk size defaults to a value chosen by experimentation,
221+
// locally and over SSH. This controls how often the progress bar
222+
// refreshes during large output bursts.
223+
let println_chunk_size = env::var("__NEXTEST_PROGRESS_PRINTLN_CHUNK_SIZE")
224+
.ok()
225+
.and_then(|s| s.parse::<usize>().ok())
226+
.unwrap_or(4096);
227+
204228
Self {
205229
bar,
206230
stats: RunStats::default(),
207231
running: 0,
208232
max_progress_running,
209233
max_running_displayed: 0,
210234
running_tests,
211-
buffer: Vec::new(),
235+
buffer: String::new(),
236+
println_chunk_size,
212237
hidden_no_capture: false,
213238
hidden_run_paused: false,
214239
hidden_info_response: false,
@@ -222,25 +247,38 @@ impl ProgressBarState {
222247
}
223248

224249
fn print_and_clear_buffer(&mut self) {
225-
self.print_buffer();
250+
self.print_and_force_redraw();
226251
self.buffer.clear();
227252
}
228253

229-
fn print_buffer(&self) {
230-
// ProgressBar::println doesn't print status lines if the bar is
231-
// hidden. The suspend method prints it in all cases.
232-
// suspend forces a full redraw, so we call it only if there is
233-
// something in the buffer
234-
if !self.buffer.is_empty() {
235-
self.bar.suspend(|| {
236-
std::io::stderr()
237-
.write_all(&self.buffer)
238-
.expect("write to succeed")
239-
});
254+
/// Prints the contents of the buffer, and always forces a redraw.
255+
fn print_and_force_redraw(&self) {
256+
if self.buffer.is_empty() {
257+
// Force a redraw as part of our contract. See the documentation for
258+
// `PROGRESS_REFRESH_RATE_HZ`.
259+
self.bar.force_draw();
260+
return;
240261
}
262+
263+
// println below also forces a redraw, so we don't need to call
264+
// force_draw in this case.
265+
266+
// ProgressBar::println is only called if there's something in the
267+
// buffer, for two reasons:
268+
//
269+
// 1. If passed in nothing at all, it prints an empty line.
270+
// 2. It forces a full redraw.
271+
//
272+
// But if self.buffer is too large, we can overwhelm the terminal with
273+
// large amounts of non-progress-bar output, causing the progress bar to
274+
// flicker in and out. To avoid those issues, we chunk the output to
275+
// maintain progress bar visibility by redrawing it regularly.
276+
print_lines_in_chunks(&self.buffer, self.println_chunk_size, |chunk| {
277+
self.bar.println(chunk);
278+
});
241279
}
242280

243-
pub(super) fn update_message(&mut self, styles: &Styles) {
281+
fn update_message(&mut self, styles: &Styles) {
244282
let mut msg = progress_bar_msg(&self.stats, self.running, styles);
245283
msg += " ";
246284

@@ -457,12 +495,12 @@ impl ProgressBarState {
457495
}
458496

459497
pub(super) fn write_buf(&mut self, buf: &str) {
460-
self.buffer.extend_from_slice(buf.as_bytes());
498+
self.buffer.push_str(buf);
461499
}
462500

463501
#[inline]
464502
pub(super) fn finish_and_clear(&self) {
465-
self.print_buffer();
503+
self.print_and_force_redraw();
466504
self.bar.finish_and_clear();
467505
}
468506

@@ -487,7 +525,10 @@ impl ProgressBarState {
487525
}
488526

489527
/// OSC 9 terminal progress reporting.
490-
pub(super) struct TerminalProgress {}
528+
#[derive(Default)]
529+
pub(super) struct TerminalProgress {
530+
last_value: TerminalProgressValue,
531+
}
491532

492533
impl TerminalProgress {
493534
const ENV: &str = "CARGO_TERM_PROGRESS_TERM_INTEGRATION";
@@ -500,7 +541,7 @@ impl TerminalProgress {
500541
if let Some(v) = config.term.progress.term_integration {
501542
if v {
502543
debug!("enabling terminal progress reporting based on {source:?}");
503-
return Some(Self {});
544+
return Some(Self::default());
504545
} else {
505546
debug!("disabling terminal progress reporting based on {source:?}");
506547
return None;
@@ -514,7 +555,7 @@ impl TerminalProgress {
514555
"enabling terminal progress reporting based on \
515556
CARGO_TERM_PROGRESS_TERM_INTEGRATION environment variable"
516557
);
517-
return Some(Self {});
558+
return Some(Self::default());
518559
} else if v == "false" {
519560
debug!(
520561
"disabling terminal progress reporting based on \
@@ -533,7 +574,7 @@ impl TerminalProgress {
533574
if let Some(v) = config.term.progress.term_integration {
534575
if v {
535576
debug!("enabling terminal progress reporting based on {source:?}");
536-
return Some(Self {});
577+
return Some(Self::default());
537578
} else {
538579
debug!("disabling terminal progress reporting based on {source:?}");
539580
return None;
@@ -543,14 +584,10 @@ impl TerminalProgress {
543584
}
544585
}
545586

546-
supports_osc_9_4(stream).then_some(TerminalProgress {})
587+
supports_osc_9_4(stream).then(Self::default)
547588
}
548589

549-
pub(super) fn update_progress(
550-
&self,
551-
event: &TestEvent<'_>,
552-
writer: &mut dyn WriteStr,
553-
) -> Result<(), io::Error> {
590+
pub(super) fn update_progress(&mut self, event: &TestEvent<'_>) {
554591
let value = match &event.kind {
555592
TestEventKind::RunStarted { .. }
556593
| TestEventKind::StressSubRunStarted { .. }
@@ -594,7 +631,11 @@ impl TerminalProgress {
594631
}
595632
};
596633

597-
write!(writer, "{value}")
634+
self.last_value = value;
635+
}
636+
637+
pub(super) fn last_value(&self) -> &TerminalProgressValue {
638+
&self.last_value
598639
}
599640
}
600641

@@ -628,9 +669,10 @@ fn supports_osc_9_4(stream: &dyn IsTerminal) -> bool {
628669
/// A progress status value printable as an ANSI OSC 9;4 escape code.
629670
///
630671
/// Adapted from Cargo 1.87.
631-
#[derive(PartialEq, Debug)]
632-
enum TerminalProgressValue {
672+
#[derive(PartialEq, Debug, Default)]
673+
pub(super) enum TerminalProgressValue {
633674
/// No output.
675+
#[default]
634676
None,
635677
/// Remove progress.
636678
Remove,

0 commit comments

Comments
 (0)