mirror of
https://github.com/alexpasmantier/television.git
synced 2025-06-06 19:45:23 +00:00
perf(async): make overall UI much smoother and snappier (#311)
This commit is contained in:
parent
ef05d65aab
commit
172ba231ee
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
# General settings
|
# General settings
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
frame_rate = 60
|
frame_rate = 60 # DEPRECATED: this option is no longer used
|
||||||
tick_rate = 50
|
tick_rate = 50
|
||||||
|
|
||||||
[ui]
|
[ui]
|
||||||
|
69
Cargo.lock
generated
69
Cargo.lock
generated
@ -473,6 +473,7 @@ dependencies = [
|
|||||||
"ciborium",
|
"ciborium",
|
||||||
"clap",
|
"clap",
|
||||||
"criterion-plot",
|
"criterion-plot",
|
||||||
|
"futures",
|
||||||
"is-terminal",
|
"is-terminal",
|
||||||
"itertools 0.10.5",
|
"itertools 0.10.5",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
@ -485,6 +486,7 @@ dependencies = [
|
|||||||
"serde_derive",
|
"serde_derive",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tinytemplate",
|
"tinytemplate",
|
||||||
|
"tokio",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -727,6 +729,67 @@ version = "0.1.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
|
checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
||||||
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
|
"futures-sink",
|
||||||
|
"futures-task",
|
||||||
|
"futures-util",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-channel"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-core"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-io"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-sink"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-task"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-util"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"futures-task",
|
||||||
|
"pin-project-lite",
|
||||||
|
"pin-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gag"
|
name = "gag"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
@ -1279,6 +1342,12 @@ version = "0.2.16"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-utils"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pkg-config"
|
name = "pkg-config"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
|
@ -69,7 +69,7 @@ toml = "0.8"
|
|||||||
winapi-util = "0.1.9"
|
winapi-util = "0.1.9"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
criterion = "0.5"
|
criterion = { version = "0.5", features = ["async_tokio"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
simd = ["dep:simdutf8"]
|
simd = ["dep:simdutf8"]
|
||||||
@ -82,7 +82,7 @@ path = "television/main.rs"
|
|||||||
name = "tv"
|
name = "tv"
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "results_list_benchmark"
|
name = "main"
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
|
7
benches/main.rs
Normal file
7
benches/main.rs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
pub mod main {
|
||||||
|
pub mod draw;
|
||||||
|
pub mod results_list_benchmark;
|
||||||
|
}
|
||||||
|
pub use main::*;
|
||||||
|
|
||||||
|
criterion::criterion_main!(results_list_benchmark::benches, draw::benches,);
|
54
benches/main/draw.rs
Normal file
54
benches/main/draw.rs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
use criterion::{black_box, Criterion};
|
||||||
|
use ratatui::backend::TestBackend;
|
||||||
|
use ratatui::layout::Rect;
|
||||||
|
use ratatui::Terminal;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use television::channels::OnAir;
|
||||||
|
use television::channels::{files::Channel, TelevisionChannel};
|
||||||
|
use television::config::Config;
|
||||||
|
use television::television::Television;
|
||||||
|
use tokio::runtime::Runtime;
|
||||||
|
|
||||||
|
fn draw(c: &mut Criterion) {
|
||||||
|
let width = 250;
|
||||||
|
let height = 80;
|
||||||
|
|
||||||
|
let rt = Runtime::new().unwrap();
|
||||||
|
|
||||||
|
c.bench_function("draw", |b| {
|
||||||
|
b.to_async(&rt).iter_batched(
|
||||||
|
// FIXME: this is kind of hacky
|
||||||
|
|| {
|
||||||
|
let config = Config::new().unwrap();
|
||||||
|
let backend = TestBackend::new(width, height);
|
||||||
|
let terminal = Terminal::new(backend).unwrap();
|
||||||
|
let mut channel =
|
||||||
|
TelevisionChannel::Files(Channel::new(vec![
|
||||||
|
PathBuf::from("."),
|
||||||
|
]));
|
||||||
|
channel.find("television");
|
||||||
|
// Wait for the channel to finish loading
|
||||||
|
for _ in 0..5 {
|
||||||
|
// tick the matcher
|
||||||
|
let _ = channel.results(10, 0);
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||||
|
}
|
||||||
|
let mut tv = Television::new(channel, config, None);
|
||||||
|
tv.select_next_entry(10);
|
||||||
|
(tv, terminal)
|
||||||
|
},
|
||||||
|
// Measurement
|
||||||
|
|(mut tv, mut terminal)| async move {
|
||||||
|
tv.draw(
|
||||||
|
black_box(&mut terminal.get_frame()),
|
||||||
|
black_box(Rect::new(0, 0, width, height)),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
},
|
||||||
|
criterion::BatchSize::SmallInput,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
criterion::criterion_group!(benches, draw);
|
||||||
|
criterion::criterion_main!(benches);
|
@ -1,4 +1,4 @@
|
|||||||
use criterion::{criterion_group, criterion_main, Criterion};
|
use criterion::{criterion_group, Criterion};
|
||||||
use devicons::FileIcon;
|
use devicons::FileIcon;
|
||||||
use ratatui::layout::Alignment;
|
use ratatui::layout::Alignment;
|
||||||
use ratatui::prelude::{Line, Style};
|
use ratatui::prelude::{Line, Style};
|
||||||
@ -664,4 +664,3 @@ pub fn results_list_benchmark(c: &mut Criterion) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
criterion_group!(benches, results_list_benchmark);
|
criterion_group!(benches, results_list_benchmark);
|
||||||
criterion_main!(benches);
|
|
@ -160,6 +160,7 @@ impl App {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
});
|
});
|
||||||
|
render_tx.send(RenderingTask::Render)?;
|
||||||
|
|
||||||
// event handling loop
|
// event handling loop
|
||||||
debug!("Starting event handling loop");
|
debug!("Starting event handling loop");
|
||||||
@ -169,6 +170,9 @@ impl App {
|
|||||||
if let Some(event) = self.event_rx.recv().await {
|
if let Some(event) = self.event_rx.recv().await {
|
||||||
let action = self.convert_event_to_action(event).await;
|
let action = self.convert_event_to_action(event).await;
|
||||||
action_tx.send(action)?;
|
action_tx.send(action)?;
|
||||||
|
// it's fine to send a rendering task here, because the rendering loop
|
||||||
|
// batches and deduplicates rendering tasks
|
||||||
|
render_tx.send(RenderingTask::Render)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let action_outcome = self.handle_actions().await?;
|
let action_outcome = self.handle_actions().await?;
|
||||||
|
@ -38,7 +38,7 @@ pub struct Cli {
|
|||||||
#[arg(short, long, value_name = "FLOAT")]
|
#[arg(short, long, value_name = "FLOAT")]
|
||||||
pub tick_rate: Option<f64>,
|
pub tick_rate: Option<f64>,
|
||||||
|
|
||||||
/// Frame rate, i.e. number of frames per second
|
/// [DEPRECATED] Frame rate, i.e. number of frames per second
|
||||||
#[arg(short, long, value_name = "FLOAT")]
|
#[arg(short, long, value_name = "FLOAT")]
|
||||||
pub frame_rate: Option<f64>,
|
pub frame_rate: Option<f64>,
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ pub struct AppConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize, Default)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
/// General application configuration
|
/// General application configuration
|
||||||
|
@ -6,15 +6,12 @@ use std::{
|
|||||||
};
|
};
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
use tokio::{
|
use tokio::sync::{mpsc, Mutex};
|
||||||
select,
|
|
||||||
sync::{mpsc, Mutex},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::television::Television;
|
use crate::television::Television;
|
||||||
use crate::{action::Action, tui::Tui};
|
use crate::{action::Action, tui::Tui};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, PartialEq, PartialOrd, Ord, Eq, Clone)]
|
||||||
pub enum RenderingTask {
|
pub enum RenderingTask {
|
||||||
ClearScreen,
|
ClearScreen,
|
||||||
Render,
|
Render,
|
||||||
@ -64,60 +61,62 @@ pub async fn render(
|
|||||||
.await
|
.await
|
||||||
.register_action_handler(action_tx.clone())?;
|
.register_action_handler(action_tx.clone())?;
|
||||||
|
|
||||||
// Rendering loop
|
let mut buffer = Vec::with_capacity(128);
|
||||||
loop {
|
|
||||||
select! {
|
|
||||||
() = tokio::time::sleep(tokio::time::Duration::from_secs_f64(1.0 / frame_rate)) => {
|
|
||||||
action_tx.send(Action::Render)?;
|
|
||||||
}
|
|
||||||
maybe_task = render_rx.recv() => {
|
|
||||||
if let Some(task) = maybe_task {
|
|
||||||
match task {
|
|
||||||
RenderingTask::ClearScreen => {
|
|
||||||
tui.terminal.clear()?;
|
|
||||||
}
|
|
||||||
RenderingTask::Render => {
|
|
||||||
let mut television = television.lock().await;
|
|
||||||
if let Ok(size) = tui.size() {
|
|
||||||
// Ratatui uses `u16`s to encode terminal dimensions and its
|
|
||||||
// content for each terminal cell is stored linearly in a
|
|
||||||
// buffer with a `u16` index which means we can't support
|
|
||||||
// terminal areas larger than `u16::MAX`.
|
|
||||||
if size.width.checked_mul(size.height).is_some() {
|
|
||||||
tui.terminal.draw(|frame| {
|
|
||||||
if let Err(err) = television.draw(frame, frame.area()) {
|
|
||||||
warn!("Failed to draw: {:?}", err);
|
|
||||||
let _ = action_tx
|
|
||||||
.send(Action::Error(format!("Failed to draw: {err:?}")));
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
|
|
||||||
} else {
|
// Rendering loop
|
||||||
warn!("Terminal area too large");
|
'rendering: while render_rx.recv_many(&mut buffer, 128).await > 0 {
|
||||||
|
// deduplicate events
|
||||||
|
buffer.sort_unstable();
|
||||||
|
buffer.dedup();
|
||||||
|
for event in buffer.drain(..) {
|
||||||
|
match event {
|
||||||
|
RenderingTask::ClearScreen => {
|
||||||
|
tui.terminal.clear()?;
|
||||||
|
}
|
||||||
|
RenderingTask::Render => {
|
||||||
|
if let Ok(size) = tui.size() {
|
||||||
|
// Ratatui uses `u16`s to encode terminal dimensions and its
|
||||||
|
// content for each terminal cell is stored linearly in a
|
||||||
|
// buffer with a `u16` index which means we can't support
|
||||||
|
// terminal areas larger than `u16::MAX`.
|
||||||
|
if size.width.checked_mul(size.height).is_some() {
|
||||||
|
let mut television = television.lock().await;
|
||||||
|
tui.terminal.draw(|frame| {
|
||||||
|
if let Err(err) =
|
||||||
|
television.draw(frame, frame.area())
|
||||||
|
{
|
||||||
|
warn!("Failed to draw: {:?}", err);
|
||||||
|
let _ = action_tx.send(Action::Error(
|
||||||
|
format!("Failed to draw: {err:?}"),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
})?;
|
||||||
}
|
} else {
|
||||||
RenderingTask::Resize(w, h) => {
|
warn!("Terminal area too large");
|
||||||
tui.resize(Rect::new(0, 0, w, h))?;
|
|
||||||
action_tx.send(Action::Render)?;
|
|
||||||
}
|
|
||||||
RenderingTask::Suspend => {
|
|
||||||
tui.suspend()?;
|
|
||||||
action_tx.send(Action::Resume)?;
|
|
||||||
action_tx.send(Action::ClearScreen)?;
|
|
||||||
tui.enter()?;
|
|
||||||
}
|
|
||||||
RenderingTask::Resume => {
|
|
||||||
tui.enter()?;
|
|
||||||
}
|
|
||||||
RenderingTask::Quit => {
|
|
||||||
debug!("Exiting rendering loop");
|
|
||||||
tui.exit()?;
|
|
||||||
break Ok(());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
RenderingTask::Resize(w, h) => {
|
||||||
|
tui.resize(Rect::new(0, 0, w, h))?;
|
||||||
|
action_tx.send(Action::Render)?;
|
||||||
|
}
|
||||||
|
RenderingTask::Suspend => {
|
||||||
|
tui.suspend()?;
|
||||||
|
action_tx.send(Action::Resume)?;
|
||||||
|
action_tx.send(Action::ClearScreen)?;
|
||||||
|
tui.enter()?;
|
||||||
|
}
|
||||||
|
RenderingTask::Resume => {
|
||||||
|
tui.enter()?;
|
||||||
|
}
|
||||||
|
RenderingTask::Quit => {
|
||||||
|
debug!("Exiting rendering loop");
|
||||||
|
tui.exit()?;
|
||||||
|
break 'rendering;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -280,6 +280,36 @@ impl Television {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn should_render(&self, action: &Action) -> bool {
|
||||||
|
matches!(
|
||||||
|
action,
|
||||||
|
Action::AddInputChar(_)
|
||||||
|
| Action::DeletePrevChar
|
||||||
|
| Action::DeletePrevWord
|
||||||
|
| Action::DeleteNextChar
|
||||||
|
| Action::GoToPrevChar
|
||||||
|
| Action::GoToNextChar
|
||||||
|
| Action::GoToInputStart
|
||||||
|
| Action::GoToInputEnd
|
||||||
|
| Action::ToggleSelectionDown
|
||||||
|
| Action::ToggleSelectionUp
|
||||||
|
| Action::ConfirmSelection
|
||||||
|
| Action::SelectNextEntry
|
||||||
|
| Action::SelectPrevEntry
|
||||||
|
| Action::SelectNextPage
|
||||||
|
| Action::SelectPrevPage
|
||||||
|
| Action::ScrollPreviewDown
|
||||||
|
| Action::ScrollPreviewUp
|
||||||
|
| Action::ScrollPreviewHalfPageDown
|
||||||
|
| Action::ScrollPreviewHalfPageUp
|
||||||
|
| Action::ToggleRemoteControl
|
||||||
|
| Action::ToggleSendToChannel
|
||||||
|
| Action::ToggleHelp
|
||||||
|
| Action::TogglePreview
|
||||||
|
| Action::CopyEntryToClipboard
|
||||||
|
) || self.channel.running()
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::unused_async)]
|
#[allow(clippy::unused_async)]
|
||||||
/// Update the state of the component based on a received action.
|
/// Update the state of the component based on a received action.
|
||||||
///
|
///
|
||||||
@ -447,7 +477,12 @@ impl Television {
|
|||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
Ok(None)
|
|
||||||
|
Ok(if self.should_render(&action) {
|
||||||
|
Some(Action::Render)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render the television on the screen.
|
/// Render the television on the screen.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user