blob: cb439d7bdd7e4de77fb361687a5861457468b20a [file] [log] [blame]
// Copyright 2022 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::error::PowerManagerError;
use crate::log_if_err;
use crate::message::{Message, MessageReturn};
use crate::node::Node;
use crate::platform_metrics::PlatformMetric;
use crate::types::ThermalLoad;
use anyhow::{format_err, Error};
use async_trait::async_trait;
use async_utils::hanging_get::server as hanging_get;
use fidl_fuchsia_thermal as fthermal;
use fuchsia_async as fasync;
use fuchsia_component::server::{ServiceFs, ServiceFsDir, ServiceObjLocal};
use fuchsia_inspect::{self as inspect, NumericProperty, Property};
use futures::prelude::*;
use futures::TryStreamExt;
use log::*;
use serde_derive::Deserialize;
use serde_json as json;
use std::cell::RefCell;
use std::collections::HashMap;
use std::iter::FromIterator as _;
use std::path::Path;
use std::rc::Rc;
use thermal_config::{ClientConfig, ThermalConfig};
/// Node: ThermalStateHandler
///
/// Summary: This node is responsible for hosting the `fuchsia.thermal.ClientStateConnector`
/// service, which allows thermal clients to connect to the service and retrieve their current
/// thermal state using a hanging-get pattern. A client's thermal state is determined according to
/// its central thermal configuration, which is detailed under
/// //src/power/power-manager/thermal_config/README.md. As thermal load changes are sent to this
/// node via the `UpdateThermalLoad` message, the resulting thermal state changes for each client
/// type are communicated to the respective clients using the `fuchsia.thermal.ClientStateWatcher`
/// protocol.
///
/// Handles Messages:
/// - UpdateThermalLoad
///
/// Sends Messages:
/// - LogPlatformMetric
///
/// FIDL dependencies:
/// - fuchsia.thermal.ClientStateConnector: the node hosts this service to allow thermal clients
/// to connect a `fuchsia.thermal.ClientStateWatcher` server end to the state of a specific
/// thermal client type.
/// - fuchsia.thermal.ClientStateWatcher: a client can provide the server end of a
/// `fuchsia.thermal.ClientStateWatcher` channel to be connected to the thermal state of a
/// specific thermal client type using the `fuchsia.thermal.ClientStateConnector/Connect`
/// method.
pub struct ThermalStateHandlerBuilder<'a, 'b> {
thermal_config: Option<ThermalConfig>,
outgoing_svc_dir: Option<ServiceFsDir<'a, ServiceObjLocal<'b, ()>>>,
inspect_root: Option<&'a inspect::Node>,
platform_metrics: Rc<dyn Node>,
}
impl<'a, 'b> ThermalStateHandlerBuilder<'a, 'b> {
const THERMAL_CONFIG_PATH: &'static str = "/pkg/config/power_manager/thermal_config.json";
pub fn new_from_json(
json_data: json::Value,
nodes: &HashMap<String, Rc<dyn Node>>,
service_fs: &'a mut ServiceFs<ServiceObjLocal<'b, ()>>,
) -> Self {
#[derive(Deserialize)]
struct Dependencies {
platform_metrics_node: String,
}
#[derive(Deserialize)]
struct JsonData {
dependencies: Dependencies,
}
let data: JsonData = json::from_value(json_data).unwrap();
Self {
thermal_config: None,
outgoing_svc_dir: Some(service_fs.dir("svc")),
inspect_root: None,
platform_metrics: nodes[&data.dependencies.platform_metrics_node].clone(),
}
}
pub fn build(self) -> Result<Rc<ThermalStateHandler>, Error> {
// Create the root Inspect node for the ThermalStateHandler node. Allow inspect_root
// override for tests.
let inspect = self
.inspect_root
.unwrap_or(inspect::component::inspector().root())
.create_child("ThermalStateHandler");
// Read the thermal config file from `THERMAL_CONFIG_PATH`. Allow override for testing.
let thermal_config = match self.thermal_config {
Some(thermal_config) => thermal_config,
None => ThermalConfig::read(&Path::new(Self::THERMAL_CONFIG_PATH))?,
};
let metrics_tracker =
MetricsTracker::new(inspect.create_child("ThermalLoadStates"), self.platform_metrics);
let node = Rc::new(ThermalStateHandler {
client_states: ClientStates::new(thermal_config, &inspect),
metrics_tracker: RefCell::new(metrics_tracker),
_inspect: inspect,
});
// Publish the Controller service only if we were provided with a ServiceFs
if let Some(outgoing_svc_dir) = self.outgoing_svc_dir {
node.clone().publish_connector_service(outgoing_svc_dir);
}
Ok(node)
}
}
/// The ThermalStateHandler node.
pub struct ThermalStateHandler {
/// Configuration and state for all supported thermal clients.
client_states: ClientStates,
/// Records metrics around thermal load and throttling state, forwarding them along to the
/// PlatformMetrics node.
metrics_tracker: RefCell<MetricsTracker>,
/// Root inspect node for the ThermalStateHandler.
_inspect: inspect::Node,
}
/// Simple newtype to represent a client's thermal state value.
#[derive(Debug, PartialEq, PartialOrd, Copy, Clone)]
struct ThermalState(u32);
/// Stores the configuration and state for all supported thermal clients.
///
/// The underlying HashMap maps client type strings to their corresponding `ClientState` entry.
struct ClientStates(RefCell<HashMap<String, ClientState>>);
impl ClientStates {
/// Creates a new `ClientStates` instance based on the provided `ThermalConfig`.
///
/// The underlying map of the new `ClientStates` instance is created to contain a `ClientState`
/// entry for each client type present in the provided `ThermalConfig`.
fn new(thermal_config: ThermalConfig, inspect_parent: &inspect::Node) -> Self {
let client_states =
HashMap::from_iter(thermal_config.into_iter().map(|(client_type, client_config)| {
let client_state = ClientState::new(
client_config,
new_state_broker(),
ClientStateInspect::new(inspect_parent.create_child(&client_type)),
);
(client_type, client_state)
}));
Self(RefCell::new(client_states))
}
/// Processes a new thermal load for the given sensor.
///
/// This function takes the new thermal load value and simply passes it through to each
/// contained `ClientState` entry.
fn process_new_thermal_load(&self, thermal_load: ThermalLoad, sensor: &str) {
fuchsia_trace::duration!(
"power_manager",
"ThermalStateHandler::process_new_thermal_load",
"thermal_load" => thermal_load.0,
"sensor" => sensor
);
self.0
.borrow_mut()
.values_mut()
.for_each(|client_state| client_state.process_new_thermal_load(thermal_load, sensor));
}
/// Connects a `fuchsia.thermal.ClientStateWatcher` request stream to a specific client type.
///
/// If successful, the incoming `Watch` requests on the stream will be completed with the
/// thermal state of the given `client_type` as the state changes.
fn connect_stream_for_client(
&self,
client_type: &str,
stream: fthermal::ClientStateWatcherRequestStream,
) -> Result<(), Error> {
fuchsia_trace::duration!(
"power_manager",
"ThermalStateHandler::connect_stream_for_client",
"client_type" => client_type
);
match self.0.borrow_mut().get_mut(client_type) {
Some(client_state) => {
client_state.connect_stream(stream);
Ok(())
}
None => Err(format_err!("Unsupported client type: {}", client_type)),
}
}
}
/// Stores the configuration and state for a single thermal client.
struct ClientState {
/// Vector of `TripPointState`s which forms the client's thermal config.
trip_point_states: Vec<TripPointState>,
/// We pass new thermal state values to the publisher, which takes care of updating the remote
/// clients using hanging-gets.
state_publisher: ClientStatePublisher,
/// We use the broker to vend a new `ClientStateSubscriber` for each new `ClientStateWatcher`
/// request stream.
state_broker: ClientStateBroker,
/// Cached `ThermalState` value. Simply used to determine if the value has changed.
thermal_state: ThermalState,
/// Structure to track and own Inspect data for the `ClientState`.
inspect: ClientStateInspect,
}
impl ClientState {
fn new(
client_config: ClientConfig,
state_broker: ClientStateBroker,
inspect: ClientStateInspect,
) -> Self {
// Create a vector of `TripPointState`s according to the provided `ClientConfig`
let mut trip_point_states = create_trip_point_states_from_client_config(client_config);
// Sort the vector in decreasing state order so when we iterate it in `get_thermal_state` we
// select the highest thermal state
trip_point_states.sort_by(|tps0, tps1| tps1.state.0.cmp(&tps0.state.0));
Self {
trip_point_states,
state_publisher: state_broker.new_publisher(),
state_broker,
thermal_state: ThermalState(0),
inspect,
}
}
/// Connects a new `ClientStateWatcher` request stream to this client's thermal state.
fn connect_stream(&mut self, stream: fthermal::ClientStateWatcherRequestStream) {
self.inspect.connect_count.add(1);
spawn_watcher_handler(stream, self.state_broker.new_subscriber());
}
/// Updates the client's active trip points based on the new thermal load for the given sensor.
///
/// The function iterates through the vector of `TripPointState`s, filtering out those which do
/// not match the sensor whose thermal load has changed, and sets each `TripPointState` to be
/// active or inactive based on the new thermal load for the sensor.
fn update_active_trip_points(&mut self, thermal_load: ThermalLoad, sensor: &str) {
self.trip_point_states
.iter_mut()
.filter(|trip_point| trip_point.sensor == sensor)
.for_each(|trip_point| {
// For a trip point to be marked active, the thermal load must be at least greater
// or equal to the `activate_at` threshold, OR the `deactivate_below` threshold if
// the trip point is already active
let activation_threshold = if trip_point.is_active {
trip_point.deactivate_below
} else {
trip_point.activate_at
};
trip_point.is_active = thermal_load >= activation_threshold;
});
}
/// Gets the current thermal state of the client.
///
/// Iterates through the client's vector of `TripPointState`s, returning the `state` of the
/// first encountered active trip point. Since `trip_point_states` is sorted in decreasing state
/// order, this iteration will yield the highest thermal state of all activate trip points.
fn get_thermal_state(&self) -> ThermalState {
match self.trip_point_states.iter().find(|trip_point| trip_point.is_active) {
Some(trip_point_state) => trip_point_state.state,
None => ThermalState(0),
}
}
/// Processes a new thermal load for the given sensor.
///
/// First, each trip point of `trip_point_states` is updated to active/inactive based on the new
/// thermal load for the given sensor. Next, the client's new thermal state is determined
/// according to the new currently activate trip points. If the thermal state has changed, then
/// the new value is passed to the publisher, where remote thermal clients will see the new
/// value.
fn process_new_thermal_load(&mut self, thermal_load: ThermalLoad, sensor: &str) {
self.update_active_trip_points(thermal_load, sensor);
let new_thermal_state = self.get_thermal_state();
if new_thermal_state != self.thermal_state {
self.thermal_state = new_thermal_state;
self.inspect.thermal_state.set(new_thermal_state.0.into());
self.state_publisher.set(new_thermal_state);
}
}
}
/// A structure that correlates a trip point (and underlying `sensor, `activate_at`, and
/// `deactivate_below` configuration) to a resulting thermal state and active status.
struct TripPointState {
sensor: String,
activate_at: ThermalLoad,
deactivate_below: ThermalLoad,
is_active: bool,
state: ThermalState,
}
/// Creates a vector of `TripPointState`s from the given `ClientConfig`.
///
/// The vector is essentially just a flattened out view of the `ClientConfig`, which allows for
/// convenient iteration later when determining which trip points and states are active.
fn create_trip_point_states_from_client_config(client_config: ClientConfig) -> Vec<TripPointState> {
client_config
.into_thermal_states()
.into_iter()
.map(|state_config| {
let state = ThermalState(state_config.state);
state_config.trip_points.into_iter().map(move |trip_point| TripPointState {
sensor: trip_point.sensor,
activate_at: ThermalLoad(trip_point.activate_at),
deactivate_below: ThermalLoad(trip_point.deactivate_below),
is_active: false,
state,
})
})
.flatten()
.collect()
}
/// Spawns a `Task` to handle `Watch` requests from a `fuchsia.thermal.ClientStateWatcher` channel.
///
/// The `Watch` requests will be fulfilled by registering them with the provided `subscriber`. The
/// `subscriber` is tied to the thermal state for a specific client. Therefore, the `Watch` requests
/// will be responded to with the thermal state of the specific client type of `subscriber`.
fn spawn_watcher_handler(
mut stream: fthermal::ClientStateWatcherRequestStream,
subscriber: ClientStateSubscriber,
) {
fuchsia_trace::duration!("power_manager", "ThermalStateHandler::spawn_watcher_handler");
fasync::Task::local(
async move {
while let Some(fthermal::ClientStateWatcherRequest::Watch { responder }) =
stream.try_next().await?
{
fuchsia_trace::duration!(
"power_manager",
"ThermalStateHandler::spawn_watcher_handler::Watch"
);
// The responder for the `Watch` FIDL request is now owned by the subscriber. The
// request will be completed with the new thermal state once it is ready.
subscriber.register(responder)?
}
Ok(())
}
.unwrap_or_else(|e: anyhow::Error| error!("{:?}", e)),
)
.detach();
}
/// A structure to own and track Inspect data for a single `ClientState` instance.
struct ClientStateInspect {
thermal_state: inspect::UintProperty,
// TODO(fxbug.dev/93970): track # of active connections instead of just connect count
connect_count: inspect::UintProperty,
_client_node: inspect::Node,
}
impl ClientStateInspect {
fn new(client_node: inspect::Node) -> Self {
let thermal_state = client_node.create_uint("thermal_state", 0);
let connect_count = client_node.create_uint("connect_count", 0);
Self { thermal_state, connect_count, _client_node: client_node }
}
}
// Below are a series of type aliases for convenience
type WatchResponder = fthermal::ClientStateWatcherWatchResponder;
type StateChangeFn = Box<dyn Fn(&ThermalState, WatchResponder) -> bool>;
type ClientStateBroker = hanging_get::HangingGet<ThermalState, WatchResponder, StateChangeFn>;
type ClientStatePublisher = hanging_get::Publisher<ThermalState, WatchResponder, StateChangeFn>;
type ClientStateSubscriber = hanging_get::Subscriber<ThermalState, WatchResponder, StateChangeFn>;
/// Convenience function to create a new `ClientStateBroker` instance.
///
/// The broker is used to vend new `ClientStatePublisher` and `ClientStateSubscriber` instances.
///
/// When `ClientStatePublisher.set()` is called with a `ThermalState` value, then all pending
/// `Watch` requests registered with a corresponding `ClientStateSubscriber` (see the
/// `spawn_watcher_handler` function) will be completed with that value, regardless of whether that
/// value differs from the previous `ThermalState` that was sent to the client. Therefore, care
/// should be taken to only call `ClientStatePublisher.set()` when the `ThermalState` value has
/// actually changed in order to properly implement the hanging-get behavior.
fn new_state_broker() -> ClientStateBroker {
let notify_fn: StateChangeFn = Box::new(|state, responder| {
match responder.send(state.0 as u64) {
Ok(()) => true, // indicates that the client was successfully updated
Err(e) => {
error!("Failed to send thermal state to client: {}", e);
false
}
}
});
hanging_get::HangingGet::new(ThermalState(0), notify_fn)
}
impl ThermalStateHandler {
/// Publishes the `fuchsia.thermal.ClientStateConnector` service.
///
/// For each new connection, `spawn_connector_handler` is called with that connection's request
/// stream.
fn publish_connector_service<'a, 'b>(
self: Rc<Self>,
mut outgoing_svc_dir: ServiceFsDir<'a, ServiceObjLocal<'b, ()>>,
) {
outgoing_svc_dir.add_fidl_service(
move |stream: fthermal::ClientStateConnectorRequestStream| {
self.clone().spawn_connector_handler(stream);
},
);
}
/// Spawns a `Task` to handle `Connect` requests from a `fuchsia.thermal.ClientStateConnector`
/// channel.
///
/// The `Connect` requests contain a `client_type` value and a
/// `fuchsia.thermal.ClientStateWatcher` server end. If the request is valid, then the `Watch`
/// requests on the provided `ClientStateWatcher` server end will be responded to with the
/// thermal state of the given `client_type`.
fn spawn_connector_handler(
self: Rc<Self>,
mut stream: fthermal::ClientStateConnectorRequestStream,
) {
fuchsia_trace::duration!("power_manager", "ThermalStateHandler::spawn_connector_handler");
fasync::Task::local(
async move {
while let Some(req) = stream.try_next().await? {
match req {
fthermal::ClientStateConnectorRequest::Connect {
client_type,
watcher,
..
} => self
.client_states
.connect_stream_for_client(&client_type, watcher.into_stream()?)?,
}
}
Ok(())
}
.unwrap_or_else(|e: anyhow::Error| error!("{:?}", e)),
)
.detach();
}
/// Handles an `UpdateThermalLoad` message.
///
/// The new thermal load is checked for validity then passed on to each `ClientState` entry for
/// further processing.
async fn handle_update_thermal_load(
&self,
thermal_load: ThermalLoad,
sensor: &str,
) -> Result<MessageReturn, PowerManagerError> {
fuchsia_trace::duration!(
"power_manager",
"ThermalStateHandler::handle_update_thermal_load",
"thermal_load" => thermal_load.0,
"sensor" => sensor
);
if thermal_load > ThermalLoad(fthermal::MAX_THERMAL_LOAD) {
return Err(PowerManagerError::InvalidArgument(format!(
"Thermal load {:?} exceeds max {}",
thermal_load,
fthermal::MAX_THERMAL_LOAD
)));
}
// `log_thermal_load` returns true if thermal load for this sensor has changed
if self.metrics_tracker.borrow_mut().log_thermal_load(sensor, thermal_load).await {
self.client_states.process_new_thermal_load(thermal_load, sensor);
}
Ok(MessageReturn::UpdateThermalLoad)
}
}
#[async_trait(?Send)]
impl Node for ThermalStateHandler {
fn name(&self) -> String {
"ThermalStateHandler".to_string()
}
async fn handle_message(&self, msg: &Message) -> Result<MessageReturn, PowerManagerError> {
match msg {
Message::UpdateThermalLoad(thermal_load, sensor) => {
self.handle_update_thermal_load(*thermal_load, sensor).await
}
_ => Err(PowerManagerError::Unsupported),
}
}
}
struct MetricsTracker {
platform_metrics: Rc<dyn Node>,
root_node: inspect::Node,
per_sensor_metrics: HashMap<String, PerSensorMetrics>,
throttling_active: bool,
}
impl MetricsTracker {
fn new(root_node: inspect::Node, platform_metrics: Rc<dyn Node>) -> Self {
Self {
root_node,
platform_metrics,
per_sensor_metrics: HashMap::new(),
throttling_active: false,
}
}
/// Logs a thermal load value for the given sensor.
///
/// Returns true if the thermal load has changed for this sensor, otherwise false. Each time
/// this function is called, the provided thermal load value is passed on to the PlatformMetrics
/// node. If the new thermal load value results in a change in throttling state ("active" or
/// "mitigated") then this metric is reported to the PlatformMetrics node as well.
async fn log_thermal_load(&mut self, sensor_path: &str, thermal_load: ThermalLoad) -> bool {
// Always send the received thermal load value to PlatformMetrics
self.log_platform_metric(PlatformMetric::ThermalLoad(
thermal_load,
sensor_path.to_string(),
))
.await;
let mut per_sensor_metrics = {
if let Some(m) = self.per_sensor_metrics.get_mut(sensor_path) {
m
} else {
self.per_sensor_metrics.insert(
sensor_path.to_string(),
PerSensorMetrics {
thermal_load: ThermalLoad(0),
thermal_load_property: self.root_node.create_uint(sensor_path, 0),
throttled: false,
},
);
self.per_sensor_metrics.get_mut(sensor_path).unwrap()
}
};
// Bail if the value hasn't changed
if thermal_load == per_sensor_metrics.thermal_load {
return false;
}
per_sensor_metrics.thermal_load = thermal_load;
per_sensor_metrics.thermal_load_property.set(thermal_load.0.into());
per_sensor_metrics.throttled = thermal_load > ThermalLoad(0);
// If this is a transition of either:
// A) no sensors are throttled --> at least one sensor is throttled, or
// B) at least one sensor is throttled --> no sensors are throttled
// then update the PlatformMetrics node with the appropriate PlatformMetric to indicate
// "throttling active" or "throttling result mitigated", respectively.
let was_throttling_active = self.throttling_active;
self.throttling_active = self.per_sensor_metrics.values().any(|m| m.throttled);
if was_throttling_active != self.throttling_active {
let send_metric = if self.throttling_active {
PlatformMetric::ThrottlingActive
} else {
PlatformMetric::ThrottlingResultMitigated
};
self.log_platform_metric(send_metric).await;
}
true
}
async fn log_platform_metric(&self, metric: PlatformMetric) {
let msg = Message::LogPlatformMetric(metric);
log_if_err!(
self.platform_metrics.handle_message(&msg).await,
format!("Failed to log platform metric {:?}", msg)
);
}
}
/// Holds the throttle state and thermal load Inspect property for a single temperature sensor.
struct PerSensorMetrics {
thermal_load: ThermalLoad,
thermal_load_property: inspect::UintProperty,
throttled: bool,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test::mock_node::{create_dummy_node, MessageMatcher, MockNodeMaker};
use crate::{msg_eq, msg_ok_return};
use assert_matches::assert_matches;
use fuchsia_inspect::assert_data_tree;
use std::task::Poll;
use thermal_config::TripPoint;
// Takes a ServiceFs which contains the ThermalStateHandler node's implementation of the
// `fuchsia.thermal.ClientStateConnector` protocol and manages the underlying test
// infrastructure required to use the service.
struct TestEnv {
env: fuchsia_component::server::NestedEnvironment,
}
impl TestEnv {
// Takes a ServiceFs and creates a nested environment which we'll later use for connecting
// to the ThermalStateHandler node's implementation of the
// `fuchsia.thermal.ClientStateConnector` protocol.
fn new(mut service_fs: ServiceFs<ServiceObjLocal<'static, ()>>) -> Self {
let env = service_fs.create_nested_environment("env").unwrap();
fasync::Task::local(service_fs.collect()).detach();
Self { env }
}
// Connects to the `ClientStateConnector` protocol contained within the `NestedEnvironment`
// and uses this protocol to connect a fake client of the given type.
fn connect_client(&self, client_type: &str) -> FakeClient {
let connector =
self.env.connect_to_protocol::<fthermal::ClientStateConnectorMarker>().unwrap();
let (watcher_proxy, watcher_server_end) =
fidl::endpoints::create_proxy::<fthermal::ClientStateWatcherMarker>().unwrap();
// Pass the `watcher_server_end` to the node, so it will be associated with thermal
// state changes of `client_type`
assert_matches!(connector.connect(client_type, watcher_server_end), Ok(()));
FakeClient { watcher_proxy, hanging_watcher_request: RefCell::new(None) }
}
}
// A fake thermal client capable of connecting to the `ThermalStateHandler` node to watch for
// thermal state changes.
struct FakeClient {
watcher_proxy: fthermal::ClientStateWatcherProxy,
hanging_watcher_request: RefCell<Option<fidl::client::QueryResponseFut<u64>>>,
}
impl FakeClient {
// Gets the thermal state for the fake client.
//
// Since requests are using hanging-get, there are three possible return values to consider:
// - Ok(None) = the watch request succeeded but there are no updates to the client's
// thermal state (the request is now "hanging")
// - Ok(Some(state)) = the watch request succeeded and returned a new thermal state
// - Err(e) = the watch request failed
fn get_thermal_state(
&self,
executor: &mut fasync::TestExecutor,
) -> Result<Option<ThermalState>, Error> {
// If there's already a hanging request (the previous call to `get_thermal_state`
// returned `Ok(None)`), then check if that request has a response for us. If there
// wasn't already a hanging request, then send a new one on the channel
let mut watch_request = self
.hanging_watcher_request
.take() // take the Option from the RefCell
.take() // take the pending request (if any) from the Option
.unwrap_or_else(|| self.watcher_proxy.watch());
match executor.run_until_stalled(&mut watch_request) {
Poll::Pending => {
// The request is now "hanging" with the server. Cache it so we can check it for
// a response in subsequent calls to `get_thermal_state`.
self.hanging_watcher_request.replace(Some(watch_request));
Ok(None)
}
Poll::Ready(Ok(state)) => Ok(Some(ThermalState(state as u32))),
Poll::Ready(Err(e)) => Err(e.into()),
}
}
}
/// 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": "ThermalStateHandler",
"name": "thermal_state_handler",
"dependencies": {
"platform_metrics_node": "platform_metrics"
}
});
let mut nodes: HashMap<String, Rc<dyn Node>> = HashMap::new();
nodes.insert("platform_metrics".to_string(), create_dummy_node());
let _ = ThermalStateHandlerBuilder::new_from_json(
json_data,
&nodes,
&mut ServiceFs::new_local(),
);
}
/// Tests that each thermal client's state is correctly published into Inspect.
#[test]
fn test_inspect() {
let mut executor = fasync::TestExecutor::new().unwrap();
let mut service_fs = ServiceFs::new_local();
// Create a test config with two clients to verify each has their respective Inspect nodes
// updated independently
let thermal_config = ThermalConfig::new()
.add_client_config(
"client1",
ClientConfig::new().add_thermal_state(vec![TripPoint::new("sensor1", 5, 10)]),
)
.add_client_config(
"client2",
ClientConfig::new().add_thermal_state(vec![TripPoint::new("sensor1", 15, 20)]),
);
let inspector = inspect::Inspector::new();
let node = ThermalStateHandlerBuilder {
inspect_root: Some(inspector.root()),
thermal_config: Some(thermal_config),
outgoing_svc_dir: Some(service_fs.root_dir()),
platform_metrics: create_dummy_node(),
}
.build()
.unwrap();
let test_env = TestEnv::new(service_fs);
// Check for default initialized values
assert_data_tree!(
inspector,
root: {
ThermalStateHandler: {
ThermalLoadStates: {},
client1: {
thermal_state: 0u64,
connect_count: 0u64
},
client2: {
thermal_state: 0u64,
connect_count: 0u64
}
}
}
);
// Connect client2 and verify its `connect_count` is incremented
let client2 = test_env.connect_client("client2");
assert_matches!(client2.get_thermal_state(&mut executor), Ok(Some(ThermalState(0))));
assert_data_tree!(
inspector,
root: {
ThermalStateHandler: {
ThermalLoadStates: {},
client1: {
thermal_state: 0u64,
connect_count: 0u64
},
client2: {
thermal_state: 0u64,
connect_count: 1u64
}
}
}
);
// Update the thermal state for client1 and verify its `thermal_state` is updated
executor
.run_singlethreaded(node.handle_update_thermal_load(ThermalLoad(10), "sensor1"))
.unwrap();
assert_data_tree!(
inspector,
root: {
ThermalStateHandler: {
ThermalLoadStates: {
sensor1: 10u64
},
client1: {
thermal_state: 1u64,
connect_count: 0u64
},
client2: {
thermal_state: 0u64,
connect_count: 1u64
}
}
}
);
}
/// Tests that the server correctly implements the hanging-get pattern.
#[test]
fn test_hanging_get() {
let mut executor = fasync::TestExecutor::new().unwrap();
let mut service_fs = ServiceFs::new_local();
let thermal_config = ThermalConfig::new().add_client_config(
"client1",
ClientConfig::new().add_thermal_state(vec![TripPoint::new("sensor1", 0, 10)]),
);
let node = ThermalStateHandlerBuilder {
inspect_root: None,
thermal_config: Some(thermal_config),
outgoing_svc_dir: Some(service_fs.root_dir()),
platform_metrics: create_dummy_node(),
}
.build()
.unwrap();
let test_env = TestEnv::new(service_fs);
let client = test_env.connect_client("client1");
// First request gives initial thermal state
assert_matches!(client.get_thermal_state(&mut executor), Ok(Some(ThermalState(0))));
// Second request has no update
assert_matches!(client.get_thermal_state(&mut executor), Ok(None));
// Now update the thermal load
executor
.run_singlethreaded(node.handle_update_thermal_load(ThermalLoad(10), "sensor1"))
.unwrap();
// Verify the client now gets the thermal state change response
assert_matches!(client.get_thermal_state(&mut executor), Ok(Some(ThermalState(1))));
// Update thermal load, but the client's state is unchanged
executor
.run_singlethreaded(node.handle_update_thermal_load(ThermalLoad(20), "sensor1"))
.unwrap();
// Verify there is no new response for the client
assert_matches!(client.get_thermal_state(&mut executor), Ok(None));
}
/// Tests that a connect request from an unsupported `client_type` returns an error.
#[test]
fn test_unsupported_client() {
let mut executor = fasync::TestExecutor::new().unwrap();
let mut service_fs = ServiceFs::new_local();
let _node = ThermalStateHandlerBuilder {
inspect_root: None,
thermal_config: Some(ThermalConfig::new()),
outgoing_svc_dir: Some(service_fs.root_dir()),
platform_metrics: create_dummy_node(),
}
.build()
.unwrap();
let test_env = TestEnv::new(service_fs);
let client = test_env.connect_client("client1");
// Connect a client for the "client1" client type, which is not specified in our
// ThermalConfig
assert_matches!(client.get_thermal_state(&mut executor), Err(_));
}
/// Tests that an invalid thermal load update is met with an InvalidArgument error
#[fasync::run_singlethreaded(test)]
async fn test_invalid_thermal_load() {
let node = ThermalStateHandlerBuilder {
thermal_config: Some(ThermalConfig::new()),
outgoing_svc_dir: None,
inspect_root: None,
platform_metrics: create_dummy_node(),
}
.build()
.unwrap();
let result = node
.handle_message(&Message::UpdateThermalLoad(
ThermalLoad(fthermal::MAX_THERMAL_LOAD + 1),
String::new(),
))
.await;
assert_matches!(result, Err(PowerManagerError::InvalidArgument(_)));
}
/// Tests that we deliver the correct thermal state to a client, even if its state has changed
/// before the client has connected.
#[test]
fn test_initial_thermal_state() {
let mut executor = fasync::TestExecutor::new().unwrap();
let mut service_fs = ServiceFs::new_local();
let thermal_config = ThermalConfig::new().add_client_config(
"client1",
ClientConfig::new().add_thermal_state(vec![TripPoint::new("sensor1", 0, 10)]),
);
let node = ThermalStateHandlerBuilder {
inspect_root: None,
thermal_config: Some(thermal_config),
outgoing_svc_dir: Some(service_fs.root_dir()),
platform_metrics: create_dummy_node(),
}
.build()
.unwrap();
let test_env = TestEnv::new(service_fs);
let client = test_env.connect_client("client1");
// Set the initial thermal load before the client connects
executor
.run_singlethreaded(node.handle_update_thermal_load(ThermalLoad(10), "sensor1"))
.unwrap();
// When the client first connects, verify they get the latest thermal state
assert_matches!(client.get_thermal_state(&mut executor), Ok(Some(ThermalState(1))));
}
/// Tests that multiple clients connected simultaneously receive their appropriate thermal state
/// updates.
#[test]
fn test_multiple_client_types() {
let mut executor = fasync::TestExecutor::new().unwrap();
let mut service_fs = ServiceFs::new_local();
let thermal_config = ThermalConfig::new()
.add_client_config(
"client1",
ClientConfig::new()
.add_thermal_state(vec![TripPoint::new("sensor1", 1, 9)])
.add_thermal_state(vec![TripPoint::new("sensor1", 10, 19)]),
)
.add_client_config(
"client2",
ClientConfig::new()
.add_thermal_state(vec![TripPoint::new("sensor1", 10, 19)])
.add_thermal_state(vec![TripPoint::new("sensor1", 20, 29)]),
);
let node = ThermalStateHandlerBuilder {
inspect_root: None,
thermal_config: Some(thermal_config),
outgoing_svc_dir: Some(service_fs.root_dir()),
platform_metrics: create_dummy_node(),
}
.build()
.unwrap();
let test_env = TestEnv::new(service_fs);
let client1 = test_env.connect_client("client1");
let client2 = test_env.connect_client("client2");
// First request gives initial thermal state for both clients
assert_matches!(client1.get_thermal_state(&mut executor), Ok(Some(ThermalState(0))));
assert_matches!(client2.get_thermal_state(&mut executor), Ok(Some(ThermalState(0))));
// Update the thermal load for "sensor1" and verify each client is in their expected state
executor
.run_singlethreaded(node.handle_update_thermal_load(ThermalLoad(19), "sensor1"))
.unwrap();
assert_matches!(client1.get_thermal_state(&mut executor), Ok(Some(ThermalState(2))));
assert_matches!(client2.get_thermal_state(&mut executor), Ok(Some(ThermalState(1))));
// Update thermal load for "sensor1" once more
executor
.run_singlethreaded(node.handle_update_thermal_load(ThermalLoad(29), "sensor1"))
.unwrap();
// client1 state is unchanged, but client2 moves to state 2
assert_matches!(client1.get_thermal_state(&mut executor), Ok(None));
assert_matches!(client2.get_thermal_state(&mut executor), Ok(Some(ThermalState(2))));
}
/// Tests that when the temperature sensors go in and out of throttling, the correct platform
/// metric messages are sent to the PlatformMetrics node.
#[fasync::run_singlethreaded(test)]
async fn test_platform_metrics() {
// Create mock platform metrics node
let mut mock_maker = MockNodeMaker::new();
let mock_platform_metrics = mock_maker.make("mock_platform_metrics", vec![]);
let node = ThermalStateHandlerBuilder {
inspect_root: None,
thermal_config: Some(ThermalConfig::new()),
outgoing_svc_dir: None,
platform_metrics: mock_platform_metrics.clone(),
}
.build()
.unwrap();
// Inject a nonzero `ThermalLoad` value to simulate throttling on multiple sensors, then
// verify `ThrottlingActive` is sent, along with the `ThermalLoad` for each sensor.
mock_platform_metrics.add_msg_response_pair((
msg_eq!(LogPlatformMetric(PlatformMetric::ThermalLoad(
ThermalLoad(10),
"sensor1".to_string()
))),
msg_ok_return!(LogPlatformMetric),
));
mock_platform_metrics.add_msg_response_pair((
msg_eq!(LogPlatformMetric(PlatformMetric::ThrottlingActive)),
msg_ok_return!(LogPlatformMetric),
));
mock_platform_metrics.add_msg_response_pair((
msg_eq!(LogPlatformMetric(PlatformMetric::ThermalLoad(
ThermalLoad(20),
"sensor2".to_string()
))),
msg_ok_return!(LogPlatformMetric),
));
assert_matches!(
node.handle_update_thermal_load(ThermalLoad(10), "sensor1").await,
Ok(MessageReturn::UpdateThermalLoad)
);
assert_matches!(
node.handle_update_thermal_load(ThermalLoad(20), "sensor2").await,
Ok(MessageReturn::UpdateThermalLoad)
);
// Verify that unchanged thermal load still gets reported to platform metrics
mock_platform_metrics.add_msg_response_pair((
msg_eq!(LogPlatformMetric(PlatformMetric::ThermalLoad(
ThermalLoad(10),
"sensor1".to_string()
))),
msg_ok_return!(LogPlatformMetric),
));
assert_matches!(
node.handle_update_thermal_load(ThermalLoad(10), "sensor1").await,
Ok(MessageReturn::UpdateThermalLoad)
);
// Remove throttling from one sensor and verify only the `ThermalLoad` message is sent
mock_platform_metrics.add_msg_response_pair((
msg_eq!(LogPlatformMetric(PlatformMetric::ThermalLoad(
ThermalLoad(0),
"sensor1".to_string()
))),
msg_ok_return!(LogPlatformMetric),
));
assert_matches!(
node.handle_update_thermal_load(ThermalLoad(0), "sensor1").await,
Ok(MessageReturn::UpdateThermalLoad)
);
// Remove throttling from the second sensor and verify both a `ThermalLoad` message is sent
// for the sensor as well as `ThrottlingResultMitigated`
mock_platform_metrics.add_msg_response_pair((
msg_eq!(LogPlatformMetric(PlatformMetric::ThermalLoad(
ThermalLoad(0),
"sensor2".to_string()
))),
msg_ok_return!(LogPlatformMetric),
));
mock_platform_metrics.add_msg_response_pair((
msg_eq!(LogPlatformMetric(PlatformMetric::ThrottlingResultMitigated)),
msg_ok_return!(LogPlatformMetric),
));
assert_matches!(
node.handle_update_thermal_load(ThermalLoad(0), "sensor2").await,
Ok(MessageReturn::UpdateThermalLoad)
);
}
}