Skip to content

Commit 4b807b0

Browse files
oech3oech3
authored andcommitted
tee: add zero-copy fast-path
1 parent d01be5b commit 4b807b0

2 files changed

Lines changed: 65 additions & 6 deletions

File tree

src/uu/tee/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ divan = { workspace = true }
3030
tempfile = { workspace = true }
3131
uucore = { workspace = true, features = ["benchmark"] }
3232

33+
[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies]
34+
rustix = { workspace = true, features = ["pipe"] }
35+
uucore = { workspace = true, features = ["pipes"] }
36+
3337
[[bin]]
3438
name = "tee"
3539
path = "src/main.rs"

src/uu/tee/src/tee.rs

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -143,22 +143,67 @@ impl MultiWriter {
143143
/// Copies all bytes from the input buffer to the output buffer
144144
/// without buffering which is POSIX requirement.
145145
pub fn copy_unbuffered(&mut self) -> Result<(), ()> {
146-
// todo: support splice() and tee() fast-path at here
147146
#[cfg(not(any(unix, target_os = "wasi")))]
148147
use io::Read as _;
149148
const BUF_SIZE: usize = 32 * 1024;
149+
#[cfg(any(unix, target_os = "wasi"))]
150+
let input = rustix::stdio::stdin();
151+
#[cfg(not(any(unix, target_os = "wasi")))]
152+
let mut input = io::stdin();
153+
#[cfg(any(target_os = "linux", target_os = "android"))]
154+
macro_rules! splice_or_detach {
155+
($pipe:expr, $writer:expr, $len:expr) => {
156+
if let Err(e) = uucore::pipes::drain_pipe($pipe, $writer, $len) {
157+
self.aborted |=
158+
process_error(self.output_error_mode, e, $writer, &mut self.ignored_errors)
159+
.is_err();
160+
$writer.name.clear(); //mark as exited
161+
}
162+
};
163+
}
164+
// needs 2 pipes to duplicate input multiple times
165+
#[cfg(any(target_os = "linux", target_os = "android"))]
166+
if let Ok((pipe_read, pipe_write)) = io::pipe()
167+
&& let Ok((pipe2_read, pipe2_write)) = io::pipe()
168+
{
169+
use rustix::pipe::fcntl_setpipe_size;
170+
use uucore::pipes::MAX_ROOTLESS_PIPE_SIZE;
171+
// improve throughput. 2nd pipe should be larger than 1st one for proper tee() length.
172+
if fcntl_setpipe_size(&pipe2_read, MAX_ROOTLESS_PIPE_SIZE).is_ok() {
173+
let _ = fcntl_setpipe_size(&pipe_read, MAX_ROOTLESS_PIPE_SIZE);
174+
let _ = fcntl_setpipe_size(&self.writers[0], MAX_ROOTLESS_PIPE_SIZE); // stdout
175+
}
176+
while let Ok(s) = uucore::pipes::splice(&input, &pipe_write, MAX_ROOTLESS_PIPE_SIZE) {
177+
if s == 0 {
178+
return Ok(());
179+
}
180+
let Some((last, others)) = self.writers.split_last_mut() else {
181+
// all writers exited
182+
return Ok(());
183+
};
184+
for other in others {
185+
// do not consume input
186+
let tee_res = uucore::pipes::tee(&pipe_read, &pipe2_write, s);
187+
assert_eq!(tee_res, Ok(s), "2nd pipe should have enough spare");
188+
splice_or_detach!(&pipe2_read, other, s);
189+
}
190+
// last one consumes input
191+
splice_or_detach!(&pipe_read, last, s);
192+
self.writers.retain(|w| !w.name.is_empty());
193+
if self.aborted {
194+
return Err(());
195+
}
196+
}
197+
}
198+
150199
#[cfg(any(unix, target_os = "wasi"))]
151200
let mut buf = [std::mem::MaybeUninit::<u8>::uninit(); BUF_SIZE];
152201
// todo: avoid cost by 0-fill keeping throughput
153202
#[cfg(not(any(unix, target_os = "wasi")))]
154203
let mut buf = [0u8; BUF_SIZE];
155-
156-
let input = io::stdin();
157-
#[cfg(not(any(unix, target_os = "wasi")))]
158-
let mut input = input;
159204
loop {
160205
#[cfg(any(unix, target_os = "wasi"))]
161-
let res = rustix::io::read(&input, &mut buf)
206+
let res = rustix::io::read(input, &mut buf)
162207
.map(|f| f.0)
163208
.map_err(Error::from);
164209
#[cfg(not(any(unix, target_os = "wasi")))]
@@ -267,3 +312,13 @@ struct NamedWriter {
267312
inner: Writer,
268313
pub name: OsString,
269314
}
315+
316+
#[cfg(any(target_os = "linux", target_os = "android"))]
317+
impl rustix::fd::AsFd for NamedWriter {
318+
fn as_fd(&self) -> rustix::fd::BorrowedFd<'_> {
319+
match &self.inner {
320+
Writer::File(f) => f.as_fd(),
321+
Writer::Stdout(s) => s.0,
322+
}
323+
}
324+
}

0 commit comments

Comments
 (0)