Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improve performance #71

Merged
merged 5 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
.DS_Store
target/

# First ignore everything in completions
.cache/

completions/*
# Then explicitly allow the shell completion files
!completions/*.bash
!completions/*.fish
!completions/_lla
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.3.7] - 2024-12-20

### Changed

- Faster recursive directory listing with optimized traversal
- Improved fuzzy search performance and accuracy
- Enhanced tree format with more efficient rendering
- Redesigned size calculation logic for faster and more accurate results
- General stability improvements and bug fixes

## [0.3.6] - 2024-12-18

### Added
Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ members = ["lla", "lla_plugin_interface", "plugins/*"]
[workspace.package]
description = "Blazing Fast and highly customizable ls Replacement with Superpowers"
authors = ["Achaq <hi@achaq.dev>"]
version = "0.3.6"
version = "0.3.7"
categories = ["utilities", "file-system", "cli", "file-management"]
edition = "2021"
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion lla/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ walkdir.workspace = true
tempfile.workspace = true
users.workspace = true
parking_lot.workspace = true
lla_plugin_interface = { version = "0.3.6", path = "../lla_plugin_interface" }
lla_plugin_interface = { version = "0.3.7", path = "../lla_plugin_interface" }
once_cell.workspace = true
dashmap.workspace = true
unicode-width.workspace = true
Expand Down
40 changes: 28 additions & 12 deletions lla/src/commands/file_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,19 +102,35 @@ pub fn convert_metadata(metadata: &std::fs::Metadata) -> EntryMetadata {
}

fn calculate_dir_size(path: &std::path::Path) -> std::io::Result<u64> {
let mut total_size = 0;
if path.is_dir() {
for entry in std::fs::read_dir(path)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
total_size += calculate_dir_size(&path)?;
} else {
total_size += entry.metadata()?.len();
}
}
use rayon::prelude::*;

if !path.is_dir() {
return Ok(0);
}
Ok(total_size)

let entries: Vec<_> = std::fs::read_dir(path)?.collect::<std::io::Result<_>>()?;

entries
.par_iter()
.try_fold(
|| 0u64,
|acc, entry| {
let metadata = entry.metadata()?;
if metadata.is_symlink() {
return Ok(acc);
}

let path = entry.path();
let size = if metadata.is_dir() {
calculate_dir_size(&path)?
} else {
metadata.len()
};

Ok(acc + size)
},
)
.try_reduce(|| 0, |a, b| Ok(a + b))
}

pub fn list_and_decorate_files(
Expand Down
233 changes: 93 additions & 140 deletions lla/src/formatter/tree.rs
Original file line number Diff line number Diff line change
@@ -1,35 +1,15 @@
use super::FileFormatter;
use crate::error::Result;
use crate::plugin::PluginManager;
use crate::theme::{self, ColorValue};
use crate::utils::color::{colorize_file_name, colorize_file_name_with_icon};
use crate::utils::color::*;
use crate::utils::icons::format_with_icon;
use colored::*;
use colored::Colorize;
use lla_plugin_interface::proto::DecoratedEntry;
use std::path::Path;
use std::collections::{HashMap, HashSet};
use std::io::{self, Write};
use std::path::{Path, PathBuf};

#[derive(PartialEq, Eq, Debug, Copy, Clone)]
enum TreePart {
Edge,
Line,
Corner,
}

impl TreePart {
#[inline]
const fn as_str(self) -> &'static str {
match self {
Self::Edge => "├── ",
Self::Line => "│ ",
Self::Corner => "└── ",
}
}

fn colored(self) -> ColoredString {
let color = theme::color_value_to_color(&ColorValue::Named("bright black".to_string()));
self.as_str().color(color)
}
}
const BUFFER_SIZE: usize = 16384;

pub struct TreeFormatter {
pub show_icons: bool,
Expand All @@ -39,149 +19,122 @@ impl TreeFormatter {
pub fn new(show_icons: bool) -> Self {
Self { show_icons }
}
}

impl TreeFormatter {
fn format_entry(
entry: &DecoratedEntry,
prefix: &str,
plugin_manager: &mut PluginManager,
buf: &mut String,
show_icons: bool,
) {
buf.clear();
let path = Path::new(&entry.path);
buf.reserve(prefix.len() + path.as_os_str().len() + 1);
buf.push_str(prefix);
fn format_entry(&self, path: &Path) -> String {
let colored_name = colorize_file_name(path).to_string();
buf.push_str(&colorize_file_name_with_icon(
path,
format_with_icon(path, colored_name, show_icons),
));

let plugin_fields = plugin_manager.format_fields(entry, "tree").join(" ");
if !plugin_fields.is_empty() {
buf.push(' ');
buf.push_str(&plugin_fields);
if self.show_icons {
format_with_icon(path, colored_name, true)
} else {
colored_name
}

buf.push('\n');
}
}

#[derive(Debug)]
struct TreeTrunk {
stack: Vec<TreePart>,
last_depth: Option<(usize, bool)>,
}

impl Default for TreeTrunk {
fn default() -> Self {
Self {
stack: Vec::with_capacity(32),
last_depth: None,
fn build_tree(
&self,
entries: &[DecoratedEntry],
) -> (Vec<PathBuf>, HashMap<PathBuf, Vec<PathBuf>>) {
let mut tree: HashMap<PathBuf, Vec<PathBuf>> = HashMap::with_capacity(entries.len());
let mut path_set: HashSet<PathBuf> = HashSet::with_capacity(entries.len());
let mut child_paths = HashSet::new();

for entry in entries {
path_set.insert(PathBuf::from(&entry.path));
}
}
}

impl TreeTrunk {
#[inline]
fn get_prefix(&mut self, depth: usize, is_absolute_last: bool, buf: &mut String) {
if let Some((last_depth, _)) = self.last_depth {
if last_depth < self.stack.len() {
self.stack[last_depth] = TreePart::Line;
for path in path_set.iter() {
if let Some(parent) = path.parent() {
if path_set.contains(parent) {
tree.entry(parent.to_path_buf())
.or_insert_with(Vec::new)
.push(path.clone());
child_paths.insert(path.clone());
}
}
}

if depth + 1 > self.stack.len() {
self.stack.resize(depth + 1, TreePart::Line);
for children in tree.values_mut() {
children.sort_unstable();
}
let mut root_paths: Vec<_> = path_set
.into_iter()
.filter(|path| !child_paths.contains(path))
.collect();
root_paths.sort_unstable();

if depth < self.stack.len() {
self.stack[depth] = if is_absolute_last {
TreePart::Corner
} else {
TreePart::Edge
};
}
(root_paths, tree)
}

self.last_depth = Some((depth, is_absolute_last));
fn write_tree_recursive(
&self,
path: &Path,
prefix: &str,
is_last: bool,
tree: &HashMap<PathBuf, Vec<PathBuf>>,
writer: &mut impl Write,
current_depth: usize,
max_depth: Option<usize>,
) -> io::Result<()> {
if let Some(max) = max_depth {
if current_depth > max {
return Ok(());
}
}

buf.clear();
buf.reserve(depth * 4);
for part in self.stack[1..=depth].iter() {
buf.push_str(&part.colored());
let node_prefix = if is_last { "└── " } else { "├── " };
let child_prefix = if is_last { " " } else { "│ " };

let formatted_name = self.format_entry(path);
write!(
writer,
"{}{}{}\n",
prefix.bright_black(),
node_prefix.bright_black(),
formatted_name
)?;

if let Some(children) = tree.get(path) {
let new_prefix = format!("{}{}", prefix, child_prefix);
let last_idx = children.len().saturating_sub(1);
for (i, child) in children.iter().enumerate() {
let is_last_child = i == last_idx;
self.write_tree_recursive(
child,
&new_prefix,
is_last_child,
tree,
writer,
current_depth + 1,
max_depth,
)?;
}
}
Ok(())
}
}

impl FileFormatter for TreeFormatter {
fn format_files(
&self,
files: &[DecoratedEntry],
plugin_manager: &mut PluginManager,
max_depth: Option<usize>,
_plugin_manager: &mut PluginManager,
depth: Option<usize>,
) -> Result<String> {
if files.is_empty() {
return Ok(String::new());
}

let mut trunk = TreeTrunk::default();
let mut prefix_buf = String::with_capacity(128);
let mut entry_buf = String::with_capacity(256);
let mut result = String::new();

let mut entries: Vec<_> = files
.iter()
.map(|entry| {
let path = Path::new(&entry.path);
let depth = path.components().count();
(entry, depth, path.to_path_buf())
})
.collect();

entries.sort_by(|a, b| a.2.cmp(&b.2));

if let Some(max_depth) = max_depth {
entries.retain(|(_, depth, _)| *depth <= max_depth);
if depth == Some(0) {
return Ok(String::new());
}

let avg_line_len = entries
.first()
.map(|(e, d, _)| {
let path = Path::new(&e.path);
let name_len = path.file_name().map_or(0, |n| n.len());
let prefix_len = *d * 4;
name_len + prefix_len + 1
})
.unwrap_or(64);

result.reserve(entries.len() * avg_line_len);

const CHUNK_SIZE: usize = 8192;
for chunk in entries.chunks(CHUNK_SIZE) {
let chunk_len = chunk.len();
for (i, (entry, depth, path)) in chunk.iter().enumerate() {
let is_last = if i + 1 < chunk_len {
let (next_entry, next_depth, _) = &chunk[i + 1];
*depth > *next_depth
|| !Path::new(&next_entry.path).starts_with(path.parent().unwrap_or(path))
} else {
true
};

trunk.get_prefix(*depth, is_last, &mut prefix_buf);
Self::format_entry(
entry,
&prefix_buf,
plugin_manager,
&mut entry_buf,
self.show_icons,
);
result.push_str(&entry_buf);
}
let (root_paths, tree) = self.build_tree(files);
let mut buffer = Vec::with_capacity(BUFFER_SIZE);

let last_idx = root_paths.len().saturating_sub(1);
for (i, path) in root_paths.iter().enumerate() {
let is_last = i == last_idx;
self.write_tree_recursive(path, "", is_last, &tree, &mut buffer, 0, depth)?;
}

Ok(result)
Ok(String::from_utf8_lossy(&buffer).into_owned())
}
}
Loading
Loading