perf: partial rendering progress and composite into a complete UI to reduce CPU consumption caused by frequent progress updates

This commit is contained in:
sxyazi 2024-01-13 02:55:44 +08:00
parent a735b3234f
commit e81ebce284
No known key found for this signature in database
26 changed files with 193 additions and 83 deletions

View file

@ -67,8 +67,8 @@ rules = [
] ]
[tasks] [tasks]
micro_workers = 5 micro_workers = 10
macro_workers = 10 macro_workers = 25
bizarre_retry = 5 bizarre_retry = 5
image_alloc = 536870912 # 512MB image_alloc = 536870912 # 512MB
image_bound = [ 0, 0 ] image_bound = [ 0, 0 ]

View file

@ -3,4 +3,3 @@ mod cancel;
mod inspect; mod inspect;
mod open; mod open;
mod toggle; mod toggle;
mod update;

View file

@ -1,28 +0,0 @@
use yazi_shared::{emit, event::Exec, render, Layer};
use crate::tasks::{Tasks, TasksProgress};
pub struct Opt {
progress: TasksProgress,
}
impl TryFrom<&Exec> for Opt {
type Error = ();
fn try_from(e: &Exec) -> Result<Self, Self::Error> { e.take_data().ok_or(()) }
}
impl Tasks {
pub fn _update(progress: TasksProgress) {
emit!(Call(Exec::call("update", vec![]).with_data(Opt { progress }).vec(), Layer::Tasks));
}
pub fn update(&mut self, opt: impl TryInto<Opt>) {
let Ok(opt) = opt.try_into() else {
return;
};
self.progress = opt.progress;
render!();
}
}

View file

@ -4,7 +4,7 @@ use tokio::time::sleep;
use tracing::debug; use tracing::debug;
use yazi_config::{manager::SortBy, open::Opener, plugin::{PluginRule, MAX_PRELOADERS}, popup::InputCfg, OPEN, PLUGIN}; use yazi_config::{manager::SortBy, open::Opener, plugin::{PluginRule, MAX_PRELOADERS}, popup::InputCfg, OPEN, PLUGIN};
use yazi_scheduler::{Scheduler, TaskSummary}; use yazi_scheduler::{Scheduler, TaskSummary};
use yazi_shared::{fs::{File, Url}, term::Term, MIME_DIR}; use yazi_shared::{emit, event::Exec, fs::{File, Url}, term::Term, Layer, MIME_DIR};
use super::{TasksProgress, TASKS_PADDING, TASKS_PERCENT}; use super::{TasksProgress, TASKS_PADDING, TASKS_PERCENT};
use crate::{folder::Files, input::Input}; use crate::{folder::Files, input::Input};
@ -35,7 +35,7 @@ impl Tasks {
let new = TasksProgress::from(&*running.read()); let new = TasksProgress::from(&*running.read());
if last != new { if last != new {
last = new; last = new;
Tasks::_update(new); emit!(Call(Exec::call("update_progress", vec![]).with_data(new).vec(), Layer::App));
} }
} }
}); });

View file

@ -3,3 +3,4 @@ mod quit;
mod render; mod render;
mod resize; mod resize;
mod stop; mod stop;
mod update_progress;

View file

@ -29,17 +29,17 @@ impl App {
return Ok(()); return Ok(());
} }
let mut patches = vec![]; let mut patch = vec![];
for x in frame.area.left()..frame.area.right() { for x in frame.area.left()..frame.area.right() {
for y in frame.area.top()..frame.area.bottom() { for y in frame.area.top()..frame.area.bottom() {
let cell = frame.buffer.get(x, y); let cell = frame.buffer.get(x, y);
if cell.skip { if cell.skip {
patches.push((x, y, cell.clone())); patch.push((x, y, cell.clone()));
} }
} }
} }
term.backend_mut().draw(patches.iter().map(|(x, y, cell)| (*x, *y, cell)))?; term.backend_mut().draw(patch.iter().map(|(x, y, cell)| (*x, *y, cell)))?;
if let Some((x, y)) = self.cx.cursor() { if let Some((x, y)) = self.cx.cursor() {
term.show_cursor()?; term.show_cursor()?;
term.set_cursor(x, y)?; term.set_cursor(x, y)?;

View file

@ -0,0 +1,41 @@
use ratatui::backend::Backend;
use yazi_core::tasks::TasksProgress;
use yazi_shared::event::Exec;
use crate::{app::App, components::Progress, lives::Lives};
pub struct Opt {
progress: TasksProgress,
}
impl TryFrom<&Exec> for Opt {
type Error = ();
fn try_from(e: &Exec) -> Result<Self, Self::Error> {
Ok(Self { progress: e.take_data().ok_or(())? })
}
}
impl App {
pub(crate) fn update_progress(&mut self, opt: impl TryInto<Opt>) {
let Ok(opt) = opt.try_into() else {
return;
};
self.cx.tasks.progress = opt.progress;
let Some(term) = &mut self.term else {
return;
};
Lives::partial_scope(&self.cx, |_| {
for patch in Progress::partial_render(term.current_buffer_mut()) {
term.backend_mut().draw(patch.iter().map(|(x, y, cell)| (*x, *y, cell))).ok();
if let Some((x, y)) = self.cx.cursor() {
term.show_cursor().ok();
term.set_cursor(x, y).ok();
}
term.backend_mut().flush().ok();
}
});
}
}

View file

@ -3,9 +3,11 @@
mod header; mod header;
mod manager; mod manager;
mod preview; mod preview;
mod progress;
mod status; mod status;
pub(super) use header::*; pub(super) use header::*;
pub(super) use manager::*; pub(super) use manager::*;
pub(super) use preview::*; pub(super) use preview::*;
pub(super) use progress::*;
pub(super) use status::*; pub(super) use status::*;

View file

@ -0,0 +1,43 @@
use std::mem;
use mlua::{AnyUserData, Table, TableExt};
use tracing::error;
use yazi_plugin::{cast_to_renderable, LUA};
pub(crate) struct Progress;
impl Progress {
pub(crate) fn partial_render(
buf: &mut ratatui::buffer::Buffer,
) -> Vec<Vec<(u16, u16, ratatui::buffer::Cell)>> {
let mut patches = vec![];
let mut f = || {
let comp: Table = LUA.globals().get("Progress")?;
for widget in comp.call_method::<_, Vec<AnyUserData>>("partial_render", ())? {
let Some(w) = cast_to_renderable(widget) else {
continue;
};
let area = w.area();
w.render(buf);
let mut patch = Vec::with_capacity(area.width as usize * area.height as usize);
for x in area.left()..area.right() {
for y in area.top()..area.bottom() {
patch.push((x, y, mem::take(buf.get_mut(x, y))));
}
}
buf.reset();
patches.push(patch);
}
Ok::<_, anyhow::Error>(())
};
if let Err(e) = f() {
error!("{:?}", e);
}
patches
}
}

View file

@ -84,6 +84,7 @@ impl<'a> Executor<'a> {
on!(plugin); on!(plugin);
on!(plugin_do); on!(plugin_do);
on!(update_progress);
on!(stop); on!(stop);
} }
@ -192,7 +193,6 @@ impl<'a> Executor<'a> {
}; };
} }
on!(update);
on!(open); on!(open);
on!(toggle, "close"); on!(toggle, "close");
on!(arrow); on!(arrow);

View file

@ -54,4 +54,20 @@ impl Lives {
error!("{e}"); error!("{e}");
} }
} }
pub(crate) fn partial_scope<'a>(cx: &'a Ctx, f: impl FnOnce(&Scope<'a, 'a>)) {
let result = LUA.scope(|scope| {
LUA.globals().set(
"cx",
LUA.create_table_from([("tasks", super::Tasks::new(scope, &cx.tasks).make()?)])?,
)?;
f(scope);
Ok(())
});
if let Err(e) = result {
error!("{e}");
}
}
} }

View file

@ -0,0 +1,45 @@
Progress = {
area = ui.Rect.default,
}
function Progress:render(area, offset)
self.area = ui.Rect {
x = math.max(0, area.w - offset - 21),
y = area.y,
w = math.max(0, math.min(20, area.w - offset - 1)),
h = 1,
}
return self:partial_render()
end
-- Progress bars usually need frequent updates to report the latest task progress.
-- We use `partial_render()` to partially render it when there is progress change,
-- which has almost no cost compared to a full render by `render()`.
--
-- However, at this time, we can only access `cx.tasks`. If you need certain data from the complete `cx`,
-- just cache it to `self` during `render()`, and read it in `partial_render()` - this process is referred to as "composition".
function Progress:partial_render()
local progress = cx.tasks.progress
if progress.total == 0 then
return { ui.Paragraph(self.area, {}) }
end
local gauge = ui.Gauge(self.area)
if progress.fail == 0 then
gauge = gauge:gauge_style(THEME.status.progress_normal)
else
gauge = gauge:gauge_style(THEME.status.progress_error)
end
local percent = 99
if progress.found ~= 0 then
percent = math.min(99, ya.round(progress.processed * 100 / progress.found))
end
local left = progress.total - progress.succ
return {
gauge
:percent(percent)
:label(ui.Span(string.format("%3d%%, %d left", percent, left)):style(THEME.status.progress_label)),
}
end

View file

@ -108,47 +108,14 @@ function Status:position()
} }
end end
function Status:progress(area, offset)
local progress = cx.tasks.progress
local left = progress.total - progress.succ
if left == 0 then
return {}
end
local gauge = ui.Gauge(ui.Rect {
x = math.max(0, area.w - offset - 21),
y = area.y,
w = math.max(0, math.min(20, area.w - offset - 1)),
h = 1,
})
if progress.fail == 0 then
gauge = gauge:gauge_style(THEME.status.progress_normal)
else
gauge = gauge:gauge_style(THEME.status.progress_error)
end
local percent = 99
if progress.found ~= 0 then
percent = math.min(99, progress.processed * 100 / progress.found)
end
return {
gauge
:percent(percent)
:label(ui.Span(string.format("%3d%%, %d left", percent, left)):style(THEME.status.progress_label)),
}
end
function Status:render(area) function Status:render(area)
self.area = area self.area = area
local left = ui.Line { self:mode(), self:size(), self:name() } local left = ui.Line { self:mode(), self:size(), self:name() }
local right = ui.Line { self:permissions(), self:percentage(), self:position() } local right = ui.Line { self:permissions(), self:percentage(), self:position() }
local progress = self:progress(area, right:width())
return { return {
ui.Paragraph(area, { left }), ui.Paragraph(area, { left }),
ui.Paragraph(area, { right }):align(ui.Paragraph.RIGHT), ui.Paragraph(area, { right }):align(ui.Paragraph.RIGHT),
table.unpack(progress), table.unpack(Progress:render(area, right:width())),
} }
end end

View file

@ -2,7 +2,7 @@ local M = {}
function M:peek() function M:peek()
local url = ya.file_cache(self) local url = ya.file_cache(self)
if not url or not fs.symlink_metadata(url) then if not url or not fs.cha(url) then
url = self.file.url url = self.file.url
end end
@ -14,7 +14,7 @@ function M:seek() end
function M:preload() function M:preload()
local cache = ya.file_cache(self) local cache = ya.file_cache(self)
if not cache or fs.symlink_metadata(cache) then if not cache or fs.cha(cache) then
return 1 return 1
end end

View file

@ -22,7 +22,7 @@ end
function M:preload() function M:preload()
local cache = ya.file_cache(self) local cache = ya.file_cache(self)
if not cache or fs.symlink_metadata(cache) then if not cache or fs.cha(cache) then
return 1 return 1
end end

View file

@ -30,7 +30,7 @@ function M:preload()
end end
local cache = ya.file_cache(self) local cache = ya.file_cache(self)
if not cache or fs.symlink_metadata(cache) then if not cache or fs.cha(cache) then
return 1 return 1
end end

View file

@ -12,6 +12,8 @@ function ya.clamp(min, x, max)
end end
end end
function ya.round(x) return x >= 0 and math.floor(x + 0.5) or math.ceil(x - 0.5) end
function ya.flat(t) function ya.flat(t)
local r = {} local r = {}
for _, v in ipairs(t) do for _, v in ipairs(t) do

View file

@ -62,6 +62,8 @@ impl UserData for Bar {
} }
impl Renderable for Bar { impl Renderable for Bar {
fn area(&self) -> ratatui::layout::Rect { self.area }
fn render(self: Box<Self>, buf: &mut ratatui::buffer::Buffer) { fn render(self: Box<Self>, buf: &mut ratatui::buffer::Buffer) {
if self.area.area() == 0 { if self.area.area() == 0 {
return; return;

View file

@ -82,6 +82,8 @@ impl UserData for Border {
} }
impl Renderable for Border { impl Renderable for Border {
fn area(&self) -> ratatui::layout::Rect { self.area }
fn render(self: Box<Self>, buf: &mut ratatui::buffer::Buffer) { fn render(self: Box<Self>, buf: &mut ratatui::buffer::Buffer) {
let mut block = let mut block =
ratatui::widgets::Block::default().borders(self.position).border_type(self.type_); ratatui::widgets::Block::default().borders(self.position).border_type(self.type_);

View file

@ -29,6 +29,8 @@ pub fn init(lua: &Lua) -> mlua::Result<()> {
} }
pub trait Renderable { pub trait Renderable {
fn area(&self) -> ratatui::layout::Rect;
fn render(self: Box<Self>, buf: &mut ratatui::buffer::Buffer); fn render(self: Box<Self>, buf: &mut ratatui::buffer::Buffer);
fn clone_render(&self, buf: &mut ratatui::buffer::Buffer); fn clone_render(&self, buf: &mut ratatui::buffer::Buffer);

View file

@ -70,6 +70,8 @@ impl UserData for Gauge {
} }
impl Renderable for Gauge { impl Renderable for Gauge {
fn area(&self) -> ratatui::layout::Rect { self.area }
fn render(self: Box<Self>, buf: &mut ratatui::buffer::Buffer) { fn render(self: Box<Self>, buf: &mut ratatui::buffer::Buffer) {
let mut gauge = ratatui::widgets::Gauge::default(); let mut gauge = ratatui::widgets::Gauge::default();

View file

@ -25,6 +25,8 @@ impl List {
impl UserData for List {} impl UserData for List {}
impl Renderable for List { impl Renderable for List {
fn area(&self) -> ratatui::layout::Rect { self.area }
fn render(self: Box<Self>, buf: &mut ratatui::buffer::Buffer) { fn render(self: Box<Self>, buf: &mut ratatui::buffer::Buffer) {
self.inner.render(self.area, buf); self.inner.render(self.area, buf);
} }

View file

@ -71,6 +71,8 @@ impl UserData for Paragraph {
} }
impl Renderable for Paragraph { impl Renderable for Paragraph {
fn area(&self) -> ratatui::layout::Rect { self.area }
fn render(self: Box<Self>, buf: &mut ratatui::buffer::Buffer) { fn render(self: Box<Self>, buf: &mut ratatui::buffer::Buffer) {
let mut p = ratatui::widgets::Paragraph::new(self.text); let mut p = ratatui::widgets::Paragraph::new(self.text);
if let Some(style) = self.style { if let Some(style) = self.style {

View file

@ -1,4 +1,4 @@
use mlua::Lua; use mlua::{IntoLua, Lua, Value};
use tokio::fs; use tokio::fs;
use crate::bindings::{Cast, Cha, UrlRef}; use crate::bindings::{Cast, Cha, UrlRef};
@ -9,20 +9,29 @@ pub fn install(lua: &Lua) -> mlua::Result<()> {
lua.create_table_from([ lua.create_table_from([
( (
"write", "write",
lua.create_async_function(|_, (url, data): (UrlRef, mlua::String)| async move { lua.create_async_function(|lua, (url, data): (UrlRef, mlua::String)| async move {
Ok(fs::write(&*url, data).await.is_ok()) Ok(match fs::write(&*url, data).await {
Ok(_) => (Value::Boolean(true), Value::Nil),
Err(e) => (Value::Boolean(false), e.raw_os_error().into_lua(lua)?),
})
})?, })?,
), ),
( (
"metadata", "cha",
lua.create_async_function(|lua, url: UrlRef| async move { lua.create_async_function(|lua, url: UrlRef| async move {
fs::metadata(&*url).await.ok().map(|m| Cha::cast(lua, m)).transpose() Ok(match fs::symlink_metadata(&*url).await {
Ok(m) => (Cha::cast(lua, m)?.into_lua(lua)?, Value::Nil),
Err(e) => (Value::Nil, e.raw_os_error().into_lua(lua)?),
})
})?, })?,
), ),
( (
"symlink_metadata", "cha_follow",
lua.create_async_function(|lua, url: UrlRef| async move { lua.create_async_function(|lua, url: UrlRef| async move {
fs::symlink_metadata(&*url).await.ok().map(|m| Cha::cast(lua, m)).transpose() Ok(match fs::metadata(&*url).await {
Ok(m) => (Cha::cast(lua, m)?.into_lua(lua)?, Value::Nil),
Err(e) => (Value::Nil, e.raw_os_error().into_lua(lua)?),
})
})?, })?,
), ),
])?, ])?,

View file

@ -25,6 +25,7 @@ pub fn init() {
lua.load(include_str!("../preset/components/manager.lua")).exec()?; lua.load(include_str!("../preset/components/manager.lua")).exec()?;
lua.load(include_str!("../preset/components/parent.lua")).exec()?; lua.load(include_str!("../preset/components/parent.lua")).exec()?;
lua.load(include_str!("../preset/components/preview.lua")).exec()?; lua.load(include_str!("../preset/components/preview.lua")).exec()?;
lua.load(include_str!("../preset/components/progress.lua")).exec()?;
lua.load(include_str!("../preset/components/status.lua")).exec()?; lua.load(include_str!("../preset/components/status.lua")).exec()?;
Ok(()) Ok(())

View file

@ -61,7 +61,7 @@ pub fn copy_with_progress(from: &Path, to: &Path) -> mpsc::Receiver<Result<u64,
select! { select! {
res = &mut tick_rx => exit = Some(res.unwrap()), res = &mut tick_rx => exit = Some(res.unwrap()),
_ = tx.closed() => break, _ = tx.closed() => break,
_ = time::sleep(time::Duration::from_secs(1)) => (), _ = time::sleep(time::Duration::from_secs(3)) => (),
} }
match exit { match exit {