Skip to content

Commit bf4d201

Browse files
committed
yes: use tee syscall as fast-path
1 parent 52e1bc1 commit bf4d201

5 files changed

Lines changed: 58 additions & 5 deletions

File tree

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.

src/uu/yes/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ doctest = false
2222
clap = { workspace = true }
2323
itertools = { workspace = true }
2424
fluent = { workspace = true }
25-
uucore = { workspace = true }
25+
rustix = { workspace = true, features = ["pipe"] }
26+
uucore = { workspace = true, features = ["pipes"] }
2627

2728
[[bin]]
2829
name = "yes"

src/uu/yes/src/yes.rs

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,14 @@ use uucore::error::{UResult, USimpleError, strip_errno};
1212
use uucore::format_usage;
1313
use uucore::translate;
1414

15+
#[cfg(any(target_os = "linux", target_os = "android"))]
16+
const PAGE_SIZE: usize = 4096;
17+
#[cfg(any(target_os = "linux", target_os = "android"))]
18+
use uucore::pipes::MAX_ROOTLESS_PIPE_SIZE;
19+
#[cfg(any(target_os = "linux", target_os = "android"))]
20+
const BUF_SIZE: usize = MAX_ROOTLESS_PIPE_SIZE;
1521
// it's possible that using a smaller or larger buffer might provide better performance
22+
#[cfg(not(any(target_os = "linux", target_os = "android")))]
1623
const BUF_SIZE: usize = 16 * 1024;
1724

1825
#[uucore::main]
@@ -21,9 +28,13 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
2128

2229
#[allow(clippy::unwrap_used, reason = "clap provides 'y' by default")]
2330
let mut buffer = args_into_buffer(matches.get_many::<OsString>("STRING").unwrap())?;
24-
prepare_buffer(&mut buffer);
31+
#[cfg(any(target_os = "linux", target_os = "android"))]
32+
let aligned = PAGE_SIZE.is_multiple_of(buffer.len());
33+
#[cfg(not(any(target_os = "linux", target_os = "android")))]
34+
let aligned = false;
2535

26-
match exec(&buffer) {
36+
prepare_buffer(&mut buffer);
37+
match exec(&buffer, aligned) {
2738
Ok(()) => Ok(()),
2839
// On Windows, silently handle broken pipe since there's no SIGPIPE
2940
#[cfg(windows)]
@@ -97,8 +108,40 @@ fn prepare_buffer(buf: &mut Vec<u8>) {
97108
}
98109
}
99110

100-
pub fn exec(bytes: &[u8]) -> io::Result<()> {
111+
pub fn exec(bytes: &[u8], aligned: bool) -> io::Result<()> {
112+
#[cfg(not(any(target_os = "linux", target_os = "android")))]
113+
let _ = aligned;
101114
let stdout = io::stdout();
115+
#[cfg(any(target_os = "linux", target_os = "android"))]
116+
let mut stdout = stdout;
117+
#[cfg(any(target_os = "linux", target_os = "android"))]
118+
{
119+
use uucore::pipes::{pipe, splice, tee};
120+
// don't show any error from fast-path and fallback to write for proper message
121+
if let Ok((p_read, mut p_write)) = pipe()
122+
// todo: zero-copy with default size when fcntl failed
123+
&& rustix::pipe::fcntl_setpipe_size(&stdout, MAX_ROOTLESS_PIPE_SIZE).is_ok()
124+
&& p_write.write_all(bytes).is_ok()
125+
{
126+
if aligned && tee(&p_read, &stdout, PAGE_SIZE).is_ok() {
127+
while let Ok(1..) = tee(&p_read, &stdout, usize::MAX) {}
128+
} else if let Ok((broker_read, broker_write)) = pipe() {
129+
// tee() cannot control offset and write to non-pipe
130+
'hybrid: while let Ok(mut remain) = tee(&p_read, &broker_write, usize::MAX) {
131+
debug_assert!(remain == bytes.len(), "splice() should cleanup pipe");
132+
while remain > 0 {
133+
if let Ok(s) = splice(&broker_read, &stdout, remain) {
134+
remain -= s;
135+
} else {
136+
// avoid output breakage with reduced remain even if it would not happen
137+
stdout.write_all(&bytes[bytes.len() - remain..])?;
138+
break 'hybrid;
139+
}
140+
}
141+
}
142+
}
143+
}
144+
}
102145
let mut stdout = stdout.lock();
103146

104147
loop {
@@ -111,6 +154,7 @@ mod tests {
111154
use super::*;
112155

113156
#[test]
157+
#[cfg(not(any(target_os = "linux", target_os = "android")))] // Linux uses different buffer size
114158
fn test_prepare_buffer() {
115159
let tests = [
116160
(150, 16350),

src/uucore/src/lib/features/pipes.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,3 +231,10 @@ pub fn dev_null() -> Option<File> {
231231
None
232232
}
233233
}
234+
235+
// Less noisy wrapper around [`rustix::pipe::tee`]
236+
#[inline]
237+
#[cfg(any(target_os = "linux", target_os = "android"))]
238+
pub fn tee(source: &impl AsFd, target: &impl AsFd, len: usize) -> rustix::io::Result<usize> {
239+
rustix::pipe::tee(source, target, len, SpliceFlags::empty())
240+
}

tests/by-util/test_yes.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ fn test_long_input() {
7070
#[cfg(windows)]
7171
const TIMES: usize = 500;
7272
let arg = "abcdef".repeat(TIMES) + "\n";
73-
let expected_out = arg.repeat(30);
73+
let expected_out = arg.repeat(5);
7474
run(&[&arg[..arg.len() - 1]], expected_out.as_bytes());
7575
}
7676

0 commit comments

Comments
 (0)