pub trait ResourceUsageExt<'a, ThreadState> {
type Output;
fn measure_resource_usage<F>(self, operation_name: &'a str, configure: F) -> Self::Output
where
F: FnOnce(ResourceUsageMeasureBuilder<'a>) -> ResourceUsageMeasureBuilder<'a>;
}
#[derive(Clone, Debug)]
pub struct ResourceUsageMeasureBuilder<'a> {
#[cfg(feature = "alloc_tracker")]
alloc_session: Option<&'a alloc_tracker::Session>,
#[cfg(feature = "all_the_time")]
time_session: Option<&'a all_the_time::Session>,
}
impl<'a> ResourceUsageMeasureBuilder<'a> {
#[must_use]
pub(crate) fn new() -> Self {
Self {
#[cfg(feature = "alloc_tracker")]
alloc_session: None,
#[cfg(feature = "all_the_time")]
time_session: None,
}
}
#[cfg(feature = "alloc_tracker")]
#[must_use]
pub fn allocs(mut self, session: &'a alloc_tracker::Session) -> Self {
self.alloc_session = Some(session);
self
}
#[cfg(feature = "all_the_time")]
#[must_use]
pub fn processor_time(mut self, session: &'a all_the_time::Session) -> Self {
self.time_session = Some(session);
self
}
}
#[derive(Debug)]
pub struct ResourceUsageOutput {
#[cfg(feature = "alloc_tracker")]
alloc_report: Option<alloc_tracker::Report>,
#[cfg(feature = "all_the_time")]
time_report: Option<all_the_time::Report>,
}
impl ResourceUsageOutput {
#[must_use]
pub(crate) fn new(
#[cfg(feature = "alloc_tracker")] alloc_report: Option<alloc_tracker::Report>,
#[cfg(feature = "all_the_time")] time_report: Option<all_the_time::Report>,
) -> Self {
Self {
#[cfg(feature = "alloc_tracker")]
alloc_report,
#[cfg(feature = "all_the_time")]
time_report,
}
}
#[cfg(feature = "alloc_tracker")]
#[must_use]
pub fn allocs(&self) -> Option<&alloc_tracker::Report> {
self.alloc_report.as_ref()
}
#[cfg(feature = "all_the_time")]
#[must_use]
pub fn processor_time(&self) -> Option<&all_the_time::Report> {
self.time_report.as_ref()
}
}
#[derive(Debug)]
pub struct ResourceUsageState {
#[cfg(feature = "alloc_tracker")]
alloc_span: Option<alloc_tracker::ThreadSpan>,
#[cfg(feature = "all_the_time")]
time_span: Option<all_the_time::ThreadSpan>,
}
impl ResourceUsageState {
#[must_use]
pub(crate) fn new(
builder: &ResourceUsageMeasureBuilder<'_>,
operation_name: &str,
iterations: u64,
) -> Self {
Self {
#[cfg(feature = "alloc_tracker")]
alloc_span: builder.alloc_session.map(|session| {
session
.operation(operation_name)
.measure_thread()
.iterations(iterations)
}),
#[cfg(feature = "all_the_time")]
time_span: builder.time_session.map(|session| {
session
.operation(operation_name)
.measure_thread()
.iterations(iterations)
}),
}
}
#[must_use]
pub(crate) fn into_output(
self,
builder: &ResourceUsageMeasureBuilder<'_>,
) -> ResourceUsageOutput {
#[cfg(feature = "alloc_tracker")]
let alloc_report = self.alloc_span.and_then(|span| {
drop(span);
builder.alloc_session.map(alloc_tracker::Session::to_report)
});
#[cfg(feature = "all_the_time")]
let time_report = self.time_span.and_then(|span| {
drop(span);
builder.time_session.map(all_the_time::Session::to_report)
});
ResourceUsageOutput::new(
#[cfg(feature = "alloc_tracker")]
alloc_report,
#[cfg(feature = "all_the_time")]
time_report,
)
}
}
fn create_resource_usage_state_factory<'a, ThreadState>(
builder: ResourceUsageMeasureBuilder<'a>,
operation_name: &'a str,
) -> impl Fn(crate::args::MeasureWrapperBegin<'_, ThreadState>) -> ResourceUsageState + 'a {
move |args| {
#[expect(
clippy::arithmetic_side_effects,
reason = "NonZero eliminates division by zero"
)]
#[expect(
clippy::integer_division,
reason = "we accept imperfect accuracy - typical iteration counts are high enough for it not to matter"
)]
let iterations =
(args.meta().iterations() / args.meta().thread_count().get() as u64).max(1);
ResourceUsageState::new(&builder, operation_name, iterations)
}
}
impl<'a> ResourceUsageExt<'a, ()> for crate::configure::RunInitial {
type Output =
crate::configure::RunWithWrapperState<'a, (), (), ResourceUsageState, ResourceUsageOutput>;
fn measure_resource_usage<F>(self, operation_name: &'a str, configure: F) -> Self::Output
where
F: FnOnce(ResourceUsageMeasureBuilder<'a>) -> ResourceUsageMeasureBuilder<'a>,
{
let builder = configure(ResourceUsageMeasureBuilder::new());
self.measure_wrapper(
create_resource_usage_state_factory(builder.clone(), operation_name),
move |state| state.into_output(&builder),
)
}
}
impl<'a, ThreadState> ResourceUsageExt<'a, ThreadState>
for crate::configure::RunWithThreadState<'a, ThreadState>
{
type Output = crate::configure::RunWithWrapperState<
'a,
ThreadState,
(),
ResourceUsageState,
ResourceUsageOutput,
>;
fn measure_resource_usage<F>(self, operation_name: &'a str, configure: F) -> Self::Output
where
F: FnOnce(ResourceUsageMeasureBuilder<'a>) -> ResourceUsageMeasureBuilder<'a>,
{
let builder = configure(ResourceUsageMeasureBuilder::new());
self.measure_wrapper(
create_resource_usage_state_factory(builder.clone(), operation_name),
move |state| state.into_output(&builder),
)
}
}
impl<'a, ThreadState, IterState> ResourceUsageExt<'a, ThreadState>
for crate::configure::RunWithIterState<'a, ThreadState, IterState>
{
type Output = crate::configure::RunWithWrapperState<
'a,
ThreadState,
IterState,
ResourceUsageState,
ResourceUsageOutput,
>;
fn measure_resource_usage<F>(self, operation_name: &'a str, configure: F) -> Self::Output
where
F: FnOnce(ResourceUsageMeasureBuilder<'a>) -> ResourceUsageMeasureBuilder<'a>,
{
let builder = configure(ResourceUsageMeasureBuilder::new());
self.measure_wrapper(
create_resource_usage_state_factory(builder.clone(), operation_name),
move |state| state.into_output(&builder),
)
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use std::sync::LazyLock;
use many_cpus::{ProcessorSet, SystemHardware};
use new_zealand::nz;
use super::ResourceUsageExt;
use crate::{Run, ThreadPool};
static TWO_PROCESSORS: LazyLock<Option<ProcessorSet>> = LazyLock::new(|| {
SystemHardware::current()
.processors()
.to_builder()
.take(nz!(2))
});
static FOUR_PROCESSORS: LazyLock<Option<ProcessorSet>> = LazyLock::new(|| {
SystemHardware::current()
.processors()
.to_builder()
.take(nz!(4))
});
#[test]
fn module_loads() {
let builder = super::ResourceUsageMeasureBuilder::new();
std::hint::black_box(builder);
}
#[test]
#[cfg(all(not(miri), feature = "alloc_tracker"))] fn measure_resource_usage_allocs_only() {
let allocs = alloc_tracker::Session::new().no_stdout().no_file();
let mut pool = ThreadPool::new(
SystemHardware::current()
.processors()
.to_builder()
.take(nz!(1))
.unwrap(),
);
let results = Run::new()
.measure_resource_usage("test_operation", |measure| measure.allocs(&allocs))
.iter(|_| {
let _data = [1, 2, 3, 4, 5].to_vec();
})
.execute_on(&mut pool, 10);
assert!(results.measure_outputs().count() > 0);
for output in results.measure_outputs() {
assert!(output.allocs().is_some());
}
let report = allocs.to_report();
assert!(!report.is_empty());
}
#[test]
#[cfg(all(not(miri), feature = "all_the_time"))] fn measure_resource_usage_processor_time_only() {
let processor_time = all_the_time::Session::new().no_stdout().no_file();
let mut pool = ThreadPool::new(
SystemHardware::current()
.processors()
.to_builder()
.take(nz!(1))
.unwrap(),
);
let results = Run::new()
.measure_resource_usage("test_operation", |measure| {
measure.processor_time(&processor_time)
})
.iter(|_| {
let mut sum = 0_u64;
for i in 0_u64..1000 {
sum = sum.wrapping_add(i.wrapping_mul(i));
}
std::hint::black_box(sum);
})
.execute_on(&mut pool, 10);
assert!(results.measure_outputs().count() > 0);
for output in results.measure_outputs() {
assert!(output.processor_time().is_some());
}
let report = processor_time.to_report();
assert!(!report.is_empty());
}
#[test]
#[cfg(all(not(miri), feature = "alloc_tracker", feature = "all_the_time"))] fn measure_resource_usage_combined() {
let allocs = alloc_tracker::Session::new().no_stdout().no_file();
let processor_time = all_the_time::Session::new().no_stdout().no_file();
let mut pool = ThreadPool::new(
SystemHardware::current()
.processors()
.to_builder()
.take(nz!(1))
.unwrap(),
);
let results = Run::new()
.measure_resource_usage("test_operation", |measure| {
measure.allocs(&allocs).processor_time(&processor_time)
})
.iter(|_| {
let _data = [1, 2, 3, 4, 5].to_vec();
let mut sum = 0_u64;
for i in 0_u64..100 {
sum = sum.wrapping_add(i);
}
std::hint::black_box(sum);
})
.execute_on(&mut pool, 5);
assert!(results.measure_outputs().count() > 0);
for output in results.measure_outputs() {
assert!(output.allocs().is_some());
assert!(output.processor_time().is_some());
}
let alloc_report = allocs.to_report();
assert!(!alloc_report.is_empty());
let time_report = processor_time.to_report();
assert!(!time_report.is_empty());
}
#[test]
#[cfg(all(not(miri), feature = "alloc_tracker", feature = "all_the_time"))] fn api_supports_groups_in_any_order() {
let Some(processors) = TWO_PROCESSORS.as_ref() else {
println!("Skipping test: not enough processors");
return;
};
let mut pool = ThreadPool::new(processors.clone());
let allocs = alloc_tracker::Session::new().no_stdout().no_file();
let processor_time = all_the_time::Session::new().no_stdout().no_file();
let results1 = Run::new()
.prepare_iter(|_| 42_i32)
.measure_resource_usage("test1", |measure| {
measure.allocs(&allocs).processor_time(&processor_time)
})
.groups(new_zealand::nz!(2))
.iter(|_| {
let _data = [1, 2, 3, 4, 5].to_vec();
})
.execute_on(&mut pool, 10);
assert!(results1.measure_outputs().count() > 0);
let results2 = Run::new()
.prepare_iter(|_| 42_i32)
.groups(new_zealand::nz!(2))
.measure_resource_usage("test2", |measure| {
measure.allocs(&allocs).processor_time(&processor_time)
})
.iter(|_| {
let _data = [1, 2, 3, 4, 5].to_vec();
})
.execute_on(&mut pool, 10);
assert!(results2.measure_outputs().count() > 0);
}
#[test]
fn api_supports_groups_immediately_after_new() {
let _run = Run::new().groups(new_zealand::nz!(2));
let _run = Run::new().groups(new_zealand::nz!(2)).iter(|_| {
std::hint::black_box(42);
});
}
#[test]
#[cfg(all(not(miri), feature = "alloc_tracker"))] fn api_supports_measure_resource_usage_after_groups_only() {
let Some(processors) = TWO_PROCESSORS.as_ref() else {
println!("Skipping test: not enough processors");
return;
};
let mut pool = ThreadPool::new(processors.clone());
let allocs = alloc_tracker::Session::new().no_stdout().no_file();
let results = Run::new()
.groups(new_zealand::nz!(2))
.measure_resource_usage("test", |measure| measure.allocs(&allocs))
.iter(|_| {
let _data = [1, 2, 3, 4, 5].to_vec();
})
.execute_on(&mut pool, 10);
assert!(results.measure_outputs().count() > 0);
}
#[test]
#[cfg(all(not(miri), feature = "alloc_tracker"))] fn api_supports_original_pattern_groups_prepare_iter_measure() {
let Some(processors) = TWO_PROCESSORS.as_ref() else {
println!("Skipping test: not enough processors");
return;
};
let mut pool = ThreadPool::new(processors.clone());
let allocs = alloc_tracker::Session::new().no_stdout().no_file();
let results = Run::new()
.groups(new_zealand::nz!(2))
.prepare_iter(|_| 42_i32)
.measure_resource_usage("test", |measure| measure.allocs(&allocs))
.iter(|_| {
let _data = [1, 2, 3, 4, 5].to_vec();
})
.execute_on(&mut pool, 10);
assert!(results.measure_outputs().count() > 0);
}
#[test]
#[cfg(all(not(miri), feature = "alloc_tracker"))] fn measure_resource_usage_with_thread_state() {
let allocs = alloc_tracker::Session::new().no_stdout().no_file();
let mut pool = ThreadPool::new(
SystemHardware::current()
.processors()
.to_builder()
.take(nz!(1))
.unwrap(),
);
let results = Run::new()
.prepare_thread(|_| String::from("thread_data"))
.measure_resource_usage("test_with_state", |measure| measure.allocs(&allocs))
.iter(|args| {
let _combined = format!("{}_allocated", args.thread_state());
})
.execute_on(&mut pool, 5);
assert!(results.measure_outputs().count() > 0);
for output in results.measure_outputs() {
assert!(output.allocs().is_some());
}
let report = allocs.to_report();
assert!(!report.is_empty());
}
#[test]
#[cfg(all(not(miri), feature = "alloc_tracker"))] fn create_resource_usage_state_factory_divides_iterations_correctly() {
let Some(processors) = FOUR_PROCESSORS.as_ref() else {
println!(
"Skipping test create_resource_usage_state_factory_divides_iterations_correctly: not enough processors"
);
return;
};
let allocs = alloc_tracker::Session::new().no_stdout().no_file();
let mut pool = ThreadPool::new(processors);
let iterations = 1000_u64;
#[expect(
clippy::integer_division,
reason = "testing the exact division behavior we want to verify"
)]
let expected_iterations_per_thread = iterations / pool.thread_count().get() as u64;
let results = Run::new()
.measure_resource_usage("iteration_division_test", |measure| measure.allocs(&allocs))
.iter(|_| {
std::hint::black_box(42);
})
.execute_on(&mut pool, iterations);
assert!(results.measure_outputs().count() > 0);
for output in results.measure_outputs() {
assert!(output.allocs().is_some());
}
let report = allocs.to_report();
assert!(!report.is_empty());
let operations: Vec<_> = report.operations().collect();
let (_operation_name, operation_stats) = operations
.iter()
.find(|(name, _)| *name == "iteration_division_test")
.unwrap();
let total_tracked_iterations = operation_stats.total_iterations();
let expected_total_tracked_iterations =
pool.thread_count().get() as u64 * expected_iterations_per_thread;
assert_eq!(
total_tracked_iterations, expected_total_tracked_iterations,
"The create_resource_usage_state_factory should divide iterations per thread \
to avoid inflating the iteration count. Expected {expected_total_tracked_iterations} iterations tracked, got {total_tracked_iterations}"
);
let mean_bytes = operation_stats.mean();
assert_eq!(
mean_bytes, 0,
"Expected 0 mean bytes since no allocations occurred, got {mean_bytes}"
);
}
}