perf: copy-on-write on command data & avoid converting primitive types to strings thereby allocating memory (#2862)

This commit is contained in:
三咲雅 misaki masa 2025-06-11 10:13:51 +08:00 committed by GitHub
parent 0752b53298
commit 414d46fd16
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 131 additions and 104 deletions

12
Cargo.lock generated
View file

@ -1025,7 +1025,7 @@ checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasi 0.11.1+wasi-snapshot-preview1",
]
[[package]]
@ -1470,7 +1470,7 @@ checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
dependencies = [
"libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasi 0.11.1+wasi-snapshot-preview1",
"windows-sys 0.59.0",
]
@ -2992,9 +2992,9 @@ dependencies = [
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasi"
@ -3318,9 +3318,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.7.10"
version = "0.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd"
dependencies = [
"memchr",
]

View file

@ -7,7 +7,7 @@ use yazi_shared::{event::CmdCow, url::Url};
use crate::{mgr::{LINKED, Mgr}, tasks::Tasks};
pub struct Opt {
updates: HashMap<Cow<'static, str>, String>,
updates: HashMap<Cow<'static, str>, Cow<'static, str>>,
}
impl TryFrom<CmdCow> for Opt {
@ -28,13 +28,13 @@ impl Mgr {
let updates = opt
.updates
.into_iter()
.map(|(url, mime)| (Url::from(url.into_owned()), mime))
.map(|(url, mime)| (Url::from(url.as_ref()), mime))
.filter(|(url, mime)| self.mimetype.by_url(url) != Some(mime))
.fold(HashMap::new(), |mut map, (u, m)| {
for u in linked.from_file(&u) {
map.insert(u, m.clone());
map.insert(u, m.to_string());
}
map.insert(u, m);
map.insert(u, m.into_owned());
map
});

View file

@ -14,7 +14,7 @@ impl Notify {
if self.messages.iter().all(|m| m != &msg) {
self.messages.push(msg);
emit!(Call(Cmd::args("app:update_notify", &[0])));
emit!(Call(Cmd::args("app:update_notify", [0])));
}
}
}

View file

@ -69,7 +69,7 @@ impl Notify {
self.tick_handle = Some(tokio::spawn(async move {
tokio::time::sleep(interval).await;
emit!(Call(Cmd::args("app:update_notify", &[interval.as_secs_f64()])));
emit!(Call(Cmd::args("app:update_notify", [interval.as_secs_f64()])));
}));
}
}

View file

@ -41,10 +41,10 @@ impl Tab {
let (Ok(s) | Err(InputError::Typed(s))) = result else { continue };
emit!(Call(
Cmd::args("mgr:filter_do", &[s])
.with_bool("smart", opt.case == FilterCase::Smart)
.with_bool("insensitive", opt.case == FilterCase::Insensitive)
.with_bool("done", done)
Cmd::args("mgr:filter_do", [s])
.with("smart", opt.case == FilterCase::Smart)
.with("insensitive", opt.case == FilterCase::Insensitive)
.with("done", done)
));
}
});

View file

@ -33,10 +33,10 @@ impl Tab {
while let Some(Ok(s)) | Some(Err(InputError::Typed(s))) = rx.next().await {
emit!(Call(
Cmd::args("mgr:find_do", &[s])
.with_bool("previous", opt.prev)
.with_bool("smart", opt.case == FilterCase::Smart)
.with_bool("insensitive", opt.case == FilterCase::Insensitive)
Cmd::args("mgr:find_do", [s])
.with("previous", opt.prev)
.with("smart", opt.case == FilterCase::Smart)
.with("insensitive", opt.case == FilterCase::Insensitive)
));
}
});

View file

@ -20,7 +20,8 @@ impl Tab {
handle.abort();
}
let mut input = InputProxy::show(InputCfg::search(opt.via.as_ref()).with_value(&*opt.subject));
let mut input =
InputProxy::show(InputCfg::search(opt.via.into_str()).with_value(&*opt.subject));
tokio::spawn(async move {
if let Some(Ok(subject)) = input.recv().await {
opt.subject = Cow::Owned(subject);

View file

@ -15,7 +15,7 @@ impl Sendable {
Value::Number(n) => Data::Number(n),
Value::String(b) => {
if let Ok(s) = b.to_str() {
Data::String(s.to_owned())
Data::String(s.to_owned().into())
} else {
Data::Bytes(b.as_bytes().to_owned())
}
@ -99,7 +99,7 @@ impl Sendable {
Data::Boolean(b) => Value::Boolean(*b),
Data::Integer(i) => Value::Integer(*i),
Data::Number(n) => Value::Number(*n),
Data::String(s) => Value::String(lua.create_string(s)?),
Data::String(s) => Value::String(lua.create_string(s.as_ref())?),
Data::List(l) => {
let mut vec = Vec::with_capacity(l.len());
for v in l {

View file

@ -25,24 +25,19 @@ impl App {
app.render();
let mut events = Vec::with_capacity(50);
let (mut timeout, mut last_render) = (None, Instant::now());
macro_rules! drain_events {
() => {
for event in events.drain(..) {
app.dispatch(event)?;
try_render!();
}
};
}
if !NEED_RENDER.load(Ordering::Relaxed) {
continue;
}
let (mut timeout, mut last_render) = (None, Instant::now());
macro_rules! try_render {
() => {
if NEED_RENDER.load(Ordering::Relaxed) {
if let Some(sub) = Duration::from_millis(10).checked_sub(last_render.elapsed()) {
timeout = Some(sleep(sub));
} else {
timeout = Duration::from_millis(10).checked_sub(last_render.elapsed());
if timeout.is_none() {
app.render();
(timeout, last_render) = (None, Instant::now());
last_render = Instant::now();
}
}
};
@ -51,7 +46,7 @@ impl App {
loop {
if let Some(t) = timeout.take() {
select! {
_ = t => {
_ = sleep(t) => {
app.render();
last_render = Instant::now();
}

View file

@ -141,7 +141,7 @@ impl<'a> Executor<'a> {
on!(TABS, switch);
on!(TABS, swap);
match cmd.name.as_str() {
match cmd.name.as_ref() {
// Help
"help" => self.app.cx.help.toggle(Layer::Mgr),
// Plugin
@ -172,7 +172,7 @@ impl<'a> Executor<'a> {
on!(open_with);
on!(process_exec);
match cmd.name.as_str() {
match cmd.name.as_ref() {
// Help
"help" => self.app.cx.help.toggle(Layer::Tasks),
// Plugin
@ -195,7 +195,7 @@ impl<'a> Executor<'a> {
on!(swipe);
on!(copy);
match cmd.name.as_str() {
match cmd.name.as_ref() {
// Help
"help" => self.app.cx.help.toggle(Layer::Spot),
// Plugin
@ -217,7 +217,7 @@ impl<'a> Executor<'a> {
on!(close);
on!(arrow);
match cmd.name.as_str() {
match cmd.name.as_ref() {
// Help
"help" => self.app.cx.help.toggle(Layer::Pick),
// Plugin
@ -241,7 +241,7 @@ impl<'a> Executor<'a> {
match self.app.cx.input.mode() {
InputMode::Normal => {
match cmd.name.as_str() {
match cmd.name.as_ref() {
// Help
"help" => return self.app.cx.help.toggle(Layer::Input),
// Plugin
@ -249,7 +249,7 @@ impl<'a> Executor<'a> {
_ => {}
}
}
InputMode::Insert => match cmd.name.as_str() {
InputMode::Insert => match cmd.name.as_ref() {
"complete" if cmd.bool("trigger") => return self.app.cx.cmp.trigger(cmd),
_ => {}
},
@ -286,7 +286,7 @@ impl<'a> Executor<'a> {
on!(arrow);
on!(filter);
match cmd.name.as_str() {
match cmd.name.as_ref() {
"close" => self.app.cx.help.toggle(Layer::Help),
// Plugin
"plugin" => self.app.plugin(cmd),
@ -308,7 +308,7 @@ impl<'a> Executor<'a> {
on!(close);
on!(arrow);
match cmd.name.as_str() {
match cmd.name.as_ref() {
// Help
"help" => self.app.cx.help.toggle(Layer::Cmp),
// Plugin

View file

@ -58,7 +58,7 @@ impl Fetcher {
impl UserData for Fetcher {
fn add_fields<F: mlua::UserDataFields<Self>>(fields: &mut F) {
cached_field!(fields, cmd, |lua, me| lua.create_string(&me.inner.run.name));
cached_field!(fields, cmd, |lua, me| lua.create_string(me.inner.run.name.as_ref()));
}
}
@ -75,7 +75,7 @@ impl Spotter {
impl UserData for Spotter {
fn add_fields<F: mlua::UserDataFields<Self>>(fields: &mut F) {
cached_field!(fields, cmd, |lua, me| lua.create_string(&me.inner.run.name));
cached_field!(fields, cmd, |lua, me| lua.create_string(me.inner.run.name.as_ref()));
}
}
@ -92,7 +92,7 @@ impl Preloader {
impl UserData for Preloader {
fn add_fields<F: mlua::UserDataFields<Self>>(fields: &mut F) {
cached_field!(fields, cmd, |lua, me| lua.create_string(&me.inner.run.name));
cached_field!(fields, cmd, |lua, me| lua.create_string(me.inner.run.name.as_ref()));
}
}
@ -109,6 +109,6 @@ impl Previewer {
impl UserData for Previewer {
fn add_fields<F: mlua::UserDataFields<Self>>(fields: &mut F) {
cached_field!(fields, cmd, |lua, me| lua.create_string(&me.inner.run.name));
cached_field!(fields, cmd, |lua, me| lua.create_string(me.inner.run.name.as_ref()));
}
}

View file

@ -19,7 +19,7 @@ pub fn peek(
skip: usize,
) -> Option<CancellationToken> {
let ct = CancellationToken::new();
if let Some(c) = LOADER.read().get(&cmd.name) {
if let Some(c) = LOADER.read().get(cmd.name.as_ref()) {
if c.sync_peek {
peek_sync(cmd, file, mime, skip);
} else {
@ -59,7 +59,7 @@ fn peek_sync(cmd: &'static Cmd, file: yazi_fs::File, mime: Cow<'static, str>, sk
plugin.call_method("peek", job)
});
AppProxy::plugin(PluginOpt::new_callback(&cmd.name, cb));
AppProxy::plugin(PluginOpt::new_callback(cmd.name.as_ref(), cb));
}
fn peek_async(

View file

@ -16,5 +16,5 @@ pub fn seek_sync(cmd: &'static Cmd, file: yazi_fs::File, units: i16) {
plugin.call_method("seek", job)
});
AppProxy::plugin(PluginOpt::new_callback(&cmd.name, cb));
AppProxy::plugin(PluginOpt::new_callback(cmd.name.as_ref(), cb));
}

View file

@ -14,8 +14,8 @@ impl Utils {
}
pub(super) fn emit(lua: &Lua) -> mlua::Result<Function> {
lua.create_function(|_, (name, args): (mlua::String, Table)| {
let mut cmd = Cmd::new_or(&name.to_str()?, Layer::Mgr)?;
lua.create_function(|_, (name, args): (String, Table)| {
let mut cmd = Cmd::new_or(name, Layer::Mgr)?;
cmd.args = Sendable::table_to_args(args)?;
Ok(emit!(Call(cmd)))
})
@ -23,7 +23,11 @@ impl Utils {
pub(super) fn mgr_emit(lua: &Lua) -> mlua::Result<Function> {
lua.create_function(|_, (name, args): (String, Table)| {
emit!(Call(Cmd { name, args: Sendable::table_to_args(args)?, layer: Layer::Mgr }));
emit!(Call(Cmd {
name: name.into(),
args: Sendable::table_to_args(args)?,
layer: Layer::Mgr,
}));
Ok(())
})
}

View file

@ -21,7 +21,7 @@ impl Utils {
let cand = cand?;
cands.push(Chord {
on: Self::parse_keys(cand.raw_get("on")?)?,
run: vec![Cmd::args("which:callback", &[i]).with_any("tx", tx.clone())],
run: vec![Cmd::args("which:callback", [i]).with_any("tx", tx.clone())],
desc: cand.raw_get("desc").ok(),
r#for: None,
});
@ -31,7 +31,7 @@ impl Utils {
emit!(Call(
Cmd::new("which:show")
.with_any("candidates", cands)
.with_bool("silent", t.raw_get("silent").unwrap_or_default())
.with("silent", t.raw_get::<bool>("silent").unwrap_or_default())
));
Ok(rx.recv().await.map(|idx| idx + 1))

View file

@ -11,6 +11,6 @@ impl CmpProxy {
#[inline]
pub fn trigger(word: &str, ticket: Id) {
emit!(Call(Cmd::args("cmp:trigger", &[word]).with("ticket", ticket)));
emit!(Call(Cmd::args("cmp:trigger", [word]).with("ticket", ticket)));
}
}

View file

@ -13,7 +13,7 @@ impl MgrProxy {
#[inline]
pub fn peek(force: bool) {
emit!(Call(Cmd::new("mgr:peek").with_bool("force", force)));
emit!(Call(Cmd::new("mgr:peek").with("force", force)));
}
#[inline]
@ -34,7 +34,7 @@ impl MgrProxy {
#[inline]
pub fn remove_do(targets: Vec<Url>, permanently: bool) {
emit!(Call(
Cmd::new("mgr:remove_do").with_bool("permanently", permanently).with_any("targets", targets)
Cmd::new("mgr:remove_do").with("permanently", permanently).with_any("targets", targets)
));
}
@ -50,6 +50,6 @@ impl MgrProxy {
#[inline]
pub fn update_paged_by(page: usize, only_if: &Url) {
emit!(Call(Cmd::args("mgr:update_paged", &[page]).with_any("only-if", only_if.clone())));
emit!(Call(Cmd::args("mgr:update_paged", [page]).with_any("only-if", only_if.clone())));
}
}

View file

@ -33,7 +33,7 @@ impl TryFrom<CmdCow> for SearchOpt {
}
// Via
#[derive(PartialEq, Eq)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum SearchOptVia {
Rg,
Rga,
@ -50,8 +50,8 @@ impl From<&str> for SearchOptVia {
}
}
impl AsRef<str> for SearchOptVia {
fn as_ref(&self) -> &str {
impl SearchOptVia {
pub fn into_str(self) -> &'static str {
match self {
Self::Rg => "rg",
Self::Rga => "rga",

View file

@ -1,3 +1,5 @@
use std::borrow::Cow;
use yazi_macro::emit;
use yazi_shared::{event::Cmd, url::Url};
@ -8,25 +10,25 @@ pub struct TabProxy;
impl TabProxy {
#[inline]
pub fn cd(target: &Url) {
emit!(Call(Cmd::args("mgr:cd", &[target])));
emit!(Call(Cmd::args("mgr:cd", [target])));
}
#[inline]
pub fn reveal(target: &Url) {
emit!(Call(Cmd::args("mgr:reveal", &[target]).with("no-dummy", true)));
emit!(Call(Cmd::args("mgr:reveal", [target]).with("no-dummy", true)));
}
#[inline]
pub fn arrow(step: impl AsRef<str>) {
emit!(Call(Cmd::args("mgr:arrow", &[step.as_ref()])));
pub fn arrow(step: impl Into<Cow<'static, str>>) {
emit!(Call(Cmd::args("mgr:arrow", [step.into()])));
}
#[inline]
pub fn search_do(opt: SearchOpt) {
emit!(Call(
// TODO: use second positional argument instead of `args` parameter
Cmd::args("mgr:search_do", &[opt.subject])
.with("via", opt.via.as_ref().to_owned())
Cmd::args("mgr:search_do", [opt.subject])
.with("via", Cow::Borrowed(opt.via.into_str()))
.with("args", opt.args_raw.into_owned())
));
}

View file

@ -1,4 +1,4 @@
use std::{any::Any, borrow::Cow, collections::HashMap, fmt::{self, Display}, str::FromStr};
use std::{any::Any, borrow::Cow, collections::HashMap, fmt::{self, Display}, mem, str::FromStr};
use anyhow::{Result, bail};
use serde::{Deserialize, de};
@ -8,33 +8,48 @@ use crate::{Layer, url::Url};
#[derive(Debug, Default)]
pub struct Cmd {
pub name: String,
pub name: Cow<'static, str>,
pub args: HashMap<DataKey, Data>,
pub layer: Layer,
}
impl Cmd {
pub fn new(s: &str) -> Self {
Self::new_or(s, Default::default())
.unwrap_or_else(|_| Self { name: "null".to_owned(), ..Default::default() })
pub fn new<N>(name: N) -> Self
where
N: Into<Cow<'static, str>>,
{
Self::new_or(name, Default::default())
.unwrap_or_else(|_| Self { name: "null".into(), ..Default::default() })
}
pub fn new_or(s: &str, default: Layer) -> Result<Self> {
let (layer, name) = match s.split_once(':') {
Some((l, n)) => (l.parse()?, n),
None => (default, s),
pub fn new_or<N>(name: N, default: Layer) -> Result<Self>
where
N: Into<Cow<'static, str>>,
{
let cow: Cow<'static, str> = name.into();
let (layer, name) = match cow.find(':') {
None => (default, cow),
Some(i) => (cow[..i].parse()?, match cow {
Cow::Borrowed(s) => Cow::Borrowed(&s[i + 1..]),
Cow::Owned(mut s) => {
s.drain(..i + 1);
Cow::Owned(s)
}
}),
};
Ok(Self { name: name.to_owned(), args: Default::default(), layer })
Ok(Self { name, args: Default::default(), layer })
}
pub fn args(name: &str, args: &[impl ToString]) -> Self {
pub fn args<N, D, I>(name: N, args: I) -> Self
where
N: Into<Cow<'static, str>>,
D: Into<Data>,
I: IntoIterator<Item = D>,
{
let mut me = Self::new(name);
me.args = args
.iter()
.enumerate()
.map(|(i, s)| (DataKey::Integer(i as i64), Data::String(s.to_string())))
.collect();
me.args =
args.into_iter().enumerate().map(|(i, a)| (DataKey::Integer(i as i64), a.into())).collect();
me
}
@ -59,12 +74,6 @@ impl Cmd {
self
}
#[inline]
pub fn with_bool(mut self, name: impl Into<DataKey>, state: bool) -> Self {
self.args.insert(name.into(), Data::Boolean(state));
self
}
#[inline]
pub fn with_any(mut self, name: impl Into<DataKey>, data: impl Any + Send + Sync) -> Self {
self.args.insert(name.into(), Data::Any(Box::new(data)));
@ -107,7 +116,7 @@ impl Cmd {
}
#[inline]
pub fn take_str(&mut self, name: impl Into<DataKey>) -> Option<String> {
pub fn take_str(&mut self, name: impl Into<DataKey>) -> Option<Cow<'static, str>> {
if let Some(Data::String(s)) = self.take(name) { Some(s) } else { None }
}
@ -115,7 +124,7 @@ impl Cmd {
pub fn take_first(&mut self) -> Option<Data> { self.take(0) }
#[inline]
pub fn take_first_str(&mut self) -> Option<String> {
pub fn take_first_str(&mut self) -> Option<Cow<'static, str>> {
if let Some(Data::String(s)) = self.take_first() { Some(s) } else { None }
}
@ -141,7 +150,7 @@ impl Cmd {
.map(|(word, normal)| {
let Some(arg) = word.strip_prefix("--").filter(|_| normal) else {
i += 1;
return Ok((DataKey::Integer(i - obase as i64), Data::String(word)));
return Ok((DataKey::Integer(i - obase as i64), Data::String(word.into())));
};
let mut parts = arg.splitn(2, '=');
@ -150,7 +159,7 @@ impl Cmd {
};
let val = if let Some(val) = parts.next() {
Data::String(val.to_owned())
Data::String(val.to_owned().into())
} else {
Data::Boolean(true)
};
@ -189,12 +198,12 @@ impl FromStr for Cmd {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (words, last) = crate::shell::split_unix(s, true)?;
let (mut words, last) = crate::shell::split_unix(s, true)?;
if words.is_empty() || words[0].is_empty() {
bail!("command name cannot be empty");
}
let mut me = Self::new(&words[0]);
let mut me = Self::new(mem::take(&mut words[0]));
me.args = Cmd::parse_args(words.into_iter().skip(1), last, true)?;
Ok(me)
}

View file

@ -40,7 +40,7 @@ impl CmdCow {
#[inline]
pub fn take_str(&mut self, name: impl Into<DataKey>) -> Option<Cow<'static, str>> {
match self {
Self::Owned(c) => c.take_str(name).map(Cow::Owned),
Self::Owned(c) => c.take_str(name),
Self::Borrowed(c) => c.str(name).map(Cow::Borrowed),
}
}
@ -56,7 +56,7 @@ impl CmdCow {
#[inline]
pub fn take_first_str(&mut self) -> Option<Cow<'static, str>> {
match self {
Self::Owned(c) => c.take_first_str().map(Cow::Owned),
Self::Owned(c) => c.take_first_str(),
Self::Borrowed(c) => c.first_str().map(Cow::Borrowed),
}
}

View file

@ -12,7 +12,7 @@ pub enum Data {
Boolean(bool),
Integer(i64),
Number(f64),
String(String),
String(Cow<'static, str>),
List(Vec<Data>),
Dict(HashMap<DataKey, Data>),
Id(Id),
@ -56,13 +56,13 @@ impl Data {
#[inline]
pub fn into_url(self) -> Option<Url> {
match self {
Data::String(s) => Some(Url::from(s)),
Data::String(s) => Some(Url::from(s.as_ref())),
Data::Url(u) => Some(u),
_ => None,
}
}
pub fn into_dict_string(self) -> HashMap<Cow<'static, str>, String> {
pub fn into_dict_string(self) -> HashMap<Cow<'static, str>, Cow<'static, str>> {
let Self::Dict(dict) = self else {
return Default::default();
};
@ -79,7 +79,7 @@ impl Data {
#[inline]
pub fn to_url(&self) -> Option<Url> {
match self {
Self::String(s) => Some(Url::from(s)),
Self::String(s) => Some(Url::from(s.as_ref())),
Self::Url(u) => Some(u.clone()),
_ => None,
}
@ -90,18 +90,34 @@ impl From<bool> for Data {
fn from(value: bool) -> Self { Self::Boolean(value) }
}
impl From<f64> for Data {
fn from(value: f64) -> Self { Self::Number(value) }
}
impl From<usize> for Data {
fn from(value: usize) -> Self { Self::Id(value.into()) }
}
impl From<String> for Data {
fn from(value: String) -> Self { Self::String(value) }
fn from(value: String) -> Self { Self::String(Cow::Owned(value)) }
}
impl From<Cow<'static, str>> for Data {
fn from(value: Cow<'static, str>) -> Self { Self::String(value) }
}
impl From<Id> for Data {
fn from(value: Id) -> Self { Self::Id(value) }
}
impl From<&Url> for Data {
fn from(value: &Url) -> Self { Self::Url(value.clone()) }
}
impl From<&str> for Data {
fn from(value: &str) -> Self { Self::String(Cow::Owned(value.to_owned())) }
}
// --- Key
#[derive(Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]