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>) {
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<File>, 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<usize> { self.iter().position(|f| &f.url == url) }
// --- Ticket
#[inline]
pub fn ticket(&self) -> u64 { self.ticket }
// --- Selected
pub fn selected(&self, pending: &BTreeSet<usize>, 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);
}
}
}

View file

@ -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<SystemTime>,
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) {

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;
@ -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());
}

View file

@ -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);

View file

@ -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();
}
}

View file

@ -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<Url>) {
// 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 {

View file

@ -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(())
}));

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_util::sync::CancellationToken;
use yazi_adaptor::ADAPTOR;
@ -16,7 +16,7 @@ pub struct Preview {
pub skip: usize,
previewer_ct: Option<CancellationToken>,
folder_handle: Option<JoinHandle<()>>,
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<SystemTime>, 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]

View file

@ -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);
}
}

View file

@ -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(())
}

View file

@ -6,12 +6,14 @@ use crate::{term::Term, Layer, RoCell};
static TX: RoCell<UnboundedSender<Event>> = RoCell::new();
#[derive(Debug)]
pub enum Event {
Quit(bool), // no-cwd-file
Key(KeyEvent),
Paste(String),
Resize(u16, u16),
Call(Vec<Exec>, Layer),
Render,
Key(KeyEvent),
Resize(u16, u16),
Paste(String),
Quit(bool), // no-cwd-file
// Manager
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 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<File>),
Full(Url, Vec<File>, Option<SystemTime>),
Part(Url, Vec<File>, u64),
Done(Url, Option<SystemTime>, u64),
Size(Url, BTreeMap<Url, u64>),
Creating(Url, Vec<File>),
@ -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)),