energy_model_config/
lib.rs

1// Copyright 2024 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use anyhow::{Context, Error, ensure};
6use serde_derive::Deserialize;
7use std::fs::File;
8use std::io::Read as _;
9use std::path::Path;
10use zx_types::{
11    ZX_CPU_SET_BITS_PER_WORD, ZX_CPU_SET_MAX_CPUS, ZX_MAX_NAME_LEN,
12    ZX_PROCESSOR_POWER_CONTROL_ARM_PSCI, ZX_PROCESSOR_POWER_CONTROL_ARM_WFI,
13    ZX_PROCESSOR_POWER_CONTROL_CPU_DRIVER, ZX_PROCESSOR_POWER_CONTROL_RISCV_SBI,
14    ZX_PROCESSOR_POWER_CONTROL_RISCV_WFI, ZX_PROCESSOR_POWER_LEVEL_OPTIONS_DOMAIN_INDEPENDENT,
15    zx_cpu_set_t, zx_processor_power_domain_t, zx_processor_power_level_t,
16    zx_processor_power_level_transition_t,
17};
18
19/// This library is used to parse a processor energy model JSON file into a data structure which
20/// also implements some convenience methods for accessing and consuming the data.
21///
22/// The intended usage is that `EnergyModel::new()` is called with an energy model JSON file path.
23/// If successful, the function returns an EnergyModel instance containing the parsed config data.
24///
25/// The parser expects a JSON5 file of the following format:
26///     [
27///         {
28///             power_domain: {
29///                 cpu_set: [
30///                     1,
31///                     2,
32///                     3,
33///                 ],
34///                 domain_id: 0,
35///             },
36///             power_levels: [
37///                 {
38///                     processing_rate: 200,
39///                     power_coefficient_nw: 100,
40///                     control_interface: 'CpuDriver',
41///                     control_argument: 0,
42///                     diagnostic_name: 'Pstate 0',
43///                 },
44///                 {
45///                     processing_rate: 100,
46///                     power_coefficient_nw: 50,
47///                     control_interface: 'CpuDriver',
48///                     control_argument: 0,
49///                     diagnostic_name: 'Pstate 1',
50///                 },
51///             ],
52///             power_level_transitions: [
53///                 {
54///                     from: 0,
55///                     to: 1,
56///                     duration_ns: 100,
57///                     energy_nj: 100,
58///                 },
59///             ],
60///         },
61///         {
62///             power_domain: {
63///                 cpu_set: [
64///                     0,
65///                     4,
66///                 ],
67///                 domain_id: 1,
68///             },
69///             power_levels: {
70///                 option: 'DomainIndependent',
71///                 processing_rate: 200,
72///                 power_coefficient_nw: 200,
73///                 control_interface: 'CpuDriver',
74///                 control_argument: 0,
75///                 diagnostic_name: 'Pstate 0',
76///             },
77///             power_level_transitions: [
78///             ],
79///         },
80///     ]
81
82/// Defines a Json compatible energy model struct of a specific set of CPUs.
83#[derive(Deserialize, Debug, Clone, PartialEq)]
84pub struct PowerLevelDomainJson {
85    // Represents CPUs that are a subset of a domain.
86    pub power_domain: PowerDomain,
87
88    // Power levels are implicitly enumerated as the indices of `power_levels`.
89    pub power_levels: Vec<PowerLevel>,
90
91    // There are no ordering restrictions on `power_level_transitions` and the
92    // power levels they encode correspond to those defined by `power_levels`.
93    // An absent transition between levels is assumed to indicate that there
94    // is no energy or latency cost borne by performing it.
95    pub power_level_transitions: Vec<PowerLevelTransition>,
96}
97
98#[derive(Deserialize, Debug, Clone, PartialEq)]
99pub struct PowerDomain {
100    // A vector of CPU indexes.
101    pub cpu_set: Vec<u16>,
102    // A unique identifier for all CPUs in the cpu_set.
103    pub domain_id: u32,
104}
105
106#[derive(Deserialize, Debug, Clone, PartialEq)]
107pub struct PowerLevel {
108    pub option: Option<PowerLevelOption>,
109    pub processing_rate: u64,
110    pub power_coefficient_nw: u64,
111    pub control_interface: ControlInterface,
112    pub control_argument: u64,
113    pub diagnostic_name: String,
114}
115
116#[derive(Deserialize, Debug, Clone, PartialEq)]
117pub struct PowerLevelTransition {
118    pub from: u8,
119    pub to: u8,
120    pub duration_ns: i64,
121    pub energy_nj: u64,
122}
123
124#[derive(Deserialize, Debug, Clone, PartialEq)]
125pub enum ControlInterface {
126    CpuDriver,
127    ArmPsci,
128    ArmWfi,
129    RiscvSbi,
130    RiscvWfi,
131}
132
133#[derive(Deserialize, Debug, Clone, PartialEq)]
134pub enum PowerLevelOption {
135    DomainIndependent,
136}
137
138impl PowerLevelDomainJson {
139    pub fn validate(&self) -> Result<(), Error> {
140        let power_level_num = self.power_levels.len();
141        for power_level_transition in self.power_level_transitions.iter() {
142            let from_index = power_level_transition.from as usize;
143            let to_index = power_level_transition.to as usize;
144            ensure!(
145                from_index != to_index
146                    && from_index < power_level_num
147                    && to_index < power_level_num,
148                "Power level transition need to be between two different valid indexes",
149            );
150            let from_processing_rate = self.power_levels[from_index].processing_rate;
151            let to_processing_rate = self.power_levels[to_index].processing_rate;
152            ensure!(
153                from_processing_rate != 0 || to_processing_rate != 0,
154                "Transitions directly from one idle power level to another idle power level are invalid",
155            );
156        }
157        Ok(())
158    }
159}
160
161/// Represents the top level of an energy model structure.
162#[derive(Debug, Clone, PartialEq)]
163pub struct EnergyModel(pub Vec<PowerLevelDomain>);
164
165/// Zircon compatible version of PowerLevelDomainJson.
166#[derive(Debug, Clone, PartialEq)]
167pub struct PowerLevelDomain {
168    pub power_domain: zx_processor_power_domain_t,
169    pub power_levels: Vec<zx_processor_power_level_t>,
170    pub power_level_transitions: Vec<zx_processor_power_level_transition_t>,
171}
172
173impl EnergyModel {
174    pub fn new(json_file_path: &Path) -> Result<EnergyModel, Error> {
175        let mut buffer = String::new();
176        File::open(&json_file_path)?.read_to_string(&mut buffer)?;
177
178        let power_level_domains = serde_json5::from_str::<Vec<PowerLevelDomainJson>>(&buffer)?;
179
180        // Iterate and validate each underlying PowerLevelDomain instance.
181        for power_level_domain in power_level_domains.iter() {
182            power_level_domain.validate().context(format!(
183                "Validation failed for power_domain {}",
184                power_level_domain.power_domain.domain_id
185            ))?;
186        }
187
188        Ok(Self(
189            power_level_domains
190                .into_iter()
191                .map(|s| s.try_into())
192                .collect::<Result<Vec<PowerLevelDomain>, _>>()?,
193        ))
194    }
195}
196
197impl TryFrom<PowerLevelDomainJson> for PowerLevelDomain {
198    type Error = anyhow::Error;
199    fn try_from(e: PowerLevelDomainJson) -> Result<Self, Error> {
200        Ok(Self {
201            power_domain: e.power_domain.try_into()?,
202            power_levels: e
203                .power_levels
204                .into_iter()
205                .map(|s| s.try_into())
206                .collect::<Result<Vec<zx_processor_power_level_t>, _>>()?,
207            power_level_transitions: e
208                .power_level_transitions
209                .into_iter()
210                .map(|s| s.into())
211                .collect(),
212        })
213    }
214}
215
216impl TryFrom<PowerDomain> for zx_processor_power_domain_t {
217    type Error = anyhow::Error;
218    fn try_from(p: PowerDomain) -> Result<Self, Error> {
219        let mut ret = Self::default();
220        ret.cpus = get_cpu_mask(p.cpu_set)?;
221        ret.domain_id = p.domain_id;
222        Ok(ret)
223    }
224}
225
226impl From<PowerLevelTransition> for zx_processor_power_level_transition_t {
227    fn from(p: PowerLevelTransition) -> Self {
228        let mut ret = Self::default();
229        ret.from = p.from;
230        ret.to = p.to;
231        ret.latency = p.duration_ns;
232        ret.energy = p.energy_nj;
233        ret
234    }
235}
236
237impl TryFrom<PowerLevel> for zx_processor_power_level_t {
238    type Error = anyhow::Error;
239    fn try_from(p: PowerLevel) -> Result<Self, Error> {
240        let control_interface = match p.control_interface {
241            ControlInterface::CpuDriver => ZX_PROCESSOR_POWER_CONTROL_CPU_DRIVER,
242            ControlInterface::ArmPsci => ZX_PROCESSOR_POWER_CONTROL_ARM_PSCI,
243            ControlInterface::ArmWfi => ZX_PROCESSOR_POWER_CONTROL_ARM_WFI,
244            ControlInterface::RiscvSbi => ZX_PROCESSOR_POWER_CONTROL_RISCV_SBI,
245            ControlInterface::RiscvWfi => ZX_PROCESSOR_POWER_CONTROL_RISCV_WFI,
246        };
247
248        let options = match p.option {
249            Some(PowerLevelOption::DomainIndependent) => {
250                ZX_PROCESSOR_POWER_LEVEL_OPTIONS_DOMAIN_INDEPENDENT
251            }
252            _ => 0,
253        };
254
255        let bytes = p.diagnostic_name.as_bytes();
256        ensure!(
257            bytes.len() <= ZX_MAX_NAME_LEN,
258            "diagnostic_name {:?} must be shorter than {:?} bytes",
259            p.diagnostic_name,
260            ZX_MAX_NAME_LEN,
261        );
262        let mut diagnostic_name = [0u8; ZX_MAX_NAME_LEN];
263        diagnostic_name[..bytes.len()].copy_from_slice(bytes);
264
265        let mut ret = Self::default();
266        ret.options = options;
267        ret.processing_rate = p.processing_rate;
268        ret.power_coefficient_nw = p.power_coefficient_nw;
269        ret.control_interface = control_interface;
270        ret.control_argument = p.control_argument;
271        ret.diagnostic_name = diagnostic_name;
272        Ok(ret)
273    }
274}
275
276// Convert from a list of CPU indexes to CPU mask.
277//
278// Zircon definition:
279// The |N|'th CPU is considered in the CPU set if the bit:
280//
281//   cpu_mask[N / ZX_CPU_SET_BITS_PER_WORD]
282//       & (1 << (N % ZX_CPU_SET_BITS_PER_WORD))
283//
284// is set.
285fn get_cpu_mask(cpu_set: Vec<u16>) -> Result<zx_cpu_set_t, anyhow::Error> {
286    let mut cpu_mask = [0 as u64; ZX_CPU_SET_MAX_CPUS / ZX_CPU_SET_BITS_PER_WORD];
287    for cpu in cpu_set {
288        let cpu_index = cpu as usize;
289        ensure!(
290            cpu_index < ZX_CPU_SET_MAX_CPUS,
291            "CPU index must be smaller than {:?}",
292            ZX_CPU_SET_MAX_CPUS,
293        );
294
295        let i = cpu_index / ZX_CPU_SET_BITS_PER_WORD;
296        cpu_mask[i] = cpu_mask[i] | (1 << (cpu_index % ZX_CPU_SET_BITS_PER_WORD));
297    }
298    Ok(zx_cpu_set_t { mask: cpu_mask })
299}
300
301#[cfg(test)]
302mod tests {
303    use crate::*;
304    use assert_matches::assert_matches;
305
306    #[fuchsia::test]
307    fn test_convert_cpu_mask() {
308        assert_eq!(
309            get_cpu_mask(vec![0u16, 1u16, 2u16, 3u16]).unwrap().mask,
310            [15u64, 0u64, 0u64, 0u64, 0u64, 0u64, 0u64, 0u64]
311        );
312
313        fn is_nth_set(
314            cpu_mask: [u64; ZX_CPU_SET_MAX_CPUS / ZX_CPU_SET_BITS_PER_WORD],
315            n: usize,
316        ) -> bool {
317            cpu_mask[n / ZX_CPU_SET_BITS_PER_WORD] & (1 << (n % ZX_CPU_SET_BITS_PER_WORD)) != 0
318        }
319
320        let mut cpu_set = vec![15u16];
321        let mut cpu_mask = get_cpu_mask(cpu_set).unwrap().mask;
322        assert!(is_nth_set(cpu_mask, 15));
323
324        cpu_set = vec![32u16];
325        cpu_mask = get_cpu_mask(cpu_set).unwrap().mask;
326        assert!(is_nth_set(cpu_mask, 32));
327
328        // CPU index must be smaller than ZX_CPU_SET_MAX_CPUS.
329        cpu_set = vec![ZX_CPU_SET_MAX_CPUS as u16];
330        assert_matches!(get_cpu_mask(cpu_set), Err(_));
331    }
332
333    #[fuchsia::test]
334    fn test_processor_power_level() {
335        let power_level = PowerLevel {
336            option: Some(PowerLevelOption::DomainIndependent),
337            processing_rate: 0,
338            power_coefficient_nw: 0,
339            control_interface: ControlInterface::ArmPsci,
340            control_argument: 0,
341            diagnostic_name:
342                "This is a very very very long diagnositc name which exceeds 32 bytes.".to_string(),
343        };
344        assert_matches!(zx_processor_power_level_t::try_from(power_level), Err(_));
345
346        let mut zx_power_level = zx_processor_power_level_t::default();
347        zx_power_level.control_interface = ZX_PROCESSOR_POWER_CONTROL_CPU_DRIVER;
348        let power_level = PowerLevel {
349            option: None,
350            processing_rate: 0,
351            power_coefficient_nw: 0,
352            control_interface: ControlInterface::CpuDriver,
353            control_argument: 0,
354            diagnostic_name: "".to_string(),
355        };
356
357        assert!(zx_processor_power_level_t::try_from(power_level).unwrap() == zx_power_level);
358    }
359}