Skip to content

Commit 8554cdf

Browse files
authored
Optimize recursive ls (#2083)
* ls: Remove allocations by eliminating collect/clones * ls: Introduce PathData structure - PathData will hold Path related metadata / strings that are required frequently in subsequent functions - All data is precomputed and cached and subsequent functions just use cached data * ls: Cache more data related to paths - Cache filename and sort by filename instead of full path - Cache uid->usr and gid->grp mappings https://github.com/uutils/coreutils/pull/2099/files * ls: Add BENCHMARKING.md * ls: Document PathData structure * tests/ls: Add testcase for error paths with width option * ls: Fix unused import warning cached will be only used for unix currently as current use of caching gid/uid mappings is only relevant on unix * ls: Suggest checking syscall count in BENCHMARKING.md * ls: Remove mentions of sort in BENCHMARKING.md * ls: Remove dependency on cached Implement caching using HashMap and lazy_static * ls: Fix MSRV error related to map_or Rust 1.40 did not support map_or for result types
1 parent b756b98 commit 8554cdf

File tree

3 files changed

+179
-71
lines changed

3 files changed

+179
-71
lines changed

src/uu/ls/BENCHMARKING.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Benchmarking ls
2+
3+
ls majorly involves fetching a lot of details (depending upon what details are requested, eg. time/date, inode details, etc) for each path using system calls. Ideally, any system call should be done only once for each of the paths - not adhering to this principle leads to a lot of system call overhead multiplying and bubbling up, especially for recursive ls, therefore it is important to always benchmark multiple scenarios.
4+
This is an overwiew over what was benchmarked, and if you make changes to `ls`, you are encouraged to check
5+
how performance was affected for the workloads listed below. Feel free to add other workloads to the
6+
list that we should improve / make sure not to regress.
7+
8+
Run `cargo build --release` before benchmarking after you make a change!
9+
10+
## Simple recursive ls
11+
12+
- Get a large tree, for example linux kernel source tree.
13+
- Benchmark simple recursive ls with hyperfine: `hyperfine --warmup 2 "target/release/coreutils ls -R tree > /dev/null"`.
14+
15+
## Recursive ls with all and long options
16+
17+
- Same tree as above
18+
- Benchmark recursive ls with -al -R options with hyperfine: `hyperfine --warmup 2 "target/release/coreutils ls -al -R tree > /dev/null"`.
19+
20+
## Comparing with GNU ls
21+
22+
Hyperfine accepts multiple commands to run and will compare them. To compare performance with GNU ls
23+
duplicate the string you passed to hyperfine but remove the `target/release/coreutils` bit from it.
24+
25+
Example: `hyperfine --warmup 2 "target/release/coreutils ls -al -R tree > /dev/null"` becomes
26+
`hyperfine --warmup 2 "target/release/coreutils ls -al -R tree > /dev/null" "ls -al -R tree > /dev/null"`
27+
(This assumes GNU ls is installed as `ls`)
28+
29+
This can also be used to compare with version of ls built before your changes to ensure your change does not regress this
30+
31+
## Checking system call count
32+
33+
- Another thing to look at would be system calls count using strace (on linux) or equivalent on other operating systems.
34+
- Example: `strace -c target/release/coreutils ls -al -R tree`

src/uu/ls/src/ls.rs

Lines changed: 137 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1038,12 +1038,43 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
10381038
list(locs, Config::from(matches))
10391039
}
10401040

1041+
/// Represents a Path along with it's associated data
1042+
/// Any data that will be reused several times makes sense to be added to this structure
1043+
/// Caching data here helps eliminate redundant syscalls to fetch same information
1044+
struct PathData {
1045+
// Result<MetaData> got from symlink_metadata() or metadata() based on config
1046+
md: std::io::Result<Metadata>,
1047+
// String formed from get_lossy_string() for the path
1048+
lossy_string: String,
1049+
// Name of the file - will be empty for . or ..
1050+
file_name: String,
1051+
// PathBuf that all above data corresponds to
1052+
p_buf: PathBuf,
1053+
}
1054+
1055+
impl PathData {
1056+
fn new(p_buf: PathBuf, config: &Config, command_line: bool) -> Self {
1057+
let md = get_metadata(&p_buf, config, command_line);
1058+
let lossy_string = p_buf.to_string_lossy().into_owned();
1059+
let name = p_buf
1060+
.file_name()
1061+
.map_or(String::new(), |s| s.to_string_lossy().into_owned());
1062+
Self {
1063+
md,
1064+
lossy_string,
1065+
file_name: name,
1066+
p_buf,
1067+
}
1068+
}
1069+
}
1070+
10411071
fn list(locs: Vec<String>, config: Config) -> i32 {
10421072
let number_of_locs = locs.len();
10431073

1044-
let mut files = Vec::<PathBuf>::new();
1045-
let mut dirs = Vec::<PathBuf>::new();
1074+
let mut files = Vec::<PathData>::new();
1075+
let mut dirs = Vec::<PathData>::new();
10461076
let mut has_failed = false;
1077+
10471078
for loc in locs {
10481079
let p = PathBuf::from(&loc);
10491080
if !p.exists() {
@@ -1054,36 +1085,28 @@ fn list(locs: Vec<String>, config: Config) -> i32 {
10541085
continue;
10551086
}
10561087

1057-
let show_dir_contents = if !config.directory {
1058-
match config.dereference {
1059-
Dereference::None => {
1060-
if let Ok(md) = p.symlink_metadata() {
1061-
md.is_dir()
1062-
} else {
1063-
show_error!("'{}': {}", &loc, "No such file or directory");
1064-
has_failed = true;
1065-
continue;
1066-
}
1067-
}
1068-
_ => p.is_dir(),
1069-
}
1088+
let path_data = PathData::new(p, &config, true);
1089+
1090+
let show_dir_contents = if let Ok(md) = path_data.md.as_ref() {
1091+
!config.directory && md.is_dir()
10701092
} else {
1093+
has_failed = true;
10711094
false
10721095
};
10731096

10741097
if show_dir_contents {
1075-
dirs.push(p);
1098+
dirs.push(path_data);
10761099
} else {
1077-
files.push(p);
1100+
files.push(path_data);
10781101
}
10791102
}
10801103
sort_entries(&mut files, &config);
1081-
display_items(&files, None, &config, true);
1104+
display_items(&files, None, &config);
10821105

10831106
sort_entries(&mut dirs, &config);
10841107
for dir in dirs {
10851108
if number_of_locs > 1 {
1086-
println!("\n{}:", dir.to_string_lossy());
1109+
println!("\n{}:", dir.lossy_string);
10871110
}
10881111
enter_directory(&dir, &config);
10891112
}
@@ -1094,22 +1117,22 @@ fn list(locs: Vec<String>, config: Config) -> i32 {
10941117
}
10951118
}
10961119

1097-
fn sort_entries(entries: &mut Vec<PathBuf>, config: &Config) {
1120+
fn sort_entries(entries: &mut Vec<PathData>, config: &Config) {
10981121
match config.sort {
10991122
Sort::Time => entries.sort_by_key(|k| {
11001123
Reverse(
1101-
get_metadata(k, false)
1124+
k.md.as_ref()
11021125
.ok()
11031126
.and_then(|md| get_system_time(&md, config))
11041127
.unwrap_or(UNIX_EPOCH),
11051128
)
11061129
}),
11071130
Sort::Size => {
1108-
entries.sort_by_key(|k| Reverse(get_metadata(k, false).map(|md| md.len()).unwrap_or(0)))
1131+
entries.sort_by_key(|k| Reverse(k.md.as_ref().map(|md| md.len()).unwrap_or(0)))
11091132
}
11101133
// The default sort in GNU ls is case insensitive
1111-
Sort::Name => entries.sort_by_key(|k| k.to_string_lossy().to_lowercase()),
1112-
Sort::Version => entries.sort_by(|a, b| version_cmp::version_cmp(a, b)),
1134+
Sort::Name => entries.sort_by_key(|k| k.file_name.to_lowercase()),
1135+
Sort::Version => entries.sort_by(|k, j| version_cmp::version_cmp(&k.p_buf, &j.p_buf)),
11131136
Sort::None => {}
11141137
}
11151138

@@ -1143,41 +1166,66 @@ fn should_display(entry: &DirEntry, config: &Config) -> bool {
11431166
true
11441167
}
11451168

1146-
fn enter_directory(dir: &Path, config: &Config) {
1147-
let mut entries: Vec<_> = safe_unwrap!(fs::read_dir(dir).and_then(Iterator::collect));
1169+
fn enter_directory(dir: &PathData, config: &Config) {
1170+
let mut entries: Vec<_> = if config.files == Files::All {
1171+
vec![
1172+
PathData::new(dir.p_buf.join("."), config, false),
1173+
PathData::new(dir.p_buf.join(".."), config, false),
1174+
]
1175+
} else {
1176+
vec![]
1177+
};
11481178

1149-
entries.retain(|e| should_display(e, config));
1179+
let mut temp: Vec<_> = safe_unwrap!(fs::read_dir(&dir.p_buf))
1180+
.map(|res| safe_unwrap!(res))
1181+
.filter(|e| should_display(e, config))
1182+
.map(|e| PathData::new(DirEntry::path(&e), config, false))
1183+
.collect();
11501184

1151-
let mut entries: Vec<_> = entries.iter().map(DirEntry::path).collect();
1152-
sort_entries(&mut entries, config);
1185+
sort_entries(&mut temp, config);
11531186

1154-
if config.files == Files::All {
1155-
let mut display_entries = entries.clone();
1156-
display_entries.insert(0, dir.join(".."));
1157-
display_entries.insert(0, dir.join("."));
1158-
display_items(&display_entries, Some(dir), config, false);
1159-
} else {
1160-
display_items(&entries, Some(dir), config, false);
1161-
}
1187+
entries.append(&mut temp);
1188+
1189+
display_items(&entries, Some(&dir.p_buf), config);
11621190

11631191
if config.recursive {
1164-
for e in entries.iter().filter(|p| p.is_dir()) {
1165-
println!("\n{}:", e.to_string_lossy());
1192+
for e in entries
1193+
.iter()
1194+
.skip(if config.files == Files::All { 2 } else { 0 })
1195+
.filter(|p| p.md.as_ref().map(|md| md.is_dir()).unwrap_or(false))
1196+
{
1197+
println!("\n{}:", e.lossy_string);
11661198
enter_directory(&e, config);
11671199
}
11681200
}
11691201
}
11701202

1171-
fn get_metadata(entry: &Path, dereference: bool) -> std::io::Result<Metadata> {
1203+
fn get_metadata(entry: &Path, config: &Config, command_line: bool) -> std::io::Result<Metadata> {
1204+
let dereference = match &config.dereference {
1205+
Dereference::All => true,
1206+
Dereference::Args => command_line,
1207+
Dereference::DirArgs => {
1208+
if command_line {
1209+
if let Ok(md) = entry.metadata() {
1210+
md.is_dir()
1211+
} else {
1212+
false
1213+
}
1214+
} else {
1215+
false
1216+
}
1217+
}
1218+
Dereference::None => false,
1219+
};
11721220
if dereference {
11731221
entry.metadata().or_else(|_| entry.symlink_metadata())
11741222
} else {
11751223
entry.symlink_metadata()
11761224
}
11771225
}
11781226

1179-
fn display_dir_entry_size(entry: &Path, config: &Config) -> (usize, usize) {
1180-
if let Ok(md) = get_metadata(entry, false) {
1227+
fn display_dir_entry_size(entry: &PathData, config: &Config) -> (usize, usize) {
1228+
if let Ok(md) = entry.md.as_ref() {
11811229
(
11821230
display_symlink_count(&md).len(),
11831231
display_file_size(&md, config).len(),
@@ -1191,7 +1239,7 @@ fn pad_left(string: String, count: usize) -> String {
11911239
format!("{:>width$}", string, width = count)
11921240
}
11931241

1194-
fn display_items(items: &[PathBuf], strip: Option<&Path>, config: &Config, command_line: bool) {
1242+
fn display_items(items: &[PathData], strip: Option<&Path>, config: &Config) {
11951243
if config.format == Format::Long {
11961244
let (mut max_links, mut max_size) = (1, 1);
11971245
for item in items {
@@ -1200,18 +1248,18 @@ fn display_items(items: &[PathBuf], strip: Option<&Path>, config: &Config, comma
12001248
max_size = size.max(max_size);
12011249
}
12021250
for item in items {
1203-
display_item_long(item, strip, max_links, max_size, config, command_line);
1251+
display_item_long(item, strip, max_links, max_size, config);
12041252
}
12051253
} else {
12061254
let names = items.iter().filter_map(|i| {
1207-
let md = get_metadata(i, false);
1255+
let md = i.md.as_ref();
12081256
match md {
12091257
Err(e) => {
1210-
let filename = get_file_name(i, strip);
1258+
let filename = get_file_name(&i.p_buf, strip);
12111259
show_error!("'{}': {}", filename, e);
12121260
None
12131261
}
1214-
Ok(md) => Some(display_file_name(&i, strip, &md, config)),
1262+
Ok(md) => Some(display_file_name(&i.p_buf, strip, &md, config)),
12151263
}
12161264
});
12171265

@@ -1271,33 +1319,15 @@ fn display_grid(names: impl Iterator<Item = Cell>, width: u16, direction: Direct
12711319
use uucore::fs::display_permissions;
12721320

12731321
fn display_item_long(
1274-
item: &Path,
1322+
item: &PathData,
12751323
strip: Option<&Path>,
12761324
max_links: usize,
12771325
max_size: usize,
12781326
config: &Config,
1279-
command_line: bool,
12801327
) {
1281-
let dereference = match &config.dereference {
1282-
Dereference::All => true,
1283-
Dereference::Args => command_line,
1284-
Dereference::DirArgs => {
1285-
if command_line {
1286-
if let Ok(md) = item.metadata() {
1287-
md.is_dir()
1288-
} else {
1289-
false
1290-
}
1291-
} else {
1292-
false
1293-
}
1294-
}
1295-
Dereference::None => false,
1296-
};
1297-
1298-
let md = match get_metadata(item, dereference) {
1328+
let md = match &item.md {
12991329
Err(e) => {
1300-
let filename = get_file_name(&item, strip);
1330+
let filename = get_file_name(&item.p_buf, strip);
13011331
show_error!("{}: {}", filename, e);
13021332
return;
13031333
}
@@ -1336,7 +1366,7 @@ fn display_item_long(
13361366
" {} {} {}",
13371367
pad_left(display_file_size(&md, config), max_size),
13381368
display_date(&md, config),
1339-
display_file_name(&item, strip, &md, config).contents,
1369+
display_file_name(&item.p_buf, strip, &md, config).contents,
13401370
);
13411371
}
13421372

@@ -1348,14 +1378,50 @@ fn get_inode(metadata: &Metadata) -> String {
13481378
// Currently getpwuid is `linux` target only. If it's broken out into
13491379
// a posix-compliant attribute this can be updated...
13501380
#[cfg(unix)]
1381+
use std::sync::Mutex;
1382+
#[cfg(unix)]
13511383
use uucore::entries;
13521384

1385+
#[cfg(unix)]
1386+
fn cached_uid2usr(uid: u32) -> String {
1387+
lazy_static! {
1388+
static ref UID_CACHE: Mutex<HashMap<u32, String>> = Mutex::new(HashMap::new());
1389+
}
1390+
1391+
let mut uid_cache = UID_CACHE.lock().unwrap();
1392+
match uid_cache.get(&uid) {
1393+
Some(usr) => usr.clone(),
1394+
None => {
1395+
let usr = entries::uid2usr(uid).unwrap_or_else(|_| uid.to_string());
1396+
uid_cache.insert(uid, usr.clone());
1397+
usr
1398+
}
1399+
}
1400+
}
1401+
13531402
#[cfg(unix)]
13541403
fn display_uname(metadata: &Metadata, config: &Config) -> String {
13551404
if config.long.numeric_uid_gid {
13561405
metadata.uid().to_string()
13571406
} else {
1358-
entries::uid2usr(metadata.uid()).unwrap_or_else(|_| metadata.uid().to_string())
1407+
cached_uid2usr(metadata.uid())
1408+
}
1409+
}
1410+
1411+
#[cfg(unix)]
1412+
fn cached_gid2grp(gid: u32) -> String {
1413+
lazy_static! {
1414+
static ref GID_CACHE: Mutex<HashMap<u32, String>> = Mutex::new(HashMap::new());
1415+
}
1416+
1417+
let mut gid_cache = GID_CACHE.lock().unwrap();
1418+
match gid_cache.get(&gid) {
1419+
Some(grp) => grp.clone(),
1420+
None => {
1421+
let grp = entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string());
1422+
gid_cache.insert(gid, grp.clone());
1423+
grp
1424+
}
13591425
}
13601426
}
13611427

@@ -1364,7 +1430,7 @@ fn display_group(metadata: &Metadata, config: &Config) -> String {
13641430
if config.long.numeric_uid_gid {
13651431
metadata.gid().to_string()
13661432
} else {
1367-
entries::gid2grp(metadata.gid()).unwrap_or_else(|_| metadata.gid().to_string())
1433+
cached_gid2grp(metadata.gid())
13681434
}
13691435
}
13701436

tests/by-util/test_ls.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,14 @@ fn test_ls_width() {
103103
.succeeds()
104104
.stdout_only("test-width-1\ntest-width-2\ntest-width-3\ntest-width-4\n");
105105
}
106+
107+
for option in &["-w 1a", "-w=1a", "--width=1a", "--width 1a"] {
108+
scene
109+
.ucmd()
110+
.args(&option.split(" ").collect::<Vec<_>>())
111+
.fails()
112+
.stderr_only("ls: error: invalid line width: ‘1a’");
113+
}
106114
}
107115

108116
#[test]

0 commit comments

Comments
 (0)