Skip to main content

joule_profiler_cli/
lib.rs

1use std::{collections::HashSet, path::PathBuf};
2
3use clap::{ArgAction, Parser, ValueEnum};
4
5use anyhow::{Result, bail};
6pub use commands::ProfilerCommand;
7#[cfg(feature = "rapl")]
8use serde::Deserialize;
9
10use crate::{
11    config::{overrides::ConfigOverride, table::ConfigTable},
12    output::{
13        displayer::Displayer,
14        formats::{OutputFormat, csv::CsvOutput, json::JsonOutput, terminal::TerminalOutput},
15    },
16};
17
18mod commands;
19pub mod config;
20mod logging;
21mod output;
22
23/// joule-profiler: measure program energy consumption
24#[allow(clippy::struct_excessive_bools)]
25#[derive(Parser, Debug)]
26#[command(name = "joule-profiler")]
27#[command(
28    version,
29    about = "Measure program metrics from various sources like RAPL, perf_event or NVML"
30)]
31pub struct CliArgs {
32    /// Verbosity (-v, -vv, -vvv)
33    #[arg(short = 'v', long = "verbose", action = ArgAction::Count)]
34    pub verbose: u8,
35
36    /// Output format to export the results in. (e.g., terminal, json, csv)
37    #[arg(long = "output-format")]
38    pub output_format: Option<OutputFormat>,
39
40    /// Output file for CSV/JSON. (else `data<TIMESTAMP>`.csv/json)
41    #[arg(short = 'o', long = "output-file")]
42    pub output_file: Option<String>,
43
44    /// Sources activation list. All sources must be separated with a comma (e.g., "rapl,nvml").
45    #[cfg_attr(
46        feature = "rapl",
47        arg(long, value_delimiter = ',', default_value = "rapl")
48    )]
49    #[cfg_attr(not(feature = "rapl"), arg(long, value_delimiter = ','))]
50    pub sources: Vec<Source>,
51
52    #[allow(clippy::doc_markdown)]
53    /// Override a configuration key, repeatable. (e.g., -D profiler.rapl_backend=powercap)
54    ///
55    /// KEY is the dotted path of the key in the configuration file, so any
56    /// key a configuration file can set is settable here:
57    ///   -D profiler.output_format=json
58    ///   -D sources.rapl.sockets_spec=[0,1]
59    ///   -D sources.cgroup.create_cgroup=false
60    ///
61    /// VALUE is read as TOML, falling back to a plain string, so durations,
62    /// regexes and paths need no quoting. Overrides are applied on top of the
63    /// --config file, and configuring a source enables it.
64    #[arg(
65        short = 'D',
66        long = "define",
67        value_name = "KEY=VALUE",
68        verbatim_doc_comment
69    )]
70    pub overrides: Vec<ConfigOverride>,
71
72    /// TOML configuration file. Every key it sets can also be set with -D.
73    #[arg(long = "config")]
74    pub config_file: Option<PathBuf>,
75
76    /// The command to execute.
77    #[command(subcommand)]
78    pub command: ProfilerCommand,
79}
80
81impl CliArgs {
82    pub fn from_args() -> Self {
83        Self::parse()
84    }
85
86    pub fn validate(&self) -> Result<()> {
87        let mut seen = HashSet::new();
88
89        for source in &self.sources {
90            if !seen.insert(source) {
91                bail!("Duplicate source specified: {source}");
92            }
93        }
94
95        Ok(())
96    }
97}
98
99#[cfg(not(any(
100    feature = "rapl",
101    feature = "perf_event",
102    feature = "nvml",
103    feature = "amdsmi",
104    feature = "procfs",
105    feature = "cgroup",
106)))]
107compile_error!("At least one source feature must be enabled");
108
109#[derive(Clone, Debug, PartialEq, Eq, Hash, ValueEnum)]
110pub enum Source {
111    #[cfg(feature = "rapl")]
112    Rapl,
113
114    #[cfg(feature = "perf_event")]
115    #[value(alias = "perf_event")]
116    Perf,
117
118    #[cfg(feature = "nvml")]
119    Nvml,
120
121    #[cfg(feature = "amdsmi")]
122    #[value(name = "amdsmi")]
123    AmdSmi,
124
125    #[cfg(feature = "procfs")]
126    Procfs,
127
128    #[cfg(feature = "cgroup")]
129    Cgroup,
130}
131
132impl std::fmt::Display for Source {
133    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134        let s = match self {
135            #[cfg(feature = "rapl")]
136            Source::Rapl => "rapl",
137
138            #[cfg(feature = "perf_event")]
139            Source::Perf => "perf",
140
141            #[cfg(feature = "nvml")]
142            Source::Nvml => "nvml",
143
144            #[cfg(feature = "amdsmi")]
145            Source::AmdSmi => "amdsmi",
146
147            #[cfg(feature = "procfs")]
148            Source::Procfs => "procfs",
149
150            #[cfg(feature = "cgroup")]
151            Source::Cgroup => "cgroup",
152        };
153        write!(f, "{s}")
154    }
155}
156
157/// RAPL backend, selected with `profiler.rapl_backend` in the configuration.
158///
159/// The RAPL source always comes with a backend, so this enum always has at
160/// least one variant.
161#[cfg(feature = "_rapl")]
162#[derive(Debug, Default, Clone, Deserialize)]
163pub enum RaplBackend {
164    #[cfg(feature = "rapl-perf")]
165    #[default]
166    #[serde(rename = "perf")]
167    Perf,
168
169    #[cfg(feature = "rapl-powercap")]
170    #[cfg_attr(not(feature = "rapl-perf"), default)]
171    #[serde(rename = "powercap")]
172    Powercap,
173}
174
175/// Builds the displayer for the configured output format.
176///
177/// Reads only from `config_table`: `apply_cli` must have already resolved
178/// any CLI override into `profiler_config` beforehand.
179pub fn config_table_to_displayer(config_table: &ConfigTable) -> Result<Box<dyn Displayer>> {
180    let output_file = config_table.profiler_config.output_file.clone();
181
182    let displayer = match config_table.profiler_config.output_format {
183        OutputFormat::Terminal => TerminalOutput.into(),
184        OutputFormat::Json => JsonOutput::new(output_file)?.into(),
185        OutputFormat::Csv => CsvOutput::try_new(output_file)?.into(),
186    };
187
188    Ok(displayer)
189}
190
191pub fn init_logging(verbose: u8) {
192    logging::init_logging(verbose);
193}
194
195#[cfg(test)]
196mod tests {
197    use clap::ValueEnum;
198    use joule_profiler_core::source::MetricReader;
199
200    use super::*;
201
202    fn cli_args_with_sources(sources: Vec<Source>) -> CliArgs {
203        CliArgs {
204            verbose: 0,
205            output_format: None,
206            output_file: None,
207            sources,
208            overrides: Vec::new(),
209            config_file: None,
210            command: ProfilerCommand::ListSensors,
211        }
212    }
213
214    #[test]
215    fn source_display_matches_every_metric_reader_get_id() {
216        #[cfg(feature = "rapl-perf")]
217        assert_eq!(
218            Source::Rapl.to_string(),
219            joule_profiler_source_rapl::perf::Rapl::get_id()
220        );
221        #[cfg(feature = "rapl-powercap")]
222        assert_eq!(
223            Source::Rapl.to_string(),
224            joule_profiler_source_rapl::powercap::Rapl::get_id()
225        );
226        #[cfg(feature = "perf_event")]
227        {
228            type DefaultPerfEvent = joule_profiler_source_perf_event::PerfEvent;
229            assert_eq!(Source::Perf.to_string(), DefaultPerfEvent::get_id());
230        }
231        #[cfg(feature = "cgroup")]
232        {
233            type DefaultCgroup = joule_profiler_source_cgroup::Cgroup;
234            assert_eq!(Source::Cgroup.to_string(), DefaultCgroup::get_id());
235        }
236        #[cfg(feature = "procfs")]
237        {
238            type DefaultProcfs = joule_profiler_source_procfs::Procfs;
239            assert_eq!(Source::Procfs.to_string(), DefaultProcfs::get_id());
240        }
241        #[cfg(feature = "nvml")]
242        {
243            type DefaultNvml = joule_profiler_source_nvml::Nvml;
244            assert_eq!(Source::Nvml.to_string(), DefaultNvml::get_id());
245        }
246        #[cfg(feature = "amdsmi")]
247        {
248            type DefaultAmdSmi = joule_profiler_source_amdsmi::AmdSmi;
249            assert_eq!(Source::AmdSmi.to_string(), DefaultAmdSmi::get_id());
250        }
251    }
252
253    #[cfg(feature = "perf_event")]
254    #[test]
255    fn source_value_enum_accepts_perf_event_alias() {
256        let parsed = Source::from_str("perf_event", false).unwrap();
257        assert_eq!(parsed, Source::Perf);
258    }
259
260    /// Every variant this build has must parse back from the name it displays.
261    #[test]
262    fn source_value_enum_accepts_canonical_names() {
263        for source in Source::value_variants() {
264            let parsed = Source::from_str(&source.to_string(), false).unwrap();
265            assert_eq!(&parsed, source);
266        }
267    }
268
269    #[test]
270    fn validate_rejects_duplicate_sources() {
271        let source = Source::value_variants()[0].clone();
272        let cli = cli_args_with_sources(vec![source.clone(), source]);
273        assert!(cli.validate().is_err());
274    }
275
276    #[test]
277    fn validate_accepts_distinct_sources() {
278        let cli = cli_args_with_sources(Source::value_variants().to_vec());
279        assert!(cli.validate().is_ok());
280    }
281}