#![allow(clippy::cast_precision_loss)]
use std::time::Duration;
#[cfg(not(target_arch = "wasm32"))]
use std::time::Instant;
use bytesize::ByteSize;
use humantime::format_duration;
use log::{debug, error, info};
use serde_derive::Serialize;
use sysinfo::{Pid, ProcessRefreshKind, ProcessesToUpdate, System};
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
#[cfg(target_arch = "wasm32")]
use web_sys::window;
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
const REFRESH_INTERVAL: Duration = Duration::from_secs(1);
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub fn get_high_res_time() -> f64 {
let perf = window().unwrap().performance().unwrap();
perf.now() }
#[derive(Serialize)]
pub struct ExecutionStatistics {
pub max_memory_usage: u64,
pub cpu_time: Duration,
pub wall_time: Duration,
pub population: usize,
pub cpu_time_per_person: Duration,
pub wall_time_per_person: Duration,
pub memory_per_person: u64,
}
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
pub(crate) struct ExecutionProfilingCollector {
#[cfg(not(target_arch = "wasm32"))]
start_time: Instant,
#[cfg(target_arch = "wasm32")]
start_time: f64,
#[cfg(not(target_arch = "wasm32"))]
last_refresh: Instant,
#[cfg(target_arch = "wasm32")]
last_refresh: f64,
start_cpu_time: u64,
max_memory_usage: u64,
system: System,
process_id: Option<Pid>,
}
impl ExecutionProfilingCollector {
pub fn new() -> ExecutionProfilingCollector {
let process_id = sysinfo::get_current_pid().ok();
#[cfg(target_arch = "wasm32")]
let now = get_high_res_time();
#[cfg(not(target_arch = "wasm32"))]
let now = Instant::now();
let mut new_stats = ExecutionProfilingCollector {
start_time: now,
last_refresh: now,
start_cpu_time: 0,
max_memory_usage: 0,
system: System::new(),
process_id,
};
if let Some(process_id) = process_id {
debug!("Process ID: {}", process_id);
let process_refresh_kind = ProcessRefreshKind::nothing().with_cpu().with_memory();
new_stats.update_system_info(process_refresh_kind);
let process = new_stats.system.process(process_id).unwrap();
new_stats.max_memory_usage = process.memory();
new_stats.start_cpu_time = process.accumulated_cpu_time();
}
new_stats
}
#[inline]
pub fn refresh(&mut self) {
#[cfg(not(target_arch = "wasm32"))]
if self.last_refresh.elapsed() >= REFRESH_INTERVAL {
self.poll_memory();
self.last_refresh = Instant::now();
}
}
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
fn poll_memory(&mut self) {
if let Some(pid) = self.process_id {
self.update_system_info(ProcessRefreshKind::nothing().with_memory());
let process = self.system.process(pid).unwrap();
self.max_memory_usage = self.max_memory_usage.max(process.memory());
}
}
#[allow(unused)]
pub fn cpu_time(&mut self) -> u64 {
if let Some(process_id) = self.process_id {
self.update_system_info(ProcessRefreshKind::nothing().with_cpu());
let process = self.system.process(process_id).unwrap();
process.accumulated_cpu_time() - self.start_cpu_time
} else {
0
}
}
#[inline]
fn update_system_info(&mut self, process_refresh_kind: ProcessRefreshKind) {
if let Some(pid) = self.process_id {
if self.system.refresh_processes_specifics(
ProcessesToUpdate::Some(&[pid]),
true,
process_refresh_kind,
) < 1
{
error!("could not refresh process statistics");
}
}
}
pub fn compute_final_statistics(&mut self, population: usize) -> ExecutionStatistics {
let mut cpu_time_millis = 0;
if let Some(pid) = self.process_id {
self.update_system_info(ProcessRefreshKind::nothing().with_cpu().with_memory());
let process = self.system.process(pid).unwrap();
self.max_memory_usage = self.max_memory_usage.max(process.memory());
cpu_time_millis = process.accumulated_cpu_time() - self.start_cpu_time;
}
let cpu_time = Duration::from_millis(cpu_time_millis);
#[cfg(target_arch = "wasm32")]
let wall_time = get_high_res_time() - self.start_time;
#[cfg(not(target_arch = "wasm32"))]
let wall_time = self.start_time.elapsed();
let cpu_time_per_person = if population > 0 {
Duration::from_secs_f64(cpu_time_millis as f64 / population as f64 / 1000.0)
} else {
Duration::new(0, 0)
};
let wall_time_per_person = if population > 0 {
#[cfg(not(target_arch = "wasm32"))]
let wall_time = wall_time.as_secs_f64();
#[cfg(target_arch = "wasm32")]
let wall_time = wall_time / 1000.0;
Duration::from_secs_f64(wall_time / population as f64)
} else {
Duration::new(0, 0)
};
let memory_per_person = if population > 0 {
self.max_memory_usage / population as u64
} else {
0
};
#[cfg(target_arch = "wasm32")]
let wall_time = Duration::from_millis(wall_time as u64);
ExecutionStatistics {
max_memory_usage: self.max_memory_usage,
cpu_time,
wall_time,
population,
cpu_time_per_person,
wall_time_per_person,
memory_per_person,
}
}
}
pub fn print_execution_statistics(summary: &ExecutionStatistics) {
println!("━━━━ Execution Summary ━━━━");
if summary.max_memory_usage == 0 {
println!("Memory and CPU statistics are not available on your platform.");
} else {
println!(
"{:<25}{}",
"Max memory usage:",
ByteSize::b(summary.max_memory_usage)
);
println!("{:<25}{}", "CPU time:", format_duration(summary.cpu_time));
}
println!("{:<25}{}", "Wall time:", format_duration(summary.wall_time));
if summary.population > 0 {
println!("{:<25}{}", "Population:", summary.population);
if summary.max_memory_usage > 0 {
println!(
"{:<25}{}",
"Memory per person:",
ByteSize::b(summary.memory_per_person)
);
println!(
"{:<25}{}",
"CPU time per person:",
format_duration(summary.cpu_time_per_person)
);
}
println!(
"{:<25}{}",
"Wall time per person:",
format_duration(summary.wall_time_per_person)
);
}
}
pub fn log_execution_statistics(stats: &ExecutionStatistics) {
info!("Execution complete.");
if stats.max_memory_usage == 0 {
info!("Memory and CPU statistics are not available on your platform.");
} else {
info!("Max memory usage: {}", ByteSize::b(stats.max_memory_usage));
info!("CPU time: {}", format_duration(stats.cpu_time));
}
info!("Wall time: {}", format_duration(stats.wall_time));
if stats.population > 0 {
info!("Population: {}", stats.population);
if stats.max_memory_usage > 0 {
info!(
"Memory per person: {}",
ByteSize::b(stats.memory_per_person)
);
info!(
"CPU time per person: {}",
format_duration(stats.cpu_time_per_person)
);
}
info!(
"Wall time per person: {}",
format_duration(stats.wall_time_per_person)
);
}
}
#[cfg(test)]
mod tests {
use std::thread;
use std::time::Duration;
use super::*;
#[test]
fn test_collector_initialization() {
let collector = ExecutionProfilingCollector::new();
assert!(collector.max_memory_usage > 0);
}
#[test]
fn test_refresh_respects_interval() {
let mut collector = ExecutionProfilingCollector::new();
let before = collector.max_memory_usage;
collector.refresh();
let after = collector.max_memory_usage;
assert_eq!(before, after);
thread::sleep(Duration::from_secs(2));
collector.refresh();
assert!(collector.max_memory_usage >= before);
}
#[test]
fn test_compute_final_statistics_structure() {
let mut collector = ExecutionProfilingCollector::new();
thread::sleep(Duration::from_millis(100));
let stats = collector.compute_final_statistics(10);
assert!(stats.max_memory_usage > 0);
assert!(stats.wall_time > Duration::ZERO);
assert_eq!(stats.population, 10);
}
#[test]
fn test_zero_population_results() {
let mut collector = ExecutionProfilingCollector::new();
let stats = collector.compute_final_statistics(0);
assert_eq!(stats.population, 0);
assert_eq!(stats.cpu_time_per_person, Duration::ZERO);
assert_eq!(stats.wall_time_per_person, Duration::ZERO);
assert_eq!(stats.memory_per_person, 0);
}
#[test]
fn test_cpu_time_increases_over_time() {
let mut collector = ExecutionProfilingCollector::new();
let start = Instant::now();
while start.elapsed().as_millis() < 30u128 {
std::hint::black_box(0); }
let cpu_time_1 = collector.cpu_time();
let start = Instant::now();
while start.elapsed().as_millis() < 50u128 {
std::hint::black_box(0); }
let cpu_time_2 = collector.cpu_time();
assert!(cpu_time_2 > cpu_time_1);
}
}