| // 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) |
| ); |
| } |
| } |