refactor(progress bars): global control for drawing (#17091)

This PR adds the concept of a global `DrawThread`, which can receive
multiple renderers to draw information on the screen (note: the
underlying thread is released back to tokio when it's not rendering). It
also separates the concept of progress bars from the existing "draw
thread". This makes it trivial for us to do stuff like show permission
prompts and progress bars at the same time in the future.

The reason this is global is because the process' tty stderr is also a
global concept.
This commit is contained in:
David Sherret 2022-12-19 11:19:33 -05:00 committed by GitHub
parent 383d40a33b
commit 2afac5bf78
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 440 additions and 243 deletions

4
Cargo.lock generated
View file

@ -501,9 +501,9 @@ dependencies = [
[[package]]
name = "console_static_text"
version = "0.3.3"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d749e1f5316d8a15ec592516a631ab9b8099cc6d085b69b905462fc071caedb"
checksum = "f166cdfb9db0607e2079b382ba64bc4164344006c733b95c1ecfa782a180a34a"
dependencies = [
"unicode-width",
"vte",

View file

@ -59,7 +59,7 @@ chrono = { version = "=0.4.22", default-features = false, features = ["clock"] }
clap = "=3.1.12"
clap_complete = "=3.1.2"
clap_complete_fig = "=3.1.5"
console_static_text = "=0.3.3"
console_static_text = "=0.3.4"
data-url.workspace = true
dissimilar = "=1.0.4"
dprint-plugin-json = "=0.17.0"

237
cli/util/draw_thread.rs Normal file
View file

@ -0,0 +1,237 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
use console_static_text::ConsoleStaticText;
use deno_core::parking_lot::Mutex;
use deno_runtime::ops::tty::ConsoleSize;
use once_cell::sync::Lazy;
use std::sync::Arc;
use std::time::Duration;
use crate::util::console::console_size;
/// Renders text that will be displayed stacked in a
/// static place on the console.
pub trait DrawThreadRenderer: Send + Sync + std::fmt::Debug {
fn render(&self, data: &ConsoleSize) -> String;
}
/// Draw thread guard. Keep this alive for the duration
/// that you wish the entry to be drawn for. Once it is
/// dropped, then the entry will be removed from the draw
/// thread.
#[derive(Debug)]
pub struct DrawThreadGuard(u16);
impl Drop for DrawThreadGuard {
fn drop(&mut self) {
DrawThread::finish_entry(self.0)
}
}
#[derive(Debug, Clone)]
struct InternalEntry {
priority: u8,
id: u16,
renderer: Arc<dyn DrawThreadRenderer>,
}
#[derive(Debug)]
struct InternalState {
// this ensures only one actual draw thread is running
drawer_id: usize,
hide: bool,
has_draw_thread: bool,
next_entry_id: u16,
entries: Vec<InternalEntry>,
static_text: ConsoleStaticText,
}
impl InternalState {
pub fn should_exit_draw_thread(&self, drawer_id: usize) -> bool {
self.drawer_id != drawer_id || self.entries.is_empty()
}
}
static INTERNAL_STATE: Lazy<Arc<Mutex<InternalState>>> = Lazy::new(|| {
Arc::new(Mutex::new(InternalState {
drawer_id: 0,
hide: false,
has_draw_thread: false,
entries: Vec::new(),
next_entry_id: 0,
static_text: ConsoleStaticText::new(|| {
let size = console_size().unwrap();
console_static_text::ConsoleSize {
cols: Some(size.cols as u16),
rows: Some(size.rows as u16),
}
}),
}))
});
/// The draw thread is responsible for rendering multiple active
/// `DrawThreadRenderer`s to stderr. It is global because the
/// concept of stderr in the process is also a global concept.
#[derive(Clone, Debug)]
pub struct DrawThread;
impl DrawThread {
/// Is using a draw thread supported.
pub fn is_supported() -> bool {
atty::is(atty::Stream::Stderr)
&& log::log_enabled!(log::Level::Info)
&& console_size()
.map(|s| s.cols > 0 && s.rows > 0)
.unwrap_or(false)
}
/// Adds a renderer to the draw thread with a given priority.
/// Renderers are sorted by priority with higher priority
/// entries appearing at the bottom of the screen.
pub fn add_entry(
priority: u8,
renderer: Arc<dyn DrawThreadRenderer>,
) -> DrawThreadGuard {
let internal_state = &*INTERNAL_STATE;
let mut internal_state = internal_state.lock();
let id = internal_state.next_entry_id;
internal_state.entries.push(InternalEntry {
id,
priority,
renderer,
});
internal_state
.entries
.sort_by(|a, b| a.priority.cmp(&b.priority));
if internal_state.next_entry_id == u16::MAX {
internal_state.next_entry_id = 0;
} else {
internal_state.next_entry_id += 1;
}
Self::maybe_start_draw_thread(&mut internal_state);
DrawThreadGuard(id)
}
/// Hides the draw thread.
#[allow(dead_code)]
pub fn hide() {
let internal_state = &*INTERNAL_STATE;
let mut internal_state = internal_state.lock();
internal_state.hide = true;
Self::clear_and_stop_draw_thread(&mut internal_state);
}
/// Shows the draw thread if it was previously hidden.
#[allow(dead_code)]
pub fn show() {
let internal_state = &*INTERNAL_STATE;
let mut internal_state = internal_state.lock();
internal_state.hide = false;
Self::maybe_start_draw_thread(&mut internal_state);
}
fn finish_entry(entry_id: u16) {
let internal_state = &*INTERNAL_STATE;
let mut internal_state = internal_state.lock();
if let Some(index) =
internal_state.entries.iter().position(|e| e.id == entry_id)
{
internal_state.entries.remove(index);
if internal_state.entries.is_empty() {
Self::clear_and_stop_draw_thread(&mut internal_state);
}
}
}
fn clear_and_stop_draw_thread(internal_state: &mut InternalState) {
if internal_state.has_draw_thread {
internal_state.static_text.eprint_clear();
// bump the drawer id to exit the draw thread
internal_state.drawer_id += 1;
internal_state.has_draw_thread = false;
}
}
fn maybe_start_draw_thread(internal_state: &mut InternalState) {
if internal_state.has_draw_thread || internal_state.hide {
return;
}
internal_state.drawer_id += 1;
internal_state.has_draw_thread = true;
let drawer_id = internal_state.drawer_id;
tokio::task::spawn_blocking(move || {
let mut previous_size = console_size().unwrap();
loop {
let mut delay_ms = 120;
{
// Get the entries to render.
let entries = {
let internal_state = &*INTERNAL_STATE;
let internal_state = internal_state.lock();
if internal_state.should_exit_draw_thread(drawer_id) {
break;
}
internal_state.entries.clone()
};
// Call into the renderers outside the lock to prevent a potential
// deadlock between our internal state lock and the renderers
// internal state lock.
//
// Example deadlock if this code didn't do this:
// 1. Other thread - Renderer - acquired internal lock to update state
// 2. This thread - Acquired internal state
// 3. Other thread - Renderer - drops DrawThreadGuard
// 4. This thread - Calls renderer.render within internal lock,
// which attempts to acquire the other thread's Render's internal
// lock causing a deadlock
let mut text = String::new();
let size = console_size().unwrap();
if size != previous_size {
// means the user is actively resizing the console...
// wait a little bit until they stop resizing
previous_size = size;
delay_ms = 200;
} else {
let mut should_new_line_next = false;
for entry in entries {
let new_text = entry.renderer.render(&size);
if should_new_line_next && !new_text.is_empty() {
text.push('\n');
}
should_new_line_next = !new_text.is_empty();
text.push_str(&new_text);
}
}
// now reacquire the lock, ensure we should still be drawing, then
// output the text
{
let internal_state = &*INTERNAL_STATE;
let mut internal_state = internal_state.lock();
if internal_state.should_exit_draw_thread(drawer_id) {
break;
}
internal_state.static_text.eprint_with_size(
&text,
console_static_text::ConsoleSize {
cols: Some(size.cols as u16),
rows: Some(size.rows as u16),
},
);
}
}
std::thread::sleep(Duration::from_millis(delay_ms));
}
});
}
}

View file

@ -5,6 +5,7 @@ pub mod checksum;
pub mod console;
pub mod diff;
pub mod display;
pub mod draw_thread;
pub mod file_watcher;
pub mod fs;
pub mod logger;

View file

@ -1,218 +0,0 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
use console_static_text::ConsoleStaticText;
use deno_core::parking_lot::Mutex;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::Duration;
use std::time::SystemTime;
use crate::util::console::console_size;
use super::renderer::ProgressBarRenderer;
use super::renderer::ProgressData;
use super::renderer::ProgressDataDisplayEntry;
#[derive(Clone, Debug)]
pub struct ProgressBarEntry {
id: usize,
pub message: String,
pos: Arc<AtomicU64>,
total_size: Arc<AtomicU64>,
draw_thread: DrawThread,
}
impl ProgressBarEntry {
pub fn position(&self) -> u64 {
self.pos.load(Ordering::Relaxed)
}
pub fn set_position(&self, new_pos: u64) {
self.pos.store(new_pos, Ordering::Relaxed);
}
pub fn total_size(&self) -> u64 {
self.total_size.load(Ordering::Relaxed)
}
pub fn set_total_size(&self, new_size: u64) {
self.total_size.store(new_size, Ordering::Relaxed);
}
pub fn finish(&self) {
self.draw_thread.finish_entry(self.id);
}
pub fn percent(&self) -> f64 {
let pos = self.pos.load(Ordering::Relaxed) as f64;
let total_size = self.total_size.load(Ordering::Relaxed) as f64;
if total_size == 0f64 {
0f64
} else {
pos / total_size
}
}
}
#[derive(Debug)]
struct InternalState {
start_time: SystemTime,
// this ensures only one draw thread is running
drawer_id: usize,
keep_alive_count: usize,
has_draw_thread: bool,
total_entries: usize,
entries: Vec<ProgressBarEntry>,
static_text: ConsoleStaticText,
renderer: Box<dyn ProgressBarRenderer>,
}
#[derive(Clone, Debug)]
pub struct DrawThread {
state: Arc<Mutex<InternalState>>,
}
impl DrawThread {
pub fn new(renderer: Box<dyn ProgressBarRenderer>) -> Self {
Self {
state: Arc::new(Mutex::new(InternalState {
start_time: SystemTime::now(),
drawer_id: 0,
keep_alive_count: 0,
has_draw_thread: false,
total_entries: 0,
entries: Vec::new(),
static_text: ConsoleStaticText::new(|| {
let size = console_size().unwrap();
console_static_text::ConsoleSize {
cols: Some(size.cols as u16),
rows: Some(size.rows as u16),
}
}),
renderer,
})),
}
}
pub fn add_entry(&self, message: String) -> ProgressBarEntry {
let mut internal_state = self.state.lock();
let id = internal_state.total_entries;
let entry = ProgressBarEntry {
id,
draw_thread: self.clone(),
message,
pos: Default::default(),
total_size: Default::default(),
};
internal_state.entries.push(entry.clone());
internal_state.total_entries += 1;
internal_state.keep_alive_count += 1;
if !internal_state.has_draw_thread {
self.start_draw_thread(&mut internal_state);
}
entry
}
fn finish_entry(&self, entry_id: usize) {
let mut internal_state = self.state.lock();
if let Ok(index) = internal_state
.entries
.binary_search_by(|e| e.id.cmp(&entry_id))
{
internal_state.entries.remove(index);
self.decrement_keep_alive(&mut internal_state);
}
}
pub fn increment_clear(&self) {
let mut internal_state = self.state.lock();
internal_state.keep_alive_count += 1;
}
pub fn decrement_clear(&self) {
let mut internal_state = self.state.lock();
self.decrement_keep_alive(&mut internal_state);
}
fn decrement_keep_alive(&self, internal_state: &mut InternalState) {
internal_state.keep_alive_count -= 1;
if internal_state.keep_alive_count == 0 {
internal_state.static_text.eprint_clear();
// bump the drawer id to exit the draw thread
internal_state.drawer_id += 1;
internal_state.has_draw_thread = false;
}
}
fn start_draw_thread(&self, internal_state: &mut InternalState) {
internal_state.drawer_id += 1;
internal_state.start_time = SystemTime::now();
internal_state.has_draw_thread = true;
let drawer_id = internal_state.drawer_id;
let internal_state = self.state.clone();
tokio::task::spawn_blocking(move || {
let mut previous_size = console_size().unwrap();
loop {
let mut delay_ms = 120;
{
let mut internal_state = internal_state.lock();
// exit if not the current draw thread
if internal_state.drawer_id != drawer_id {
break;
}
let size = console_size().unwrap();
if size != previous_size {
// means the user is actively resizing the console...
// wait a little bit until they stop resizing
previous_size = size;
delay_ms = 200;
} else if !internal_state.entries.is_empty() {
let preferred_entry = internal_state
.entries
.iter()
.find(|e| e.percent() > 0f64)
.or_else(|| internal_state.entries.iter().last())
.unwrap();
let text = internal_state.renderer.render(ProgressData {
duration: internal_state.start_time.elapsed().unwrap(),
terminal_width: size.cols,
pending_entries: internal_state.entries.len(),
total_entries: internal_state.total_entries,
display_entry: ProgressDataDisplayEntry {
message: preferred_entry.message.clone(),
position: preferred_entry.position(),
total_size: preferred_entry.total_size(),
},
percent_done: {
let mut total_percent_sum = 0f64;
for entry in &internal_state.entries {
total_percent_sum += entry.percent();
}
total_percent_sum += (internal_state.total_entries
- internal_state.entries.len())
as f64;
total_percent_sum / (internal_state.total_entries as f64)
},
});
internal_state.static_text.eprint_with_size(
&text,
console_static_text::ConsoleSize {
cols: Some(size.cols as u16),
rows: Some(size.rows as u16),
},
);
}
}
std::thread::sleep(Duration::from_millis(delay_ms));
}
});
}
}

View file

@ -1,13 +1,23 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::SystemTime;
use deno_core::parking_lot::Mutex;
use deno_runtime::ops::tty::ConsoleSize;
use crate::colors;
use self::draw_thread::DrawThread;
use self::draw_thread::ProgressBarEntry;
use self::renderer::ProgressBarRenderer;
use self::renderer::ProgressData;
use self::renderer::ProgressDataDisplayEntry;
use super::console::console_size;
use super::draw_thread::DrawThread;
use super::draw_thread::DrawThreadGuard;
use super::draw_thread::DrawThreadRenderer;
mod draw_thread;
mod renderer;
// Inspired by Indicatif, but this custom implementation allows
@ -45,30 +55,197 @@ pub enum ProgressBarStyle {
TextOnly,
}
#[derive(Clone, Debug)]
struct ProgressBarEntry {
id: usize,
pub message: String,
pos: Arc<AtomicU64>,
total_size: Arc<AtomicU64>,
progress_bar: ProgressBarInner,
}
impl ProgressBarEntry {
pub fn position(&self) -> u64 {
self.pos.load(Ordering::Relaxed)
}
pub fn set_position(&self, new_pos: u64) {
self.pos.store(new_pos, Ordering::Relaxed);
}
pub fn total_size(&self) -> u64 {
self.total_size.load(Ordering::Relaxed)
}
pub fn set_total_size(&self, new_size: u64) {
self.total_size.store(new_size, Ordering::Relaxed);
}
pub fn finish(&self) {
self.progress_bar.finish_entry(self.id);
}
pub fn percent(&self) -> f64 {
let pos = self.pos.load(Ordering::Relaxed) as f64;
let total_size = self.total_size.load(Ordering::Relaxed) as f64;
if total_size == 0f64 {
0f64
} else {
pos / total_size
}
}
}
#[derive(Debug)]
struct InternalState {
/// If this guard exists, then it means the progress
/// bar is displaying in the draw thread.
draw_thread_guard: Option<DrawThreadGuard>,
start_time: SystemTime,
keep_alive_count: usize,
total_entries: usize,
entries: Vec<ProgressBarEntry>,
}
#[derive(Clone, Debug)]
struct ProgressBarInner {
state: Arc<Mutex<InternalState>>,
renderer: Arc<dyn ProgressBarRenderer>,
}
impl ProgressBarInner {
fn new(renderer: Arc<dyn ProgressBarRenderer>) -> Self {
Self {
state: Arc::new(Mutex::new(InternalState {
draw_thread_guard: None,
start_time: SystemTime::now(),
keep_alive_count: 0,
total_entries: 0,
entries: Vec::new(),
})),
renderer,
}
}
pub fn add_entry(&self, message: String) -> ProgressBarEntry {
let mut internal_state = self.state.lock();
let id = internal_state.total_entries;
let entry = ProgressBarEntry {
id,
message,
pos: Default::default(),
total_size: Default::default(),
progress_bar: self.clone(),
};
internal_state.entries.push(entry.clone());
internal_state.total_entries += 1;
internal_state.keep_alive_count += 1;
self.maybe_start_draw_thread(&mut internal_state);
entry
}
fn finish_entry(&self, entry_id: usize) {
let mut internal_state = self.state.lock();
if let Ok(index) = internal_state
.entries
.binary_search_by(|e| e.id.cmp(&entry_id))
{
internal_state.entries.remove(index);
self.decrement_keep_alive(&mut internal_state);
}
}
pub fn increment_clear(&self) {
let mut internal_state = self.state.lock();
internal_state.keep_alive_count += 1;
}
pub fn decrement_clear(&self) {
let mut internal_state = self.state.lock();
self.decrement_keep_alive(&mut internal_state);
}
fn decrement_keep_alive(&self, state: &mut InternalState) {
state.keep_alive_count -= 1;
if state.keep_alive_count == 0 {
// drop the guard to remove this from the draw thread
state.draw_thread_guard.take();
}
}
fn maybe_start_draw_thread(&self, internal_state: &mut InternalState) {
if internal_state.draw_thread_guard.is_none()
&& internal_state.keep_alive_count > 0
{
internal_state.start_time = SystemTime::now();
internal_state.draw_thread_guard =
Some(DrawThread::add_entry(0, Arc::new(self.clone())));
}
}
}
impl DrawThreadRenderer for ProgressBarInner {
fn render(&self, size: &ConsoleSize) -> String {
let data = {
let state = self.state.lock();
if state.entries.is_empty() {
return String::new();
}
let preferred_entry = state
.entries
.iter()
.find(|e| e.percent() > 0f64)
.or_else(|| state.entries.iter().last())
.unwrap();
ProgressData {
duration: state.start_time.elapsed().unwrap(),
terminal_width: size.cols,
pending_entries: state.entries.len(),
total_entries: state.total_entries,
display_entry: ProgressDataDisplayEntry {
message: preferred_entry.message.clone(),
position: preferred_entry.position(),
total_size: preferred_entry.total_size(),
},
percent_done: {
let mut total_percent_sum = 0f64;
for entry in &state.entries {
total_percent_sum += entry.percent();
}
total_percent_sum +=
(state.total_entries - state.entries.len()) as f64;
total_percent_sum / (state.total_entries as f64)
},
}
};
self.renderer.render(data)
}
}
#[derive(Clone, Debug)]
pub struct ProgressBar {
draw_thread: Option<DrawThread>,
inner: Option<ProgressBarInner>,
}
impl ProgressBar {
/// Checks if progress bars are supported
pub fn are_supported() -> bool {
atty::is(atty::Stream::Stderr)
&& log::log_enabled!(log::Level::Info)
&& console_size()
.map(|s| s.cols > 0 && s.rows > 0)
.unwrap_or(false)
DrawThread::is_supported()
}
pub fn new(style: ProgressBarStyle) -> Self {
Self {
draw_thread: match Self::are_supported() {
true => Some(DrawThread::new(match style {
inner: match Self::are_supported() {
true => Some(ProgressBarInner::new(match style {
ProgressBarStyle::DownloadBars => {
Box::new(renderer::BarProgressBarRenderer)
Arc::new(renderer::BarProgressBarRenderer)
}
ProgressBarStyle::TextOnly => {
Box::new(renderer::TextOnlyProgressBarRenderer)
Arc::new(renderer::TextOnlyProgressBarRenderer)
}
})),
false => None,
@ -77,9 +254,9 @@ impl ProgressBar {
}
pub fn update(&self, msg: &str) -> UpdateGuard {
match &self.draw_thread {
Some(draw_thread) => {
let entry = draw_thread.add_entry(msg.to_string());
match &self.inner {
Some(inner) => {
let entry = inner.add_entry(msg.to_string());
UpdateGuard {
maybe_entry: Some(entry),
}
@ -95,15 +272,15 @@ impl ProgressBar {
}
pub fn clear_guard(&self) -> ClearGuard {
if let Some(draw_thread) = &self.draw_thread {
draw_thread.increment_clear();
if let Some(inner) = &self.inner {
inner.increment_clear();
}
ClearGuard { pb: self.clone() }
}
fn decrement_clear(&self) {
if let Some(draw_thread) = &self.draw_thread {
draw_thread.decrement_clear();
if let Some(inner) = &self.inner {
inner.decrement_clear();
}
}
}

View file

@ -23,7 +23,7 @@ pub struct ProgressData {
pub duration: Duration,
}
pub trait ProgressBarRenderer: Send + std::fmt::Debug {
pub trait ProgressBarRenderer: Send + Sync + std::fmt::Debug {
fn render(&self, data: ProgressData) -> String;
}