Implement --output={null,pipe,<FILE>}

Fixes #377.
This commit is contained in:
Tavian Barnes 2022-05-14 13:21:41 -04:00 committed by David Peter
parent f75f0a10fe
commit 277807cb5a
6 changed files with 129 additions and 23 deletions

22
Cargo.lock generated
View File

@ -274,6 +274,7 @@ dependencies = [
"csv",
"indicatif",
"libc",
"nix",
"predicates",
"rand 0.8.5",
"rust_decimal",
@ -356,6 +357,27 @@ version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
[[package]]
name = "memoffset"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
dependencies = [
"autocfg 1.1.0",
]
[[package]]
name = "nix"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f17df307904acd05aa8e32e97bb20f2a0df1728bbc2d771ae8f9a90463441e9"
dependencies = [
"bitflags",
"cfg-if",
"libc",
"memoffset",
]
[[package]]
name = "normalize-line-endings"
version = "0.3.0"

View File

@ -31,6 +31,9 @@ libc = "0.2"
[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3", features = ["processthreadsapi", "minwindef", "winnt"] }
[target.'cfg(target_os="linux")'.dependencies]
nix = { version = "0.24.1", features = ["zerocopy"] }
[dependencies.clap]
version = "3"
default-features = false

View File

@ -36,10 +36,10 @@ pub trait Executor {
fn run_command_and_measure_common(
mut command: std::process::Command,
command_failure_action: CmdFailureAction,
command_output_policy: CommandOutputPolicy,
command_output_policy: &CommandOutputPolicy,
command_name: &str,
) -> Result<TimerResult> {
let (stdout, stderr) = command_output_policy.get_stdout_stderr();
let (stdout, stderr) = command_output_policy.get_stdout_stderr()?;
command.stdin(Stdio::null()).stdout(stdout).stderr(stderr);
command.env(
@ -83,7 +83,7 @@ impl<'a> Executor for RawExecutor<'a> {
let result = run_command_and_measure_common(
command.get_command()?,
command_failure_action.unwrap_or(self.options.command_failure_action),
self.options.command_output_policy,
&self.options.command_output_policy,
&command.get_command_line(),
)?;
@ -136,7 +136,7 @@ impl<'a> Executor for ShellExecutor<'a> {
let mut result = run_command_and_measure_common(
command_builder,
command_failure_action.unwrap_or(self.options.command_failure_action),
self.options.command_output_policy,
&self.options.command_output_policy,
&command.get_command_line(),
)?;

View File

@ -259,6 +259,20 @@ fn build_command() -> Command<'static> {
when trying to benchmark output speed.",
),
)
.arg(
Arg::new("output")
.long("output")
.conflicts_with("show-output")
.takes_value(true)
.value_name("WHERE")
.help(
"Control where the output of the benchmark is redirected.\n\n \
null: Redirect output to /dev/null (the default).\n\n \
pipe: Feed the output through a pipe before discarding it.\n \
Useful if the benchmark special-cases /dev/null.\n\n \
<FILE>: Write the output to the given file.",
),
)
.arg(
Arg::new("command-name")
.long("command-name")

View File

@ -1,5 +1,7 @@
use std::fs::File;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::{cmp, fmt};
use std::{cmp, fmt, io};
use anyhow::ensure;
use atty::Stream;
@ -109,11 +111,17 @@ impl Default for RunBounds {
}
/// How to handle the output of benchmarked commands
#[derive(Debug, Clone, Copy, PartialEq)]
#[derive(Debug, Clone, PartialEq)]
pub enum CommandOutputPolicy {
/// Discard all output
Discard,
/// Feed output through a pipe before discarding it
Pipe,
/// Redirect output to a file
File(PathBuf),
/// Show command output on the terminal
Forward,
}
@ -125,11 +133,22 @@ impl Default for CommandOutputPolicy {
}
impl CommandOutputPolicy {
pub fn get_stdout_stderr(&self) -> (Stdio, Stdio) {
match self {
pub fn get_stdout_stderr(&self) -> io::Result<(Stdio, Stdio)> {
let streams = match self {
CommandOutputPolicy::Discard => (Stdio::null(), Stdio::null()),
// Typically only stdout is performance-relevant, so just pipe that
CommandOutputPolicy::Pipe => (Stdio::piped(), Stdio::null()),
CommandOutputPolicy::File(path) => {
let file = File::create(&path)?;
(file.into(), Stdio::null())
}
CommandOutputPolicy::Forward => (Stdio::inherit(), Stdio::inherit()),
}
};
Ok(streams)
}
}
@ -251,6 +270,12 @@ impl Options {
options.command_output_policy = if matches.is_present("show-output") {
CommandOutputPolicy::Forward
} else if let Some(output) = matches.value_of("output") {
match output {
"null" => CommandOutputPolicy::Discard,
"pipe" => CommandOutputPolicy::Pipe,
path => CommandOutputPolicy::File(path.into()),
}
} else {
CommandOutputPolicy::Discard
};
@ -262,7 +287,7 @@ impl Options {
Some("color") => OutputStyleOption::Color,
Some("none") => OutputStyleOption::Disabled,
_ => {
if options.command_output_policy == CommandOutputPolicy::Discard
if options.command_output_policy != CommandOutputPolicy::Forward
&& atty::is(Stream::Stdout)
{
OutputStyleOption::Full

View File

@ -6,10 +6,20 @@ mod windows_timer;
#[cfg(not(windows))]
mod unix_timer;
#[cfg(target_os = "linux")]
use nix::fcntl::{splice, SpliceFFlags};
#[cfg(target_os = "linux")]
use std::fs::File;
#[cfg(target_os = "linux")]
use std::os::unix::io::AsRawFd;
#[cfg(not(target_os = "linux"))]
use std::io::Read;
use crate::util::units::Second;
use wall_clock_timer::WallClockTimer;
use std::process::{Command, ExitStatus};
use std::process::{ChildStdout, Command, ExitStatus};
use anyhow::Result;
@ -33,26 +43,58 @@ pub struct TimerResult {
pub status: ExitStatus,
}
/// Discard the output of a child process.
fn discard(output: ChildStdout) {
const CHUNK_SIZE: usize = 64 << 10;
#[cfg(target_os = "linux")]
{
if let Ok(file) = File::create("/dev/null") {
while let Ok(bytes) = splice(
output.as_raw_fd(),
None,
file.as_raw_fd(),
None,
CHUNK_SIZE,
SpliceFFlags::empty(),
) {
if bytes == 0 {
break;
}
}
}
}
#[cfg(not(target_os = "linux"))]
{
let mut output = output;
let mut buf = [0; CHUNK_SIZE];
while let Ok(bytes) = output.read(&mut buf) {
if bytes == 0 {
break;
}
}
}
}
/// Execute the given command and return a timing summary
pub fn execute_and_measure(mut command: Command) -> Result<TimerResult> {
let wallclock_timer = WallClockTimer::start();
let mut child = command.spawn()?;
let output = child.stdout.take();
#[cfg(not(windows))]
let ((time_user, time_system), status) = {
let cpu_timer = self::unix_timer::CPUTimer::start();
let status = command.status()?;
(cpu_timer.stop(), status)
};
let cpu_timer = self::unix_timer::CPUTimer::start();
#[cfg(windows)]
let ((time_user, time_system), status) = {
let mut child = command.spawn()?;
let cpu_timer = self::windows_timer::CPUTimer::start_for_process(&child);
let status = child.wait()?;
let cpu_timer = self::windows_timer::CPUTimer::start_for_process(&child);
(cpu_timer.stop(), status)
};
if let Some(output) = output {
discard(output);
}
let status = child.wait()?;
let (time_user, time_system) = cpu_timer.stop();
let time_real = wallclock_timer.stop();
Ok(TimerResult {