mirror of
https://github.com/sharkdp/hyperfine.git
synced 2026-11-03 01:21:25 +00:00
Support hardware counters (instructions, cache misses, ...) (#983)
* Add hardware counters to JSON exports * Clarify Linux hardware counter setup
This commit is contained in:
1 parent
ab86cb99cc
commit
bc3a3f0524
13 files changed
+432
-34
No files matched your search
Generated
+10
@@ -393,6 +393,7 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
"nix",
|
"nix",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"perf-event-open-sys",
|
||||||
"platform-info",
|
"platform-info",
|
||||||
"predicates",
|
"predicates",
|
||||||
"rand 0.10.3",
|
"rand 0.10.3",
|
||||||
@@ -539,6 +540,15 @@ version = "1.70.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "perf-event-open-sys"
|
||||||
|
version = "6.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a5f8d1487a4ffa23c80a1c355dd27235f9b66fb71ba0f261eb417e4fe8451347"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "platform-info"
|
name = "platform-info"
|
||||||
version = "2.2.0"
|
version = "2.2.0"
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ once_cell = "1.21"
|
|||||||
[target.'cfg(target_os="linux")'.dependencies]
|
[target.'cfg(target_os="linux")'.dependencies]
|
||||||
nix = { version = "0.31", features = ["zerocopy"] }
|
nix = { version = "0.31", features = ["zerocopy"] }
|
||||||
|
|
||||||
|
[target.'cfg(all(target_os = "linux", target_pointer_width = "64", any(target_arch = "aarch64", target_arch = "x86_64", target_arch = "riscv64", target_arch = "powerpc64", target_arch = "loongarch64")))'.dependencies]
|
||||||
|
perf-event-open-sys = "6"
|
||||||
|
|
||||||
[dependencies.clap]
|
[dependencies.clap]
|
||||||
version = "4"
|
version = "4"
|
||||||
default-features = false
|
default-features = false
|
||||||
|
|||||||
@@ -187,6 +187,34 @@ The JSON export includes the following metrics for each measured run (excluding
|
|||||||
*not the simultaneous total memory of the full process tree*.
|
*not the simultaneous total memory of the full process tree*.
|
||||||
- Windows: Currently not supported.
|
- Windows: Currently not supported.
|
||||||
|
|
||||||
|
- **`cpu_cycles`**: CPU cycles consumed.
|
||||||
|
|
||||||
|
- Linux: Includes threads and child processes, but excludes kernel and hypervisor execution.
|
||||||
|
- macOS: Includes the process's threads and kernel execution, but excludes child processes.
|
||||||
|
- Windows: Currently not supported.
|
||||||
|
|
||||||
|
- **`instructions`**: Completed CPU instructions.
|
||||||
|
|
||||||
|
- Linux/macOS: Same scope as `cpu_cycles`.
|
||||||
|
- Windows: Currently not supported.
|
||||||
|
|
||||||
|
- **`cache_references`**: Cache accesses counted by the CPU's generic cache event.
|
||||||
|
|
||||||
|
- Linux: Same scope as `cpu_cycles`. Cache-event definitions depend on the CPU.
|
||||||
|
- macOS/Windows: Currently not supported.
|
||||||
|
|
||||||
|
- **`cache_misses`**: Cache misses.
|
||||||
|
|
||||||
|
- Linux: Same scope as `cpu_cycles`. Cache-event definitions depend on the CPU.
|
||||||
|
- macOS/Windows: Currently not supported.
|
||||||
|
|
||||||
|
- **`branch_misses`**: Mispredicted branches.
|
||||||
|
|
||||||
|
- Linux: Same scope as `cpu_cycles`.
|
||||||
|
- macOS/Windows: Currently not supported.
|
||||||
|
|
||||||
|
Hardware counters are not available if a `--shell` is used.
|
||||||
|
|
||||||
The JSON output is useful if you want to analyze the benchmark results in more detail. The
|
The JSON output is useful if you want to analyze the benchmark results in more detail. The
|
||||||
[`scripts/`](https://github.com/sharkdp/hyperfine/tree/master/scripts) folder includes a lot
|
[`scripts/`](https://github.com/sharkdp/hyperfine/tree/master/scripts) folder includes a lot
|
||||||
of helpful Python programs to further analyze benchmark results and create helpful
|
of helpful Python programs to further analyze benchmark results and create helpful
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ fn run_command_and_measure_common(
|
|||||||
command_input_policy: &CommandInputPolicy,
|
command_input_policy: &CommandInputPolicy,
|
||||||
command_output_policy: &CommandOutputPolicy,
|
command_output_policy: &CommandOutputPolicy,
|
||||||
command_name: &str,
|
command_name: &str,
|
||||||
|
collect_hardware_counters: bool,
|
||||||
) -> Result<Measurement> {
|
) -> Result<Measurement> {
|
||||||
let stdin = command_input_policy.get_stdin()?;
|
let stdin = command_input_policy.get_stdin()?;
|
||||||
let (stdout, stderr) = command_output_policy.get_stdout_stderr()?;
|
let (stdout, stderr) = command_output_policy.get_stdout_stderr()?;
|
||||||
@@ -76,7 +77,7 @@ fn run_command_and_measure_common(
|
|||||||
command.env("HYPERFINE_ITERATION", value);
|
command.env("HYPERFINE_ITERATION", value);
|
||||||
}
|
}
|
||||||
|
|
||||||
let measurement = execute_and_measure(command)
|
let measurement = execute_and_measure(command, collect_hardware_counters)
|
||||||
.with_context(|| format!("Failed to run command '{command_name}'"))?;
|
.with_context(|| format!("Failed to run command '{command_name}'"))?;
|
||||||
|
|
||||||
if !measurement.exit_status.success() {
|
if !measurement.exit_status.success() {
|
||||||
@@ -144,6 +145,7 @@ impl Executor for RawExecutor<'_> {
|
|||||||
&self.options.command_input_policy,
|
&self.options.command_input_policy,
|
||||||
output_policy,
|
output_policy,
|
||||||
&command.get_command_line(),
|
&command.get_command_line(),
|
||||||
|
matches!(iteration, BenchmarkIteration::Benchmark(_)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,6 +201,7 @@ impl Executor for ShellExecutor<'_> {
|
|||||||
&self.options.command_input_policy,
|
&self.options.command_input_policy,
|
||||||
output_policy,
|
output_policy,
|
||||||
&command.get_command_line(),
|
&command.get_command_line(),
|
||||||
|
false,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Subtract shell spawning time
|
// Subtract shell spawning time
|
||||||
@@ -278,6 +281,7 @@ impl Executor for ShellExecutor<'_> {
|
|||||||
time_user: measurements.time_user_mean(),
|
time_user: measurements.time_user_mean(),
|
||||||
time_system: measurements.time_system_mean(),
|
time_system: measurements.time_system_mean(),
|
||||||
memory_peak_resident: None,
|
memory_peak_resident: None,
|
||||||
|
hardware_counters: Default::default(),
|
||||||
exit_status: ExitStatus::default(),
|
exit_status: ExitStatus::default(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -336,6 +340,7 @@ impl Executor for MockExecutor {
|
|||||||
time_user: Time::zero(),
|
time_user: Time::zero(),
|
||||||
time_system: Time::zero(),
|
time_system: Time::zero(),
|
||||||
memory_peak_resident: Some(Information::zero()),
|
memory_peak_resident: Some(Information::zero()),
|
||||||
|
hardware_counters: Default::default(),
|
||||||
exit_status,
|
exit_status,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,47 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn serialize_optional_count<S>(value: &Option<u64>, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Count {
|
||||||
|
value: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
value.map(|value| Count { value }).serialize(serializer)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, PartialEq)]
|
||||||
|
pub struct HardwareCounters {
|
||||||
|
#[serde(
|
||||||
|
skip_serializing_if = "Option::is_none",
|
||||||
|
serialize_with = "serialize_optional_count"
|
||||||
|
)]
|
||||||
|
pub cpu_cycles: Option<u64>,
|
||||||
|
#[serde(
|
||||||
|
skip_serializing_if = "Option::is_none",
|
||||||
|
serialize_with = "serialize_optional_count"
|
||||||
|
)]
|
||||||
|
pub instructions: Option<u64>,
|
||||||
|
#[serde(
|
||||||
|
skip_serializing_if = "Option::is_none",
|
||||||
|
serialize_with = "serialize_optional_count"
|
||||||
|
)]
|
||||||
|
pub cache_references: Option<u64>,
|
||||||
|
#[serde(
|
||||||
|
skip_serializing_if = "Option::is_none",
|
||||||
|
serialize_with = "serialize_optional_count"
|
||||||
|
)]
|
||||||
|
pub cache_misses: Option<u64>,
|
||||||
|
#[serde(
|
||||||
|
skip_serializing_if = "Option::is_none",
|
||||||
|
serialize_with = "serialize_optional_count"
|
||||||
|
)]
|
||||||
|
pub branch_misses: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Performance metric measurements and exit code for a single run
|
/// Performance metric measurements and exit code for a single run
|
||||||
#[derive(Debug, Clone, Default, Serialize, PartialEq)]
|
#[derive(Debug, Clone, Default, Serialize, PartialEq)]
|
||||||
pub struct Measurement {
|
pub struct Measurement {
|
||||||
@@ -51,6 +92,9 @@ pub struct Measurement {
|
|||||||
)]
|
)]
|
||||||
pub memory_peak_resident: Option<Information>,
|
pub memory_peak_resident: Option<Information>,
|
||||||
|
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub hardware_counters: HardwareCounters,
|
||||||
|
|
||||||
// The exit status of the process
|
// The exit status of the process
|
||||||
#[serde(rename = "exit_code", serialize_with = "serialize_exit_status")]
|
#[serde(rename = "exit_code", serialize_with = "serialize_exit_status")]
|
||||||
pub exit_status: ExitStatus,
|
pub exit_status: ExitStatus,
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ fn test_csv() {
|
|||||||
time_user: Time::new::<second>(7.0),
|
time_user: Time::new::<second>(7.0),
|
||||||
time_system: Time::zero(),
|
time_system: Time::zero(),
|
||||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||||
|
hardware_counters: Default::default(),
|
||||||
exit_status: ExitStatus::default(),
|
exit_status: ExitStatus::default(),
|
||||||
},
|
},
|
||||||
Measurement {
|
Measurement {
|
||||||
@@ -96,6 +97,7 @@ fn test_csv() {
|
|||||||
time_user: Time::new::<second>(8.0),
|
time_user: Time::new::<second>(8.0),
|
||||||
time_system: Time::zero(),
|
time_system: Time::zero(),
|
||||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||||
|
hardware_counters: Default::default(),
|
||||||
exit_status: ExitStatus::default(),
|
exit_status: ExitStatus::default(),
|
||||||
},
|
},
|
||||||
Measurement {
|
Measurement {
|
||||||
@@ -103,6 +105,7 @@ fn test_csv() {
|
|||||||
time_user: Time::new::<second>(12.0),
|
time_user: Time::new::<second>(12.0),
|
||||||
time_system: Time::zero(),
|
time_system: Time::zero(),
|
||||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||||
|
hardware_counters: Default::default(),
|
||||||
exit_status: ExitStatus::default(),
|
exit_status: ExitStatus::default(),
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
@@ -133,6 +136,7 @@ fn test_csv() {
|
|||||||
time_user: Time::new::<second>(17.0),
|
time_user: Time::new::<second>(17.0),
|
||||||
time_system: Time::zero(),
|
time_system: Time::zero(),
|
||||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||||
|
hardware_counters: Default::default(),
|
||||||
exit_status: ExitStatus::default(),
|
exit_status: ExitStatus::default(),
|
||||||
},
|
},
|
||||||
Measurement {
|
Measurement {
|
||||||
@@ -140,6 +144,7 @@ fn test_csv() {
|
|||||||
time_user: Time::new::<second>(18.0),
|
time_user: Time::new::<second>(18.0),
|
||||||
time_system: Time::zero(),
|
time_system: Time::zero(),
|
||||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||||
|
hardware_counters: Default::default(),
|
||||||
exit_status: ExitStatus::default(),
|
exit_status: ExitStatus::default(),
|
||||||
},
|
},
|
||||||
Measurement {
|
Measurement {
|
||||||
@@ -147,6 +152,7 @@ fn test_csv() {
|
|||||||
time_user: Time::new::<second>(19.0),
|
time_user: Time::new::<second>(19.0),
|
||||||
time_system: Time::zero(),
|
time_system: Time::zero(),
|
||||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||||
|
hardware_counters: Default::default(),
|
||||||
exit_status: ExitStatus::default(),
|
exit_status: ExitStatus::default(),
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
|
|||||||
+94
-32
@@ -8,7 +8,7 @@ use crate::benchmark::benchmark_result::BenchmarkResult;
|
|||||||
use crate::benchmark::measurement::Measurements;
|
use crate::benchmark::measurement::Measurements;
|
||||||
use crate::options::SortOrder;
|
use crate::options::SortOrder;
|
||||||
use crate::quantity::statistics::{max, mean, median, min, standard_deviation, UnsafeRawValue};
|
use crate::quantity::statistics::{max, mean, median, min, standard_deviation, UnsafeRawValue};
|
||||||
use crate::quantity::{byte, second, QuantityInUnit, Ratio, TimeUnit, Zero};
|
use crate::quantity::{byte, ratio, second, QuantityInUnit, Ratio, TimeUnit, Zero};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
@@ -40,36 +40,92 @@ struct BenchmarkSummary {
|
|||||||
time_system: StatisticalSummary,
|
time_system: StatisticalSummary,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
memory_peak_resident: Option<StatisticalSummary>,
|
memory_peak_resident: Option<StatisticalSummary>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
cpu_cycles: Option<StatisticalSummary>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
instructions: Option<StatisticalSummary>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
cache_references: Option<StatisticalSummary>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
cache_misses: Option<StatisticalSummary>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
branch_misses: Option<StatisticalSummary>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BenchmarkSummary {
|
impl BenchmarkSummary {
|
||||||
fn from_measurements(measurements: &Measurements) -> Self {
|
fn from_measurements(measurements: &Measurements) -> Self {
|
||||||
let values = &measurements.measurements;
|
let values = &measurements.measurements;
|
||||||
let mut memory_values = values
|
|
||||||
.iter()
|
|
||||||
.filter_map(|m| m.memory_peak_resident)
|
|
||||||
.peekable();
|
|
||||||
Self {
|
Self {
|
||||||
time_wall_clock: StatisticalSummary::from_values(
|
time_wall_clock: StatisticalSummary::from_values(
|
||||||
values.iter().map(|m| m.time_wall_clock),
|
values.iter().map(|m| Some(m.time_wall_clock)),
|
||||||
second,
|
Some(second),
|
||||||
),
|
)
|
||||||
time_user: StatisticalSummary::from_values(values.iter().map(|m| m.time_user), second),
|
.expect("Benchmarks have at least one measurement"),
|
||||||
|
time_user: StatisticalSummary::from_values(
|
||||||
|
values.iter().map(|m| Some(m.time_user)),
|
||||||
|
Some(second),
|
||||||
|
)
|
||||||
|
.expect("Benchmarks have at least one measurement"),
|
||||||
time_system: StatisticalSummary::from_values(
|
time_system: StatisticalSummary::from_values(
|
||||||
values.iter().map(|m| m.time_system),
|
values.iter().map(|m| Some(m.time_system)),
|
||||||
second,
|
Some(second),
|
||||||
|
)
|
||||||
|
.expect("Benchmarks have at least one measurement"),
|
||||||
|
memory_peak_resident: StatisticalSummary::from_values(
|
||||||
|
values
|
||||||
|
.iter()
|
||||||
|
.filter_map(|m| m.memory_peak_resident)
|
||||||
|
.map(Some),
|
||||||
|
Some(byte),
|
||||||
|
),
|
||||||
|
cpu_cycles: StatisticalSummary::from_values(
|
||||||
|
values.iter().map(|m| {
|
||||||
|
m.hardware_counters
|
||||||
|
.cpu_cycles
|
||||||
|
.map(|v| Ratio::new::<ratio>(v as f64))
|
||||||
|
}),
|
||||||
|
None::<ratio>,
|
||||||
|
),
|
||||||
|
instructions: StatisticalSummary::from_values(
|
||||||
|
values.iter().map(|m| {
|
||||||
|
m.hardware_counters
|
||||||
|
.instructions
|
||||||
|
.map(|v| Ratio::new::<ratio>(v as f64))
|
||||||
|
}),
|
||||||
|
None::<ratio>,
|
||||||
|
),
|
||||||
|
cache_references: StatisticalSummary::from_values(
|
||||||
|
values.iter().map(|m| {
|
||||||
|
m.hardware_counters
|
||||||
|
.cache_references
|
||||||
|
.map(|v| Ratio::new::<ratio>(v as f64))
|
||||||
|
}),
|
||||||
|
None::<ratio>,
|
||||||
|
),
|
||||||
|
cache_misses: StatisticalSummary::from_values(
|
||||||
|
values.iter().map(|m| {
|
||||||
|
m.hardware_counters
|
||||||
|
.cache_misses
|
||||||
|
.map(|v| Ratio::new::<ratio>(v as f64))
|
||||||
|
}),
|
||||||
|
None::<ratio>,
|
||||||
|
),
|
||||||
|
branch_misses: StatisticalSummary::from_values(
|
||||||
|
values.iter().map(|m| {
|
||||||
|
m.hardware_counters
|
||||||
|
.branch_misses
|
||||||
|
.map(|v| Ratio::new::<ratio>(v as f64))
|
||||||
|
}),
|
||||||
|
None::<ratio>,
|
||||||
),
|
),
|
||||||
memory_peak_resident: memory_values
|
|
||||||
.peek()
|
|
||||||
.is_some()
|
|
||||||
.then(|| StatisticalSummary::from_values(memory_values, byte)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct StatisticalSummary {
|
struct StatisticalSummary {
|
||||||
unit: &'static str,
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
unit: Option<&'static str>,
|
||||||
count: usize,
|
count: usize,
|
||||||
mean: f64,
|
mean: f64,
|
||||||
stddev: Option<f64>,
|
stddev: Option<f64>,
|
||||||
@@ -79,7 +135,10 @@ struct StatisticalSummary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl StatisticalSummary {
|
impl StatisticalSummary {
|
||||||
fn from_values<Q, P, U>(values: impl IntoIterator<Item = Q>, _unit: U) -> Self
|
fn from_values<Q, P, U>(
|
||||||
|
values: impl IntoIterator<Item = Option<Q>>,
|
||||||
|
unit: Option<U>,
|
||||||
|
) -> Option<Self>
|
||||||
where
|
where
|
||||||
Q: Copy
|
Q: Copy
|
||||||
+ PartialOrd
|
+ PartialOrd
|
||||||
@@ -92,13 +151,13 @@ impl StatisticalSummary {
|
|||||||
P: Into<Q>,
|
P: Into<Q>,
|
||||||
U: uom::si::Unit,
|
U: uom::si::Unit,
|
||||||
{
|
{
|
||||||
let values: Vec<_> = values.into_iter().collect();
|
// Only summarize a metric when it is available for every supplied measurement.
|
||||||
assert!(
|
let values: Vec<_> = values.into_iter().collect::<Option<_>>()?;
|
||||||
!values.is_empty(),
|
if values.is_empty() {
|
||||||
"Statistical summaries require at least one measurement"
|
return None;
|
||||||
);
|
}
|
||||||
Self {
|
Some(Self {
|
||||||
unit: U::singular(),
|
unit: unit.map(|_| U::singular()),
|
||||||
count: values.len(),
|
count: values.len(),
|
||||||
mean: mean(values.iter().copied()).value_in_unit(),
|
mean: mean(values.iter().copied()).value_in_unit(),
|
||||||
stddev: (values.len() > 1)
|
stddev: (values.len() > 1)
|
||||||
@@ -106,7 +165,7 @@ impl StatisticalSummary {
|
|||||||
median: median(values.iter().copied()).value_in_unit(),
|
median: median(values.iter().copied()).value_in_unit(),
|
||||||
min: min(values.iter().copied()).value_in_unit(),
|
min: min(values.iter().copied()).value_in_unit(),
|
||||||
max: max(values.iter().copied()).value_in_unit(),
|
max: max(values.iter().copied()).value_in_unit(),
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,22 +208,25 @@ fn test_statistical_summary_unit_conversion() {
|
|||||||
use crate::quantity::{kibibyte, millisecond, Information, Time};
|
use crate::quantity::{kibibyte, millisecond, Information, Time};
|
||||||
|
|
||||||
let time = StatisticalSummary::from_values(
|
let time = StatisticalSummary::from_values(
|
||||||
[Time::new::<second>(1.0), Time::new::<second>(3.0)],
|
[Time::new::<second>(1.0), Time::new::<second>(3.0)].map(Some),
|
||||||
millisecond,
|
Some(millisecond),
|
||||||
);
|
)
|
||||||
|
.unwrap();
|
||||||
let information = StatisticalSummary::from_values(
|
let information = StatisticalSummary::from_values(
|
||||||
[
|
[
|
||||||
Information::new::<byte>(1024.0),
|
Information::new::<byte>(1024.0),
|
||||||
Information::new::<byte>(3072.0),
|
Information::new::<byte>(3072.0),
|
||||||
],
|
]
|
||||||
kibibyte,
|
.map(Some),
|
||||||
);
|
Some(kibibyte),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
for (summary, unit, scale) in [
|
for (summary, unit, scale) in [
|
||||||
(time, "millisecond", 1000.0),
|
(time, "millisecond", 1000.0),
|
||||||
(information, "kibibyte", 1.0),
|
(information, "kibibyte", 1.0),
|
||||||
] {
|
] {
|
||||||
assert_eq!(summary.unit, unit);
|
assert_eq!(summary.unit, Some(unit));
|
||||||
assert_eq!(summary.count, 2);
|
assert_eq!(summary.count, 2);
|
||||||
approx::assert_relative_eq!(summary.mean, 2.0 * scale);
|
approx::assert_relative_eq!(summary.mean, 2.0 * scale);
|
||||||
approx::assert_relative_eq!(summary.median, 2.0 * scale);
|
approx::assert_relative_eq!(summary.median, 2.0 * scale);
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ fn test_markup_export_auto_ms() {
|
|||||||
time_user: Time::new::<second>(0.09),
|
time_user: Time::new::<second>(0.09),
|
||||||
time_system: Time::zero(),
|
time_system: Time::zero(),
|
||||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||||
|
hardware_counters: Default::default(),
|
||||||
exit_status: ExitStatus::default(),
|
exit_status: ExitStatus::default(),
|
||||||
},
|
},
|
||||||
Measurement {
|
Measurement {
|
||||||
@@ -43,6 +44,7 @@ fn test_markup_export_auto_ms() {
|
|||||||
time_user: Time::new::<second>(0.10),
|
time_user: Time::new::<second>(0.10),
|
||||||
time_system: Time::zero(),
|
time_system: Time::zero(),
|
||||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||||
|
hardware_counters: Default::default(),
|
||||||
exit_status: ExitStatus::default(),
|
exit_status: ExitStatus::default(),
|
||||||
},
|
},
|
||||||
Measurement {
|
Measurement {
|
||||||
@@ -50,6 +52,7 @@ fn test_markup_export_auto_ms() {
|
|||||||
time_user: Time::new::<second>(0.14),
|
time_user: Time::new::<second>(0.14),
|
||||||
time_system: Time::zero(),
|
time_system: Time::zero(),
|
||||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||||
|
hardware_counters: Default::default(),
|
||||||
exit_status: ExitStatus::default(),
|
exit_status: ExitStatus::default(),
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
@@ -65,6 +68,7 @@ fn test_markup_export_auto_ms() {
|
|||||||
time_user: Time::new::<second>(2.0),
|
time_user: Time::new::<second>(2.0),
|
||||||
time_system: Time::zero(),
|
time_system: Time::zero(),
|
||||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||||
|
hardware_counters: Default::default(),
|
||||||
exit_status: ExitStatus::default(),
|
exit_status: ExitStatus::default(),
|
||||||
},
|
},
|
||||||
Measurement {
|
Measurement {
|
||||||
@@ -72,6 +76,7 @@ fn test_markup_export_auto_ms() {
|
|||||||
time_user: Time::new::<second>(3.0),
|
time_user: Time::new::<second>(3.0),
|
||||||
time_system: Time::zero(),
|
time_system: Time::zero(),
|
||||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||||
|
hardware_counters: Default::default(),
|
||||||
exit_status: ExitStatus::default(),
|
exit_status: ExitStatus::default(),
|
||||||
},
|
},
|
||||||
Measurement {
|
Measurement {
|
||||||
@@ -79,6 +84,7 @@ fn test_markup_export_auto_ms() {
|
|||||||
time_user: Time::new::<second>(4.0),
|
time_user: Time::new::<second>(4.0),
|
||||||
time_system: Time::zero(),
|
time_system: Time::zero(),
|
||||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||||
|
hardware_counters: Default::default(),
|
||||||
exit_status: ExitStatus::default(),
|
exit_status: ExitStatus::default(),
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
@@ -139,6 +145,7 @@ fn test_markup_export_auto_s() {
|
|||||||
time_user: Time::new::<second>(2.1),
|
time_user: Time::new::<second>(2.1),
|
||||||
time_system: Time::zero(),
|
time_system: Time::zero(),
|
||||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||||
|
hardware_counters: Default::default(),
|
||||||
exit_status: ExitStatus::default(),
|
exit_status: ExitStatus::default(),
|
||||||
},
|
},
|
||||||
Measurement {
|
Measurement {
|
||||||
@@ -146,6 +153,7 @@ fn test_markup_export_auto_s() {
|
|||||||
time_user: Time::new::<second>(2.2),
|
time_user: Time::new::<second>(2.2),
|
||||||
time_system: Time::zero(),
|
time_system: Time::zero(),
|
||||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||||
|
hardware_counters: Default::default(),
|
||||||
exit_status: ExitStatus::default(),
|
exit_status: ExitStatus::default(),
|
||||||
},
|
},
|
||||||
Measurement {
|
Measurement {
|
||||||
@@ -153,6 +161,7 @@ fn test_markup_export_auto_s() {
|
|||||||
time_user: Time::new::<second>(2.3),
|
time_user: Time::new::<second>(2.3),
|
||||||
time_system: Time::zero(),
|
time_system: Time::zero(),
|
||||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||||
|
hardware_counters: Default::default(),
|
||||||
exit_status: ExitStatus::default(),
|
exit_status: ExitStatus::default(),
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
@@ -168,6 +177,7 @@ fn test_markup_export_auto_s() {
|
|||||||
time_user: Time::new::<second>(0.1),
|
time_user: Time::new::<second>(0.1),
|
||||||
time_system: Time::zero(),
|
time_system: Time::zero(),
|
||||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||||
|
hardware_counters: Default::default(),
|
||||||
exit_status: ExitStatus::default(),
|
exit_status: ExitStatus::default(),
|
||||||
},
|
},
|
||||||
Measurement {
|
Measurement {
|
||||||
@@ -175,6 +185,7 @@ fn test_markup_export_auto_s() {
|
|||||||
time_user: Time::new::<second>(0.2),
|
time_user: Time::new::<second>(0.2),
|
||||||
time_system: Time::zero(),
|
time_system: Time::zero(),
|
||||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||||
|
hardware_counters: Default::default(),
|
||||||
exit_status: ExitStatus::default(),
|
exit_status: ExitStatus::default(),
|
||||||
},
|
},
|
||||||
Measurement {
|
Measurement {
|
||||||
@@ -182,6 +193,7 @@ fn test_markup_export_auto_s() {
|
|||||||
time_user: Time::new::<second>(0.3),
|
time_user: Time::new::<second>(0.3),
|
||||||
time_system: Time::zero(),
|
time_system: Time::zero(),
|
||||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||||
|
hardware_counters: Default::default(),
|
||||||
exit_status: ExitStatus::default(),
|
exit_status: ExitStatus::default(),
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
@@ -242,6 +254,7 @@ fn test_markup_export_manual_ms() {
|
|||||||
time_user: Time::new::<second>(2.1),
|
time_user: Time::new::<second>(2.1),
|
||||||
time_system: Time::zero(),
|
time_system: Time::zero(),
|
||||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||||
|
hardware_counters: Default::default(),
|
||||||
exit_status: ExitStatus::default(),
|
exit_status: ExitStatus::default(),
|
||||||
},
|
},
|
||||||
Measurement {
|
Measurement {
|
||||||
@@ -249,6 +262,7 @@ fn test_markup_export_manual_ms() {
|
|||||||
time_user: Time::new::<second>(2.2),
|
time_user: Time::new::<second>(2.2),
|
||||||
time_system: Time::zero(),
|
time_system: Time::zero(),
|
||||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||||
|
hardware_counters: Default::default(),
|
||||||
exit_status: ExitStatus::default(),
|
exit_status: ExitStatus::default(),
|
||||||
},
|
},
|
||||||
Measurement {
|
Measurement {
|
||||||
@@ -256,6 +270,7 @@ fn test_markup_export_manual_ms() {
|
|||||||
time_user: Time::new::<second>(2.3),
|
time_user: Time::new::<second>(2.3),
|
||||||
time_system: Time::zero(),
|
time_system: Time::zero(),
|
||||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||||
|
hardware_counters: Default::default(),
|
||||||
exit_status: ExitStatus::default(),
|
exit_status: ExitStatus::default(),
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
@@ -271,6 +286,7 @@ fn test_markup_export_manual_ms() {
|
|||||||
time_user: Time::new::<second>(0.1),
|
time_user: Time::new::<second>(0.1),
|
||||||
time_system: Time::zero(),
|
time_system: Time::zero(),
|
||||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||||
|
hardware_counters: Default::default(),
|
||||||
exit_status: ExitStatus::default(),
|
exit_status: ExitStatus::default(),
|
||||||
},
|
},
|
||||||
Measurement {
|
Measurement {
|
||||||
@@ -278,6 +294,7 @@ fn test_markup_export_manual_ms() {
|
|||||||
time_user: Time::new::<second>(0.2),
|
time_user: Time::new::<second>(0.2),
|
||||||
time_system: Time::zero(),
|
time_system: Time::zero(),
|
||||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||||
|
hardware_counters: Default::default(),
|
||||||
exit_status: ExitStatus::default(),
|
exit_status: ExitStatus::default(),
|
||||||
},
|
},
|
||||||
Measurement {
|
Measurement {
|
||||||
@@ -285,6 +302,7 @@ fn test_markup_export_manual_ms() {
|
|||||||
time_user: Time::new::<second>(0.3),
|
time_user: Time::new::<second>(0.3),
|
||||||
time_system: Time::zero(),
|
time_system: Time::zero(),
|
||||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||||
|
hardware_counters: Default::default(),
|
||||||
exit_status: ExitStatus::default(),
|
exit_status: ExitStatus::default(),
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
@@ -344,6 +362,7 @@ fn test_markup_export_manual_s() {
|
|||||||
time_user: Time::new::<second>(2.01),
|
time_user: Time::new::<second>(2.01),
|
||||||
time_system: Time::zero(),
|
time_system: Time::zero(),
|
||||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||||
|
hardware_counters: Default::default(),
|
||||||
exit_status: ExitStatus::default(),
|
exit_status: ExitStatus::default(),
|
||||||
},
|
},
|
||||||
Measurement {
|
Measurement {
|
||||||
@@ -351,6 +370,7 @@ fn test_markup_export_manual_s() {
|
|||||||
time_user: Time::new::<second>(2.02),
|
time_user: Time::new::<second>(2.02),
|
||||||
time_system: Time::zero(),
|
time_system: Time::zero(),
|
||||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||||
|
hardware_counters: Default::default(),
|
||||||
exit_status: ExitStatus::default(),
|
exit_status: ExitStatus::default(),
|
||||||
},
|
},
|
||||||
Measurement {
|
Measurement {
|
||||||
@@ -358,6 +378,7 @@ fn test_markup_export_manual_s() {
|
|||||||
time_user: Time::new::<second>(2.03),
|
time_user: Time::new::<second>(2.03),
|
||||||
time_system: Time::zero(),
|
time_system: Time::zero(),
|
||||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||||
|
hardware_counters: Default::default(),
|
||||||
exit_status: ExitStatus::default(),
|
exit_status: ExitStatus::default(),
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
@@ -373,6 +394,7 @@ fn test_markup_export_manual_s() {
|
|||||||
time_user: Time::new::<second>(0.11),
|
time_user: Time::new::<second>(0.11),
|
||||||
time_system: Time::zero(),
|
time_system: Time::zero(),
|
||||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||||
|
hardware_counters: Default::default(),
|
||||||
exit_status: ExitStatus::default(),
|
exit_status: ExitStatus::default(),
|
||||||
},
|
},
|
||||||
Measurement {
|
Measurement {
|
||||||
@@ -380,6 +402,7 @@ fn test_markup_export_manual_s() {
|
|||||||
time_user: Time::new::<second>(0.12),
|
time_user: Time::new::<second>(0.12),
|
||||||
time_system: Time::zero(),
|
time_system: Time::zero(),
|
||||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||||
|
hardware_counters: Default::default(),
|
||||||
exit_status: ExitStatus::default(),
|
exit_status: ExitStatus::default(),
|
||||||
},
|
},
|
||||||
Measurement {
|
Measurement {
|
||||||
@@ -387,6 +410,7 @@ fn test_markup_export_manual_s() {
|
|||||||
time_user: Time::new::<second>(0.13),
|
time_user: Time::new::<second>(0.13),
|
||||||
time_system: Time::zero(),
|
time_system: Time::zero(),
|
||||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||||
|
hardware_counters: Default::default(),
|
||||||
exit_status: ExitStatus::default(),
|
exit_status: ExitStatus::default(),
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
|
|||||||
@@ -34,6 +34,12 @@ impl<U: uom::si::information::Unit + uom::Conversion<f64, T = f64>> QuantityInUn
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<U: uom::si::ratio::Unit + uom::Conversion<f64, T = f64>> QuantityInUnit<U> for Ratio {
|
||||||
|
fn value_in_unit(self) -> f64 {
|
||||||
|
self.get::<U>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub trait FormatQuantity {
|
pub trait FormatQuantity {
|
||||||
type Unit;
|
type Unit;
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,16 @@ impl UnsafeRawValue for Information {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl UnsafeRawValue for Ratio {
|
||||||
|
fn unsafe_raw_value(&self) -> f64 {
|
||||||
|
self.get::<ratio>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unsafe_from_raw_value(value: f64) -> Self {
|
||||||
|
Ratio::new::<ratio>(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn standard_deviation_f64(values: &[f64]) -> f64 {
|
fn standard_deviation_f64(values: &[f64]) -> f64 {
|
||||||
let mean_value = mean(values.iter().copied());
|
let mean_value = mean(values.iter().copied());
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
use std::fs::File;
|
||||||
|
use std::io::Read;
|
||||||
|
|
||||||
|
use crate::benchmark::measurement::HardwareCounters;
|
||||||
|
|
||||||
|
/// Disabled events on the spawning thread, inherited and enabled by the child's exec.
|
||||||
|
/// The parent never enables its own events, so its work is not counted.
|
||||||
|
pub struct CounterGroup {
|
||||||
|
events: [Option<File>; 5],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CounterGroup {
|
||||||
|
#[cfg(all(
|
||||||
|
target_pointer_width = "64",
|
||||||
|
any(
|
||||||
|
target_arch = "aarch64",
|
||||||
|
target_arch = "x86_64",
|
||||||
|
target_arch = "riscv64",
|
||||||
|
target_arch = "powerpc64",
|
||||||
|
target_arch = "loongarch64"
|
||||||
|
)
|
||||||
|
))]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
use perf_event_open_sys::{bindings, perf_event_open};
|
||||||
|
use std::os::fd::{AsRawFd, FromRawFd};
|
||||||
|
|
||||||
|
let mut group_leader = -1;
|
||||||
|
let events = [
|
||||||
|
bindings::PERF_COUNT_HW_CPU_CYCLES,
|
||||||
|
bindings::PERF_COUNT_HW_INSTRUCTIONS,
|
||||||
|
bindings::PERF_COUNT_HW_CACHE_REFERENCES,
|
||||||
|
bindings::PERF_COUNT_HW_CACHE_MISSES,
|
||||||
|
bindings::PERF_COUNT_HW_BRANCH_MISSES,
|
||||||
|
]
|
||||||
|
.map(|config| {
|
||||||
|
let mut attr = bindings::perf_event_attr {
|
||||||
|
type_: bindings::PERF_TYPE_HARDWARE,
|
||||||
|
size: std::mem::size_of::<bindings::perf_event_attr>() as u32,
|
||||||
|
config: u64::from(config),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
// Don't start counting immediately.
|
||||||
|
attr.set_disabled(1);
|
||||||
|
// Start counting when the child executes the benchmark program.
|
||||||
|
attr.set_enable_on_exec(1);
|
||||||
|
// New child processes and threads inherit the counters.
|
||||||
|
attr.set_inherit(1);
|
||||||
|
// Exclude kernel execution.
|
||||||
|
attr.set_exclude_kernel(1);
|
||||||
|
// Exclude hypervisor execution.
|
||||||
|
attr.set_exclude_hv(1);
|
||||||
|
|
||||||
|
// Attach counters to the calling thread.
|
||||||
|
const CURRENT_THREAD: libc::pid_t = 0;
|
||||||
|
// Count on whichever CPU the task runs on.
|
||||||
|
const ANY_CPU: libc::c_int = -1;
|
||||||
|
|
||||||
|
// SAFETY: attr is initialized and sized for the kernel API.
|
||||||
|
let fd = unsafe {
|
||||||
|
perf_event_open(
|
||||||
|
&mut attr,
|
||||||
|
CURRENT_THREAD,
|
||||||
|
ANY_CPU,
|
||||||
|
group_leader,
|
||||||
|
bindings::PERF_FLAG_FD_CLOEXEC.into(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if fd < 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// SAFETY: perf_event_open returned a new, valid descriptor.
|
||||||
|
let event = unsafe { File::from_raw_fd(fd) };
|
||||||
|
if group_leader == -1 {
|
||||||
|
group_leader = event.as_raw_fd();
|
||||||
|
}
|
||||||
|
Some(event)
|
||||||
|
});
|
||||||
|
Self { events }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(all(
|
||||||
|
target_pointer_width = "64",
|
||||||
|
any(
|
||||||
|
target_arch = "aarch64",
|
||||||
|
target_arch = "x86_64",
|
||||||
|
target_arch = "riscv64",
|
||||||
|
target_arch = "powerpc64",
|
||||||
|
target_arch = "loongarch64"
|
||||||
|
)
|
||||||
|
)))]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
events: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read raw counts after the child has exited. Like poop, do not scale for multiplexing.
|
||||||
|
pub fn read(mut self) -> HardwareCounters {
|
||||||
|
let [cpu_cycles, instructions, cache_references, cache_misses, branch_misses] =
|
||||||
|
self.events.each_mut().map(|event| {
|
||||||
|
let mut bytes = [0; 8];
|
||||||
|
event.as_mut()?.read_exact(&mut bytes).ok()?;
|
||||||
|
Some(u64::from_ne_bytes(bytes))
|
||||||
|
});
|
||||||
|
HardwareCounters {
|
||||||
|
cpu_cycles,
|
||||||
|
instructions,
|
||||||
|
cache_references,
|
||||||
|
cache_misses,
|
||||||
|
branch_misses,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
use std::io;
|
||||||
|
use std::mem::MaybeUninit;
|
||||||
|
use std::process::Child;
|
||||||
|
|
||||||
|
use crate::benchmark::measurement::HardwareCounters;
|
||||||
|
|
||||||
|
/// Wait for exit without reaping: proc_pid_rusage can still inspect the zombie,
|
||||||
|
/// and the existing wait4 call can subsequently collect CPU times and peak RSS.
|
||||||
|
pub fn wait(child: &mut Child) -> io::Result<()> {
|
||||||
|
drop(child.stdin.take());
|
||||||
|
let mut info = MaybeUninit::uninit();
|
||||||
|
loop {
|
||||||
|
// SAFETY: info points to writable siginfo_t storage. WNOWAIT preserves the child.
|
||||||
|
let result = unsafe {
|
||||||
|
libc::waitid(
|
||||||
|
libc::P_PID,
|
||||||
|
child.id(),
|
||||||
|
info.as_mut_ptr(),
|
||||||
|
libc::WEXITED | libc::WNOWAIT,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if result == 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let error = io::Error::last_os_error();
|
||||||
|
if error.kind() != io::ErrorKind::Interrupted {
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read(pid: u32) -> HardwareCounters {
|
||||||
|
let mut usage = MaybeUninit::<libc::rusage_info_v4>::zeroed();
|
||||||
|
// SAFETY: the buffer has the layout and size requested by RUSAGE_INFO_V4.
|
||||||
|
// Despite the pointer-to-pointer signature, this API writes directly into the buffer.
|
||||||
|
let result = unsafe {
|
||||||
|
libc::proc_pid_rusage(
|
||||||
|
pid as libc::pid_t,
|
||||||
|
libc::RUSAGE_INFO_V4,
|
||||||
|
usage.as_mut_ptr().cast(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if result != 0 {
|
||||||
|
return HardwareCounters::default();
|
||||||
|
}
|
||||||
|
// SAFETY: the call succeeded and initialized the buffer.
|
||||||
|
let usage = unsafe { usage.assume_init() };
|
||||||
|
HardwareCounters {
|
||||||
|
// Like macOS time(1), omit zero counters: the API can succeed on systems
|
||||||
|
// where instruction/cycle accounting is unavailable.
|
||||||
|
cpu_cycles: (usage.ri_cycles != 0).then_some(usage.ri_cycles),
|
||||||
|
instructions: (usage.ri_instructions != 0).then_some(usage.ri_instructions),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
-1
@@ -1,5 +1,10 @@
|
|||||||
mod wall_clock_timer;
|
mod wall_clock_timer;
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
mod linux_counters;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
mod macos_counters;
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
mod windows_timer;
|
mod windows_timer;
|
||||||
|
|
||||||
@@ -56,7 +61,12 @@ fn discard(output: ChildStdout) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Execute the given command and return a timing summary
|
/// Execute the given command and return a timing summary
|
||||||
pub fn execute_and_measure(mut command: Command) -> Result<Measurement> {
|
pub fn execute_and_measure(
|
||||||
|
mut command: Command,
|
||||||
|
_collect_hardware_counters: bool,
|
||||||
|
) -> Result<Measurement> {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
let counters = _collect_hardware_counters.then(linux_counters::CounterGroup::new);
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
let cpu_timer = self::unix_timer::CPUTimer::start();
|
let cpu_timer = self::unix_timer::CPUTimer::start();
|
||||||
|
|
||||||
@@ -82,14 +92,36 @@ pub fn execute_and_measure(mut command: Command) -> Result<Measurement> {
|
|||||||
discard(output);
|
discard(output);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let (time_wall_clock, hardware_counters) =
|
||||||
|
if _collect_hardware_counters && macos_counters::wait(&mut child).is_ok() {
|
||||||
|
// Stop timing before querying counters, but leave the child available for wait4.
|
||||||
|
(
|
||||||
|
Some(wallclock_timer.stop()),
|
||||||
|
macos_counters::read(child.id()),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(None, Default::default())
|
||||||
|
};
|
||||||
let (time_user, time_system, memory_peak_resident, exit_status) = cpu_timer.stop(child)?;
|
let (time_user, time_system, memory_peak_resident, exit_status) = cpu_timer.stop(child)?;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let time_wall_clock = time_wall_clock.unwrap_or_else(|| wallclock_timer.stop());
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
let time_wall_clock = wallclock_timer.stop();
|
let time_wall_clock = wallclock_timer.stop();
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
let hardware_counters = counters
|
||||||
|
.map(linux_counters::CounterGroup::read)
|
||||||
|
.unwrap_or_default();
|
||||||
|
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
||||||
|
let hardware_counters = Default::default();
|
||||||
|
|
||||||
Ok(Measurement {
|
Ok(Measurement {
|
||||||
time_wall_clock,
|
time_wall_clock,
|
||||||
time_user,
|
time_user,
|
||||||
time_system,
|
time_system,
|
||||||
memory_peak_resident,
|
memory_peak_resident,
|
||||||
|
hardware_counters,
|
||||||
exit_status,
|
exit_status,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user