mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41:05 +00:00
perf: partial rendering progress and composite into a complete UI to reduce CPU consumption caused by frequent progress updates
This commit is contained in:
parent
a735b3234f
commit
e81ebce284
26 changed files with 193 additions and 83 deletions
|
|
@ -67,8 +67,8 @@ rules = [
|
|||
]
|
||||
|
||||
[tasks]
|
||||
micro_workers = 5
|
||||
macro_workers = 10
|
||||
micro_workers = 10
|
||||
macro_workers = 25
|
||||
bizarre_retry = 5
|
||||
image_alloc = 536870912 # 512MB
|
||||
image_bound = [ 0, 0 ]
|
||||
|
|
|
|||
|
|
@ -3,4 +3,3 @@ mod cancel;
|
|||
mod inspect;
|
||||
mod open;
|
||||
mod toggle;
|
||||
mod update;
|
||||
|
|
|
|||
|
|
@ -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!();
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ use tokio::time::sleep;
|
|||
use tracing::debug;
|
||||
use yazi_config::{manager::SortBy, open::Opener, plugin::{PluginRule, MAX_PRELOADERS}, popup::InputCfg, OPEN, PLUGIN};
|
||||
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 crate::{folder::Files, input::Input};
|
||||
|
|
@ -35,7 +35,7 @@ impl Tasks {
|
|||
let new = TasksProgress::from(&*running.read());
|
||||
if last != new {
|
||||
last = new;
|
||||
Tasks::_update(new);
|
||||
emit!(Call(Exec::call("update_progress", vec![]).with_data(new).vec(), Layer::App));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,3 +3,4 @@ mod quit;
|
|||
mod render;
|
||||
mod resize;
|
||||
mod stop;
|
||||
mod update_progress;
|
||||
|
|
|
|||
|
|
@ -29,17 +29,17 @@ impl App {
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
let mut patches = vec![];
|
||||
let mut patch = vec![];
|
||||
for x in frame.area.left()..frame.area.right() {
|
||||
for y in frame.area.top()..frame.area.bottom() {
|
||||
let cell = frame.buffer.get(x, y);
|
||||
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() {
|
||||
term.show_cursor()?;
|
||||
term.set_cursor(x, y)?;
|
||||
|
|
|
|||
41
yazi-fm/src/app/commands/update_progress.rs
Normal file
41
yazi-fm/src/app/commands/update_progress.rs
Normal 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -3,9 +3,11 @@
|
|||
mod header;
|
||||
mod manager;
|
||||
mod preview;
|
||||
mod progress;
|
||||
mod status;
|
||||
|
||||
pub(super) use header::*;
|
||||
pub(super) use manager::*;
|
||||
pub(super) use preview::*;
|
||||
pub(super) use progress::*;
|
||||
pub(super) use status::*;
|
||||
|
|
|
|||
43
yazi-fm/src/components/progress.rs
Normal file
43
yazi-fm/src/components/progress.rs
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -84,6 +84,7 @@ impl<'a> Executor<'a> {
|
|||
|
||||
on!(plugin);
|
||||
on!(plugin_do);
|
||||
on!(update_progress);
|
||||
on!(stop);
|
||||
}
|
||||
|
||||
|
|
@ -192,7 +193,6 @@ impl<'a> Executor<'a> {
|
|||
};
|
||||
}
|
||||
|
||||
on!(update);
|
||||
on!(open);
|
||||
on!(toggle, "close");
|
||||
on!(arrow);
|
||||
|
|
|
|||
|
|
@ -54,4 +54,20 @@ impl Lives {
|
|||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
45
yazi-plugin/preset/components/progress.lua
Normal file
45
yazi-plugin/preset/components/progress.lua
Normal 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
|
||||
|
|
@ -108,47 +108,14 @@ function Status:position()
|
|||
}
|
||||
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)
|
||||
self.area = area
|
||||
|
||||
local left = ui.Line { self:mode(), self:size(), self:name() }
|
||||
local right = ui.Line { self:permissions(), self:percentage(), self:position() }
|
||||
local progress = self:progress(area, right:width())
|
||||
return {
|
||||
ui.Paragraph(area, { left }),
|
||||
ui.Paragraph(area, { right }):align(ui.Paragraph.RIGHT),
|
||||
table.unpack(progress),
|
||||
table.unpack(Progress:render(area, right:width())),
|
||||
}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ local M = {}
|
|||
|
||||
function M:peek()
|
||||
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
|
||||
end
|
||||
|
||||
|
|
@ -14,7 +14,7 @@ function M:seek() end
|
|||
|
||||
function M:preload()
|
||||
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
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ end
|
|||
|
||||
function M:preload()
|
||||
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
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ function M:preload()
|
|||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ function ya.clamp(min, x, max)
|
|||
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)
|
||||
local r = {}
|
||||
for _, v in ipairs(t) do
|
||||
|
|
|
|||
|
|
@ -62,6 +62,8 @@ impl UserData for Bar {
|
|||
}
|
||||
|
||||
impl Renderable for Bar {
|
||||
fn area(&self) -> ratatui::layout::Rect { self.area }
|
||||
|
||||
fn render(self: Box<Self>, buf: &mut ratatui::buffer::Buffer) {
|
||||
if self.area.area() == 0 {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -82,6 +82,8 @@ impl UserData for Border {
|
|||
}
|
||||
|
||||
impl Renderable for Border {
|
||||
fn area(&self) -> ratatui::layout::Rect { self.area }
|
||||
|
||||
fn render(self: Box<Self>, buf: &mut ratatui::buffer::Buffer) {
|
||||
let mut block =
|
||||
ratatui::widgets::Block::default().borders(self.position).border_type(self.type_);
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ pub fn init(lua: &Lua) -> mlua::Result<()> {
|
|||
}
|
||||
|
||||
pub trait Renderable {
|
||||
fn area(&self) -> ratatui::layout::Rect;
|
||||
|
||||
fn render(self: Box<Self>, buf: &mut ratatui::buffer::Buffer);
|
||||
|
||||
fn clone_render(&self, buf: &mut ratatui::buffer::Buffer);
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@ impl UserData for Gauge {
|
|||
}
|
||||
|
||||
impl Renderable for Gauge {
|
||||
fn area(&self) -> ratatui::layout::Rect { self.area }
|
||||
|
||||
fn render(self: Box<Self>, buf: &mut ratatui::buffer::Buffer) {
|
||||
let mut gauge = ratatui::widgets::Gauge::default();
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ impl List {
|
|||
impl UserData for List {}
|
||||
|
||||
impl Renderable for List {
|
||||
fn area(&self) -> ratatui::layout::Rect { self.area }
|
||||
|
||||
fn render(self: Box<Self>, buf: &mut ratatui::buffer::Buffer) {
|
||||
self.inner.render(self.area, buf);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,8 @@ impl UserData for Paragraph {
|
|||
}
|
||||
|
||||
impl Renderable for Paragraph {
|
||||
fn area(&self) -> ratatui::layout::Rect { self.area }
|
||||
|
||||
fn render(self: Box<Self>, buf: &mut ratatui::buffer::Buffer) {
|
||||
let mut p = ratatui::widgets::Paragraph::new(self.text);
|
||||
if let Some(style) = self.style {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use mlua::Lua;
|
||||
use mlua::{IntoLua, Lua, Value};
|
||||
use tokio::fs;
|
||||
|
||||
use crate::bindings::{Cast, Cha, UrlRef};
|
||||
|
|
@ -9,20 +9,29 @@ pub fn install(lua: &Lua) -> mlua::Result<()> {
|
|||
lua.create_table_from([
|
||||
(
|
||||
"write",
|
||||
lua.create_async_function(|_, (url, data): (UrlRef, mlua::String)| async move {
|
||||
Ok(fs::write(&*url, data).await.is_ok())
|
||||
lua.create_async_function(|lua, (url, data): (UrlRef, mlua::String)| async move {
|
||||
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 {
|
||||
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 {
|
||||
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)?),
|
||||
})
|
||||
})?,
|
||||
),
|
||||
])?,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ pub fn init() {
|
|||
lua.load(include_str!("../preset/components/manager.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/progress.lua")).exec()?;
|
||||
lua.load(include_str!("../preset/components/status.lua")).exec()?;
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ pub fn copy_with_progress(from: &Path, to: &Path) -> mpsc::Receiver<Result<u64,
|
|||
select! {
|
||||
res = &mut tick_rx => exit = Some(res.unwrap()),
|
||||
_ = tx.closed() => break,
|
||||
_ = time::sleep(time::Duration::from_secs(1)) => (),
|
||||
_ = time::sleep(time::Duration::from_secs(3)) => (),
|
||||
}
|
||||
|
||||
match exit {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue