Skip to content

Commit 8ae8eba

Browse files
committed
yes: Add zero copy fast-path
1 parent d916606 commit 8ae8eba

5 files changed

Lines changed: 58 additions & 7 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ advapi32-sys
88
aho-corasick
99
backtrace
1010
blake2b_simd
11+
rustix
1112

1213
# * uutils project
1314
uutils

Cargo.lock

Lines changed: 1 addition & 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
@@ -428,6 +428,7 @@ rlimit = "0.11.0"
428428
rstest = "0.26.0"
429429
rustc-hash = "2.1.1"
430430
rust-ini = "0.21.0"
431+
rustix = { version = "1.1.4", features = ["fs", "param", "pipe"] }
431432
same-file = "1.0.6"
432433
self_cell = "1.0.4"
433434
selinux = "=0.6.0"

src/uu/yes/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ path = "src/yes.rs"
2222
clap = { workspace = true }
2323
itertools = { workspace = true }
2424
fluent = { workspace = true }
25+
rustix = { workspace = true }
2526

2627
[target.'cfg(unix)'.dependencies]
2728
uucore = { workspace = true, features = ["pipes", "signals"] }

src/uu/yes/src/yes.rs

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// For the full copyright and license information, please view the LICENSE
44
// file that was distributed with this source code.
55

6-
// cSpell:ignore strs
6+
// cSpell:ignore strs setpipe
77

88
use clap::{Arg, ArgAction, Command, builder::ValueParser};
99
use std::error::Error;
@@ -13,18 +13,46 @@ use uucore::error::{UResult, USimpleError};
1313
use uucore::format_usage;
1414
use uucore::translate;
1515

16+
#[cfg(target_os = "linux")]
17+
use rustix::{
18+
fd::{AsRawFd, BorrowedFd},
19+
param::page_size,
20+
pipe::{IoSliceRaw, SpliceFlags, fcntl_setpipe_size, vmsplice},
21+
};
1622
// it's possible that using a smaller or larger buffer might provide better performance on some
1723
// systems, but honestly this is good enough
1824
const BUF_SIZE: usize = 16 * 1024;
1925

2026
#[uucore::main]
2127
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
2228
let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?;
29+
// use larger pipe if zero-copy is possible
30+
// todo: deduplicate logic
31+
#[cfg(target_os = "linux")]
32+
let buf_size = {
33+
use std::os::unix::fs::FileTypeExt;
34+
// todo: detect pipe under masked /dev. This is really bad detection.
35+
if let Ok(m) = std::fs::metadata("/dev/stdout")
36+
&& m.file_type().is_fifo()
37+
{
38+
const ROOTLESS_MAX_PIPE_SIZE: usize = 1024 * 1024;
39+
let fd_raw = io::stdout().as_raw_fd();
40+
fcntl_setpipe_size(
41+
unsafe { BorrowedFd::borrow_raw(fd_raw) },
42+
ROOTLESS_MAX_PIPE_SIZE,
43+
)
44+
.unwrap_or(BUF_SIZE)
45+
} else {
46+
BUF_SIZE
47+
}
48+
};
49+
#[cfg(not(target_os = "linux"))]
50+
let buf_size = BUF_SIZE;
2351

24-
let mut buffer = Vec::with_capacity(BUF_SIZE);
52+
let mut buffer = Vec::with_capacity(buf_size);
2553
#[allow(clippy::unwrap_used, reason = "clap provides 'y' by default")]
2654
let _ = args_into_buffer(&mut buffer, matches.get_many::<OsString>("STRING").unwrap());
27-
prepare_buffer(&mut buffer);
55+
prepare_buffer(&mut buffer, buf_size);
2856

2957
match exec(&buffer) {
3058
Ok(()) => Ok(()),
@@ -91,15 +119,15 @@ fn args_into_buffer<'a>(
91119

92120
/// Assumes buf holds a single output line forged from the command line arguments, copies it
93121
/// repeatedly until the buffer holds as many copies as it can under [`BUF_SIZE`].
94-
fn prepare_buffer(buf: &mut Vec<u8>) {
95-
if buf.len() * 2 > BUF_SIZE {
122+
fn prepare_buffer(buf: &mut Vec<u8>, buf_size: usize) {
123+
if buf.len() * 2 > buf_size {
96124
return;
97125
}
98126

99127
assert!(!buf.is_empty());
100128

101129
let line_len = buf.len();
102-
let target_size = line_len * (BUF_SIZE / line_len);
130+
let target_size = line_len * (buf_size / line_len);
103131

104132
while buf.len() < target_size {
105133
let to_copy = std::cmp::min(target_size - buf.len(), buf.len());
@@ -108,6 +136,7 @@ fn prepare_buffer(buf: &mut Vec<u8>) {
108136
}
109137
}
110138

139+
#[cfg(not(target_os = "linux"))]
111140
pub fn exec(bytes: &[u8]) -> io::Result<()> {
112141
let stdout = io::stdout();
113142
let mut stdout = stdout.lock();
@@ -117,6 +146,24 @@ pub fn exec(bytes: &[u8]) -> io::Result<()> {
117146
}
118147
}
119148

149+
#[cfg(target_os = "linux")]
150+
pub fn exec(bytes: &[u8]) -> io::Result<()> {
151+
let stdout = io::stdout();
152+
//zero copy fast-path
153+
//todo: we should align instead of giving up fast-path
154+
let aligned = bytes.len().is_multiple_of(page_size());
155+
if aligned {
156+
let fd_raw = stdout.as_raw_fd();
157+
let fd = unsafe { BorrowedFd::borrow_raw(fd_raw) };
158+
let iovec = [IoSliceRaw::from_slice(bytes)];
159+
while unsafe { vmsplice(fd, &iovec, SpliceFlags::empty()) }.is_ok() {}
160+
}
161+
let mut stdout = stdout.lock();
162+
loop {
163+
stdout.write_all(bytes)?;
164+
}
165+
}
166+
120167
#[cfg(test)]
121168
mod tests {
122169
use super::*;
@@ -143,7 +190,7 @@ mod tests {
143190

144191
for (line, final_len) in tests {
145192
let mut v = std::iter::repeat_n(b'a', line).collect::<Vec<_>>();
146-
prepare_buffer(&mut v);
193+
prepare_buffer(&mut v, BUF_SIZE);
147194
assert_eq!(v.len(), final_len);
148195
}
149196
}

0 commit comments

Comments
 (0)