Skip to content

Commit 97326d3

Browse files
Merge macOS kqueue support with Andrea's NULL fix
2 parents b4eff49 + eea156c commit 97326d3

File tree

5 files changed

+248
-49
lines changed

5 files changed

+248
-49
lines changed

benchmark_for_pr.sh

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#!/bin/bash
2+
# Benchmark for PR - shows GNU du vs Parallel du (uncached) vs Parallel du (cached)
3+
4+
TARGET="/home/founder"
5+
6+
echo "=== Benchmark: Three-Way Comparison ==="
7+
echo "Target: $TARGET"
8+
echo "System: $(uname -r)"
9+
echo "CPU: $(lscpu | grep "Model name" | sed 's/Model name:[[:space:]]*//')"
10+
echo ""
11+
12+
# Clear cache
13+
sudo rm -f ~/.cache/uutils/du_cache.bin 2>/dev/null
14+
15+
echo "1. GNU du (baseline, with -L to follow symlinks)"
16+
time du -L -sh "$TARGET" 2>&1 | tail -1
17+
echo ""
18+
19+
echo "2. Parallel du (cache disabled)"
20+
time DU_CACHE=0 ./target/release/du -L -sh "$TARGET" 2>&1 | tail -1
21+
echo ""
22+
23+
echo "3. Parallel du (cold cache)"
24+
time ./target/release/du -L -sh "$TARGET" 2>&1 | tail -1
25+
echo ""
26+
27+
echo "4. Parallel du (warm cache)"
28+
time ./target/release/du -L -sh "$TARGET" 2>&1 | tail -1

src/uu/timeout/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ path = "src/timeout.rs"
2020
[dependencies]
2121
clap = { workspace = true }
2222
libc = { workspace = true }
23-
nix = { workspace = true, features = ["signal"] }
23+
nix = { workspace = true, features = ["signal", "event"] }
2424
uucore = { workspace = true, features = ["parser", "process", "signals"] }
2525
fluent = { workspace = true }
2626

src/uu/timeout/src/timeout.rs

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

6-
// spell-checker:ignore (ToDO) tstr sigstr cmdname setpgid sigchld getpid
6+
// spell-checker:ignore (ToDO) tstr sigstr cmdname setpgid sigchld getpid kqueue kevent
77
mod status;
88

99
use crate::status::ExitStatus;
1010
use clap::{Arg, ArgAction, Command};
1111
use nix::errno::Errno;
1212
use nix::sys::signal::{SigSet, SigmaskHow, Signal, sigprocmask};
1313
use std::io::{self, ErrorKind};
14+
15+
#[cfg(target_os = "macos")]
16+
use nix::sys::event::{EventFilter, EventFlag, FilterFlag, KEvent, Kqueue};
1417
use std::os::unix::process::{CommandExt, ExitStatusExt};
1518
use std::process::{self, Child, Stdio};
1619
use std::sync::atomic::{self, AtomicBool};
@@ -211,8 +214,11 @@ fn send_signal(process: &mut Child, signal: usize, foreground: bool) {
211214

212215
/// Wait for one of the specified signals to be delivered, with optional timeout.
213216
///
214-
/// This function uses `sigtimedwait()` to efficiently wait for signals without polling.
215-
/// It handles EINTR by retrying the wait.
217+
/// This function uses platform-specific mechanisms for efficient signal waiting:
218+
/// - Linux/FreeBSD: `sigtimedwait()` for direct signal waiting
219+
/// - macOS: `kqueue` with EVFILT_SIGNAL for event-driven signal monitoring
220+
///
221+
/// Both approaches avoid polling and provide sub-millisecond precision.
216222
///
217223
/// # Arguments
218224
/// * `signals` - Signals to wait for (typically SIGCHLD and SIGTERM)
@@ -222,7 +228,9 @@ fn send_signal(process: &mut Child, signal: usize, foreground: bool) {
222228
/// * `Ok(Some(signal))` - A signal was received
223229
/// * `Ok(None)` - Timeout expired
224230
/// * `Err(e)` - An error occurred
231+
#[cfg(not(target_os = "macos"))]
225232
fn wait_for_signal(signals: &[Signal], until: Option<Instant>) -> io::Result<Option<Signal>> {
233+
// Linux/FreeBSD: Use sigtimedwait() for efficient signal waiting
226234
// Create signal set from the provided signals
227235
let mut sigset = SigSet::empty();
228236
for &sig in signals {
@@ -253,9 +261,9 @@ fn wait_for_signal(signals: &[Signal], until: Option<Instant>) -> io::Result<Opt
253261

254262
let result = unsafe {
255263
libc::sigtimedwait(
256-
sigset.as_ref() as *const libc::sigset_t,
264+
std::ptr::from_ref(sigset.as_ref()),
257265
std::ptr::null_mut(), // We don't need siginfo
258-
&timeout_spec as *const libc::timespec,
266+
std::ptr::from_ref(&timeout_spec),
259267
)
260268
};
261269

@@ -268,12 +276,74 @@ fn wait_for_signal(signals: &[Signal], until: Option<Instant>) -> io::Result<Opt
268276
// Some other error
269277
err => return Err(io::Error::from(err)),
270278
}
271-
} else {
272-
// Signal received - convert signal number to Signal enum
273-
return Signal::try_from(result)
279+
}
280+
// Signal received - convert signal number to Signal enum
281+
return Signal::try_from(result).map(Some).map_err(io::Error::other);
282+
}
283+
}
284+
285+
/// macOS implementation using kqueue with EVFILT_SIGNAL
286+
///
287+
/// kqueue is the native BSD/macOS mechanism for event notification, providing
288+
/// efficient signal monitoring without polling. This is equivalent in performance
289+
/// to sigtimedwait() on Linux.
290+
#[cfg(target_os = "macos")]
291+
fn wait_for_signal(signals: &[Signal], until: Option<Instant>) -> io::Result<Option<Signal>> {
292+
// Create a kqueue for signal monitoring
293+
let kq = Kqueue::new().map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
294+
295+
// Create events for each signal we want to monitor
296+
let mut changelist = Vec::with_capacity(signals.len());
297+
for &sig in signals {
298+
let event = KEvent::new(
299+
sig as usize,
300+
EventFilter::EVFILT_SIGNAL,
301+
EventFlag::EV_ADD | EventFlag::EV_ONESHOT,
302+
FilterFlag::empty(),
303+
0,
304+
0,
305+
);
306+
changelist.push(event);
307+
}
308+
309+
// Calculate timeout
310+
let timeout = if let Some(deadline) = until {
311+
let remaining = deadline.saturating_duration_since(Instant::now());
312+
Some(libc::timespec {
313+
tv_sec: remaining.as_secs() as libc::time_t,
314+
tv_nsec: remaining.subsec_nanos() as libc::c_long,
315+
})
316+
} else {
317+
None
318+
};
319+
320+
// Wait for signal events
321+
let mut eventlist = vec![KEvent::new(
322+
0,
323+
EventFilter::EVFILT_SIGNAL,
324+
EventFlag::empty(),
325+
FilterFlag::empty(),
326+
0,
327+
0,
328+
)];
329+
330+
match kq.kevent(&changelist, &mut eventlist, timeout) {
331+
Ok(n) if n > 0 => {
332+
// Signal received - extract signal number from event
333+
let sig_num = eventlist[0].ident() as i32;
334+
Signal::try_from(sig_num)
274335
.map(Some)
275-
.map_err(|e| io::Error::new(io::ErrorKind::Other, e));
336+
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
337+
}
338+
Ok(_) => {
339+
// Timeout expired with no events
340+
Ok(None)
341+
}
342+
Err(Errno::EINTR) => {
343+
// Interrupted - retry
344+
wait_for_signal(signals, until)
276345
}
346+
Err(e) => Err(io::Error::new(io::ErrorKind::Other, e.to_string())),
277347
}
278348
}
279349

@@ -443,44 +513,43 @@ fn timeout(
443513
let deadline = Instant::now()
444514
.checked_add(duration)
445515
.unwrap_or_else(|| Instant::now() + Duration::from_secs(86400 * 365 * 100));
446-
let wait_result: Option<std::process::ExitStatus> = loop {
447-
// Wait for signals with timeout
448-
// If child has already exited, SIGCHLD will be delivered immediately
449-
let signal_result = wait_for_signal(&[Signal::SIGCHLD, Signal::SIGTERM], Some(deadline));
450-
match signal_result {
451-
Ok(Some(Signal::SIGCHLD)) => {
452-
// Child state changed, reap it
453-
match process.wait() {
454-
Ok(status) => break Some(status),
455-
Err(e) => {
456-
// Restore mask before returning error
457-
let _ = sigprocmask(SigmaskHow::SIG_SETMASK, Some(&old_sigset), None);
458-
return Err(e.into());
459-
}
516+
517+
// Wait for signals with timeout
518+
// If child has already exited, SIGCHLD will be delivered immediately
519+
let signal_result = wait_for_signal(&[Signal::SIGCHLD, Signal::SIGTERM], Some(deadline));
520+
let wait_result: Option<std::process::ExitStatus> = match signal_result {
521+
Ok(Some(Signal::SIGCHLD)) => {
522+
// Child state changed, reap it
523+
match process.wait() {
524+
Ok(status) => Some(status),
525+
Err(e) => {
526+
// Restore mask before returning error
527+
let _ = sigprocmask(SigmaskHow::SIG_SETMASK, Some(&old_sigset), None);
528+
return Err(e.into());
460529
}
461530
}
462-
Ok(Some(Signal::SIGTERM)) => {
463-
// External termination request
464-
SIGNALED.store(true, atomic::Ordering::Relaxed);
465-
break None; // Treat as timeout
466-
}
467-
Ok(None) => {
468-
// Timeout expired
469-
break None;
470-
}
471-
Ok(Some(sig)) => {
472-
// Unexpected signal (shouldn't happen since we only wait for SIGCHLD/SIGTERM)
473-
let _ = sigprocmask(SigmaskHow::SIG_SETMASK, Some(&old_sigset), None);
474-
return Err(USimpleError::new(
475-
ExitStatus::TimeoutFailed.into(),
476-
format!("Unexpected signal received: {:?}", sig),
477-
));
478-
}
479-
Err(e) => {
480-
// wait_for_signal failed
481-
let _ = sigprocmask(SigmaskHow::SIG_SETMASK, Some(&old_sigset), None);
482-
return Err(e.into());
483-
}
531+
}
532+
Ok(Some(Signal::SIGTERM)) => {
533+
// External termination request
534+
SIGNALED.store(true, atomic::Ordering::Relaxed);
535+
None // Treat as timeout
536+
}
537+
Ok(None) => {
538+
// Timeout expired
539+
None
540+
}
541+
Ok(Some(sig)) => {
542+
// Unexpected signal (shouldn't happen since we only wait for SIGCHLD/SIGTERM)
543+
let _ = sigprocmask(SigmaskHow::SIG_SETMASK, Some(&old_sigset), None);
544+
return Err(USimpleError::new(
545+
ExitStatus::TimeoutFailed.into(),
546+
format!("Unexpected signal received: {sig:?}"),
547+
));
548+
}
549+
Err(e) => {
550+
// wait_for_signal failed
551+
let _ = sigprocmask(SigmaskHow::SIG_SETMASK, Some(&old_sigset), None);
552+
return Err(e.into());
484553
}
485554
};
486555

src/uucore/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ nix = { workspace = true, features = [
9595
"signal",
9696
"dir",
9797
"user",
98+
"event",
9899
] }
99100
xattr = { workspace = true, optional = true }
100101

0 commit comments

Comments
 (0)