scx_raw_pmu/
json.rs

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
31// Intel offcore (OCR) events return two event codes, because
32// codes represent event slots and not the events themselves.
33fn 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    // There's a typo in the AMD Zen4/5 JSON for
67    // one of the PMUs.
68    #[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    // Derived fields
93    #[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    /// Identify the architecture of the local machine and
141    /// retrieve the paths to the relevant JSON files.
142    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    /// List all available counters for the current machine.
156    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            // metricgroups.json isn't actually PMU definitions.
224            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}