Skip to content

Commit bf8b12e

Browse files
authored
Merge branch 'main' into bench-nl
2 parents e742d9c + 1331ff1 commit bf8b12e

File tree

37 files changed

+1168
-267
lines changed

37 files changed

+1168
-267
lines changed

.github/workflows/CICD.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,9 @@ jobs:
782782
# We also create a couple optional files pinky looks for
783783
touch /home/runner/.project
784784
echo "foo" > /home/runner/.plan
785+
# add user with digital username for testing with issue #7787
786+
echo 200:x:2000:2000::/home/200:/bin/bash | sudo tee -a /etc/passwd
787+
echo 200:x:2000: | sudo tee -a /etc/group
785788
;;
786789
esac
787790
- uses: taiki-e/install-action@v2
@@ -1156,6 +1159,9 @@ jobs:
11561159
# We also create a couple optional files pinky looks for
11571160
touch /home/runner/.project
11581161
echo "foo" > /home/runner/.plan
1162+
# add user with digital username for testing with issue #7787
1163+
echo 200:x:2000:2000::/home/200:/bin/bash | sudo tee -a /etc/passwd
1164+
echo 200:x:2000: | sudo tee -a /etc/group
11591165
;;
11601166
esac
11611167

.vscode/cspell.dictionaries/workspace.wordlist.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ ENOSYS
128128
ENOTEMPTY
129129
EOPNOTSUPP
130130
EPERM
131+
EPIPE
131132
EROFS
132133

133134
# * vars/fcntl

Cargo.lock

Lines changed: 10 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: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -600,12 +600,14 @@ lto = true
600600
[profile.release-fast]
601601
inherits = "release"
602602
panic = "abort"
603+
codegen-units = 1
603604

604605
# A release-like profile that is as small as possible.
605606
[profile.release-small]
606607
inherits = "release"
607608
opt-level = "z"
608609
panic = "abort"
610+
codegen-units = 1
609611
strip = true
610612

611613
# A release-like profile with debug info, useful for profiling.
@@ -688,20 +690,16 @@ unused_self = "allow"
688690
map_unwrap_or = "allow"
689691
enum_glob_use = "allow"
690692
ptr_cast_constness = "allow"
691-
if_not_else = "allow"
692693
borrow_as_ptr = "allow"
693694
ptr_as_ptr = "allow"
694695
manual_let_else = "allow"
695696
unnecessary_semicolon = "allow"
696-
bool_to_int_with_if = "allow"
697697
needless_raw_string_hashes = "allow"
698698
unreadable_literal = "allow"
699699
unnested_or_patterns = "allow"
700-
semicolon_if_nothing_returned = "allow"
701700
implicit_hasher = "allow"
702701
struct_field_names = "allow"
703702
doc_link_with_quotes = "allow"
704-
single_char_pattern = "allow"
705703
format_push_string = "allow"
706704
flat_map_option = "allow"
707705
from_iter_instead_of_collect = "allow"

src/uu/base64/Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@ uucore = { workspace = true, features = ["encoding"] }
2323
uu_base32 = { workspace = true }
2424
fluent = { workspace = true }
2525

26+
[dev-dependencies]
27+
divan = { workspace = true }
28+
tempfile = { workspace = true }
29+
uucore = { workspace = true, features = ["benchmark"] }
30+
2631
[[bin]]
2732
name = "base64"
2833
path = "src/main.rs"
34+
35+
[[bench]]
36+
name = "base64_bench"
37+
harness = false
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// This file is part of the uutils coreutils package.
2+
//
3+
// For the full copyright and license information, please view the LICENSE
4+
// file that was distributed with this source code.
5+
6+
use divan::{Bencher, black_box};
7+
use std::ffi::OsString;
8+
use uu_base64::uumain;
9+
use uucore::benchmark::{create_test_file, run_util_function, text_data};
10+
11+
fn create_tmp_file(size_mb: usize) -> String {
12+
let temp_dir = tempfile::tempdir().unwrap();
13+
let data = text_data::generate_by_size(size_mb, 80);
14+
let file_path = create_test_file(&data, temp_dir.path());
15+
String::from(file_path.to_str().unwrap())
16+
}
17+
18+
/// Benchmark for base64 encoding
19+
#[divan::bench()]
20+
fn b64_encode_synthetic(bencher: Bencher) {
21+
let file_path_str = &create_tmp_file(5_000);
22+
23+
bencher.bench(|| {
24+
black_box(run_util_function(uumain, &[file_path_str]));
25+
});
26+
}
27+
28+
// Benchmark for base64 decoding
29+
#[divan::bench()]
30+
fn b64_decode_synthetic(bencher: Bencher) {
31+
let temp_dir = tempfile::tempdir().unwrap();
32+
let file_path_str = &create_tmp_file(5_000);
33+
let in_file = create_test_file(b"", temp_dir.path());
34+
let in_file_str = in_file.to_str().unwrap();
35+
uumain(
36+
[
37+
OsString::from(file_path_str),
38+
OsString::from(format!(">{in_file_str}")),
39+
]
40+
.iter()
41+
.map(|x| (*x).clone()),
42+
);
43+
44+
bencher.bench(|| {
45+
black_box(run_util_function(uumain, &["-d", in_file_str]));
46+
});
47+
}
48+
49+
// Benchmark different file sizes for base64 decoding ignoring garbage characters
50+
#[divan::bench()]
51+
fn b64_decode_ignore_garbage_synthetic(bencher: Bencher) {
52+
let temp_dir = tempfile::tempdir().unwrap();
53+
let file_path_str = &create_tmp_file(5_000);
54+
let in_file = create_test_file(b"", temp_dir.path());
55+
let in_file_str = in_file.to_str().unwrap();
56+
uumain(
57+
[
58+
OsString::from(file_path_str),
59+
OsString::from(format!(">{in_file_str}")),
60+
]
61+
.iter()
62+
.map(|x| (*x).clone()),
63+
);
64+
65+
bencher.bench(|| {
66+
black_box(run_util_function(uumain, &["-d", "-i", in_file_str]));
67+
});
68+
}
69+
70+
fn main() {
71+
divan::main();
72+
}

src/uu/cp/BENCHMARKING.md

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
<!-- spell-checker:ignore hyperfine tmpfs reflink fsxattr xattrs clonefile vmtouch APFS pathlib Btrfs fallocate journaling -->
2+
3+
# Benchmarking cp
4+
5+
`cp` copies file contents together with metadata such as permissions, ownership,
6+
timestamps, extended attributes, and directory structures. Although copying
7+
looks simple, `cp` exercises many filesystem features. Its performance depends
8+
heavily on the workload shape (large sequential files, many tiny files, special
9+
files, sparse images) and the storage stack underneath.
10+
11+
## Understanding cp
12+
13+
Most of the time spent inside `cp` falls into two broad categories:
14+
15+
- **Data transfer path**: When copying large contiguous files, throughput is
16+
dominated by read/write bandwidth. The overhead from `cp` itself comes from
17+
performing buffered reads and writes, copying memory between buffers, and the
18+
number of system calls issued per block.
19+
- **Metadata handling**: When recursively copying trees with thousands of small
20+
files, performance is limited by metadata work such as `open`, `stat`,
21+
`lstat`, attribute preservation, directory creation, and link handling.
22+
23+
`cp` supports many switches that alter these paths, including attribute
24+
preservation, hard-link and reflink creation, sparse detection, and
25+
`--remove-destination` semantics. Benchmarks should call out which pathways are
26+
being exercised so results can be interpreted correctly.
27+
28+
## Benchmarking guidelines
29+
30+
- Build a release binary first: `cargo build --release -p uu_cp`.
31+
- Use `hyperfine` for timing and rely on the `--prepare` hook to reset state
32+
between runs.
33+
- Prefer running on a fast device (RAM disk, tmpfs, NVMe) to minimize raw
34+
storage latency when isolating the cost of the tool.
35+
- On Linux, control the page cache where appropriate using tools like
36+
`vmtouch` or `echo 3 > /proc/sys/vm/drop_caches` (root required). Prioritize
37+
repeatability and stay within the policies of the host system.
38+
- Keep the workload definition explicit. When comparing against GNU `cp` or
39+
other implementations, ensure identical datasets and mount options.
40+
41+
## Large-file throughput
42+
43+
1. Create a clean working directory and reduce cache interference.
44+
2. Generate an input file of known size, for example with `truncate` or `dd`.
45+
3. Run repeated copies with `hyperfine`, deleting the destination beforehand.
46+
47+
```shell
48+
mkdir -p benchmark/cp && cd benchmark/cp
49+
truncate -s 2G input.bin
50+
hyperfine \
51+
--warmup 2 \
52+
--prepare 'rm -f output.bin' \
53+
'../target/release/cp input.bin output.bin'
54+
```
55+
56+
What to record:
57+
58+
- Achieved throughput (MB/s) for large sequential copies.
59+
- Behavior with `--reflink=auto` or `--sparse=auto` on filesystems that
60+
support copy-on-write or sparse regions.
61+
- CPU overhead when enabling attribute preservation such as
62+
`--preserve=mode,timestamps,xattr`.
63+
64+
If the underlying filesystem performs transparent copy-on-write (for example,
65+
APFS via `clonefile`), consider running the same benchmark with `--reflink=never`
66+
or on a filesystem without reflink support to measure raw data transfer.
67+
68+
## Many small files
69+
70+
Large directory trees stress metadata throughput. Pre-create a synthetic tree
71+
and copy it recursively.
72+
73+
```shell
74+
mkdir -p dataset/src
75+
python3 - <<'PY'
76+
from pathlib import Path
77+
root = Path('dataset/src')
78+
for i in range(2000):
79+
sub = root / f'dir_{i//200}'
80+
sub.mkdir(parents=True, exist_ok=True)
81+
for j in range(5):
82+
path = sub / f'file_{i}_{j}.txt'
83+
path.write_text('payload' * 16)
84+
PY
85+
hyperfine \
86+
--warmup 1 \
87+
--prepare 'rm -rf dataset/dst && mkdir -p dataset/dst' \
88+
'../target/release/cp -r dataset/src dataset/dst'
89+
```
90+
91+
What to record:
92+
93+
- Time spent in directory traversal and metadata replication.
94+
- Impact of toggling options such as `--preserve`, `--no-preserve`, `--link`,
95+
`--hard-link`, and `--archive`.
96+
- Behavior when symbolic links or hard links are present, especially with
97+
`--dereference` versus `--no-dereference`.
98+
99+
## Copy-on-write and sparse files
100+
101+
`--reflink=always` can dramatically reduce work on Btrfs, XFS, APFS, and other
102+
reflink-aware filesystems. Compare results with `--reflink=never` to understand
103+
how much time is spent in copy-on-write system calls versus fallback copying.
104+
Sparse workloads benefit from dedicated benchmarks as well.
105+
106+
```shell
107+
truncate -s 4G sparse.img
108+
fallocate -d sparse.img # On filesystems that support punching holes
109+
hyperfine \
110+
--prepare 'rm -f sparse-copy.img' \
111+
'../target/release/cp --sparse=always sparse.img sparse-copy.img'
112+
```
113+
114+
Check both the elapsed time and the on-disk size of the destination (for
115+
example using `du -h sparse-copy.img`) to confirm sparse regions are preserved.
116+
117+
## Evaluating attribute preservation and extras
118+
119+
Measure the incremental cost of individual options by enabling them one at a
120+
time:
121+
122+
- Test `--preserve=context` or `--preserve=xattr` on files that actually carry
123+
extended attributes.
124+
- Evaluate ACL and SELinux handling with `--archive` on systems where those
125+
features are active.
126+
- Compare modes that remove or back up the destination (`--remove-destination`,
127+
`--backup=numbered`) to see the impact of extra file operations.
128+
129+
Supplementary analysis with `strace -c` or `perf record` can show which system
130+
calls dominate and guide optimization work.
131+
132+
## Interpreting results
133+
134+
- If a benchmark completes in well under a second, increase the dataset size to
135+
reduce process start-up noise.
136+
- Document filesystem features such as journaling, compression, or encryption
137+
that may skew results.
138+
- When changes are made to `cp`, track how system call counts, I/O patterns,
139+
and CPU time shift between runs to catch regressions early.
140+
141+
Use these guidelines to isolate the workloads you care about (large sequential
142+
transfers, directory-heavy copies, attribute preservation, reflink paths) and
143+
collect reproducible measurements.

src/uu/cp/Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,15 @@ exacl = { workspace = true, optional = true }
4747
name = "cp"
4848
path = "src/main.rs"
4949

50+
[dev-dependencies]
51+
divan = { workspace = true }
52+
tempfile = { workspace = true }
53+
uucore = { workspace = true, features = ["benchmark"] }
54+
55+
[[bench]]
56+
name = "cp_bench"
57+
harness = false
58+
5059
[features]
5160
feat_selinux = ["selinux", "uucore/selinux"]
5261
feat_acl = ["exacl"]

0 commit comments

Comments
 (0)