From 43878fd92e35a8c06884ec80bbec30257fc0373b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20=C2=B7=20Misaki=20Masa?= Date: Fri, 5 Jan 2024 01:53:28 +0800 Subject: [PATCH] perf: detection of changes in the directory (#476) --- yazi-core/src/folder/files.rs | 23 ++++--- yazi-core/src/folder/folder.rs | 19 ++++-- yazi-core/src/manager/commands/peek.rs | 13 ++-- yazi-core/src/manager/commands/refresh.rs | 4 +- .../src/manager/commands/update_files.rs | 19 ++++-- yazi-core/src/manager/watcher.rs | 32 ++++++---- yazi-core/src/tab/commands/search.rs | 7 +-- yazi-core/src/tab/preview.rs | 60 +++++++++++++------ yazi-core/src/tab/tab.rs | 18 +++--- yazi-fm/src/app/app.rs | 19 ++++-- yazi-shared/src/event/event.rs | 10 ++-- yazi-shared/src/fs/op.rs | 11 ++-- 12 files changed, 152 insertions(+), 83 deletions(-) diff --git a/yazi-core/src/folder/files.rs b/yazi-core/src/folder/files.rs index 24d744e1..df31b365 100644 --- a/yazi-core/src/folder/files.rs +++ b/yazi-core/src/folder/files.rs @@ -131,14 +131,12 @@ impl Files { } pub fn update_full(&mut self, files: Vec) { - if files.is_empty() { - return; - } - self.ticket = FILES_TICKET.fetch_add(1, Ordering::Relaxed); - self.revision += 1; (self.hidden, self.items) = self.split_files(files); + if !self.items.is_empty() { + self.revision += 1; + } } pub fn update_part(&mut self, files: Vec, ticket: u64) { @@ -147,8 +145,11 @@ impl Files { return; } - self.revision += 1; let (hidden, items) = self.split_files(files); + if !items.is_empty() { + self.revision += 1; + } + self.hidden.extend(hidden); self.items.extend(items); return; @@ -340,6 +341,10 @@ impl Files { #[inline] pub fn position(&self, url: &Url) -> Option { self.iter().position(|f| &f.url == url) } + // --- Ticket + #[inline] + pub fn ticket(&self) -> u64 { self.ticket } + // --- Selected pub fn selected(&self, pending: &BTreeSet, unset: bool) -> Vec<&File> { 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) }; let (hidden, items) = self.split_files(take); - self.revision += 1; self.hidden.extend(hidden); - self.items.extend(items); + if !items.is_empty() { + self.revision += 1; + self.items.extend(items); + } } } diff --git a/yazi-core/src/folder/folder.rs b/yazi-core/src/folder/folder.rs index 2b8a9b35..4daa3e23 100644 --- a/yazi-core/src/folder/folder.rs +++ b/yazi-core/src/folder/folder.rs @@ -1,3 +1,5 @@ +use std::time::SystemTime; + use ratatui::layout::Rect; use yazi_config::LAYOUT; use yazi_shared::{emit, fs::{File, FilesOp, Url}}; @@ -7,6 +9,7 @@ use crate::{folder::Files, Step}; #[derive(Default)] pub struct Folder { pub cwd: Url, + pub mtime: Option, pub files: Files, pub offset: usize, @@ -25,9 +28,17 @@ impl From<&Url> for Folder { impl Folder { pub fn update(&mut self, op: FilesOp) -> bool { + let revision = self.files.revision; 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::Done(..) => {} FilesOp::Size(_, sizes) => self.files.update_size(sizes), FilesOp::Creating(_, files) => self.files.update_creating(files), @@ -35,10 +46,8 @@ impl Folder { FilesOp::Updating(_, files) => _ = self.files.update_updating(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; self.prev(Default::default()); @@ -46,7 +55,7 @@ impl Folder { self.set_page(true); // Force update } - true + self.files.revision != revision } pub fn set_page(&mut self, force: bool) { diff --git a/yazi-core/src/manager/commands/peek.rs b/yazi-core/src/manager/commands/peek.rs index 8bb8ffa1..bb12712a 100644 --- a/yazi-core/src/manager/commands/peek.rs +++ b/yazi-core/src/manager/commands/peek.rs @@ -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; @@ -56,16 +56,13 @@ impl Manager { } if hovered.is_dir() { - if self.active().history.contains_key(&hovered.url) { - self.active_mut().preview.go(hovered, MIME_DIR, opt.force); - } else { - self.active_mut().preview.go_folder(hovered, opt.force); - } + let mtime = self.active().history.get(&hovered.url).and_then(|f| f.mtime); + self.active_mut().preview.go_folder(hovered, mtime, opt.force); return; } - if let Some(s) = self.mimetype.get(&hovered.url).cloned() { - self.active_mut().preview.go(hovered, &s, opt.force); + if let Some(m) = self.mimetype.get(&hovered.url).cloned() { + self.active_mut().preview.go(hovered, &m, opt.force); } else { render!(self.active_mut().preview.reset()); } diff --git a/yazi-core/src/manager/commands/refresh.rs b/yazi-core/src/manager/commands/refresh.rs index f538e4af..ee91f8fe 100644 --- a/yazi-core/src/manager/commands/refresh.rs +++ b/yazi-core/src/manager/commands/refresh.rs @@ -17,9 +17,9 @@ impl Manager { self.active_mut().apply_files_attrs(); if let Some(p) = self.parent() { - self.watcher.trigger_dirs(&[self.cwd(), &p.cwd]); + self.watcher.trigger_dirs(&[self.current(), p]); } else { - self.watcher.trigger_dirs(&[self.cwd()]); + self.watcher.trigger_dirs(&[self.current()]); } self.hover(None); diff --git a/yazi-core/src/manager/commands/update_files.rs b/yazi-core/src/manager/commands/update_files.rs index 491d596f..6f165af9 100644 --- a/yazi-core/src/manager/commands/update_files.rs +++ b/yazi-core/src/manager/commands/update_files.rs @@ -19,6 +19,7 @@ impl Manager { if let Some(p) = self.active_mut().parent.as_mut() { render!(p.update(op)); + render!(p.files.catchup_revision()); render!(p.hover(&cwd)); } @@ -32,6 +33,7 @@ impl Manager { let calc = !matches!(op, FilesOp::Size(..) | FilesOp::Deleting(..)); 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))); if hovered.as_ref() != self.hovered().map(|h| &h.url) { @@ -44,9 +46,20 @@ impl Manager { fn update_hovered(&mut self, op: FilesOp) { 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) { @@ -84,7 +97,5 @@ impl Manager { self.update_history(op); } } - - self.active_mut().apply_files_attrs(); } } diff --git a/yazi-core/src/manager/watcher.rs b/yazi-core/src/manager/watcher.rs index 6b251744..42546080 100644 --- a/yazi-core/src/manager/watcher.rs +++ b/yazi-core/src/manager/watcher.rs @@ -9,7 +9,7 @@ use yazi_plugin::isolate; use yazi_shared::fs::{File, FilesOp, Url}; use super::Linked; -use crate::folder::Files; +use crate::folder::{Files, Folder}; pub struct Watcher { watcher: RecommendedWatcher, @@ -83,19 +83,31 @@ impl Watcher { self.sync_linked(); } - pub(super) fn trigger_dirs(&self, dirs: &[&Url]) { - let urls: Vec<_> = dirs.iter().filter(|&u| u.is_regular()).map(|&u| u.clone()).collect(); - if urls.is_empty() { + pub(super) fn trigger_dirs(&self, folders: &[&Folder]) { + let todo: Vec<_> = + folders.iter().filter(|&f| f.cwd.is_regular()).map(|&f| (f.cwd.clone(), f.mtime)).collect(); + if todo.is_empty() { return; } tokio::spawn(async move { - for u in urls { - if let Ok(rx) = Files::from_dir(&u).await { + for (url, mtime) in todo { + 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; - FilesOp::Full(u, files).emit(); - } else if let Some(p) = u.parent_url() { - FilesOp::Deleting(p, vec![u]).emit(); + FilesOp::Full(url, files, meta.modified().ok()).emit(); } } }); @@ -129,7 +141,7 @@ impl Watcher { async fn on_changed(rx: UnboundedReceiver) { // 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); while let Some(urls) = rx.next().await { diff --git a/yazi-core/src/tab/commands/search.rs b/yazi-core/src/tab/commands/search.rs index b5cd4ee1..909c429e 100644 --- a/yazi-core/src/tab/commands/search.rs +++ b/yazi-core/src/tab/commands/search.rs @@ -59,15 +59,14 @@ impl Tab { let rx = UnboundedReceiverStream::new(rx).chunks_timeout(1000, Duration::from_millis(300)); pin!(rx); - let ticket = FilesOp::prepare(&cwd); - let mut first = true; + let (mut first, ticket) = (true, FilesOp::prepare(&cwd)); while let Some(chunk) = rx.next().await { - if first { + if mem::replace(&mut first, false) { Tab::_cd(&cwd); - first = false; } FilesOp::Part(cwd.clone(), chunk, ticket).emit(); } + FilesOp::Done(cwd, None, ticket).emit(); Ok(()) })); diff --git a/yazi-core/src/tab/preview.rs b/yazi-core/src/tab/preview.rs index ea310cef..4db88e1b 100644 --- a/yazi-core/src/tab/preview.rs +++ b/yazi-core/src/tab/preview.rs @@ -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_util::sync::CancellationToken; use yazi_adaptor::ADAPTOR; @@ -16,7 +16,7 @@ pub struct Preview { pub skip: usize, previewer_ct: Option, - folder_handle: Option>, + folder_loader: Option<(Url, JoinHandle<()>)>, } 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, force: bool) { if !force && self.content_unchanged(&file.url, &file.cha) { 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()); - self.folder_handle = Some(tokio::spawn(async move { - let Ok(rx) = Files::from_dir(&file.url).await else { - file.url.parent_url().map(|p| FilesOp::Deleting(p, vec![file.url]).emit()); - return; - }; + if matches!(&self.folder_loader, Some((u, _)) if *u == url) { + return; + } - let stream = - UnboundedReceiverStream::new(rx).chunks_timeout(10000, Duration::from_millis(350)); - pin!(stream); + self.folder_loader.take().map(|(_, h)| h.abort()); + self.folder_loader = Some(( + 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); - while let Some(chunk) = stream.next().await { - FilesOp::Part(file.url.clone(), chunk, ticket).emit(); - } - })); + if meta.modified().ok() == mtime { + return; + } + + 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] diff --git a/yazi-core/src/tab/tab.rs b/yazi-core/src/tab/tab.rs index 31694759..071bf15c 100644 --- a/yazi-core/src/tab/tab.rs +++ b/yazi-core/src/tab/tab.rs @@ -82,15 +82,15 @@ impl Tab { 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); - 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); } } diff --git a/yazi-fm/src/app/app.rs b/yazi-fm/src/app/app.rs index 15059a7b..5ccacb42 100644 --- a/yazi-fm/src/app/app.rs +++ b/yazi-fm/src/app/app.rs @@ -4,7 +4,7 @@ use anyhow::{Ok, Result}; use crossterm::event::KeyEvent; use yazi_config::keymap::Key; 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}; @@ -26,24 +26,31 @@ impl App { app.render()?; let mut events = Vec::with_capacity(10); + let mut render_in_place = false; while app.signals.rx.recv_many(&mut events, 10).await > 0 { for event in events.drain(..) { 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) => { app.quit(no_cwd_file)?; 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), } } - if NEED_RENDER.swap(false, Ordering::Relaxed) { + if render_in_place { + render_in_place = false; app.render()?; } + + if NEED_RENDER.swap(false, Ordering::Relaxed) { + emit!(Render); + } } Ok(()) } diff --git a/yazi-shared/src/event/event.rs b/yazi-shared/src/event/event.rs index 3a410a4d..1cc2223f 100644 --- a/yazi-shared/src/event/event.rs +++ b/yazi-shared/src/event/event.rs @@ -6,12 +6,14 @@ use crate::{term::Term, Layer, RoCell}; static TX: RoCell> = RoCell::new(); +#[derive(Debug)] pub enum Event { - Quit(bool), // no-cwd-file - Key(KeyEvent), - Paste(String), - Resize(u16, u16), Call(Vec, Layer), + Render, + Key(KeyEvent), + Resize(u16, u16), + Paste(String), + Quit(bool), // no-cwd-file // Manager Pages(usize), diff --git a/yazi-shared/src/fs/op.rs b/yazi-shared/src/fs/op.rs index 7310ae35..96a4d5df 100644 --- a/yazi-shared/src/fs/op.rs +++ b/yazi-shared/src/fs/op.rs @@ -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 crate::{emit, event::Exec, fs::Url, Layer}; @@ -7,8 +7,9 @@ pub static FILES_TICKET: AtomicU64 = AtomicU64::new(0); #[derive(Debug)] pub enum FilesOp { - Full(Url, Vec), + Full(Url, Vec, Option), Part(Url, Vec, u64), + Done(Url, Option, u64), Size(Url, BTreeMap), Creating(Url, Vec), @@ -21,8 +22,9 @@ impl FilesOp { #[inline] pub fn url(&self) -> &Url { match self { - Self::Full(url, _) => url, + Self::Full(url, ..) => url, Self::Part(url, ..) => url, + Self::Done(url, ..) => url, Self::Size(url, _) => url, Self::Creating(url, _) => url, @@ -75,8 +77,9 @@ impl FilesOp { let u = new.clone(); 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::Done(_, mtime, ticket) => Self::Done(u, *mtime, *ticket), Self::Size(_, map) => Self::Size(u, map.iter().map(|(k, v)| (new!(k), *v)).collect()), Self::Creating(_, files) => Self::Creating(u, files!(files)),