blob: 73ed7febb32e11cc939eb787551ad20e7cbf56ef [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use crate::dev_control_handler;
use crate::error::PowerManagerError;
use crate::message::{Message, MessageReturn};
use crate::node::Node;
use crate::types::{Farads, Hertz, Volts, Watts};
use crate::utils::connect_proxy;
use anyhow::{format_err, Context, Error};
use async_trait::async_trait;
use fidl_fuchsia_hardware_cpu_ctrl as fcpuctrl;
use fuchsia_inspect::{self as inspect, Property};
use serde_derive::Deserialize;
use serde_json as json;
use std::cell::Cell;
use std::collections::HashMap;
use std::rc::Rc;
/// Node: CpuControlHandler
///
/// Summary: Provides a mechanism for controlling the performance state of a CPU domain. The node
/// mostly relies on functionality provided by the DeviceControlHandler node for
/// performance state control, but this node enhances that basic functionality by
/// integrating performance state metadata (CPU P-state information) from the CpuCtrl
/// interface.
///
/// Handles Messages:
/// - SetMaxPowerConsumption
///
/// Sends Messages:
/// - GetTotalCpuLoad
/// - GetPerformanceState
/// - SetPerformanceState
///
/// FIDL dependencies:
/// - fuchsia.hardware.cpu.ctrl.Device: the node uses this protocol to communicate with the
/// CpuCtrl interface of the CPU device specified in the CpuControlHandler constructor
/// Describes a processor performance state.
#[derive(Clone, Debug, Copy)]
pub struct PState {
pub frequency: Hertz,
pub voltage: Volts,
}
/// Describes the parameters of the CPU domain.
pub struct CpuControlParams {
/// Available P-states of the CPU. These must be in order of descending power usage, per
/// section 8.4.6.2 of ACPI spec version 6.3.
pub p_states: Vec<PState>,
/// Model capacitance of each CPU core. Required to estimate power usage.
pub capacitance: Farads,
/// Number of cores contained within this CPU domain.
pub num_cores: u32,
}
impl CpuControlParams {
/// Checks that the list of P-states is valid:
/// - Contains at least one element;
/// - Is in order of decreasing nominal power consumption.
fn validate(&self) -> Result<(), Error> {
if self.num_cores == 0 {
return Err(format_err!("Must have > 0 cores"));
}
if self.p_states.len() == 0 {
return Err(format_err!("Must have at least one P-state"));
} else if self.p_states.len() > 1 {
let mut last_power = get_cpu_power(
self.capacitance,
self.p_states[0].voltage,
self.p_states[0].frequency,
);
for i in 1..self.p_states.len() {
let p_state = &self.p_states[i];
let power = get_cpu_power(self.capacitance, p_state.voltage, p_state.frequency);
if power >= last_power {
return Err(format_err!(
"P-states must be in order of decreasing power consumption \
(violated by state {})",
i
));
}
last_power = power;
}
}
Ok(())
}
}
/// Returns the modeled power consumed by the CPU completing operations at the specified rate.
/// Note that we assume zero static power consumption in this model, and that `op_completion_rate`
/// is only the CPU's clock speed if the CPU is never idle.
pub fn get_cpu_power(capacitance: Farads, voltage: Volts, op_completion_rate: Hertz) -> Watts {
Watts(capacitance.0 * voltage.0.powi(2) * op_completion_rate.0)
}
/// A builder for constructing the CpuControlHandler node. The fields of this struct are documented
/// as part of the CpuControlHandler struct.
pub struct CpuControlHandlerBuilder<'a> {
cpu_driver_path: String,
capacitance: Farads,
min_cpu_clock_speed: Hertz,
cpu_stats_handler: Rc<dyn Node>,
cpu_dev_handler_node: Rc<dyn Node>,
cpu_ctrl_proxy: Option<fcpuctrl::DeviceProxy>,
inspect_root: Option<&'a inspect::Node>,
}
impl<'a> CpuControlHandlerBuilder<'a> {
pub fn new_with_driver_path(
cpu_driver_path: String,
// TODO(fxbug.dev/45507): Determine a proper owner for this value.
capacitance: Farads,
cpu_stats_handler: Rc<dyn Node>,
cpu_dev_handler_node: Rc<dyn Node>,
) -> Self {
Self {
cpu_driver_path,
capacitance,
min_cpu_clock_speed: Hertz(0.0),
cpu_stats_handler,
cpu_dev_handler_node,
cpu_ctrl_proxy: None,
inspect_root: None,
}
}
#[cfg(test)]
pub fn new_with_proxy(
cpu_driver_path: String,
proxy: fcpuctrl::DeviceProxy,
capacitance: Farads,
cpu_stats_handler: Rc<dyn Node>,
cpu_dev_handler_node: Rc<dyn Node>,
) -> Self {
Self {
cpu_driver_path,
cpu_ctrl_proxy: Some(proxy),
min_cpu_clock_speed: Hertz(0.0),
capacitance,
cpu_stats_handler,
cpu_dev_handler_node,
inspect_root: None,
}
}
pub fn new_from_json(json_data: json::Value, nodes: &HashMap<String, Rc<dyn Node>>) -> Self {
#[derive(Deserialize)]
struct Config {
driver_path: String,
capacitance: f64,
min_cpu_clock_speed: f64,
}
#[derive(Deserialize)]
struct Dependencies {
cpu_stats_handler_node: String,
cpu_dev_handler_node: String,
}
#[derive(Deserialize)]
struct JsonData {
config: Config,
dependencies: Dependencies,
}
let data: JsonData = json::from_value(json_data).unwrap();
Self::new_with_driver_path(
data.config.driver_path,
Farads(data.config.capacitance),
nodes[&data.dependencies.cpu_stats_handler_node].clone(),
nodes[&data.dependencies.cpu_dev_handler_node].clone(),
)
.with_min_cpu_clock_speed(Hertz(data.config.min_cpu_clock_speed))
}
#[cfg(test)]
pub fn with_inspect_root(mut self, root: &'a inspect::Node) -> Self {
self.inspect_root = Some(root);
self
}
pub fn with_min_cpu_clock_speed(mut self, clock_speed: Hertz) -> Self {
self.min_cpu_clock_speed = clock_speed;
self
}
pub async fn build(self) -> Result<Rc<CpuControlHandler>, Error> {
// Optionally use the default proxy
let proxy = if self.cpu_ctrl_proxy.is_none() {
connect_proxy::<fcpuctrl::DeviceMarker>(&self.cpu_driver_path)
.context("Failed connecting to CPU driver")?
} else {
self.cpu_ctrl_proxy.unwrap()
};
// Optionally use the default inspect root node
let inspect_root = self.inspect_root.unwrap_or(inspect::component::inspector().root());
let inspect_data =
InspectData::new(inspect_root, format!("CpuControlHandler ({})", self.cpu_driver_path));
// Query the CPU parameters
let cpu_control_params = Self::get_cpu_params(
&self.cpu_driver_path,
&proxy,
self.capacitance,
self.min_cpu_clock_speed,
)
.await
.context("Failed getting CPU params")?;
inspect_data.set_cpu_control_params(&cpu_control_params);
let node = Rc::new(CpuControlHandler {
cpu_driver_path: self.cpu_driver_path,
cpu_control_params,
current_p_state_index: Cell::new(0),
cpu_stats_handler: self.cpu_stats_handler,
cpu_dev_handler_node: self.cpu_dev_handler_node,
_cpu_ctrl_proxy: proxy,
inspect: inspect_data,
});
// Initialize current P-state index
node.get_current_p_state_index().await?;
Ok(node)
}
/// Query the CPU parameters from the CpuCtrl driver.
async fn get_cpu_params(
cpu_driver_path: &String,
cpu_ctrl_proxy: &fcpuctrl::DeviceProxy,
capacitance: Farads,
min_cpu_clock_speed: Hertz,
) -> Result<CpuControlParams, Error> {
fuchsia_trace::duration!(
"power_manager",
"CpuControlHandlerBuilder::get_cpu_params",
"driver" => cpu_driver_path.as_str()
);
// Query P-state metadata from the CpuCtrl interface. Each supported performance state
// has accompanying P-state metadata.
let mut p_states = Vec::new();
let mut skipped_p_states = Vec::new();
for i in 0..dev_control_handler::MAX_PERF_STATES {
if let Ok(info) = cpu_ctrl_proxy.get_performance_state_info(i).await? {
let frequency = Hertz(info.frequency_hz as f64);
let voltage = Volts(info.voltage_uv as f64 / 1e6);
let p_state = PState { frequency, voltage };
// Filter out P-states where CPU frequency is unacceptably low
if frequency >= min_cpu_clock_speed {
p_states.push(p_state);
} else {
skipped_p_states.push(p_state);
}
} else {
break;
}
}
let params = CpuControlParams {
p_states,
num_cores: cpu_ctrl_proxy.get_num_logical_cores().await? as u32,
capacitance,
};
params.validate()?;
fuchsia_trace::instant!(
"power_manager",
"CpuControlHandlerBuilder::received_cpu_params",
fuchsia_trace::Scope::Thread,
"valid" => 1,
"p_states" => format!("{:?}", params.p_states).as_str(),
"capacitance" => params.capacitance.0,
"num_cores" => params.num_cores,
"skipped_p_states" => format!("{:?}", skipped_p_states).as_str()
);
Ok(params)
}
}
pub struct CpuControlHandler {
/// The path to the driver that this node controls.
cpu_driver_path: String,
/// The parameters of the CPU domain which are queried from the CPU driver.
cpu_control_params: CpuControlParams,
/// The current CPU P-state index which is queried from the CPU DeviceControlHandler node.
current_p_state_index: Cell<usize>,
/// The node which will provide CPU load information. It is expected that this node responds to
/// the GetTotalCpuLoad message.
cpu_stats_handler: Rc<dyn Node>,
/// The node to be used for CPU performance state control. It is expected that this node
/// responds to the Get/SetPerformanceState messages.
cpu_dev_handler_node: Rc<dyn Node>,
/// A proxy handle to communicate with the CPU driver CpuCtrl interface.
_cpu_ctrl_proxy: fcpuctrl::DeviceProxy,
/// A struct for managing Component Inspection data
inspect: InspectData,
}
impl CpuControlHandler {
/// Returns the total CPU load (averaged since the previous call)
async fn get_load(&self) -> Result<f32, Error> {
fuchsia_trace::duration!(
"power_manager",
"CpuControlHandler::get_load",
"driver" => self.cpu_driver_path.as_str()
);
match self.send_message(&self.cpu_stats_handler, &Message::GetTotalCpuLoad).await {
Ok(MessageReturn::GetTotalCpuLoad(load)) => Ok(load),
Ok(r) => Err(format_err!("GetTotalCpuLoad had unexpected return value: {:?}", r)),
Err(e) => Err(format_err!("GetTotalCpuLoad failed: {:?}", e)),
}
}
/// Returns the current CPU P-state index
async fn get_current_p_state_index(&self) -> Result<usize, Error> {
fuchsia_trace::duration!(
"power_manager",
"CpuControlHandler::get_current_p_state_index",
"driver" => self.cpu_driver_path.as_str()
);
match self.send_message(&self.cpu_dev_handler_node, &Message::GetPerformanceState).await {
Ok(MessageReturn::GetPerformanceState(state)) => Ok(state as usize),
Ok(r) => Err(format_err!("GetPerformanceState had unexpected return value: {:?}", r)),
Err(e) => Err(format_err!("GetPerformanceState failed: {:?}", e)),
}
}
/// Sets the P-state to the highest-power state with consumption below `max_power`.
///
/// The estimated power consumption depends on the operation completion rate by the CPU.
/// We assume that the rate of operations requested over the next sample interval will be
/// the same as it was over the previous sample interval, up to CPU's max completion rate
/// for a P-state under consideration.
async fn handle_set_max_power_consumption(
&self,
max_power: &Watts,
) -> Result<MessageReturn, PowerManagerError> {
fuchsia_trace::duration!(
"power_manager",
"CpuControlHandler::handle_set_max_power_consumption",
"driver" => self.cpu_driver_path.as_str(),
"max_power" => max_power.0
);
let current_p_state_index = self.current_p_state_index.get();
// This is reused several times.
let num_cores = self.cpu_control_params.num_cores as f64;
// The operation completion rate over the last sample interval is
// num_operations / sample_interval,
// where
// num_operations = last_load * last_frequency * sample_interval.
// Hence,
// last_op_rate = last_load * last_frequency.
let (last_op_rate, last_max_op_rate) = {
let last_load = self.get_load().await? as f64;
self.inspect.last_load.set(last_load);
fuchsia_trace::counter!(
"power_manager",
"CpuControlHandler last_load",
0,
"last_load" => last_load
);
// TODO(pshickel): Eventually we'll need a way to query the load only from the cores we
// care about. As far as I can tell, there isn't currently a way to correlate the CPU
// info coming from CpuStats with that from CpuCtrl.
let last_frequency = self.cpu_control_params.p_states[current_p_state_index].frequency;
(last_frequency.mul_scalar(last_load), last_frequency.mul_scalar(num_cores))
};
self.inspect.last_op_rate.set(last_op_rate.0);
fuchsia_trace::instant!(
"power_manager",
"CpuControlHandler::set_max_power_consumption_data",
fuchsia_trace::Scope::Thread,
"driver" => self.cpu_driver_path.as_str(),
"current_p_state_index" => current_p_state_index as u32,
"last_op_rate" => last_op_rate.0
);
let mut p_state_index = 0;
let mut estimated_power = Watts(0.0);
// Iterate through the list of available P-states (guaranteed to be sorted in order of
// decreasing power consumption) and choose the first that will operate within the
// `max_power` constraint.
for (i, state) in self.cpu_control_params.p_states.iter().enumerate() {
// We assume that the last operation rate carries over to the next interval unless:
// - It exceeds the max operation rate at the new frequency, in which case it is
// truncated to the new max.
// - It is within a small delta of the max rate at the last frequency, in which case we
// assume that it would rise to the new maximum if the clock speed were to increase.
const ESSENTIALLY_MAX_LOAD_FRACTION: f64 = 0.99;
let new_max_op_rate = state.frequency.mul_scalar(num_cores);
let estimated_op_rate = if last_op_rate > new_max_op_rate
|| last_op_rate > last_max_op_rate.mul_scalar(ESSENTIALLY_MAX_LOAD_FRACTION)
{
new_max_op_rate
} else {
last_op_rate
};
p_state_index = i;
estimated_power = get_cpu_power(
self.cpu_control_params.capacitance,
state.voltage,
estimated_op_rate,
);
if estimated_power <= *max_power {
break;
}
}
if p_state_index != current_p_state_index {
fuchsia_trace::instant!(
"power_manager",
"CpuControlHandler::updated_p_state_index",
fuchsia_trace::Scope::Thread,
"driver" => self.cpu_driver_path.as_str(),
"old_index" => current_p_state_index as u32,
"new_index" => p_state_index as u32
);
// Tell the CPU DeviceControlHandler to update the performance state
self.send_message(
&self.cpu_dev_handler_node,
&Message::SetPerformanceState(p_state_index as u32),
)
.await?;
// Cache the new P-state index for calculations on the next iteration
self.current_p_state_index.set(p_state_index);
self.inspect.p_state_index.set(p_state_index as u64);
}
fuchsia_trace::counter!(
"power_manager",
"CpuControlHandler p_state",
0,
"P-state index" => p_state_index as u32
);
Ok(MessageReturn::SetMaxPowerConsumption(estimated_power))
}
}
#[async_trait(?Send)]
impl Node for CpuControlHandler {
fn name(&self) -> String {
"CpuControlHandler".to_string()
}
async fn handle_message(&self, msg: &Message) -> Result<MessageReturn, PowerManagerError> {
match msg {
Message::SetMaxPowerConsumption(p) => self.handle_set_max_power_consumption(p).await,
_ => Err(PowerManagerError::Unsupported),
}
}
}
struct InspectData {
// Nodes
root_node: inspect::Node,
// Properties
p_state_index: inspect::UintProperty,
last_op_rate: inspect::DoubleProperty,
last_load: inspect::DoubleProperty,
}
impl InspectData {
fn new(parent: &inspect::Node, node_name: String) -> Self {
// Create a local root node and properties
let root_node = parent.create_child(node_name);
let p_state_index = root_node.create_uint("p_state_index", 0);
let last_op_rate = root_node.create_double("last_op_rate", 0.0);
let last_load = root_node.create_double("last_load", 0.0);
InspectData { root_node, p_state_index, last_op_rate, last_load }
}
fn set_cpu_control_params(&self, params: &CpuControlParams) {
let cpu_params_node = self.root_node.create_child("cpu_control_params");
// Iterate `params.p_states` in reverse order so that the Inspect nodes appear in the same
// order as the vector (`create_child` inserts nodes at the head).
for (i, p_state) in params.p_states.iter().enumerate().rev() {
let p_state_node = cpu_params_node.create_child(format!("p_state_{}", i));
p_state_node.record_double("voltage (V)", p_state.voltage.0);
p_state_node.record_double("frequency (Hz)", p_state.frequency.0);
// Pass ownership of the new P-state node to the parent `cpu_params_node`
cpu_params_node.record(p_state_node);
}
cpu_params_node.record_double("capacitance (F)", params.capacitance.0);
cpu_params_node.record_uint("num_cores", params.num_cores.into());
// Pass ownership of the new `cpu_params_node` to the root node
self.root_node.record(cpu_params_node);
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::test::mock_node::{MessageMatcher, MockNodeMaker};
use crate::{msg_eq, msg_ok_return};
use fuchsia_async as fasync;
use fuchsia_zircon as zx;
use futures::TryStreamExt;
use inspect::assert_inspect_tree;
use matches::assert_matches;
fn setup_fake_service(params: CpuControlParams) -> fcpuctrl::DeviceProxy {
let (proxy, mut stream) =
fidl::endpoints::create_proxy_and_stream::<fcpuctrl::DeviceMarker>().unwrap();
fasync::Task::local(async move {
while let Ok(req) = stream.try_next().await {
match req {
Some(fcpuctrl::DeviceRequest::GetNumLogicalCores { responder }) => {
let _ = responder.send(params.num_cores as u64);
}
Some(fcpuctrl::DeviceRequest::GetPerformanceStateInfo { state, responder }) => {
let index = state as usize;
let mut result = if index < params.p_states.len() {
Ok(fcpuctrl::CpuPerformanceStateInfo {
frequency_hz: params.p_states[index].frequency.0 as i64,
voltage_uv: (params.p_states[index].voltage.0 * 1e6) as i64,
})
} else {
Err(zx::Status::NOT_SUPPORTED.into_raw())
};
let _ = responder.send(&mut result);
}
_ => assert!(false),
}
}
})
.detach();
proxy
}
pub async fn setup_test_node(
params: CpuControlParams,
cpu_stats_handler: Rc<dyn Node>,
cpu_dev_handler_node: Rc<dyn Node>,
) -> Rc<CpuControlHandler> {
let capacitance = params.capacitance;
CpuControlHandlerBuilder::new_with_proxy(
"Fake".to_string(),
setup_fake_service(params),
capacitance,
cpu_stats_handler,
cpu_dev_handler_node,
)
.build()
.await
.unwrap()
}
#[test]
fn test_get_cpu_power() {
assert_eq!(get_cpu_power(Farads(100.0e-12), Volts(1.0), Hertz(1.0e9)), Watts(0.1));
}
/// Tests that an unsupported message is handled gracefully and an Unsupported error is returned
#[fasync::run_singlethreaded(test)]
async fn test_unsupported_msg() {
let mut mock_maker = MockNodeMaker::new();
let cpu_ctrl_node = setup_test_node(
CpuControlParams {
num_cores: 1,
p_states: vec![PState { frequency: Hertz(0.0), voltage: Volts(0.0) }],
capacitance: Farads(0.0),
},
mock_maker.make("StatsNode", vec![]),
mock_maker.make(
"DevHostNode",
vec![
// The CpuControlHandler node queries the current performance state from the
// DevHost node during initialization.
(msg_eq!(GetPerformanceState), msg_ok_return!(GetPerformanceState(0))),
],
),
)
.await;
assert_matches!(
cpu_ctrl_node.handle_message(&Message::ReadTemperature).await,
Err(PowerManagerError::Unsupported)
);
}
/// Tests that CpuControlParams' `validate` correctly returns an error under invalid inputs.
#[test]
fn test_invalid_cpu_params() {
// Num_cores == 0
assert!(CpuControlParams {
num_cores: 0,
p_states: vec![PState { frequency: Hertz(0.0), voltage: Volts(0.0) }],
capacitance: Farads(100e-12)
}
.validate()
.is_err());
// Empty p_states
assert!(CpuControlParams { num_cores: 1, p_states: vec![], capacitance: Farads(100e-12) }
.validate()
.is_err());
// p_states in order of increasing power usage
assert!(CpuControlParams {
num_cores: 1,
p_states: vec![
PState { frequency: Hertz(1.0), voltage: Volts(1.0) },
PState { frequency: Hertz(2.0), voltage: Volts(1.0) }
],
capacitance: Farads(100e-12)
}
.validate()
.is_err());
// p_states with identical power usage
assert!(CpuControlParams {
num_cores: 1,
p_states: vec![
PState { frequency: Hertz(1.0), voltage: Volts(1.0) },
PState { frequency: Hertz(1.0), voltage: Volts(1.0) }
],
capacitance: Farads(100e-12)
}
.validate()
.is_err());
}
async fn get_perf_state(devhost_node: Rc<dyn Node>) -> u32 {
match devhost_node.handle_message(&Message::GetPerformanceState).await.unwrap() {
MessageReturn::GetPerformanceState(state) => state,
e => panic!("Unexpected return value: {:?}", e),
}
}
/// Tests that the SetMaxPowerConsumption message causes the node to correctly consider CPU load
/// and parameters to choose the appropriate P-states.
#[fasync::run_singlethreaded(test)]
async fn test_set_max_power_consumption() {
let mut mock_maker = MockNodeMaker::new();
// Arbitrary CpuControlParams chosen to allow the node to demonstrate P-state selection
let cpu_params = CpuControlParams {
num_cores: 4,
p_states: vec![
PState { frequency: Hertz(2.0e9), voltage: Volts(5.0) },
PState { frequency: Hertz(2.0e9), voltage: Volts(4.0) },
PState { frequency: Hertz(2.0e9), voltage: Volts(3.0) },
],
capacitance: Farads(100.0e-12),
};
// The modeled power consumption at each P-state
let power_consumption: Vec<Watts> = cpu_params
.p_states
.iter()
.map(|p_state| {
get_cpu_power(
cpu_params.capacitance,
p_state.voltage,
p_state.frequency.mul_scalar(cpu_params.num_cores as f64),
)
})
.collect();
let stats_node = mock_maker.make(
"StatsNode",
// The CpuControlHandler node queries the current CPU load each time it receives a
// SetMaxPowerConsumption message
vec![
(msg_eq!(GetTotalCpuLoad), msg_ok_return!(GetTotalCpuLoad(4.0))),
(msg_eq!(GetTotalCpuLoad), msg_ok_return!(GetTotalCpuLoad(4.0))),
(msg_eq!(GetTotalCpuLoad), msg_ok_return!(GetTotalCpuLoad(4.0))),
],
);
let devhost_node = mock_maker.make(
"DevHostNode",
vec![
// CpuControlHandler queries performance state during its initialiation
(msg_eq!(GetPerformanceState), msg_ok_return!(GetPerformanceState(0))),
// The test queries for current performance state
(msg_eq!(GetPerformanceState), msg_ok_return!(GetPerformanceState(0))),
// CpuControlHandler changes performance state to 1
(msg_eq!(SetPerformanceState(1)), msg_ok_return!(SetPerformanceState)),
// The test queries for current performance state
(msg_eq!(GetPerformanceState), msg_ok_return!(GetPerformanceState(1))),
// CpuControlHandler changes performance state to 2
(msg_eq!(SetPerformanceState(2)), msg_ok_return!(SetPerformanceState)),
// The test queries for current performance state
(msg_eq!(GetPerformanceState), msg_ok_return!(GetPerformanceState(1))),
],
);
let cpu_ctrl_node = setup_test_node(cpu_params, stats_node, devhost_node.clone()).await;
// Test case 1: Allow power consumption of the highest P-state; expect to be in P-state 0
let commanded_power = power_consumption[0].mul_scalar(1.01);
let expected_power = power_consumption[0];
assert_matches!(
cpu_ctrl_node.handle_message(&Message::SetMaxPowerConsumption(commanded_power)).await,
Ok(MessageReturn::SetMaxPowerConsumption(power)) if power == expected_power
);
assert_eq!(get_perf_state(devhost_node.clone()).await, 0);
// Test case 2: Lower power consumption to that of P-state 1; expect to be in P-state 1
let commanded_power = power_consumption[1].mul_scalar(1.01);
let expected_power = power_consumption[1];
assert_matches!(
cpu_ctrl_node.handle_message(&Message::SetMaxPowerConsumption(commanded_power)).await,
Ok(MessageReturn::SetMaxPowerConsumption(power)) if power == expected_power
);
assert_eq!(get_perf_state(devhost_node.clone()).await, 1);
// Test case 3: Reduce the power consumption limit below the lowest P-state; expect to drop
// to the lowest P-state
let commanded_power = Watts(0.0);
let expected_power = power_consumption[2];
assert_matches!(
cpu_ctrl_node.handle_message(&Message::SetMaxPowerConsumption(commanded_power)).await,
Ok(MessageReturn::SetMaxPowerConsumption(power)) if power == expected_power
);
assert_eq!(get_perf_state(devhost_node.clone()).await, 1);
}
/// Tests that when a minimum CPU clock speed is specified, a P-state with a lower CPU frequency
/// is never selected.
#[fasync::run_singlethreaded(test)]
async fn test_min_cpu_clock_speed() {
let mut mock_maker = MockNodeMaker::new();
// Arbitrary CpuControlParams chosen to allow the node to demonstrate P-state selection
let capacitance = Farads(100.0e-12);
let cpu_params = CpuControlParams {
num_cores: 4,
p_states: vec![
PState { frequency: Hertz(2.0e9), voltage: Volts(3.0) },
PState { frequency: Hertz(1.0e9), voltage: Volts(3.0) },
PState { frequency: Hertz(0.5e9), voltage: Volts(3.0) },
],
capacitance,
};
// The modeled power consumption at each P-state
let power_consumption: Vec<Watts> = cpu_params
.p_states
.iter()
.map(|p_state| {
get_cpu_power(
capacitance,
p_state.voltage,
Hertz(p_state.frequency.0 * cpu_params.num_cores as f64),
)
})
.collect();
let stats_node = mock_maker.make(
"StatsNode",
// The CpuControlHandler node queries the current CPU load each time it receives a
// SetMaxPowerConsumption message
vec![
(msg_eq!(GetTotalCpuLoad), msg_ok_return!(GetTotalCpuLoad(4.0))),
(msg_eq!(GetTotalCpuLoad), msg_ok_return!(GetTotalCpuLoad(4.0))),
],
);
let devhost_node = mock_maker.make(
"DevHostNode",
vec![
// CpuControlHandler lazy queries performance state during its initialiation
(msg_eq!(GetPerformanceState), msg_ok_return!(GetPerformanceState(0))),
// The test queries for current performance state
(msg_eq!(GetPerformanceState), msg_ok_return!(GetPerformanceState(0))),
// CpuControlHandler changes performance state to 1
(msg_eq!(SetPerformanceState(1)), msg_ok_return!(SetPerformanceState)),
// The test queries for current performance state
(msg_eq!(GetPerformanceState), msg_ok_return!(GetPerformanceState(1))),
],
);
let cpu_ctrl_node = CpuControlHandlerBuilder::new_with_proxy(
"Fake".to_string(),
setup_fake_service(cpu_params),
capacitance,
stats_node,
devhost_node.clone(),
)
.with_min_cpu_clock_speed(Hertz(1.0e9))
.build()
.await
.unwrap();
// Test case 1: Allow power consumption of the highest P-state; expect to be in P-state 0
let commanded_power = power_consumption[0].mul_scalar(1.01);
let expected_power = power_consumption[0];
assert_matches!(
cpu_ctrl_node.handle_message(&Message::SetMaxPowerConsumption(commanded_power)).await,
Ok(MessageReturn::SetMaxPowerConsumption(power)) if power == expected_power
);
assert_eq!(get_perf_state(devhost_node.clone()).await, 0);
// Test case 2: Reduce power consumption to below the lowest P-state; expect to be in
// P-state 1 (state 2 should be disallowed).
let commanded_power = Watts(0.0);
let expected_power = power_consumption[1];
assert_matches!(
cpu_ctrl_node.handle_message(&Message::SetMaxPowerConsumption(commanded_power)).await,
Ok(MessageReturn::SetMaxPowerConsumption(power)) if power == expected_power
);
assert_eq!(get_perf_state(devhost_node.clone()).await, 1);
}
/// Tests for the presence and correctness of dynamically-added inspect data
#[fasync::run_singlethreaded(test)]
async fn test_inspect_data() {
let mut mock_maker = MockNodeMaker::new();
// Some dummy CpuControlParams to verify the params get published in Inspect
let num_cores = 4;
let p_state = PState { frequency: Hertz(2.0e9), voltage: Volts(4.5) };
let capacitance = Farads(100.0e-12);
let params = CpuControlParams { num_cores, p_states: vec![p_state], capacitance };
let inspector = inspect::Inspector::new();
let _node = CpuControlHandlerBuilder::new_with_proxy(
"Fake".to_string(),
setup_fake_service(params),
capacitance,
mock_maker.make("StatsNode", vec![]),
mock_maker.make(
"DevHostNode",
vec![
// The CpuControlHandler node queries the current performance state from the
// DevHost node during initialization.
(msg_eq!(GetPerformanceState), msg_ok_return!(GetPerformanceState(0))),
],
),
)
.with_inspect_root(inspector.root())
.build()
.await
.unwrap();
assert_inspect_tree!(
inspector,
root: {
"CpuControlHandler (Fake)": contains {
cpu_control_params: {
"capacitance (F)": capacitance.0,
num_cores: num_cores as u64,
p_state_0: {
"voltage (V)": p_state.voltage.0,
"frequency (Hz)": p_state.frequency.0
}
},
}
}
);
}
/// Tests that well-formed configuration JSON does not panic the `new_from_json` function.
#[fasync::run_singlethreaded(test)]
async fn test_new_from_json() {
let json_data = json::json!({
"type": "CpuControlHandler",
"name": "cpu_control",
"config": {
"driver_path": "/dev/class/cpu-ctrl/000",
"capacitance": 1.2E-10,
"min_cpu_clock_speed": 1.0e9
},
"dependencies": {
"cpu_stats_handler_node": "cpu_stats",
"cpu_dev_handler_node": "cpu_dev"
}
});
let mut mock_maker = MockNodeMaker::new();
let mut nodes: HashMap<String, Rc<dyn Node>> = HashMap::new();
nodes.insert("cpu_stats".to_string(), mock_maker.make("MockNode", vec![]));
nodes.insert("cpu_dev".to_string(), mock_maker.make("MockNode", vec![]));
let _ = CpuControlHandlerBuilder::new_from_json(json_data, &nodes);
}
}