perf: make preload tasks discardable (#2875)

This commit is contained in:
三咲雅 misaki masa 2025-06-14 16:13:48 +08:00 committed by GitHub
parent aae3f9ea4a
commit 917fee939f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 145 additions and 63 deletions

6
Cargo.lock generated
View file

@ -1308,9 +1308,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.172" version = "0.2.173"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" checksum = "d8cfeafaffdbc32176b64fb251369d52ea9f0a8fbc6f8759edffef7b525d64bb"
[[package]] [[package]]
name = "libfuzzer-sys" name = "libfuzzer-sys"
@ -3735,6 +3735,7 @@ dependencies = [
"parking_lot", "parking_lot",
"scopeguard", "scopeguard",
"tokio", "tokio",
"tokio-util",
"tracing", "tracing",
"trash", "trash",
"yazi-config", "yazi-config",
@ -3752,6 +3753,7 @@ version = "25.6.11"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"crossterm 0.29.0", "crossterm 0.29.0",
"foldhash",
"futures", "futures",
"libc", "libc",
"memchr", "memchr",

View file

@ -32,7 +32,7 @@ foldhash = "0.1.5"
futures = "0.3.31" futures = "0.3.31"
globset = "0.4.16" globset = "0.4.16"
indexmap = { version = "2.9.0", features = [ "serde" ] } indexmap = { version = "2.9.0", features = [ "serde" ] }
libc = "0.2.172" libc = "0.2.173"
lru = "0.14.0" lru = "0.14.0"
mlua = { version = "0.10.5", features = [ "anyhow", "async", "error-send", "lua54", "macros", "serialize" ] } mlua = { version = "0.10.5", features = [ "anyhow", "async", "error-send", "lua54", "macros", "serialize" ] }
objc = "0.2.7" objc = "0.2.7"

View file

@ -122,7 +122,7 @@ impl Watcher {
async fn fan_out(rx: UnboundedReceiver<Url>) { async fn fan_out(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(1000, Duration::from_millis(350)); let rx = UnboundedReceiverStream::new(rx).chunks_timeout(1000, Duration::from_millis(250));
pin!(rx); pin!(rx);
while let Some(chunk) = rx.next().await { while let Some(chunk) = rx.next().await {

View file

@ -9,7 +9,7 @@ impl Tasks {
let mut loaded = self.scheduler.prework.loaded.lock(); let mut loaded = self.scheduler.prework.loaded.lock();
let mut tasks: [Vec<_>; MAX_PREWORKERS as usize] = Default::default(); let mut tasks: [Vec<_>; MAX_PREWORKERS as usize] = Default::default();
for f in paged { for f in paged {
let hash = f.hash(); let hash = f.hash_u64();
for g in YAZI.plugin.fetchers(&f.url, mimetype.by_file(f).unwrap_or_default()) { for g in YAZI.plugin.fetchers(&f.url, mimetype.by_file(f).unwrap_or_default()) {
match loaded.get_mut(&hash) { match loaded.get_mut(&hash) {
Some(n) if *n & (1 << g.idx) != 0 => continue, Some(n) if *n & (1 << g.idx) != 0 => continue,
@ -31,7 +31,7 @@ impl Tasks {
pub fn preload_paged(&self, paged: &[File], mimetype: &Mimetype) { pub fn preload_paged(&self, paged: &[File], mimetype: &Mimetype) {
let mut loaded = self.scheduler.prework.loaded.lock(); let mut loaded = self.scheduler.prework.loaded.lock();
for f in paged { for f in paged {
let hash = f.hash(); let hash = f.hash_u64();
for p in YAZI.plugin.preloaders(&f.url, mimetype.by_file(f).unwrap_or_default()) { for p in YAZI.plugin.preloaders(&f.url, mimetype.by_file(f).unwrap_or_default()) {
match loaded.get_mut(&hash) { match loaded.get_mut(&hash) {
Some(n) if *n & (1 << p.idx) != 0 => continue, Some(n) if *n & (1 << p.idx) != 0 => continue,
@ -49,7 +49,7 @@ impl Tasks {
} }
let targets: Vec<_> = { let targets: Vec<_> = {
let loading = self.scheduler.prework.size_loading.read(); let loading = self.scheduler.prework.sizing.read();
targets targets
.iter() .iter()
.filter(|f| f.is_dir() && !targets.sizes.contains_key(f.urn()) && !loading.contains(&f.url)) .filter(|f| f.is_dir() && !targets.sizes.contains_key(f.urn()) && !loading.contains(&f.url))
@ -60,7 +60,7 @@ impl Tasks {
return; return;
} }
let mut loading = self.scheduler.prework.size_loading.write(); let mut loading = self.scheduler.prework.sizing.write();
for &target in &targets { for &target in &targets {
loading.insert(target.clone()); loading.insert(target.clone());
} }

View file

@ -45,7 +45,7 @@ impl File {
} }
#[inline] #[inline]
pub fn hash(&self) -> u64 { pub fn hash_u64(&self) -> u64 {
let mut h = foldhash::fast::FixedState::default().build_hasher(); let mut h = foldhash::fast::FixedState::default().build_hasher();
self.url.hash(&mut h); self.url.hash(&mut h);
h.write_u8(0); h.write_u8(0);

View file

@ -1,5 +1,6 @@
use mlua::{ExternalResult, IntoLua, ObjectLike}; use mlua::{ExternalError, ExternalResult, HookTriggers, IntoLua, ObjectLike, VmState};
use tokio::runtime::Handle; use tokio::{runtime::Handle, select};
use tokio_util::sync::CancellationToken;
use yazi_binding::Error; use yazi_binding::Error;
use yazi_config::LAYOUT; use yazi_config::LAYOUT;
use yazi_dds::Sendable; use yazi_dds::Sendable;
@ -11,21 +12,49 @@ use crate::{elements::Rect, file::File, loader::LOADER};
pub async fn preload( pub async fn preload(
cmd: &'static Cmd, cmd: &'static Cmd,
file: yazi_fs::File, file: yazi_fs::File,
ct: CancellationToken,
) -> mlua::Result<(bool, Option<Error>)> { ) -> mlua::Result<(bool, Option<Error>)> {
LOADER.ensure(&cmd.name, |_| ()).await.into_lua_err()?; let ct_ = ct.clone();
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
let lua = slim_lua(&cmd.name)?; let future = async {
let plugin = LOADER.load_once(&lua, &cmd.name)?; LOADER.ensure(&cmd.name, |_| ()).await.into_lua_err()?;
let job = lua.create_table_from([ let lua = slim_lua(&cmd.name)?;
("area", Rect::from(LAYOUT.get().preview).into_lua(&lua)?), lua.set_hook(
("args", Sendable::args_to_table_ref(&lua, &cmd.args)?.into_lua(&lua)?), HookTriggers::new().on_calls().on_returns().every_nth_instruction(2000),
("file", File::new(file).into_lua(&lua)?), move |_, dbg| {
("skip", 0.into_lua(&lua)?), if ct.is_cancelled() && dbg.source().what != "C" {
])?; Err("Preload task cancelled".into_lua_err())
} else {
Ok(VmState::Continue)
}
},
);
Handle::current().block_on(plugin.call_async_method("preload", job)) let plugin = LOADER.load_once(&lua, &cmd.name)?;
let job = lua.create_table_from([
("area", Rect::from(LAYOUT.get().preview).into_lua(&lua)?),
("args", Sendable::args_to_table_ref(&lua, &cmd.args)?.into_lua(&lua)?),
("file", File::new(file).into_lua(&lua)?),
("skip", 0.into_lua(&lua)?),
])?;
if ct_.is_cancelled() {
Ok((false, None))
} else {
plugin.call_async_method("preload", job).await
}
};
Handle::current().block_on(async {
select! {
_ = ct_.cancelled() => Ok((false, None)),
r = future => match r {
Err(e) if e.to_string().contains("Preload task cancelled") => Ok((false, None)),
Ok(_) | Err(_) => r,
},
}
})
}) })
.await .await
.into_lua_err()? .into_lua_err()?

View file

@ -50,18 +50,15 @@ pub fn spot(
if ct2.is_cancelled() { Ok(()) } else { plugin.call_async_method("spot", job).await } if ct2.is_cancelled() { Ok(()) } else { plugin.call_async_method("spot", job).await }
}; };
let result = Handle::current().block_on(async { Handle::current().block_on(async {
select! { select! {
_ = ct2.cancelled() => Ok(()), _ = ct2.cancelled() => {},
r = future => r, Err(e) = future => if !e.to_string().contains("Spot task cancelled") {
error!("{e}");
},
else => {}
} }
}); });
if let Err(e) = result {
if !e.to_string().contains("Spot task cancelled") {
error!("{e}");
}
}
}); });
ct ct

View file

@ -101,7 +101,7 @@ macro_rules! impl_file_fields {
#[macro_export] #[macro_export]
macro_rules! impl_file_methods { macro_rules! impl_file_methods {
($methods:ident) => { ($methods:ident) => {
$methods.add_method("hash", |_, me, ()| Ok(me.hash())); $methods.add_method("hash", |_, me, ()| Ok(me.hash_u64()));
$methods.add_method("icon", |_, me, ()| { $methods.add_method("icon", |_, me, ()| {
use yazi_shared::theme::IconCache; use yazi_shared::theme::IconCache;

View file

@ -25,6 +25,7 @@ lru = { workspace = true }
parking_lot = { workspace = true } parking_lot = { workspace = true }
scopeguard = { workspace = true } scopeguard = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
tokio-util = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
[target."cfg(unix)".dependencies] [target."cfg(unix)".dependencies]

View file

@ -0,0 +1,43 @@
use std::collections::HashMap;
use futures::future::BoxFuture;
use yazi_shared::Id;
#[derive(Default)]
pub(super) struct Hooks {
inner: HashMap<Id, Hook>,
}
impl Hooks {
#[allow(dead_code)]
pub(super) fn add_sync<F>(&mut self, id: Id, f: F)
where
F: FnOnce(bool) + Send + Sync + 'static,
{
self.inner.insert(id, Hook::Sync(Box::new(f)));
}
pub(super) fn add_async<F>(&mut self, id: Id, f: F)
where
F: FnOnce(bool) -> BoxFuture<'static, ()> + Send + Sync + 'static,
{
self.inner.insert(id, Hook::Async(Box::new(f)));
}
pub(super) fn run_or_pop(&mut self, id: Id, cancel: bool) -> Option<BoxFuture<'static, ()>> {
match self.inner.remove(&id)? {
Hook::Sync(f) => {
f(cancel);
None
}
Hook::Async(f) => Some(f(cancel)),
}
}
}
// --- Hook
// TODO: remove Send + Sync bounds when not needed
pub(super) enum Hook {
Sync(Box<dyn FnOnce(bool) + Send + Sync>),
Async(Box<dyn (FnOnce(bool) -> BoxFuture<'static, ()>) + Send + Sync>),
}

View file

@ -2,7 +2,7 @@
yazi_macro::mod_pub!(file plugin prework process); yazi_macro::mod_pub!(file plugin prework process);
yazi_macro::mod_flat!(ongoing out r#in scheduler task); yazi_macro::mod_flat!(hooks ongoing out r#in scheduler task);
const LOW: u8 = yazi_config::Priority::Low as u8; const LOW: u8 = yazi_config::Priority::Low as u8;
const NORMAL: u8 = yazi_config::Priority::Normal as u8; const NORMAL: u8 = yazi_config::Priority::Normal as u8;

View file

@ -5,11 +5,11 @@ use yazi_config::YAZI;
use yazi_shared::{Id, Ids}; use yazi_shared::{Id, Ids};
use super::{Task, TaskStage}; use super::{Task, TaskStage};
use crate::TaskKind; use crate::{Hooks, TaskKind};
#[derive(Default)] #[derive(Default)]
pub struct Ongoing { pub struct Ongoing {
pub(super) hooks: HashMap<Id, Box<dyn (FnOnce(bool) -> BoxFuture<'static, ()>) + Send + Sync>>, pub(super) hooks: Hooks,
pub(super) all: HashMap<Id, Task>, pub(super) all: HashMap<Id, Task>,
} }
@ -67,8 +67,8 @@ impl Ongoing {
if task.succ < task.total { if task.succ < task.total {
return None; return None;
} }
if let Some(hook) = self.hooks.remove(&id) { if let Some(fut) = self.hooks.run_or_pop(id, false) {
return Some(hook(false)); return Some(fut);
} }
} }
TaskStage::Hooked => {} TaskStage::Hooked => {}

View file

@ -4,6 +4,7 @@ use anyhow::{Result, anyhow};
use lru::LruCache; use lru::LruCache;
use parking_lot::{Mutex, RwLock}; use parking_lot::{Mutex, RwLock};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
use tracing::error; use tracing::error;
use yazi_config::Priority; use yazi_config::Priority;
use yazi_fs::{FilesOp, SizeCalculator}; use yazi_fs::{FilesOp, SizeCalculator};
@ -17,8 +18,9 @@ pub struct Prework {
r#macro: async_priority_channel::Sender<TaskOp, u8>, r#macro: async_priority_channel::Sender<TaskOp, u8>,
prog: mpsc::UnboundedSender<TaskProg>, prog: mpsc::UnboundedSender<TaskProg>,
pub loaded: Mutex<LruCache<u64, u32>>, pub loaded: Mutex<LruCache<u64, u32>>,
pub size_loading: RwLock<HashSet<Url>>, pub loading: Mutex<LruCache<u64, CancellationToken>>,
pub sizing: RwLock<HashSet<Url>>,
} }
impl Prework { impl Prework {
@ -30,14 +32,16 @@ impl Prework {
r#macro, r#macro,
prog, prog,
loaded: Mutex::new(LruCache::new(NonZeroUsize::new(4096).unwrap())), loaded: Mutex::new(LruCache::new(NonZeroUsize::new(4096).unwrap())),
size_loading: Default::default(), loading: Mutex::new(LruCache::new(NonZeroUsize::new(256).unwrap())),
sizing: Default::default(),
} }
} }
pub async fn work(&self, r#in: PreworkIn) -> Result<()> { pub async fn work(&self, r#in: PreworkIn) -> Result<()> {
let id = r#in.id();
match r#in { match r#in {
PreworkIn::Fetch(task) => { PreworkIn::Fetch(task) => {
let hashes: Vec<_> = task.targets.iter().map(|f| f.hash()).collect(); let hashes: Vec<_> = task.targets.iter().map(|f| f.hash_u64()).collect();
let result = isolate::fetch(CmdCow::from(&task.plugin.run), task.targets).await; let result = isolate::fetch(CmdCow::from(&task.plugin.run), task.targets).await;
if let Err(e) = result { if let Err(e) = result {
self.fail(task.id, format!("Failed to run fetcher `{}`:\n{e}", task.plugin.run.name))?; self.fail(task.id, format!("Failed to run fetcher `{}`:\n{e}", task.plugin.run.name))?;
@ -52,11 +56,15 @@ impl Prework {
if let Some(e) = err { if let Some(e) = err {
error!("Error when running fetcher `{}`:\n{e}", task.plugin.run.name); error!("Error when running fetcher `{}`:\n{e}", task.plugin.run.name);
} }
self.prog.send(TaskProg::Adv(task.id, 1, 0))?;
} }
PreworkIn::Load(task) => { PreworkIn::Load(task) => {
let hash = task.target.hash(); let ct = CancellationToken::new();
let result = isolate::preload(&task.plugin.run, task.target).await; if let Some(ct) = self.loading.lock().put(task.target.url.hash_u64(), ct.clone()) {
ct.cancel();
}
let hash = task.target.hash_u64();
let result = isolate::preload(&task.plugin.run, task.target, ct).await;
if let Err(e) = result { if let Err(e) = result {
self self
.fail(task.id, format!("Failed to run preloader `{}`:\n{e}", task.plugin.run.name))?; .fail(task.id, format!("Failed to run preloader `{}`:\n{e}", task.plugin.run.name))?;
@ -70,13 +78,12 @@ impl Prework {
if let Some(e) = err { if let Some(e) = err {
error!("Error when running preloader `{}`:\n{e}", task.plugin.run.name); error!("Error when running preloader `{}`:\n{e}", task.plugin.run.name);
} }
self.prog.send(TaskProg::Adv(task.id, 1, 0))?;
} }
PreworkIn::Size(task) => { PreworkIn::Size(task) => {
let length = SizeCalculator::total(&task.target).await.unwrap_or(0); let length = SizeCalculator::total(&task.target).await.unwrap_or(0);
task.throttle.done((task.target, length), |buf| { task.throttle.done((task.target, length), |buf| {
{ {
let mut loading = self.size_loading.write(); let mut loading = self.sizing.write();
for (path, _) in &buf { for (path, _) in &buf {
loading.remove(path); loading.remove(path);
} }
@ -89,10 +96,9 @@ impl Prework {
) )
.emit(); .emit();
}); });
self.prog.send(TaskProg::Adv(task.id, 1, 0))?;
} }
} }
Ok(()) Ok(self.prog.send(TaskProg::Adv(id, 1, 0))?)
} }
pub async fn fetch(&self, task: PreworkInFetch) -> Result<()> { pub async fn fetch(&self, task: PreworkInFetch) -> Result<()> {

View file

@ -58,8 +58,8 @@ impl Scheduler {
pub fn cancel(&self, id: Id) -> bool { pub fn cancel(&self, id: Id) -> bool {
let mut ongoing = self.ongoing.lock(); let mut ongoing = self.ongoing.lock();
if let Some(hook) = ongoing.hooks.remove(&id) { if let Some(fut) = ongoing.hooks.run_or_pop(id, true) {
self.micro.try_send(hook(true), HIGH).ok(); self.micro.try_send(fut, HIGH).ok();
return false; return false;
} }
@ -81,11 +81,11 @@ impl Scheduler {
return; return;
} }
ongoing.hooks.insert(id, { ongoing.hooks.add_async(id, {
let ongoing = self.ongoing.clone(); let ongoing = self.ongoing.clone();
let (from, to) = (from.clone(), to.clone()); let (from, to) = (from.clone(), to.clone());
Box::new(move |canceled: bool| { move |canceled: bool| {
async move { async move {
if !canceled { if !canceled {
remove_dir_clean(&from).await; remove_dir_clean(&from).await;
@ -94,7 +94,7 @@ impl Scheduler {
ongoing.lock().try_remove(id, TaskStage::Hooked); ongoing.lock().try_remove(id, TaskStage::Hooked);
} }
.boxed() .boxed()
}) }
}); });
let file = self.file.clone(); let file = self.file.clone();
@ -158,11 +158,11 @@ impl Scheduler {
let mut ongoing = self.ongoing.lock(); let mut ongoing = self.ongoing.lock();
let id = ongoing.add(TaskKind::User, format!("Delete {target}")); let id = ongoing.add(TaskKind::User, format!("Delete {target}"));
ongoing.hooks.insert(id, { ongoing.hooks.add_async(id, {
let target = target.clone(); let target = target.clone();
let ongoing = self.ongoing.clone(); let ongoing = self.ongoing.clone();
Box::new(move |canceled: bool| { move |canceled: bool| {
async move { async move {
if !canceled { if !canceled {
fs::remove_dir_all(&target).await.ok(); fs::remove_dir_all(&target).await.ok();
@ -172,7 +172,7 @@ impl Scheduler {
ongoing.lock().try_remove(id, TaskStage::Hooked); ongoing.lock().try_remove(id, TaskStage::Hooked);
} }
.boxed() .boxed()
}) }
}); });
let file = self.file.clone(); let file = self.file.clone();
@ -187,11 +187,11 @@ impl Scheduler {
let mut ongoing = self.ongoing.lock(); let mut ongoing = self.ongoing.lock();
let id = ongoing.add(TaskKind::User, format!("Trash {target}")); let id = ongoing.add(TaskKind::User, format!("Trash {target}"));
ongoing.hooks.insert(id, { ongoing.hooks.add_async(id, {
let target = target.clone(); let target = target.clone();
let ongoing = self.ongoing.clone(); let ongoing = self.ongoing.clone();
Box::new(move |canceled: bool| { move |canceled: bool| {
async move { async move {
if !canceled { if !canceled {
MgrProxy::update_tasks(&target); MgrProxy::update_tasks(&target);
@ -200,7 +200,7 @@ impl Scheduler {
ongoing.lock().try_remove(id, TaskStage::Hooked); ongoing.lock().try_remove(id, TaskStage::Hooked);
} }
.boxed() .boxed()
}) }
}); });
let file = self.file.clone(); let file = self.file.clone();
@ -275,9 +275,9 @@ impl Scheduler {
let mut ongoing = self.ongoing.lock(); let mut ongoing = self.ongoing.lock();
let id = ongoing.add(TaskKind::User, name); let id = ongoing.add(TaskKind::User, name);
ongoing.hooks.insert(id, { ongoing.hooks.add_async(id, {
let ongoing = self.ongoing.clone(); let ongoing = self.ongoing.clone();
Box::new(move |canceled: bool| { move |canceled: bool| {
async move { async move {
if canceled { if canceled {
cancel_tx.send(()).await.ok(); cancel_tx.send(()).await.ok();
@ -289,7 +289,7 @@ impl Scheduler {
ongoing.lock().try_remove(id, TaskStage::Hooked); ongoing.lock().try_remove(id, TaskStage::Hooked);
} }
.boxed() .boxed()
}) }
}); });
let cmd = OsString::from(&opener.run); let cmd = OsString::from(&opener.run);

View file

@ -15,6 +15,7 @@ yazi-macro = { path = "../yazi-macro", version = "25.6.11" }
# External dependencies # External dependencies
anyhow = { workspace = true } anyhow = { workspace = true }
crossterm = { workspace = true } crossterm = { workspace = true }
foldhash = { workspace = true }
futures = { workspace = true } futures = { workspace = true }
memchr = "2.7.5" memchr = "2.7.5"
parking_lot = { workspace = true } parking_lot = { workspace = true }

View file

@ -1,4 +1,4 @@
use std::{ffi::OsStr, fmt::{Debug, Display, Formatter}, hash::{Hash, Hasher}, ops::Deref, path::{Path, PathBuf}}; use std::{ffi::OsStr, fmt::{Debug, Display, Formatter}, hash::{BuildHasher, Hash, Hasher}, ops::Deref, path::{Path, PathBuf}};
use percent_encoding::{AsciiSet, CONTROLS, percent_decode_str, percent_encode}; use percent_encoding::{AsciiSet, CONTROLS, percent_decode_str, percent_encode};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -171,6 +171,9 @@ impl Url {
#[inline] #[inline]
pub fn pair(&self) -> Option<(Self, UrnBuf)> { Some((self.parent_url()?, self.loc.urn_owned())) } pub fn pair(&self) -> Option<(Self, UrnBuf)> { Some((self.parent_url()?, self.loc.urn_owned())) }
#[inline]
pub fn hash_u64(&self) -> u64 { foldhash::fast::FixedState::default().hash_one(self) }
#[inline] #[inline]
pub fn rebase(&self, parent: &Path) -> Self { pub fn rebase(&self, parent: &Path) -> Self {
debug_assert!(self.is_regular()); debug_assert!(self.is_regular());