Skip to content

Commit 18f5f2a

Browse files
feat: limit thread number to 1 when HDD is detected (#269)
* feat: limit thread number to 1 when HDD is detected On HDD, multi-threaded du won't provide any performance benefit over the single-threaded one. Therefore, when HDD is detected on one of the files, limit the thread number to 1. This PR fixes issue #257. * Fix change requests * refactor: create a module * refactor: rename a test * refactor: unnecessary visibility scope * refactor: make `detect_hdd_in_files` testable * feat: improve warning messages * refactor: rename a function * refactor: rename an argument * refactor: extract a function * Add tests for path_is_in_hdd() & any_path_is_in_hdd() * Apply suggestions * Update src/app/sub/hdd.rs Co-authored-by: Khải <hvksmr1996@gmail.com> * Remove unused import * refactor: use mocked trait * style: remove unnecessary trailing comma * refactor: simplify the code * refactor: use constructor to reduce lines of code * test: add more cases * refactor: shorten the code * refactor: descriptive paths * refactor: rearrange * refactor: shorten the code * style: add trailing comma * refactor: early return * docs: add --------- Co-authored-by: khai96_ <hvksmr1996@gmail.com>
1 parent a463576 commit 18f5f2a

File tree

5 files changed

+313
-0
lines changed

5 files changed

+313
-0
lines changed

Cargo.lock

Lines changed: 105 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ clap_complete = { version = "^4.5.36", optional = true }
6565
clap-utilities = { version = "^0.2.0", optional = true }
6666
serde = { version = "^1.0.214", optional = true }
6767
serde_json = { version = "^1.0.132", optional = true }
68+
sysinfo = "0.32.0"
6869

6970
[dev-dependencies]
7071
build-fs-tree = "^0.7.1"

src/app/sub.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
mod hdd;
2+
mod mount_point;
3+
14
use crate::{
25
args::Fraction,
36
data_tree::{DataTree, DataTreeReflection},
@@ -11,8 +14,10 @@ use crate::{
1114
status_board::GLOBAL_STATUS_BOARD,
1215
visualizer::{BarAlignment, ColumnWidthDistribution, Direction, Visualizer},
1316
};
17+
use hdd::any_path_is_in_hdd;
1418
use serde::Serialize;
1519
use std::{io::stdout, iter::once, num::NonZeroUsize, path::PathBuf};
20+
use sysinfo::Disks;
1621

1722
/// The sub program of the main application.
1823
pub struct Sub<Size, SizeGetter, Report>
@@ -69,6 +74,17 @@ where
6974
no_sort,
7075
} = self;
7176

77+
// If one of the files is on HDD, set thread number to 1
78+
let disks = Disks::new_with_refreshed_list();
79+
80+
if any_path_is_in_hdd::<hdd::RealApi>(&files, &disks) {
81+
eprintln!("warning: HDD detected, the thread limit will be set to 1");
82+
rayon::ThreadPoolBuilder::new()
83+
.num_threads(1)
84+
.build_global()
85+
.unwrap_or_else(|_| eprintln!("warning: Failed to set thread limit to 1"));
86+
}
87+
7288
let mut iter = files
7389
.into_iter()
7490
.map(|root| -> DataTree<OsStringDisplay, Size> {

src/app/sub/hdd.rs

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
use super::mount_point::find_mount_point;
2+
use std::{
3+
fs::canonicalize,
4+
io,
5+
path::{Path, PathBuf},
6+
};
7+
use sysinfo::{Disk, DiskKind};
8+
9+
/// Mockable APIs to interact with the system.
10+
pub trait Api {
11+
type Disk;
12+
fn get_disk_kind(disk: &Self::Disk) -> DiskKind;
13+
fn get_mount_point(disk: &Self::Disk) -> &Path;
14+
fn canonicalize(path: &Path) -> io::Result<PathBuf>;
15+
}
16+
17+
/// Implementation of [`Api`] that interacts with the real system.
18+
pub struct RealApi;
19+
impl Api for RealApi {
20+
type Disk = Disk;
21+
22+
fn get_disk_kind(disk: &Self::Disk) -> DiskKind {
23+
disk.kind()
24+
}
25+
26+
fn get_mount_point(disk: &Self::Disk) -> &Path {
27+
disk.mount_point()
28+
}
29+
30+
fn canonicalize(path: &Path) -> io::Result<PathBuf> {
31+
canonicalize(path)
32+
}
33+
}
34+
35+
/// Check if any path is in any HDD.
36+
pub fn any_path_is_in_hdd<Api: self::Api>(paths: &[PathBuf], disks: &[Api::Disk]) -> bool {
37+
paths
38+
.iter()
39+
.filter_map(|file| Api::canonicalize(file).ok())
40+
.any(|path| path_is_in_hdd::<Api>(&path, disks))
41+
}
42+
43+
/// Check if path is in any HDD.
44+
fn path_is_in_hdd<Api: self::Api>(path: &Path, disks: &[Api::Disk]) -> bool {
45+
let Some(mount_point) = find_mount_point(path, disks.iter().map(Api::get_mount_point)) else {
46+
return false;
47+
};
48+
disks
49+
.iter()
50+
.filter(|disk| Api::get_disk_kind(disk) == DiskKind::HDD)
51+
.any(|disk| Api::get_mount_point(disk) == mount_point)
52+
}
53+
54+
#[cfg(test)]
55+
mod tests {
56+
use super::{any_path_is_in_hdd, path_is_in_hdd, Api};
57+
use pipe_trait::Pipe;
58+
use pretty_assertions::assert_eq;
59+
use std::path::{Path, PathBuf};
60+
use sysinfo::DiskKind;
61+
62+
/// Fake disk for [`Api`].
63+
struct Disk {
64+
kind: DiskKind,
65+
mount_point: &'static str,
66+
}
67+
68+
impl Disk {
69+
fn new(kind: DiskKind, mount_point: &'static str) -> Self {
70+
Self { kind, mount_point }
71+
}
72+
}
73+
74+
/// Mocked implementation of [`Api`] for testing purposes.
75+
struct MockedApi;
76+
impl Api for MockedApi {
77+
type Disk = Disk;
78+
79+
fn get_disk_kind(disk: &Self::Disk) -> DiskKind {
80+
disk.kind
81+
}
82+
83+
fn get_mount_point(disk: &Self::Disk) -> &Path {
84+
Path::new(disk.mount_point)
85+
}
86+
87+
fn canonicalize(path: &Path) -> std::io::Result<PathBuf> {
88+
path.to_path_buf().pipe(Ok)
89+
}
90+
}
91+
92+
#[test]
93+
fn test_any_path_in_hdd() {
94+
let disks = &[
95+
Disk::new(DiskKind::SSD, "/"),
96+
Disk::new(DiskKind::HDD, "/home"),
97+
Disk::new(DiskKind::HDD, "/mnt/hdd-data"),
98+
Disk::new(DiskKind::SSD, "/mnt/ssd-data"),
99+
Disk::new(DiskKind::HDD, "/mnt/hdd-data/repo"),
100+
];
101+
102+
let cases: &[(&[&str], bool)] = &[
103+
(&[], false),
104+
(&["/"], false),
105+
(&["/home"], true),
106+
(&["/mnt"], false),
107+
(&["/mnt/ssd-data"], false),
108+
(&["/mnt/hdd-data"], true),
109+
(&["/mnt/hdd-data/repo"], true),
110+
(&["/etc/fstab"], false),
111+
(&["/home/usr/file"], true),
112+
(&["/home/data/repo/test"], true),
113+
(&["/usr/share"], false),
114+
(&["/mnt/ssd-data/test"], false),
115+
(&["/etc/fstab", "/home/user/file"], true),
116+
(&["/mnt/hdd-data/file", "/mnt/hdd-data/repo/test"], true),
117+
(&["/usr/share", "/mnt/ssd-data/test"], false),
118+
(
119+
&["/etc/fstab", "/home/user", "/mnt/hdd-data", "/usr/share"],
120+
true,
121+
),
122+
];
123+
124+
for (paths, in_hdd) in cases {
125+
let paths: Vec<_> = paths.iter().map(PathBuf::from).collect();
126+
println!("CASE: {paths:?} → {in_hdd:?}");
127+
assert_eq!(any_path_is_in_hdd::<MockedApi>(&paths, disks), *in_hdd);
128+
}
129+
}
130+
131+
#[test]
132+
fn test_path_in_hdd() {
133+
let disks = &[
134+
Disk::new(DiskKind::SSD, "/"),
135+
Disk::new(DiskKind::HDD, "/home"),
136+
Disk::new(DiskKind::HDD, "/mnt/hdd-data"),
137+
Disk::new(DiskKind::SSD, "/mnt/ssd-data"),
138+
Disk::new(DiskKind::HDD, "/mnt/hdd-data/repo"),
139+
];
140+
141+
for (path, in_hdd) in [
142+
("/etc/fstab", false),
143+
("/mnt/", false),
144+
("/mnt/hdd-data/repo/test", true),
145+
("/mnt/hdd-data/test/test", true),
146+
("/mnt/ssd-data/test/test", false),
147+
] {
148+
println!("CASE: {path} → {in_hdd:?}");
149+
assert_eq!(path_is_in_hdd::<MockedApi>(Path::new(path), disks), in_hdd);
150+
}
151+
}
152+
}

src/app/sub/mount_point.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
use std::{ffi::OsStr, path::Path};
2+
3+
/// Find a mount point that contains `absolute_path`.
4+
pub fn find_mount_point<'a>(
5+
absolute_path: &Path,
6+
all_mount_points: impl IntoIterator<Item = &'a Path>,
7+
) -> Option<&'a Path> {
8+
all_mount_points
9+
.into_iter()
10+
.filter(|mnt| absolute_path.starts_with(mnt))
11+
.max_by_key(|mnt| AsRef::<OsStr>::as_ref(mnt).len()) // Mount points can be nested in each other
12+
}
13+
14+
#[cfg(test)]
15+
mod tests {
16+
use super::find_mount_point;
17+
use pretty_assertions::assert_eq;
18+
use std::path::Path;
19+
20+
#[test]
21+
fn test_mount_point() {
22+
let all_mount_points = ["/", "/home", "/mnt/data", "/mnt/data/repo", "/mnt/repo"];
23+
24+
for (path, expected_mount_point) in &[
25+
("/etc/fstab", "/"),
26+
("/home/user", "/home"),
27+
("/mnt/data/repo/test", "/mnt/data/repo"),
28+
("/mnt/data/test/test", "/mnt/data/"),
29+
("/mnt/repo/test/test", "/mnt/repo/"),
30+
] {
31+
println!("CASE: {path} → {expected_mount_point}");
32+
let all_mount_points = all_mount_points.map(Path::new);
33+
assert_eq!(
34+
find_mount_point(Path::new(path), all_mount_points).unwrap(),
35+
Path::new(expected_mount_point),
36+
);
37+
}
38+
}
39+
}

0 commit comments

Comments
 (0)