use crate::connection::Throughput;
use crate::estimate::{ChangeEstimates, Estimates};
use crate::report::{BenchmarkId, ComparisonData, MeasurementData};
use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use linked_hash_map::LinkedHashMap;
use std::collections::HashSet;
use std::ffi::OsStr;
use std::fs::File;
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
#[derive(Debug)]
pub struct Benchmark {
pub latest_stats: SavedStatistics,
pub previous_stats: Option<SavedStatistics>,
pub target: Option<String>,
}
impl Benchmark {
fn new(stats: SavedStatistics) -> Self {
Benchmark {
latest_stats: stats,
previous_stats: None,
target: None,
}
}
fn add_stats(&mut self, stats: SavedStatistics) {
let previous_stats = std::mem::replace(&mut self.latest_stats, stats);
self.previous_stats = Some(previous_stats);
}
}
#[derive(Debug)]
pub struct BenchmarkGroup {
pub benchmarks: LinkedHashMap<BenchmarkId, Benchmark>,
pub target: Option<String>,
}
impl Default for BenchmarkGroup {
fn default() -> Self {
BenchmarkGroup {
benchmarks: LinkedHashMap::new(),
target: None,
}
}
}
#[derive(Debug)]
pub struct Model {
data_directory: PathBuf,
all_titles: HashSet<String>,
all_directories: HashSet<PathBuf>,
pub groups: LinkedHashMap<String, BenchmarkGroup>,
history_id: Option<String>,
history_description: Option<String>,
}
impl Model {
pub fn load(
criterion_home: PathBuf,
timeline: PathBuf,
history_id: Option<String>,
history_description: Option<String>,
) -> Model {
let mut model = Model {
data_directory: path!(criterion_home, "data", timeline),
all_titles: HashSet::new(),
all_directories: HashSet::new(),
groups: LinkedHashMap::new(),
history_id,
history_description,
};
for entry in WalkDir::new(&model.data_directory)
.into_iter()
.filter_map(::std::result::Result::ok)
.filter(|entry| entry.file_name() == OsStr::new("benchmark.cbor"))
{
if let Err(e) = model.load_stored_benchmark(entry.path()) {
error!("Encountered error while loading stored data: {}", e)
}
}
model
}
fn load_stored_benchmark(&mut self, benchmark_path: &Path) -> Result<()> {
if !benchmark_path.is_file() {
return Ok(());
}
let mut benchmark_file = File::open(&benchmark_path)
.with_context(|| format!("Failed to open benchmark file {:?}", benchmark_path))?;
let benchmark_record: BenchmarkRecord = serde_cbor::from_reader(&mut benchmark_file)
.with_context(|| format!("Failed to read benchmark file {:?}", benchmark_path))?;
let measurement_path = benchmark_path.with_file_name(benchmark_record.latest_record);
if !measurement_path.is_file() {
return Ok(());
}
let mut measurement_file = File::open(&measurement_path)
.with_context(|| format!("Failed to open measurement file {:?}", measurement_path))?;
let saved_stats: SavedStatistics = serde_cbor::from_reader(&mut measurement_file)
.with_context(|| format!("Failed to read measurement file {:?}", measurement_path))?;
self.groups
.entry(benchmark_record.id.group_id.clone())
.or_insert_with(Default::default)
.benchmarks
.insert(benchmark_record.id.into(), Benchmark::new(saved_stats));
Ok(())
}
pub fn add_benchmark_id(&mut self, target: &str, id: &mut BenchmarkId) {
id.ensure_directory_name_unique(&self.all_directories);
self.all_directories
.insert(id.as_directory_name().to_owned());
id.ensure_title_unique(&self.all_titles);
self.all_titles.insert(id.as_title().to_owned());
let group = self
.groups
.entry(id.group_id.clone())
.or_insert_with(Default::default);
if let Some(mut benchmark) = group.benchmarks.remove(id) {
if let Some(target) = &benchmark.target {
warn!("Benchmark ID {} encountered multiple times. Benchmark IDs must be unique. First seen in the benchmark target '{}'", id.as_title(), target);
} else {
benchmark.target = Some(target.to_owned());
}
group.benchmarks.insert(id.clone(), benchmark);
}
}
pub fn benchmark_complete(
&mut self,
id: &BenchmarkId,
analysis_results: &MeasurementData,
) -> Result<()> {
let dir = path!(&self.data_directory, id.as_directory_name());
std::fs::create_dir_all(&dir)
.with_context(|| format!("Failed to create directory {:?}", dir))?;
let measurement_name = chrono::Local::now()
.format("measurement_%y%m%d%H%M%S.cbor")
.to_string();
let saved_stats = SavedStatistics {
datetime: chrono::Utc::now(),
iterations: analysis_results.iter_counts().to_vec(),
values: analysis_results.sample_times().to_vec(),
avg_values: analysis_results.avg_times.to_vec(),
estimates: analysis_results.absolute_estimates.clone(),
throughput: analysis_results.throughput.clone(),
changes: analysis_results
.comparison
.as_ref()
.map(|c| c.relative_estimates.clone()),
change_direction: analysis_results
.comparison
.as_ref()
.map(get_change_direction),
history_id: self.history_id.clone(),
history_description: self.history_description.clone(),
};
let measurement_path = dir.join(&measurement_name);
let mut measurement_file = File::create(&measurement_path)
.with_context(|| format!("Failed to create measurement file {:?}", measurement_path))?;
serde_cbor::to_writer(&mut measurement_file, &saved_stats).with_context(|| {
format!("Failed to save measurements to file {:?}", measurement_path)
})?;
let record = BenchmarkRecord {
id: id.into(),
latest_record: PathBuf::from(&measurement_name),
};
let benchmark_path = dir.join("benchmark.cbor");
let mut benchmark_file = File::create(&benchmark_path)
.with_context(|| format!("Failed to create benchmark file {:?}", benchmark_path))?;
serde_cbor::to_writer(&mut benchmark_file, &record)
.with_context(|| format!("Failed to save benchmark file {:?}", benchmark_path))?;
let benchmark_entry = self
.groups
.get_mut(&id.group_id)
.unwrap()
.benchmarks
.entry(id.clone());
match benchmark_entry {
vacant @ linked_hash_map::Entry::Vacant(_) => {
vacant.or_insert(Benchmark::new(saved_stats));
}
linked_hash_map::Entry::Occupied(mut occupied) => {
occupied.get_mut().add_stats(saved_stats)
}
};
Ok(())
}
pub fn get_last_sample(&self, id: &BenchmarkId) -> Option<&SavedStatistics> {
self.groups
.get(&id.group_id)
.and_then(|g| g.benchmarks.get(id))
.map(|b| &b.latest_stats)
}
pub fn check_benchmark_group(&self, current_target: &str, group: &str) {
if let Some(benchmark_group) = self.groups.get(group) {
if let Some(target) = &benchmark_group.target {
if target != current_target {
warn!("Benchmark group {} encountered again. Benchmark group IDs must be unique. First seen in the benchmark target '{}'", group, target);
}
}
}
}
pub fn add_benchmark_group(&mut self, target: &str, group_name: &str) -> &BenchmarkGroup {
let mut group = self.groups.remove(group_name).unwrap_or_default();
group.target = Some(target.to_owned());
self.groups.insert(group_name.to_owned(), group);
self.groups.get(group_name).unwrap()
}
pub fn load_history(&self, id: &BenchmarkId) -> Result<Vec<SavedStatistics>> {
let dir = path!(&self.data_directory, id.as_directory_name());
fn load_from(measurement_path: &Path) -> Result<SavedStatistics> {
let mut measurement_file = File::open(&measurement_path).with_context(|| {
format!("Failed to open measurement file {:?}", measurement_path)
})?;
serde_cbor::from_reader(&mut measurement_file)
.with_context(|| format!("Failed to read measurement file {:?}", measurement_path))
}
let mut stats = Vec::new();
for entry in WalkDir::new(dir)
.max_depth(1)
.into_iter()
.filter_map(::std::result::Result::ok)
{
let name_str = entry.file_name().to_string_lossy();
if name_str.starts_with("measurement_") && name_str.ends_with(".cbor") {
match load_from(entry.path()) {
Ok(saved_stats) => stats.push(saved_stats),
Err(e) => error!(
"Unexpected error loading benchmark history from file {}: {:?}",
entry.path().display(),
e
),
}
}
}
stats.sort_unstable_by_key(|st| st.datetime);
Ok(stats)
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct SavedBenchmarkId {
group_id: String,
function_id: Option<String>,
value_str: Option<String>,
throughput: Option<Throughput>,
}
impl From<BenchmarkId> for SavedBenchmarkId {
fn from(other: BenchmarkId) -> Self {
SavedBenchmarkId {
group_id: other.group_id,
function_id: other.function_id,
value_str: other.value_str,
throughput: other.throughput,
}
}
}
impl From<&BenchmarkId> for SavedBenchmarkId {
fn from(other: &BenchmarkId) -> Self {
other.clone().into()
}
}
impl From<SavedBenchmarkId> for BenchmarkId {
fn from(other: SavedBenchmarkId) -> Self {
BenchmarkId::new(
other.group_id,
other.function_id,
other.value_str,
other.throughput,
)
}
}
impl From<&SavedBenchmarkId> for BenchmarkId {
fn from(other: &SavedBenchmarkId) -> Self {
other.into()
}
}
#[derive(Debug, Serialize, Deserialize)]
struct BenchmarkRecord {
id: SavedBenchmarkId,
latest_record: PathBuf,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum ChangeDirection {
NoChange,
NotSignificant,
Improved,
Regressed,
}
fn get_change_direction(comp: &ComparisonData) -> ChangeDirection {
if comp.p_value < comp.significance_threshold {
return ChangeDirection::NoChange;
}
let ci = &comp.relative_estimates.mean.confidence_interval;
let lb = ci.lower_bound;
let ub = ci.upper_bound;
let noise = comp.noise_threshold;
if lb < -noise && ub < -noise {
ChangeDirection::Improved
} else if lb > noise && ub > noise {
ChangeDirection::Regressed
} else {
ChangeDirection::NotSignificant
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SavedStatistics {
pub datetime: DateTime<Utc>,
pub iterations: Vec<f64>,
pub values: Vec<f64>,
pub avg_values: Vec<f64>,
pub estimates: Estimates,
pub throughput: Option<Throughput>,
pub changes: Option<ChangeEstimates>,
pub change_direction: Option<ChangeDirection>,
pub history_id: Option<String>,
pub history_description: Option<String>,
}