perf: detection of changes in the directory (#476)

This commit is contained in:
三咲雅 · Misaki Masa 2024-01-05 01:53:28 +08:00 committed by sxyazi
parent 63f81f6431
commit 43878fd92e
No known key found for this signature in database
12 changed files with 152 additions and 83 deletions

View file

@ -131,14 +131,12 @@ impl Files {
} }
pub fn update_full(&mut self, files: Vec<File>) { pub fn update_full(&mut self, files: Vec<File>) {
if files.is_empty() {
return;
}
self.ticket = FILES_TICKET.fetch_add(1, Ordering::Relaxed); self.ticket = FILES_TICKET.fetch_add(1, Ordering::Relaxed);
self.revision += 1;
(self.hidden, self.items) = self.split_files(files); (self.hidden, self.items) = self.split_files(files);
if !self.items.is_empty() {
self.revision += 1;
}
} }
pub fn update_part(&mut self, files: Vec<File>, ticket: u64) { pub fn update_part(&mut self, files: Vec<File>, ticket: u64) {
@ -147,8 +145,11 @@ impl Files {
return; return;
} }
self.revision += 1;
let (hidden, items) = self.split_files(files); let (hidden, items) = self.split_files(files);
if !items.is_empty() {
self.revision += 1;
}
self.hidden.extend(hidden); self.hidden.extend(hidden);
self.items.extend(items); self.items.extend(items);
return; return;
@ -340,6 +341,10 @@ impl Files {
#[inline] #[inline]
pub fn position(&self, url: &Url) -> Option<usize> { self.iter().position(|f| &f.url == url) } pub fn position(&self, url: &Url) -> Option<usize> { self.iter().position(|f| &f.url == url) }
// --- Ticket
#[inline]
pub fn ticket(&self) -> u64 { self.ticket }
// --- Selected // --- Selected
pub fn selected(&self, pending: &BTreeSet<usize>, unset: bool) -> Vec<&File> { pub fn selected(&self, pending: &BTreeSet<usize>, unset: bool) -> Vec<&File> {
if self.selected.is_empty() && (unset || pending.is_empty()) { if self.selected.is_empty() && (unset || pending.is_empty()) {
@ -435,8 +440,10 @@ impl Files {
if self.show_hidden { mem::take(&mut self.hidden) } else { mem::take(&mut self.items) }; if self.show_hidden { mem::take(&mut self.hidden) } else { mem::take(&mut self.items) };
let (hidden, items) = self.split_files(take); let (hidden, items) = self.split_files(take);
self.revision += 1;
self.hidden.extend(hidden); self.hidden.extend(hidden);
self.items.extend(items); if !items.is_empty() {
self.revision += 1;
self.items.extend(items);
}
} }
} }

View file

@ -1,3 +1,5 @@
use std::time::SystemTime;
use ratatui::layout::Rect; use ratatui::layout::Rect;
use yazi_config::LAYOUT; use yazi_config::LAYOUT;
use yazi_shared::{emit, fs::{File, FilesOp, Url}}; use yazi_shared::{emit, fs::{File, FilesOp, Url}};
@ -7,6 +9,7 @@ use crate::{folder::Files, Step};
#[derive(Default)] #[derive(Default)]
pub struct Folder { pub struct Folder {
pub cwd: Url, pub cwd: Url,
pub mtime: Option<SystemTime>,
pub files: Files, pub files: Files,
pub offset: usize, pub offset: usize,
@ -25,9 +28,17 @@ impl From<&Url> for Folder {
impl Folder { impl Folder {
pub fn update(&mut self, op: FilesOp) -> bool { pub fn update(&mut self, op: FilesOp) -> bool {
let revision = self.files.revision;
match op { match op {
FilesOp::Full(_, files) => self.files.update_full(files), FilesOp::Full(_, _, mtime) => self.mtime = mtime,
FilesOp::Done(_, mtime, ticket) if ticket == self.files.ticket() => self.mtime = mtime,
_ => {}
}
match op {
FilesOp::Full(_, files, _) => self.files.update_full(files),
FilesOp::Part(_, files, ticket) => self.files.update_part(files, ticket), FilesOp::Part(_, files, ticket) => self.files.update_part(files, ticket),
FilesOp::Done(..) => {}
FilesOp::Size(_, sizes) => self.files.update_size(sizes), FilesOp::Size(_, sizes) => self.files.update_size(sizes),
FilesOp::Creating(_, files) => self.files.update_creating(files), FilesOp::Creating(_, files) => self.files.update_creating(files),
@ -35,10 +46,8 @@ impl Folder {
FilesOp::Updating(_, files) => _ = self.files.update_updating(files), FilesOp::Updating(_, files) => _ = self.files.update_updating(files),
FilesOp::Upserting(_, files) => self.files.update_upserting(files), FilesOp::Upserting(_, files) => self.files.update_upserting(files),
} }
if !self.files.catchup_revision() {
return false;
}
// TODO: use a better way to detect if the page is changed
let old = self.page; let old = self.page;
self.prev(Default::default()); self.prev(Default::default());
@ -46,7 +55,7 @@ impl Folder {
self.set_page(true); // Force update self.set_page(true); // Force update
} }
true self.files.revision != revision
} }
pub fn set_page(&mut self, force: bool) { pub fn set_page(&mut self, force: bool) {

View file

@ -1,4 +1,4 @@
use yazi_shared::{emit, event::Exec, fs::Url, render, Layer, MIME_DIR}; use yazi_shared::{emit, event::Exec, fs::Url, render, Layer};
use crate::manager::Manager; use crate::manager::Manager;
@ -56,16 +56,13 @@ impl Manager {
} }
if hovered.is_dir() { if hovered.is_dir() {
if self.active().history.contains_key(&hovered.url) { let mtime = self.active().history.get(&hovered.url).and_then(|f| f.mtime);
self.active_mut().preview.go(hovered, MIME_DIR, opt.force); self.active_mut().preview.go_folder(hovered, mtime, opt.force);
} else {
self.active_mut().preview.go_folder(hovered, opt.force);
}
return; return;
} }
if let Some(s) = self.mimetype.get(&hovered.url).cloned() { if let Some(m) = self.mimetype.get(&hovered.url).cloned() {
self.active_mut().preview.go(hovered, &s, opt.force); self.active_mut().preview.go(hovered, &m, opt.force);
} else { } else {
render!(self.active_mut().preview.reset()); render!(self.active_mut().preview.reset());
} }

View file

@ -17,9 +17,9 @@ impl Manager {
self.active_mut().apply_files_attrs(); self.active_mut().apply_files_attrs();
if let Some(p) = self.parent() { if let Some(p) = self.parent() {
self.watcher.trigger_dirs(&[self.cwd(), &p.cwd]); self.watcher.trigger_dirs(&[self.current(), p]);
} else { } else {
self.watcher.trigger_dirs(&[self.cwd()]); self.watcher.trigger_dirs(&[self.current()]);
} }
self.hover(None); self.hover(None);

View file

@ -19,6 +19,7 @@ impl Manager {
if let Some(p) = self.active_mut().parent.as_mut() { if let Some(p) = self.active_mut().parent.as_mut() {
render!(p.update(op)); render!(p.update(op));
render!(p.files.catchup_revision());
render!(p.hover(&cwd)); render!(p.hover(&cwd));
} }
@ -32,6 +33,7 @@ impl Manager {
let calc = !matches!(op, FilesOp::Size(..) | FilesOp::Deleting(..)); let calc = !matches!(op, FilesOp::Size(..) | FilesOp::Deleting(..));
render!(self.current_mut().update(op)); render!(self.current_mut().update(op));
render!(self.current_mut().files.catchup_revision());
render!(hovered.as_ref().is_some_and(|h| self.current_mut().hover(h))); render!(hovered.as_ref().is_some_and(|h| self.current_mut().hover(h)));
if hovered.as_ref() != self.hovered().map(|h| &h.url) { if hovered.as_ref() != self.hovered().map(|h| &h.url) {
@ -44,9 +46,20 @@ impl Manager {
fn update_hovered(&mut self, op: FilesOp) { fn update_hovered(&mut self, op: FilesOp) {
let url = op.url(); let url = op.url();
self.active_mut().history.entry(url.clone()).or_insert_with(|| Folder::from(url)).update(op); let folder = match self.active_mut().history.get_mut(url) {
Some(f) => f,
None => {
let f = Folder::from(url);
self.active_mut().history.insert(url.clone(), f);
self.active_mut().apply_files_attrs();
self.active_mut().history.get_mut(url).unwrap()
}
};
self.peek(true); if folder.update(op) {
folder.files.catchup_revision();
self.peek(true);
}
} }
fn update_history(&mut self, op: FilesOp) { fn update_history(&mut self, op: FilesOp) {
@ -84,7 +97,5 @@ impl Manager {
self.update_history(op); self.update_history(op);
} }
} }
self.active_mut().apply_files_attrs();
} }
} }

View file

@ -9,7 +9,7 @@ use yazi_plugin::isolate;
use yazi_shared::fs::{File, FilesOp, Url}; use yazi_shared::fs::{File, FilesOp, Url};
use super::Linked; use super::Linked;
use crate::folder::Files; use crate::folder::{Files, Folder};
pub struct Watcher { pub struct Watcher {
watcher: RecommendedWatcher, watcher: RecommendedWatcher,
@ -83,19 +83,31 @@ impl Watcher {
self.sync_linked(); self.sync_linked();
} }
pub(super) fn trigger_dirs(&self, dirs: &[&Url]) { pub(super) fn trigger_dirs(&self, folders: &[&Folder]) {
let urls: Vec<_> = dirs.iter().filter(|&u| u.is_regular()).map(|&u| u.clone()).collect(); let todo: Vec<_> =
if urls.is_empty() { folders.iter().filter(|&f| f.cwd.is_regular()).map(|&f| (f.cwd.clone(), f.mtime)).collect();
if todo.is_empty() {
return; return;
} }
tokio::spawn(async move { tokio::spawn(async move {
for u in urls { for (url, mtime) in todo {
if let Ok(rx) = Files::from_dir(&u).await { let Ok(meta) = fs::metadata(&url).await else {
if let Ok(m) = fs::symlink_metadata(&url).await {
FilesOp::Full(url, vec![], m.modified().ok()).emit();
} else if let Some(p) = url.parent_url() {
FilesOp::Deleting(p, vec![url]).emit();
}
continue;
};
if meta.modified().ok() == mtime {
continue;
}
if let Ok(rx) = Files::from_dir(&url).await {
let files: Vec<_> = UnboundedReceiverStream::new(rx).collect().await; let files: Vec<_> = UnboundedReceiverStream::new(rx).collect().await;
FilesOp::Full(u, files).emit(); FilesOp::Full(url, files, meta.modified().ok()).emit();
} else if let Some(p) = u.parent_url() {
FilesOp::Deleting(p, vec![u]).emit();
} }
} }
}); });
@ -129,7 +141,7 @@ impl Watcher {
async fn on_changed(rx: UnboundedReceiver<Url>) { async fn on_changed(rx: UnboundedReceiver<Url>) {
// TODO: revert this once a new notification is implemented // TODO: revert this once a new notification is implemented
let rx = UnboundedReceiverStream::new(rx).chunks_timeout(10, Duration::from_millis(20)); let rx = UnboundedReceiverStream::new(rx).chunks_timeout(50, Duration::from_millis(20));
pin!(rx); pin!(rx);
while let Some(urls) = rx.next().await { while let Some(urls) = rx.next().await {

View file

@ -59,15 +59,14 @@ impl Tab {
let rx = UnboundedReceiverStream::new(rx).chunks_timeout(1000, Duration::from_millis(300)); let rx = UnboundedReceiverStream::new(rx).chunks_timeout(1000, Duration::from_millis(300));
pin!(rx); pin!(rx);
let ticket = FilesOp::prepare(&cwd); let (mut first, ticket) = (true, FilesOp::prepare(&cwd));
let mut first = true;
while let Some(chunk) = rx.next().await { while let Some(chunk) = rx.next().await {
if first { if mem::replace(&mut first, false) {
Tab::_cd(&cwd); Tab::_cd(&cwd);
first = false;
} }
FilesOp::Part(cwd.clone(), chunk, ticket).emit(); FilesOp::Part(cwd.clone(), chunk, ticket).emit();
} }
FilesOp::Done(cwd, None, ticket).emit();
Ok(()) Ok(())
})); }));

View file

@ -1,6 +1,6 @@
use std::time::Duration; use std::time::{Duration, SystemTime};
use tokio::{pin, task::JoinHandle}; use tokio::{fs, pin, task::JoinHandle};
use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt};
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use yazi_adaptor::ADAPTOR; use yazi_adaptor::ADAPTOR;
@ -16,7 +16,7 @@ pub struct Preview {
pub skip: usize, pub skip: usize,
previewer_ct: Option<CancellationToken>, previewer_ct: Option<CancellationToken>,
folder_handle: Option<JoinHandle<()>>, folder_loader: Option<(Url, JoinHandle<()>)>,
} }
impl Preview { impl Preview {
@ -38,29 +38,51 @@ impl Preview {
} }
} }
pub fn go_folder(&mut self, file: File, force: bool) { pub fn go_folder(&mut self, file: File, mtime: Option<SystemTime>, force: bool) {
if !force && self.content_unchanged(&file.url, &file.cha) { if !force && self.content_unchanged(&file.url, &file.cha) {
return; return;
} }
self.go(file.clone(), MIME_DIR, force); let url = file.url();
self.go(file, MIME_DIR, force);
self.folder_handle.take().map(|h| h.abort()); if matches!(&self.folder_loader, Some((u, _)) if *u == url) {
self.folder_handle = Some(tokio::spawn(async move { return;
let Ok(rx) = Files::from_dir(&file.url).await else { }
file.url.parent_url().map(|p| FilesOp::Deleting(p, vec![file.url]).emit());
return;
};
let stream = self.folder_loader.take().map(|(_, h)| h.abort());
UnboundedReceiverStream::new(rx).chunks_timeout(10000, Duration::from_millis(350)); self.folder_loader = Some((
pin!(stream); url.clone(),
tokio::spawn(async move {
let Ok(meta) = fs::metadata(&url).await else {
if let Ok(m) = fs::symlink_metadata(&url).await {
FilesOp::Full(url, vec![], m.modified().ok()).emit();
} else if let Some(p) = url.parent_url() {
FilesOp::Deleting(p, vec![url]).emit();
}
return;
};
let ticket = FilesOp::prepare(&file.url); if meta.modified().ok() == mtime {
while let Some(chunk) = stream.next().await { return;
FilesOp::Part(file.url.clone(), chunk, ticket).emit(); }
}
})); let Ok(rx) = Files::from_dir(&url).await else {
FilesOp::Full(url, vec![], meta.modified().ok()).emit();
return;
};
let stream =
UnboundedReceiverStream::new(rx).chunks_timeout(10000, Duration::from_millis(350));
pin!(stream);
let ticket = FilesOp::prepare(&url);
while let Some(chunk) = stream.next().await {
FilesOp::Part(url.clone(), chunk, ticket).emit();
}
FilesOp::Done(url, meta.modified().ok(), ticket).emit();
}),
));
} }
#[inline] #[inline]

View file

@ -82,15 +82,15 @@ impl Tab {
render!(f.repos(hovered)); render!(f.repos(hovered));
}; };
if let Some(f) =
self.current.hovered().filter(|h| h.is_dir()).and_then(|h| self.history.get_mut(&h.url))
{
apply(f);
}
apply(&mut self.current); apply(&mut self.current);
if let Some(parent) = self.parent.as_mut() {
apply(parent); self
} .current
.hovered()
.filter(|h| h.is_dir())
.and_then(|h| self.history.get_mut(&h.url))
.map(apply);
self.parent.as_mut().map(apply);
} }
} }

View file

@ -4,7 +4,7 @@ use anyhow::{Ok, Result};
use crossterm::event::KeyEvent; use crossterm::event::KeyEvent;
use yazi_config::keymap::Key; use yazi_config::keymap::Key;
use yazi_core::input::InputMode; use yazi_core::input::InputMode;
use yazi_shared::{event::{Event, Exec, NEED_RENDER}, term::Term, Layer}; use yazi_shared::{emit, event::{Event, Exec, NEED_RENDER}, term::Term, Layer};
use crate::{lives::Lives, Ctx, Executor, Logs, Panic, Signals}; use crate::{lives::Lives, Ctx, Executor, Logs, Panic, Signals};
@ -26,24 +26,31 @@ impl App {
app.render()?; app.render()?;
let mut events = Vec::with_capacity(10); let mut events = Vec::with_capacity(10);
let mut render_in_place = false;
while app.signals.rx.recv_many(&mut events, 10).await > 0 { while app.signals.rx.recv_many(&mut events, 10).await > 0 {
for event in events.drain(..) { for event in events.drain(..) {
match event { match event {
Event::Call(exec, layer) => app.dispatch_call(exec, layer),
Event::Render => render_in_place = true,
Event::Key(key) => app.dispatch_key(key),
Event::Resize(cols, rows) => app.dispatch_resize(cols, rows)?,
Event::Paste(str) => app.dispatch_paste(str),
Event::Quit(no_cwd_file) => { Event::Quit(no_cwd_file) => {
app.quit(no_cwd_file)?; app.quit(no_cwd_file)?;
break; break;
} }
Event::Key(key) => app.dispatch_key(key),
Event::Paste(str) => app.dispatch_paste(str),
Event::Resize(cols, rows) => app.dispatch_resize(cols, rows)?,
Event::Call(exec, layer) => app.dispatch_call(exec, layer),
event => app.dispatch_module(event), event => app.dispatch_module(event),
} }
} }
if NEED_RENDER.swap(false, Ordering::Relaxed) { if render_in_place {
render_in_place = false;
app.render()?; app.render()?;
} }
if NEED_RENDER.swap(false, Ordering::Relaxed) {
emit!(Render);
}
} }
Ok(()) Ok(())
} }

View file

@ -6,12 +6,14 @@ use crate::{term::Term, Layer, RoCell};
static TX: RoCell<UnboundedSender<Event>> = RoCell::new(); static TX: RoCell<UnboundedSender<Event>> = RoCell::new();
#[derive(Debug)]
pub enum Event { pub enum Event {
Quit(bool), // no-cwd-file
Key(KeyEvent),
Paste(String),
Resize(u16, u16),
Call(Vec<Exec>, Layer), Call(Vec<Exec>, Layer),
Render,
Key(KeyEvent),
Resize(u16, u16),
Paste(String),
Quit(bool), // no-cwd-file
// Manager // Manager
Pages(usize), Pages(usize),

View file

@ -1,4 +1,4 @@
use std::{collections::BTreeMap, sync::atomic::{AtomicU64, Ordering}}; use std::{collections::BTreeMap, sync::atomic::{AtomicU64, Ordering}, time::SystemTime};
use super::File; use super::File;
use crate::{emit, event::Exec, fs::Url, Layer}; use crate::{emit, event::Exec, fs::Url, Layer};
@ -7,8 +7,9 @@ pub static FILES_TICKET: AtomicU64 = AtomicU64::new(0);
#[derive(Debug)] #[derive(Debug)]
pub enum FilesOp { pub enum FilesOp {
Full(Url, Vec<File>), Full(Url, Vec<File>, Option<SystemTime>),
Part(Url, Vec<File>, u64), Part(Url, Vec<File>, u64),
Done(Url, Option<SystemTime>, u64),
Size(Url, BTreeMap<Url, u64>), Size(Url, BTreeMap<Url, u64>),
Creating(Url, Vec<File>), Creating(Url, Vec<File>),
@ -21,8 +22,9 @@ impl FilesOp {
#[inline] #[inline]
pub fn url(&self) -> &Url { pub fn url(&self) -> &Url {
match self { match self {
Self::Full(url, _) => url, Self::Full(url, ..) => url,
Self::Part(url, ..) => url, Self::Part(url, ..) => url,
Self::Done(url, ..) => url,
Self::Size(url, _) => url, Self::Size(url, _) => url,
Self::Creating(url, _) => url, Self::Creating(url, _) => url,
@ -75,8 +77,9 @@ impl FilesOp {
let u = new.clone(); let u = new.clone();
match self { match self {
Self::Full(_, files) => Self::Full(u, files!(files)), Self::Full(_, files, mtime) => Self::Full(u, files!(files), *mtime),
Self::Part(_, files, ticket) => Self::Part(u, files!(files), *ticket), Self::Part(_, files, ticket) => Self::Part(u, files!(files), *ticket),
Self::Done(_, mtime, ticket) => Self::Done(u, *mtime, *ticket),
Self::Size(_, map) => Self::Size(u, map.iter().map(|(k, v)| (new!(k), *v)).collect()), Self::Size(_, map) => Self::Size(u, map.iter().map(|(k, v)| (new!(k), *v)).collect()),
Self::Creating(_, files) => Self::Creating(u, files!(files)), Self::Creating(_, files) => Self::Creating(u, files!(files)),