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#[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 #[arg(short = 'v', long = "verbose", action = ArgAction::Count)]
34 pub verbose: u8,
35
36 #[arg(long = "output-format")]
38 pub output_format: Option<OutputFormat>,
39
40 #[arg(short = 'o', long = "output-file")]
42 pub output_file: Option<String>,
43
44 #[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 #[arg(
65 short = 'D',
66 long = "define",
67 value_name = "KEY=VALUE",
68 verbatim_doc_comment
69 )]
70 pub overrides: Vec<ConfigOverride>,
71
72 #[arg(long = "config")]
74 pub config_file: Option<PathBuf>,
75
76 #[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#[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
175pub 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 #[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}