1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-27 01:29:14 -05:00
denoland-deno/cli/util/draw_thread.rs
Matt Mastracci 9845361153
refactor(core): bake single-thread assumptions into spawn/spawn_blocking (#19056)
Partially supersedes #19016.

This migrates `spawn` and `spawn_blocking` to `deno_core`, and removes
the requirement for `spawn` tasks to be `Send` given our single-threaded
executor.

While we don't need to technically do anything w/`spawn_blocking`, this
allows us to have a single `JoinHandle` type that works for both cases,
and allows us to more easily experiment with alternative
`spawn_blocking` implementations that do not require tokio (ie: rayon).

Async ops (+~35%):

Before: 

```
time 1310 ms rate 763358
time 1267 ms rate 789265
time 1259 ms rate 794281
time 1266 ms rate 789889
```

After:

```
time 956 ms rate 1046025
time 954 ms rate 1048218
time 924 ms rate 1082251
time 920 ms rate 1086956
```

HTTP serve (+~4.4%):

Before:

```
Running 10s test @ http://localhost:4500
  2 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    68.78us   19.77us   1.43ms   86.84%
    Req/Sec    68.78k     5.00k   73.84k    91.58%
  1381833 requests in 10.10s, 167.36MB read
Requests/sec: 136823.29
Transfer/sec:     16.57MB
```

After:

```
Running 10s test @ http://localhost:4500
  2 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    63.12us   17.43us   1.11ms   85.13%
    Req/Sec    71.82k     3.71k   77.02k    79.21%
  1443195 requests in 10.10s, 174.79MB read
Requests/sec: 142921.99
Transfer/sec:     17.31MB
```

Suggested-By: alice@ryhl.io
Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>
2023-05-14 15:40:01 -06:00

236 lines
7.2 KiB
Rust

// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
use console_static_text::ConsoleStaticText;
use deno_core::parking_lot::Mutex;
use deno_core::task::spawn_blocking;
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 {
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),
}
}),
}))
});
static IS_TTY_WITH_CONSOLE_SIZE: Lazy<bool> = Lazy::new(|| {
atty::is(atty::Stream::Stderr)
&& console_size()
.map(|s| s.cols > 0 && s.rows > 0)
.unwrap_or(false)
});
/// 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 {
// don't put the log level in the lazy because the
// log level may change as the application runs
log::log_enabled!(log::Level::Info) && *IS_TTY_WITH_CONSOLE_SIZE
}
/// Adds a renderer to the draw thread.
pub fn add_entry(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, renderer });
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.
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.
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
|| internal_state.entries.is_empty()
|| !DrawThread::is_supported()
{
return;
}
internal_state.drawer_id += 1;
internal_state.has_draw_thread = true;
let drawer_id = internal_state.drawer_id;
spawn_blocking(move || {
let mut previous_size = console_size();
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()
};
// this should always be set, but have the code handle
// it not being for some reason
let size = console_size();
// 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();
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 let Some(size) = size {
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));
}
});
}
}