diff --git a/README.md b/README.md index beaf79c..125daf6 100644 --- a/README.md +++ b/README.md @@ -179,14 +179,18 @@ Runs a single observation. ### Stats -`cardamon stats [scenario_name]` +`cardamon stats [-o text | json] [scenario_name]` Shows the stats for previous runs of scenarios. +Output formats: + - text: Ascii table (default) + - json: Json format **_Options_** - **\*scenario_name**: An optional argument for the scenario you want to show stats for\* - **\*previous_runs**: The number of previous runs to show\* +- **\*output: The output format ### Ui diff --git a/src/carbon_intensity.rs b/src/carbon_intensity.rs index 4a7716d..80a3318 100644 --- a/src/carbon_intensity.rs +++ b/src/carbon_intensity.rs @@ -115,7 +115,7 @@ pub async fn fetch_ci(code: &str, date: &DateTime) -> anyhow::Result { #[cfg(test)] mod tests { - use chrono::NaiveDate; + use chrono::{Datelike, NaiveDate}; use super::*; diff --git a/src/lib.rs b/src/lib.rs index ead3bd3..e9e9703 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,7 @@ pub mod metrics_logger; pub mod migrations; pub mod models; pub mod server; +pub mod stats; use crate::{ config::{Config, ExecutionMode}, diff --git a/src/main.rs b/src/main.rs index 44346d9..69130ae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,13 +3,13 @@ use cardamon::{ carbon_intensity::{fetch_ci, fetch_region_code, valid_region_code, GLOBAL_CI}, cleanup_stdout_stderr, config::{self, Config, ExecutionPlan, ProcessToObserve}, - data::{dataset::LiveDataFilter, dataset_builder::DatasetBuilder, Data}, + data::{dataset::LiveDataFilter, Data}, db_connect, db_migrate, init_config, models::rab_model, - run, server, + run, server, stats::{stats_output_json, stats_output_text}, }; -use chrono::{TimeZone, Utc}; -use clap::{Parser, Subcommand}; +use chrono::Utc; +use clap::{arg, Parser, Subcommand, ValueEnum}; use colored::Colorize; use dotenvy::dotenv; use itertools::Itertools; @@ -28,6 +28,15 @@ pub struct Cli { pub command: Commands, } +#[derive(Clone, Debug, ValueEnum)] +pub enum StatsOutputFormat { + #[value(alias("text"))] + Text, + + #[value(alias("json"))] + Json, +} + #[derive(Subcommand, Debug)] pub enum Commands { #[command(about = "Runs a single observation")] @@ -61,6 +70,8 @@ pub enum Commands { #[arg(value_name = "NUMBER OF PREVIOUS", short = 'n')] previous_runs: Option, + #[arg(value_enum, default_value_t=StatsOutputFormat::Text, short ='o', long = "output")] + output: StatsOutputFormat, }, #[command(about = "Start the Cardamon UI server")] @@ -303,75 +314,15 @@ async fn main() -> anyhow::Result<()> { Commands::Stats { scenario_name, previous_runs, + output, } => { - // build dataset - let dataset_builder = DatasetBuilder::new(); - let dataset_rows = match scenario_name { - Some(scenario_name) => dataset_builder.scenario(&scenario_name).all(), - None => dataset_builder.scenarios_all().all(), - }; - let dataset_cols = match previous_runs { - Some(n) => dataset_rows.last_n_runs(n).all(), - None => dataset_rows.runs_all().all(), - }; - let dataset = dataset_cols.build(&db_conn).await?; - - println!("\n{}", " Cardamon Stats \n".reversed().green()); - if dataset.is_empty() { - println!("\nno data found!"); - } - - for scenario_dataset in dataset.by_scenario(LiveDataFilter::IncludeLive) { - println!( - "Scenario {}:", - scenario_dataset.scenario_name().to_string().green() - ); - - let mut table = Table::builder() - .rows(rows![row![ - TableCell::builder("Datetime (Utc)".bold()).build(), - TableCell::builder("Region".bold()).build(), - TableCell::builder("Duration (s)".bold()).build(), - TableCell::builder("Power (Wh)".bold()).build(), - TableCell::builder("CI (gWh)".bold()).build(), - TableCell::builder("CO2 (g)".bold()).build() - ]]) - .style(TableStyle::rounded()) - .build(); - - // let mut points: Vec<(f32, f32)> = vec![]; - // let mut run = 0.0; - for run_dataset in scenario_dataset.by_run() { - let run_data = run_dataset.apply_model(&db_conn, &rab_model).await?; - let run_region = run_data.region; - let run_ci = run_data.ci; - let run_start_time = Utc.timestamp_opt(run_data.start_time / 1000, 0).unwrap(); - let run_duration = (run_data.stop_time - run_data.start_time) as f64 / 1000.0; - let _per_min_factor = 60.0 / run_duration; - - table.add_row(row![ - TableCell::new(run_start_time.format("%d/%m/%y %H:%M")), - TableCell::new(run_region.unwrap_or_default()), - TableCell::new(format!("{:.3}s", run_duration)), - TableCell::new(format!("{:.4}Wh", run_data.data.pow)), - TableCell::new(format!("{:.4}gWh", run_ci)), - TableCell::new(format!("{:.4}g", run_data.data.co2)), - ]); - // points.push((run, run_data.data.pow as f32)); - // run += 1.0; + match output { + StatsOutputFormat::Text => { + stats_output_text(scenario_name, previous_runs, &db_conn).await?; + } + StatsOutputFormat::Json => { + stats_output_json(scenario_name, previous_runs, &db_conn).await?; } - println!("{}", table.render()); - - // let x_max = points.len() as f32; - // let y_data = points.iter().map(|(_, y)| *y); - // let y_min = y_data.clone().reduce(f32::min).unwrap_or(0.0); - // let y_max = y_data.clone().reduce(f32::max).unwrap_or(0.0); - // - // Chart::new_with_y_range(128, 64, 0.0, x_max, y_min, y_max) - // .x_axis_style(textplots::LineStyle::Solid) - // .y_tick_display(TickDisplay::Sparse) - // .lineplot(&Shape::Lines(&points)) - // .nice(); } } diff --git a/src/stats.rs b/src/stats.rs new file mode 100644 index 0000000..4cafc64 --- /dev/null +++ b/src/stats.rs @@ -0,0 +1,147 @@ +use chrono::{TimeZone, Utc}; +use colored::Colorize; +use sea_orm::DatabaseConnection; +use serde::Serialize; +use term_table::table_cell::TableCell; +use term_table::{row, rows, Table, TableStyle}; +use term_table::row::Row; + +use crate::data::{dataset::LiveDataFilter, dataset_builder::DatasetBuilder}; +use crate::models::rab_model; + + +pub async fn stats_output_text(scenario_name: Option,previous_runs: Option, db_conn: &DatabaseConnection)->anyhow::Result<()> { + // build dataset + let dataset_builder = DatasetBuilder::new(); + let dataset_rows = match scenario_name { + Some(scenario_name) => dataset_builder.scenario(&scenario_name).all(), + None => dataset_builder.scenarios_all().all(), + }; + let dataset_cols = match previous_runs { + Some(n) => dataset_rows.last_n_runs(n).all(), + None => dataset_rows.runs_all().all(), + }; + let dataset = dataset_cols.build(&db_conn).await?; + + println!("\n{}", " Cardamon Stats \n".reversed().green()); + if dataset.is_empty() { + println!("\nno data found!"); + } + + for scenario_dataset in dataset.by_scenario(LiveDataFilter::IncludeLive) { + println!( + "Scenario {}:", + scenario_dataset.scenario_name().to_string().green() + ); + + let mut table = Table::builder() + .rows(rows![row![ + TableCell::builder("Datetime (Utc)".bold()).build(), + TableCell::builder("Region".bold()).build(), + TableCell::builder("Duration (s)".bold()).build(), + TableCell::builder("Power (Wh)".bold()).build(), + TableCell::builder("CI (gWh)".bold()).build(), + TableCell::builder("CO2 (g)".bold()).build() + ]]) + .style(TableStyle::rounded()) + .build(); + + for run_dataset in scenario_dataset.by_run() { + let run_data = run_dataset.apply_model(&db_conn, &rab_model).await?; + let run_region = run_data.region; + let run_ci = run_data.ci; + let run_start_time = Utc.timestamp_opt(run_data.start_time / 1000, 0).unwrap(); + let run_duration = (run_data.stop_time - run_data.start_time) as f64 / 1000.0; + let _per_min_factor = 60.0 / run_duration; + + table.add_row(row![ + TableCell::new(run_start_time.format("%d/%m/%y %H:%M")), + TableCell::new(run_region.unwrap_or_default()), + TableCell::new(format!("{:.3}s", run_duration)), + TableCell::new(format!("{:.4}Wh", run_data.data.pow)), + TableCell::new(format!("{:.4}gWh", run_ci)), + TableCell::new(format!("{:.4}g", run_data.data.co2)), + ]); + // points.push((run, run_data.data.pow as f32)); + // run += 1.0; + } + println!("{}", table.render()); + + // let x_max = points.len() as f32; + // let y_data = points.iter().map(|(_, y)| *y); + // let y_min = y_data.clone().reduce(f32::min).unwrap_or(0.0); + // let y_max = y_data.clone().reduce(f32::max).unwrap_or(0.0); + // + // Chart::new_with_y_range(128, 64, 0.0, x_max, y_min, y_max) + // .x_axis_style(textplots::LineStyle::Solid) + // .y_tick_display(TickDisplay::Sparse) + // .lineplot(&Shape::Lines(&points)) + // .nice(); + } + Ok(()) + +} + +#[derive(Debug, Clone, Serialize)] +struct ScenarioOutput { + name: String, + region: String, + duration: f64, + power: f64, + ci: f64, + co2: f64, +} + +#[derive(Debug, Clone, Serialize)] +struct RunOutput { + name: String, + outputs: Vec, +} + +pub async fn stats_output_json(scenario_name: Option,previous_runs: Option, db_conn: &DatabaseConnection)->anyhow::Result<()> { + // build dataset + let dataset_builder = DatasetBuilder::new(); + let dataset_rows = match scenario_name { + Some(scenario_name) => dataset_builder.scenario(&scenario_name).all(), + None => dataset_builder.scenarios_all().all(), + }; + let dataset_cols = match previous_runs { + Some(n) => dataset_rows.last_n_runs(n).all(), + None => dataset_rows.runs_all().all(), + }; + let dataset = dataset_cols.build(&db_conn).await?; + let mut run_outputs: Vec = vec![]; + for scenario_dataset in dataset.by_scenario(LiveDataFilter::IncludeLive) { + let mut scenario_outputs: Vec = vec![]; + let scenario_name = scenario_dataset.scenario_name().to_string(); + for run_dataset in scenario_dataset.by_run() { + let run_data = run_dataset.apply_model(&db_conn, &rab_model).await?; + let run_region = run_data.region; + let run_ci = run_data.ci; + let _run_start_time = Utc.timestamp_opt(run_data.start_time / 1000, 0).unwrap(); + let run_duration = (run_data.stop_time - run_data.start_time) as f64 / 1000.0; + let _per_min_factor = 60.0 / run_duration; + let stats_output = ScenarioOutput { + name: scenario_name.clone(), + region: run_region.unwrap_or_default(), + duration: run_duration, + power: run_data.data.pow, + ci: run_ci, + co2: run_data.data.co2, + }; + scenario_outputs.push(stats_output); + } + let scenario_output = RunOutput { + name: scenario_name, + outputs: scenario_outputs, + }; + run_outputs.push(scenario_output); + } + println!( + "{}", + serde_json::to_string_pretty(&run_outputs)? + ); + + Ok(()) + +}