performance(render): improve rendering performance by consolidating renders and introducing repaint_delay (#4100)
* initial draft * style(comment): add explanation to async render * remove timeouts from terminal_bytes * some cleanups * cleanups * chore(docs): update pr
This commit is contained in:
parent
6be8c495bc
commit
f3351f4f75
6 changed files with 62 additions and 96 deletions
|
|
@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
||||||
* chore(deps): Use workspace dependencies (https://github.com/zellij-org/zellij/pull/4085)
|
* chore(deps): Use workspace dependencies (https://github.com/zellij-org/zellij/pull/4085)
|
||||||
* build: Don't use default features (https://github.com/zellij-org/zellij/pull/4086)
|
* build: Don't use default features (https://github.com/zellij-org/zellij/pull/4086)
|
||||||
* build: Don't re-export foreign crates (https://github.com/zellij-org/zellij/pull/4087)
|
* build: Don't re-export foreign crates (https://github.com/zellij-org/zellij/pull/4087)
|
||||||
|
* performance(terminal): reduce render count to mitigate flickering issues in apps that don't implement synchronized renders (https://github.com/zellij-org/zellij/pull/4100)
|
||||||
|
|
||||||
## [0.42.1] - 2025-03-21
|
## [0.42.1] - 2025-03-21
|
||||||
* fix(mouse): fix mouse handling in windows terminal (https://github.com/zellij-org/zellij/pull/4076)
|
* fix(mouse): fix mouse handling in windows terminal (https://github.com/zellij-org/zellij/pull/4076)
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ pub enum BackgroundJob {
|
||||||
Vec<u8>, // body
|
Vec<u8>, // body
|
||||||
BTreeMap<String, String>, // context
|
BTreeMap<String, String>, // context
|
||||||
),
|
),
|
||||||
|
RenderToClients,
|
||||||
Exit,
|
Exit,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,6 +75,7 @@ impl From<&BackgroundJob> for BackgroundJobContext {
|
||||||
BackgroundJob::RunCommand(..) => BackgroundJobContext::RunCommand,
|
BackgroundJob::RunCommand(..) => BackgroundJobContext::RunCommand,
|
||||||
BackgroundJob::WebRequest(..) => BackgroundJobContext::WebRequest,
|
BackgroundJob::WebRequest(..) => BackgroundJobContext::WebRequest,
|
||||||
BackgroundJob::ReportPluginList(..) => BackgroundJobContext::ReportPluginList,
|
BackgroundJob::ReportPluginList(..) => BackgroundJobContext::ReportPluginList,
|
||||||
|
BackgroundJob::RenderToClients => BackgroundJobContext::ReportPluginList,
|
||||||
BackgroundJob::Exit => BackgroundJobContext::Exit,
|
BackgroundJob::Exit => BackgroundJobContext::Exit,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -83,6 +85,7 @@ static FLASH_DURATION_MS: u64 = 1000;
|
||||||
static PLUGIN_ANIMATION_OFFSET_DURATION_MD: u64 = 500;
|
static PLUGIN_ANIMATION_OFFSET_DURATION_MD: u64 = 500;
|
||||||
static SESSION_READ_DURATION: u64 = 1000;
|
static SESSION_READ_DURATION: u64 = 1000;
|
||||||
static DEFAULT_SERIALIZATION_INTERVAL: u64 = 60000;
|
static DEFAULT_SERIALIZATION_INTERVAL: u64 = 60000;
|
||||||
|
static REPAINT_DELAY_MS: u64 = 10;
|
||||||
|
|
||||||
pub(crate) fn background_jobs_main(
|
pub(crate) fn background_jobs_main(
|
||||||
bus: Bus<BackgroundJob>,
|
bus: Bus<BackgroundJob>,
|
||||||
|
|
@ -100,6 +103,7 @@ pub(crate) fn background_jobs_main(
|
||||||
let last_serialization_time = Arc::new(Mutex::new(Instant::now()));
|
let last_serialization_time = Arc::new(Mutex::new(Instant::now()));
|
||||||
let serialization_interval = serialization_interval.map(|s| s * 1000); // convert to
|
let serialization_interval = serialization_interval.map(|s| s * 1000); // convert to
|
||||||
// milliseconds
|
// milliseconds
|
||||||
|
let last_render_request: Arc<Mutex<Option<Instant>>> = Arc::new(Mutex::new(None));
|
||||||
|
|
||||||
let http_client = HttpClient::builder()
|
let http_client = HttpClient::builder()
|
||||||
// TODO: timeout?
|
// TODO: timeout?
|
||||||
|
|
@ -360,6 +364,53 @@ pub(crate) fn background_jobs_main(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
BackgroundJob::RenderToClients => {
|
||||||
|
// last_render_request being Some() represents a render request that is pending
|
||||||
|
// last_render_request is only ever set to Some() if an async task is spawned to
|
||||||
|
// send the actual render instruction
|
||||||
|
//
|
||||||
|
// given this:
|
||||||
|
// - if last_render_request is None and we received this job, we should spawn an
|
||||||
|
// async task to send the render instruction and log the current task time
|
||||||
|
// - if last_render_request is Some(), it means we're currently waiting to render,
|
||||||
|
// so we should log the render request and do nothing, once the async task has
|
||||||
|
// finished running, it will check to see if the render time was updated while it
|
||||||
|
// was running, and if so send this instruction again so the process can start anew
|
||||||
|
let (should_run_task, current_time) = {
|
||||||
|
let mut last_render_request = last_render_request.lock().unwrap();
|
||||||
|
let should_run_task = last_render_request.is_none();
|
||||||
|
let current_time = Instant::now();
|
||||||
|
*last_render_request = Some(current_time);
|
||||||
|
(should_run_task, current_time)
|
||||||
|
};
|
||||||
|
if should_run_task {
|
||||||
|
task::spawn({
|
||||||
|
let senders = bus.senders.clone();
|
||||||
|
let last_render_request = last_render_request.clone();
|
||||||
|
let task_start_time = current_time;
|
||||||
|
async move {
|
||||||
|
task::sleep(std::time::Duration::from_millis(REPAINT_DELAY_MS)).await;
|
||||||
|
let _ = senders.send_to_screen(ScreenInstruction::Render);
|
||||||
|
{
|
||||||
|
let mut last_render_request = last_render_request.lock().unwrap();
|
||||||
|
if let Some(last_render_request) = *last_render_request {
|
||||||
|
if last_render_request > task_start_time {
|
||||||
|
// another render request was received while we were
|
||||||
|
// sleeping, schedule this job again so that we can also
|
||||||
|
// render that request
|
||||||
|
let _ = senders.send_to_background_jobs(
|
||||||
|
BackgroundJob::RenderToClients,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// reset the last_render_request so that the task will be spawned
|
||||||
|
// again once a new request is received
|
||||||
|
*last_render_request = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
BackgroundJob::Exit => {
|
BackgroundJob::Exit => {
|
||||||
for loading_plugin in loading_plugins.values() {
|
for loading_plugin in loading_plugins.values() {
|
||||||
loading_plugin.store(false, Ordering::SeqCst);
|
loading_plugin.store(false, Ordering::SeqCst);
|
||||||
|
|
|
||||||
|
|
@ -433,7 +433,6 @@ impl Output {
|
||||||
client_serialized_render_instructions.push_str(&vte_instruction);
|
client_serialized_render_instructions.push_str(&vte_instruction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
serialized_render_instructions.insert(client_id, client_serialized_render_instructions);
|
serialized_render_instructions.insert(client_id, client_serialized_render_instructions);
|
||||||
}
|
}
|
||||||
Ok(serialized_render_instructions)
|
Ok(serialized_render_instructions)
|
||||||
|
|
|
||||||
|
|
@ -2900,6 +2900,10 @@ pub(crate) fn screen_thread_main(
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let _ = screen
|
||||||
|
.bus
|
||||||
|
.senders
|
||||||
|
.send_to_background_jobs(BackgroundJob::RenderToClients);
|
||||||
},
|
},
|
||||||
ScreenInstruction::PluginBytes(mut plugin_render_assets) => {
|
ScreenInstruction::PluginBytes(mut plugin_render_assets) => {
|
||||||
for plugin_render_asset in plugin_render_assets.iter_mut() {
|
for plugin_render_asset in plugin_render_assets.iter_mut() {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use crate::{
|
||||||
screen::ScreenInstruction,
|
screen::ScreenInstruction,
|
||||||
thread_bus::ThreadSenders,
|
thread_bus::ThreadSenders,
|
||||||
};
|
};
|
||||||
use async_std::{future::timeout as async_timeout, task};
|
use async_std::task;
|
||||||
use std::{
|
use std::{
|
||||||
os::unix::io::RawFd,
|
os::unix::io::RawFd,
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
|
|
@ -13,32 +13,12 @@ use zellij_utils::{
|
||||||
logging::debug_to_file,
|
logging::debug_to_file,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum ReadResult {
|
|
||||||
Ok(usize),
|
|
||||||
Timeout,
|
|
||||||
Err(std::io::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<std::io::Result<usize>> for ReadResult {
|
|
||||||
fn from(e: std::io::Result<usize>) -> ReadResult {
|
|
||||||
match e {
|
|
||||||
Err(e) => ReadResult::Err(e),
|
|
||||||
Ok(n) => ReadResult::Ok(n),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct TerminalBytes {
|
pub(crate) struct TerminalBytes {
|
||||||
pid: RawFd,
|
pid: RawFd,
|
||||||
terminal_id: u32,
|
terminal_id: u32,
|
||||||
senders: ThreadSenders,
|
senders: ThreadSenders,
|
||||||
async_reader: Box<dyn AsyncReader>,
|
async_reader: Box<dyn AsyncReader>,
|
||||||
debug: bool,
|
debug: bool,
|
||||||
render_deadline: Option<Instant>,
|
|
||||||
backed_up: bool,
|
|
||||||
minimum_render_send_time: Option<Duration>,
|
|
||||||
buffering_pause: Duration,
|
|
||||||
last_render: Instant,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TerminalBytes {
|
impl TerminalBytes {
|
||||||
|
|
@ -55,11 +35,6 @@ impl TerminalBytes {
|
||||||
senders,
|
senders,
|
||||||
debug,
|
debug,
|
||||||
async_reader: os_input.async_file_reader(pid),
|
async_reader: os_input.async_file_reader(pid),
|
||||||
render_deadline: None,
|
|
||||||
backed_up: false,
|
|
||||||
minimum_render_send_time: None,
|
|
||||||
buffering_pause: Duration::from_millis(30),
|
|
||||||
last_render: Instant::now(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub async fn listen(&mut self) -> Result<()> {
|
pub async fn listen(&mut self) -> Result<()> {
|
||||||
|
|
@ -80,25 +55,13 @@ impl TerminalBytes {
|
||||||
err_ctx.add_call(ContextType::AsyncTask);
|
err_ctx.add_call(ContextType::AsyncTask);
|
||||||
let mut buf = [0u8; 65536];
|
let mut buf = [0u8; 65536];
|
||||||
loop {
|
loop {
|
||||||
match self.deadline_read(&mut buf).await {
|
match self.async_reader.read(&mut buf).await {
|
||||||
// EOF
|
Ok(0) => break, // EOF
|
||||||
ReadResult::Ok(0) => break,
|
Err(err) => {
|
||||||
// Some error occured
|
|
||||||
ReadResult::Err(err) => {
|
|
||||||
log::error!("{}", err);
|
log::error!("{}", err);
|
||||||
break;
|
break;
|
||||||
},
|
},
|
||||||
ReadResult::Timeout => {
|
Ok(n_bytes) => {
|
||||||
let time_to_send_render = self
|
|
||||||
.async_send_to_screen(ScreenInstruction::Render)
|
|
||||||
.await
|
|
||||||
.with_context(err_context)?;
|
|
||||||
self.update_render_send_time(time_to_send_render);
|
|
||||||
// next read does not need a deadline as we just rendered everything
|
|
||||||
self.render_deadline = None;
|
|
||||||
self.last_render = Instant::now();
|
|
||||||
},
|
|
||||||
ReadResult::Ok(n_bytes) => {
|
|
||||||
let bytes = &buf[..n_bytes];
|
let bytes = &buf[..n_bytes];
|
||||||
if self.debug {
|
if self.debug {
|
||||||
let _ = debug_to_file(bytes, self.pid);
|
let _ = debug_to_file(bytes, self.pid);
|
||||||
|
|
@ -109,19 +72,6 @@ impl TerminalBytes {
|
||||||
))
|
))
|
||||||
.await
|
.await
|
||||||
.with_context(err_context)?;
|
.with_context(err_context)?;
|
||||||
if !self.backed_up {
|
|
||||||
// we're not backed up, let's send an immediate render instruction
|
|
||||||
let time_to_send_render = self
|
|
||||||
.async_send_to_screen(ScreenInstruction::Render)
|
|
||||||
.await
|
|
||||||
.with_context(err_context)?;
|
|
||||||
self.update_render_send_time(time_to_send_render);
|
|
||||||
self.last_render = Instant::now();
|
|
||||||
}
|
|
||||||
// if we already have a render_deadline we keep it, otherwise we set it
|
|
||||||
// to buffering_pause since the last time we rendered.
|
|
||||||
self.render_deadline
|
|
||||||
.get_or_insert(self.last_render + self.buffering_pause);
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -156,44 +106,4 @@ impl TerminalBytes {
|
||||||
.context("failed to async-send to screen")?;
|
.context("failed to async-send to screen")?;
|
||||||
Ok(sent_at.elapsed())
|
Ok(sent_at.elapsed())
|
||||||
}
|
}
|
||||||
fn update_render_send_time(&mut self, time_to_send_render: Duration) {
|
|
||||||
match self.minimum_render_send_time.as_mut() {
|
|
||||||
Some(minimum_render_time) => {
|
|
||||||
if time_to_send_render < *minimum_render_time {
|
|
||||||
*minimum_render_time = time_to_send_render;
|
|
||||||
}
|
|
||||||
if time_to_send_render > *minimum_render_time * 10 {
|
|
||||||
// sending the render instruction took an especially long time, we can safely
|
|
||||||
// assume the screen thread is backed up and we should only send render
|
|
||||||
// instructions sparingly
|
|
||||||
self.backed_up = true;
|
|
||||||
} else if time_to_send_render < *minimum_render_time * 5 {
|
|
||||||
// the screen thread is not backed up, we atomically unset the backed_up
|
|
||||||
// indication
|
|
||||||
self.backed_up = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => {
|
|
||||||
self.minimum_render_send_time = Some(time_to_send_render);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async fn deadline_read(&mut self, buf: &mut [u8]) -> ReadResult {
|
|
||||||
if !self.backed_up {
|
|
||||||
self.async_reader.read(buf).await.into()
|
|
||||||
} else if let Some(deadline) = self.render_deadline {
|
|
||||||
let timeout = deadline.checked_duration_since(Instant::now());
|
|
||||||
if let Some(timeout) = timeout {
|
|
||||||
match async_timeout(timeout, self.async_reader.read(buf)).await {
|
|
||||||
Ok(res) => res.into(),
|
|
||||||
_ => ReadResult::Timeout,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// deadline has already elapsed
|
|
||||||
ReadResult::Timeout
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.async_reader.read(buf).await.into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -511,6 +511,7 @@ pub enum BackgroundJobContext {
|
||||||
RunCommand,
|
RunCommand,
|
||||||
WebRequest,
|
WebRequest,
|
||||||
ReportPluginList,
|
ReportPluginList,
|
||||||
|
RenderToClients,
|
||||||
Exit,
|
Exit,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue