1use anyhow::bail;
2use csv::Reader;
3use procfs::CpuInfo;
4use regex::Regex;
5use serde::Deserialize;
6use serde::Deserializer;
7use serde::Serialize;
8use std::env;
9use std::env::consts::ARCH;
10use std::ffi::OsString;
11use std::fs;
12use std::fs::File;
13use std::io::BufReader;
14use std::path::Path;
15
16use crate::resources::ResourceDir;
17use anyhow::Result;
18
19use std::collections::HashMap;
20
21fn hex_to_u64<'de, D>(de: D) -> Result<u64, D::Error>
22where
23 D: Deserializer<'de>,
24{
25 let s: &str = Deserialize::deserialize(de)?;
26 let s = s.to_lowercase();
27 let s = s.trim_start_matches("0x");
28 u64::from_str_radix(s, 16).map_err(serde::de::Error::custom)
29}
30
31fn hexlist<'de, D>(de: D) -> Result<Vec<u64>, D::Error>
34where
35 D: Deserializer<'de>,
36{
37 let mut result = vec![];
38 let s: &str = Deserialize::deserialize(de)?;
39 for token in s.split(',') {
40 let radix = if token.to_lowercase().starts_with("0x") {
41 16
42 } else {
43 10
44 };
45 let event = u64::from_str_radix(token.to_lowercase().trim_start_matches("0x"), radix)
46 .map_err(serde::de::Error::custom)?;
47
48 result.push(event);
49 }
50
51 Ok(result)
52}
53
54fn num_to_bool<'de, D>(de: D) -> Result<bool, D::Error>
55where
56 D: Deserializer<'de>,
57{
58 let s: &str = Deserialize::deserialize(de)?;
59 let num = u64::from_str_radix(s, 16).map_err(serde::de::Error::custom)?;
60 Ok(num != 0)
61}
62
63#[derive(Clone, Debug, Serialize, Deserialize, Default)]
64#[serde(default)]
65pub struct PMUSpec {
66 #[serde(alias = "BriefDescription", alias = "BriefDescript6ion")]
69 desc: Option<String>,
70
71 #[serde(alias = "PublicDescription")]
72 desc_public: Option<String>,
73
74 #[serde(alias = "EventCode")]
75 #[serde(deserialize_with = "hexlist")]
76 pub event: Vec<u64>,
77
78 #[serde(alias = "EventName")]
79 pub name: String,
80
81 #[serde(alias = "UMask")]
82 #[serde(deserialize_with = "hex_to_u64")]
83 pub umask: u64,
84
85 #[serde(alias = "Unit")]
86 pmu: Option<String>,
87
88 #[serde(alias = "ConfigCode")]
89 #[serde(deserialize_with = "hex_to_u64")]
90 config: u64,
91
92 #[serde(alias = "MetricExpr")]
94 metric_expr: Option<String>,
95
96 #[serde(alias = "MetricName")]
97 metric_name: Option<String>,
98
99 #[serde(alias = "MetricGroup")]
100 metric_group: Option<String>,
101
102 #[serde(alias = "MetricConstraint")]
103 metric_constraint: Option<String>,
104
105 #[serde(alias = "PerPkg")]
106 #[serde(deserialize_with = "num_to_bool")]
107 per_pkg: bool,
108
109 #[serde(alias = "Invert")]
110 #[serde(deserialize_with = "num_to_bool")]
111 invert: bool,
112
113 #[serde(alias = "MSRIndex")]
114 #[serde(deserialize_with = "hexlist", default)]
115 msr_index: Vec<u64>,
116
117 #[serde(alias = "MSRValue")]
118 #[serde(deserialize_with = "hex_to_u64")]
119 msr_value: u64,
120
121 #[serde(alias = "Counter")]
122 counter: Option<String>,
123
124 #[serde(alias = "CounterNumFixed")]
125 counters_num_fixed: Option<u64>,
126
127 #[serde(alias = "CounterNumGeneric")]
128 counters_num_generic: Option<u64>,
129}
130
131pub struct PMUManager {
132 pub dataroot: OsString,
133 pub arch: String,
134 pub tuple: String,
135 pub codename: String,
136 pub pmus: HashMap<String, PMUSpec>,
137}
138
139impl PMUManager {
140 fn identify_architecture() -> Result<String> {
143 let file = File::open("/proc/cpuinfo")?;
144 let bufreader = BufReader::new(file);
145
146 let cpuinfo = CpuInfo::from_reader(bufreader)?;
147 Ok(format!(
148 "{}-{}-{:X}",
149 cpuinfo.fields["vendor_id"],
150 cpuinfo.fields["cpu family"],
151 cpuinfo.fields["model"].parse::<i32>().unwrap()
152 ))
153 }
154
155 pub fn list_counters(&self) -> Result<()> {
157 for pmu in self.pmus.iter() {
158 println!("{}", serde_json::to_string_pretty(&pmu)?);
159 }
160
161 Ok(())
162 }
163
164 pub fn list_metadata(&self) -> () {
165 println!("Dataroot {}", self.dataroot.to_string_lossy());
166 println!("Arch: {}", self.arch);
167 println!("Tuple: {}", self.tuple);
168 println!("Codename: {}", self.codename);
169 }
170
171 fn read_file_counters(json_bytes: &[u8]) -> Result<Vec<PMUSpec>> {
172 Ok(serde_json::from_slice(json_bytes)?)
173 }
174
175 fn arch_code() -> String {
176 String::from(match ARCH {
177 "x86" => "x86",
178 "x86_64" => "x86",
179 "aarch64" => "arm64",
180 "powerpc" => "powerpc",
181 "powerpc64" => "powerpc",
182 "riscv32" => "riscv",
183 "riscv64" => "riscv",
184 "s390x" => "s390",
185 "loongarch32" => "x86",
186 "loongarch64" => "x86",
187 _ => panic!("unsupported architecture"),
188 })
189 }
190
191 fn spec_dir(resource_dir: &ResourceDir, arch: &str, tuple: &str) -> Result<String> {
192 let arch_dir = resource_dir.get_dir(arch)?;
193 let mapfile = arch_dir.get_file("mapfile.csv")?;
194 let mapfile_contents = mapfile.read()?;
195
196 for record in Reader::from_reader(mapfile_contents.as_ref()).records() {
197 let record = record?;
198 let regex = record.get(0).expect("no regex found in csv");
199 let codename = record.get(2).expect("no codename found in csv");
200 let re = Regex::new(regex)?;
201
202 if re.is_match(tuple) {
203 return Ok(codename.to_string());
204 }
205 }
206
207 bail!("No matching config for tuple")
208 }
209
210 fn new_with_resource_dir(
211 resource_dir: ResourceDir,
212 dataroot_display: OsString,
213 ) -> Result<Self> {
214 let tuple = Self::identify_architecture()?;
215 let arch = Self::arch_code();
216 let codename = Self::spec_dir(&resource_dir, &arch, &tuple)?;
217
218 let arch_dir = resource_dir.get_dir(&arch)?;
219 let spec_dir = arch_dir.get_dir(&codename)?;
220
221 let mut pmus = HashMap::new();
222 for file in spec_dir.files()? {
223 if file.path().ends_with("metricgroups.json") {
225 continue;
226 }
227
228 let file_contents = file.read()?;
229 let counters = Self::read_file_counters(file_contents.as_ref())?;
230
231 for counter in counters.iter() {
232 pmus.insert(String::from(&counter.name), counter.clone());
233 }
234 }
235
236 Ok(Self {
237 dataroot: dataroot_display,
238 arch,
239 tuple,
240 codename,
241 pmus,
242 })
243 }
244
245 pub fn new() -> Result<Self> {
246 let mut dataroot: OsString = env::current_exe()?.into();
247 dataroot.push(":embedded");
248
249 let resource_dir = ResourceDir::default();
250 Self::new_with_resource_dir(resource_dir, dataroot)
251 }
252
253 pub fn new_with_dataroot(dataroot: Option<&Path>) -> Result<Self> {
254 let dataroot = match dataroot {
255 Some(path) => fs::canonicalize(path)?,
256 None => env::current_dir()?.to_path_buf(),
257 };
258
259 let resource_dir = ResourceDir::new_filesystem(dataroot.clone());
260 Self::new_with_resource_dir(resource_dir, dataroot.into())
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 #[test]
269 fn test_with_resources() {
270 let manager = PMUManager::new().expect("could not create PMU manager");
271
272 manager.list_metadata();
273 manager.list_counters().expect("could not list counters");
274 }
275
276 #[test]
277 fn test_with_dir() {
278 let td = tempfile::tempdir().unwrap();
279 crate::resources::extract_arch_resources(td.path()).expect("could not extract resources");
280
281 let manager =
282 PMUManager::new_with_dataroot(Some(td.path())).expect("could not create PMU manager");
283
284 manager.list_metadata();
285 manager.list_counters().expect("could not list counters");
286 }
287}