Skip to content

Commit 630f4b1

Browse files
oech3Hermes Agent
authored andcommitted
tee: add splice() and tee() zero-copy fast-path
1 parent 47d7c76 commit 630f4b1

2 files changed

Lines changed: 64 additions & 5 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: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -153,22 +153,67 @@ impl MultiWriter {
153153
/// Copies all bytes from the input buffer to the output buffer
154154
/// without buffering which is POSIX requirement.
155155
pub fn copy_unbuffered(&mut self) -> Result<()> {
156-
// todo: support splice() and tee() fast-path at here
157156
#[cfg(not(any(unix, target_os = "wasi")))]
158157
use std::io::Read as _;
159158
const BUF_SIZE: usize = 32 * 1024;
159+
#[cfg(any(unix, target_os = "wasi"))]
160+
let input = rustix::stdio::stdin();
161+
#[cfg(not(any(unix, target_os = "wasi")))]
162+
let mut input = std::io::stdin();
163+
#[cfg(any(target_os = "linux", target_os = "android"))]
164+
macro_rules! splice_or_detach {
165+
($pipe:expr, $writer:expr, $len:expr) => {
166+
if let Err(e) = uucore::pipes::drain_pipe($pipe, $writer, $len) {
167+
if let Err(e) =
168+
process_error(self.output_error_mode, e, $writer, &mut self.ignored_errors)
169+
{
170+
self.aborted.get_or_insert(e);
171+
}
172+
$writer.name.clear(); //mark as exited
173+
}
174+
};
175+
}
176+
#[cfg(any(target_os = "linux", target_os = "android"))]
177+
{
178+
use rustix::pipe::fcntl_setpipe_size;
179+
use uucore::pipes::MAX_ROOTLESS_PIPE_SIZE;
180+
let (pipe_read, pipe_write) = std::io::pipe()?; // needed to duplicate input
181+
let (pipe2_read, pipe2_write) = std::io::pipe()?; // force-tee() even output is not pipe
182+
// improve throughput. 2nd pipe should be larger than 1st one for proper tee() length.
183+
if fcntl_setpipe_size(&pipe2_read, MAX_ROOTLESS_PIPE_SIZE).is_ok() {
184+
let _ = fcntl_setpipe_size(&pipe_read, MAX_ROOTLESS_PIPE_SIZE);
185+
let _ = fcntl_setpipe_size(&self.writers[0], MAX_ROOTLESS_PIPE_SIZE);
186+
}
187+
while let Ok(s) = uucore::pipes::splice(&input, &pipe_write, MAX_ROOTLESS_PIPE_SIZE) {
188+
if s == 0 {
189+
return Ok(());
190+
}
191+
let (last, others) = self
192+
.writers
193+
.split_last_mut()
194+
.ok_or_else(|| Error::from(ErrorKind::Other))?; // all writers exited
195+
for other in others {
196+
// do not consume input
197+
let tee_res = uucore::pipes::tee(&pipe_read, &pipe2_write, s);
198+
assert_eq!(tee_res, Ok(s), "2nd pipe should have enough spare");
199+
splice_or_detach!(&pipe2_read, other, s);
200+
}
201+
// last one consumes input
202+
splice_or_detach!(&pipe_read, last, s);
203+
self.writers.retain(|w| !w.name.is_empty());
204+
self.aborted.take().map_or(Ok(()), Err)?;
205+
}
206+
}
207+
160208
#[cfg(any(unix, target_os = "wasi"))]
161209
let mut buf = [std::mem::MaybeUninit::<u8>::uninit(); BUF_SIZE];
162210
// todo: avoid cost by 0-fill keeping throughput
163211
#[cfg(not(any(unix, target_os = "wasi")))]
164212
let mut buf = [0u8; BUF_SIZE];
165213

166-
let input = std::io::stdin();
167-
#[cfg(not(any(unix, target_os = "wasi")))]
168-
let mut input = input;
169214
loop {
170215
#[cfg(any(unix, target_os = "wasi"))]
171-
let res = rustix::io::read(&input, &mut buf)
216+
let res = rustix::io::read(input, &mut buf)
172217
.map(|f| f.0)
173218
.map_err(Error::from);
174219
#[cfg(not(any(unix, target_os = "wasi")))]
@@ -276,3 +321,13 @@ struct NamedWriter {
276321
inner: Writer,
277322
pub name: OsString,
278323
}
324+
325+
#[cfg(any(target_os = "linux", target_os = "android"))]
326+
impl rustix::fd::AsFd for NamedWriter {
327+
fn as_fd(&self) -> rustix::fd::BorrowedFd<'_> {
328+
match &self.inner {
329+
Writer::File(f) => f.as_fd(),
330+
Writer::Stdout(s) => s.0,
331+
}
332+
}
333+
}

0 commit comments

Comments
 (0)