From 43b346ef8ab91dc13b773dda871433cf98adade8 Mon Sep 17 00:00:00 2001 From: Aria Shrimpton Date: Sat, 20 Jan 2024 00:15:57 +0000 Subject: feat(cli): command for breakdown of cost estimate --- src/crates/candelabra/src/profiler.rs | 47 ++++++++++++++++++++++- src/crates/cli/src/estimate.rs | 70 +++++++++++++++++++++++++++++++++++ src/crates/cli/src/main.rs | 3 ++ 3 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 src/crates/cli/src/estimate.rs diff --git a/src/crates/candelabra/src/profiler.rs b/src/crates/candelabra/src/profiler.rs index 3897d97..b6e010c 100644 --- a/src/crates/candelabra/src/profiler.rs +++ b/src/crates/candelabra/src/profiler.rs @@ -17,7 +17,7 @@ use tempfile::tempdir; use crate::cache::{gen_tree_hash, FileCache}; use crate::candidates::ConTypeName; -use crate::cost::CostModel; +use crate::cost::{Cost, CostModel}; use crate::project::Project; use crate::{Paths, State}; @@ -32,6 +32,20 @@ pub(crate) struct CacheEntry { #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct ProfilerInfo(pub Vec); +/// Breakdown of a cost value +#[derive(Clone, Debug)] +pub struct CostBreakdown { + pub contains: Cost, + pub insert: Cost, + pub clear: Cost, + pub remove: Cost, + pub first: Cost, + pub last: Cost, + pub nth: Cost, + pub push: Cost, + pub pop: Cost, +} + /// Profiler info collected from the lifetime of a single collection instance #[derive(Clone, Debug, Serialize, Deserialize)] pub struct CollectionLifetime { @@ -47,12 +61,43 @@ pub struct CollectionLifetime { pub pop: usize, } +macro_rules! agg_op_cost { + ($this:ident, $cost_model:ident, $op:ident) => { + $this + .0 + .iter() + .map(|cl| { + $cost_model + .by_op + .get(stringify!($op)) + .unwrap() + .estimatef(cl.avg_n()) + * cl.$op as f64 + }) + .sum() + }; +} + impl ProfilerInfo { pub fn estimate_cost(&self, cost_model: &CostModel) -> f64 { let sum: f64 = self.0.iter().map(|cl| cl.estimate_cost(cost_model)).sum(); sum / self.0.len() as f64 } + + pub fn cost_breakdown(&self, cost_model: &CostModel) -> CostBreakdown { + CostBreakdown { + contains: agg_op_cost!(self, cost_model, contains), + insert: agg_op_cost!(self, cost_model, insert), + clear: agg_op_cost!(self, cost_model, clear), + remove: agg_op_cost!(self, cost_model, remove), + first: agg_op_cost!(self, cost_model, first), + last: agg_op_cost!(self, cost_model, last), + nth: agg_op_cost!(self, cost_model, nth), + push: agg_op_cost!(self, cost_model, push), + pop: agg_op_cost!(self, cost_model, pop), + } + } } impl CollectionLifetime { diff --git a/src/crates/cli/src/estimate.rs b/src/crates/cli/src/estimate.rs new file mode 100644 index 0000000..27948b8 --- /dev/null +++ b/src/crates/cli/src/estimate.rs @@ -0,0 +1,70 @@ +use std::collections::HashMap; + +use anyhow::{anyhow, bail, Result}; +use argh::FromArgs; +use log::info; +use tabled::{builder::Builder, settings::Style}; + +use crate::State; + +/// Estimate the cost of a given set of assignments for a specific project, and detail how that was reached. +#[derive(FromArgs)] +#[argh(subcommand, name = "estimate")] +pub struct Args { + /// the assignments, written ... + #[argh(positional, greedy)] + assignments: Vec, +} + +impl State { + pub fn cmd_estimate(&self, args: Args) -> Result<()> { + if self.projects.len() > 1 { + bail!("estimate must target only one project!") + } + let proj = &self.projects[0]; + + // parse assignment list + if args.assignments.len() % 2 != 0 { + bail!("assignments list must have even length"); + } + let assignments = args + .assignments + .iter() + .step_by(2) + .zip(args.assignments.iter().skip(1)) + .collect::>(); + + // get breakdown by operation for each assignment + info!("Contribution to cost by operation:"); + + let profile_info = self.inner.profiler_info(proj)?; + let mut builder = Builder::default(); + builder.set_header([ + "con type", "contains", "insert", "clear", "remove", "first", "last", "nth", "push", + "pop", + ]); + for (con_type_name, impl_name) in assignments.iter() { + let profiler = profile_info + .get(*con_type_name) + .ok_or_else(|| anyhow!("no profiling info for {} - wrong name?", con_type_name))?; + let cost_model = self.inner.cost_model(&impl_name)?; + let breakdown = profiler.cost_breakdown(&cost_model); + builder.push_record([ + *impl_name, + &breakdown.contains.to_string(), + &breakdown.insert.to_string(), + &breakdown.clear.to_string(), + &breakdown.remove.to_string(), + &breakdown.first.to_string(), + &breakdown.last.to_string(), + &breakdown.nth.to_string(), + &breakdown.push.to_string(), + &breakdown.pop.to_string(), + ]); + } + + println!("{}", builder.build().with(Style::sharp())); + + Ok(()) + } +} diff --git a/src/crates/cli/src/main.rs b/src/crates/cli/src/main.rs index 1b926a7..a63f5d5 100644 --- a/src/crates/cli/src/main.rs +++ b/src/crates/cli/src/main.rs @@ -4,6 +4,7 @@ use candelabra::{Paths, Project}; use log::info; mod candidates; +mod estimate; mod library; mod model; mod profile; @@ -32,6 +33,7 @@ pub enum Subcommand { Candidates(candidates::Args), Profile(profile::Args), Select(select::Args), + Estimate(estimate::Args), } fn main() -> Result<()> { @@ -52,6 +54,7 @@ fn main() -> Result<()> { Subcommand::Candidates(a) => state.cmd_candidates(a), Subcommand::Profile(a) => state.cmd_profile(a), Subcommand::Select(a) => state.cmd_select(a), + Subcommand::Estimate(a) => state.cmd_estimate(a), } } -- cgit v1.2.3