Skip to main content

scx_raw_pmu/
json.rs

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