blob: bdb8b5e5fbedac245923fd10b9506fafc0fb6830 [file] [log] [blame]
// Copyright 2020 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::types::{Celsius, Microseconds, Nanoseconds};
use crate::utils::{CobaltIntHistogram, CobaltIntHistogramConfig};
use fuchsia_async as fasync;
use fuchsia_cobalt::{CobaltConnector, CobaltSender, ConnectionType};
use lazy_static::lazy_static;
use parking_lot::Mutex;
use power_manager_metrics::power_manager_metrics as power_metrics_registry;
use power_metrics_registry::ThermalLimitResultMetricDimensionResult as thermal_limit_result;
use std::sync::Arc;
/// The CobaltMetricsImpl struct is implemented as a singleton accessible via the public
/// `get_cobalt_metrics_instance` function. This is in contrast to the node structure seen in most
/// other parts of power_manager. While it would likely be very natural to implement CobaltMetrics
/// as a node, there are some considerations that favor the current approach.
/// 1) The Cobalt metrics structure is largely a logging mechanism and as such, benefits nicely
/// global access. By providing access to the CobaltMetricsImpl instance via the public function,
/// it means that any node can be easily updated to contribute information to the instance
/// without requiring major changes to that node (adding fields to the struct, updating the
/// constructor, changing the node config file and schema, updating all affected tests, etc.).
/// 2) One of the great benefits of the node architecture is the ability to configure the node on a
/// per-product basis. For the Cobalt metrics, there is (currently) no such configuration
/// required. The CobaltMetricsImpl singleton is created without any parameters and is the same
/// without considering the product. Should the need arise for per-product configration of the
/// Cobalt metrics, it may be best to convert the structure to using a node format.
/// Functions provided by CobaltMetricsImpl.
pub trait CobaltMetrics {
fn log_raw_temperature(&self, temperature: Celsius);
fn log_throttle_start(&self, timestamp: Nanoseconds);
fn log_throttle_end_mitigated(&self, timestamp: Nanoseconds);
fn log_throttle_end_shutdown(&self, timestamp: Nanoseconds);
}
lazy_static! {
/// Global instance for Cobalt metrics.
static ref COBALT_METRICS: CobaltMetricsImpl = CobaltMetricsImpl::new();
}
/// Gets a clone of the CobaltMetricsImpl instance.
pub fn get_cobalt_metrics_instance() -> CobaltMetricsImpl {
COBALT_METRICS.clone()
}
/// Stores and dispatches Cobalt metric data.
#[derive(Clone)]
pub struct CobaltMetricsImpl {
/// Metric data with interior mutability and reference counting.
inner: Arc<Mutex<CobaltMetricsInner>>,
}
/// Metric data for CobaltMetricsImpl.
struct CobaltMetricsInner {
/// Sends Cobalt events to the Cobalt FIDL service.
cobalt_sender: CobaltSender,
/// Timestamp of the start of a throttling duration.
throttle_start_time: Option<Nanoseconds>,
/// Histogram of raw temperature readings.
temperature_histogram: CobaltIntHistogram,
}
impl CobaltMetricsInner {
/// Creates a new CobaltMetricsInner and connects to the Cobalt FIDL service.
fn new() -> Self {
let (cobalt_sender, sender_future) = CobaltConnector::default()
.serve(ConnectionType::project_id(power_metrics_registry::PROJECT_ID));
// Spawn the future that handles sending data to Cobalt
fasync::Task::local(sender_future).detach();
Self::new_with_cobalt_sender(cobalt_sender)
}
/// Creates a new CobaltMetricsInner without connecting to the Cobalt FIDL service. Returns a
/// channel to receive the Cobalt events that would have otherwise been delivered to the Cobalt
/// service.
fn new_with_cobalt_sender(cobalt_sender: CobaltSender) -> Self {
Self {
cobalt_sender: cobalt_sender,
throttle_start_time: None,
temperature_histogram: CobaltIntHistogram::new(CobaltIntHistogramConfig {
floor: power_metrics_registry::RAW_TEMPERATURE_INT_BUCKETS_FLOOR,
num_buckets: power_metrics_registry::RAW_TEMPERATURE_INT_BUCKETS_NUM_BUCKETS,
step_size: power_metrics_registry::RAW_TEMPERATURE_INT_BUCKETS_STEP_SIZE,
}),
}
}
}
impl CobaltMetrics for CobaltMetricsImpl {
/// Log the start of a thermal throttling duration. Since the timestamp is recorded and used to
/// track duration of throttling events, it is invalid to call this function consecutively
/// without first ending the existing throttling duration by calling
/// `log_throttle_end_[mitigated|shutdown]` first. If the invalid scenario is encountered, the
/// function will panic on debug builds or have no effect otherwise.
fn log_throttle_start(&self, timestamp: Nanoseconds) {
let mut data = self.inner.lock();
debug_assert!(
data.throttle_start_time.is_none(),
"throttle_start called before ending previous throttle"
);
// Record the new timestamp only if it was previously None
data.throttle_start_time = data.throttle_start_time.or(Some(timestamp));
}
/// Log the end of a thermal throttling duration due to successful mitigation. To track the
/// elapsed time of a throttling event, it is expected that `log_throttle_start` is called
/// first. Failure to do so results in a panic on debug builds or missed Cobalt events for the
/// elapsed throttling time metric otherwise.
fn log_throttle_end_mitigated(&self, timestamp: Nanoseconds) {
debug_assert!(self.inner.lock().throttle_start_time.is_some());
self.log_throttle_end_with_result(thermal_limit_result::Mitigated, timestamp)
}
/// Log the end of a thermal throttling duration due to a shutdown. To track the elapsed time of
/// a throttling event, it is expected that `log_throttle_start` is called first. However, this
/// is not strictly enforced because in a theoretical scenario we could see a call to this
/// function from the ThermalShutdown node without the ThermalPolicy node having first called
/// the `log_throttle_start` function.
fn log_throttle_end_shutdown(&self, timestamp: Nanoseconds) {
self.log_throttle_end_with_result(thermal_limit_result::Shutdown, timestamp)
}
/// Log a raw temperature reading. After `CobaltMetricsImpl::NUM_TEMPERATURE_READINGS` calls, a
/// Cobalt event is dispatched and the local data is cleared.
fn log_raw_temperature(&self, temperature: Celsius) {
let mut data = self.inner.lock();
data.temperature_histogram.add_data(temperature.0 as i64);
if data.temperature_histogram.count() == Self::NUM_TEMPERATURE_READINGS {
let hist_data = data.temperature_histogram.get_data();
data.cobalt_sender.log_int_histogram(
power_metrics_registry::RAW_TEMPERATURE_METRIC_ID,
(),
hist_data,
);
data.temperature_histogram.clear();
}
}
}
impl CobaltMetricsImpl {
/// Number of temperature readings before dispatching a Cobalt event.
const NUM_TEMPERATURE_READINGS: u32 = 100;
fn new() -> Self {
Self { inner: Arc::new(Mutex::new(CobaltMetricsInner::new())) }
}
#[cfg(test)]
fn new_with_cobalt_sender(cobalt_sender: CobaltSender) -> Self {
Self {
inner: Arc::new(Mutex::new(CobaltMetricsInner::new_with_cobalt_sender(cobalt_sender))),
}
}
/// Log the end of a thermal throttling duration with a specified reason. To track the elapsed
/// time of a throttling event, it is expected that `log_throttle_start` is called first.
/// However, this is not strictly enforced because of a special case described in
/// `log_throttle_end_shutdown`.
fn log_throttle_end_with_result(&self, result: thermal_limit_result, timestamp: Nanoseconds) {
let mut data = self.inner.lock();
if let Some(time) = data.throttle_start_time.take() {
let elapsed_time = timestamp - time;
if elapsed_time >= Nanoseconds(0) {
data.cobalt_sender.log_elapsed_time(
power_metrics_registry::THERMAL_LIMITING_ELAPSED_TIME_METRIC_ID,
(),
Microseconds::from(elapsed_time).0,
);
} else {
debug_assert!(false, "Elapsed time must not be negative");
}
}
data.cobalt_sender
.log_event(power_metrics_registry::THERMAL_LIMIT_RESULT_METRIC_ID, vec![result as u32]);
}
}
#[cfg(test)]
mod cobalt_metrics_impl_test {
use super::*;
use crate::types::Seconds;
use fidl_fuchsia_cobalt::{CobaltEvent, Event, EventPayload};
/// Tests that we can call `throttle_start` a second time if we first call `throttle_end`.
#[fasync::run_singlethreaded(test)]
async fn test_throttle_restart() {
let cobalt_metrics = CobaltMetricsImpl::new();
cobalt_metrics.log_throttle_start(Nanoseconds(0));
cobalt_metrics.log_throttle_end_mitigated(Nanoseconds(1000));
cobalt_metrics.log_throttle_start(Nanoseconds(2000));
}
/// Tests that calling CobaltMetrics `throttle_start` twice without first calling `throttle_end`
/// causes a panic in the debug configuration.
#[fasync::run_singlethreaded(test)]
#[should_panic(expected = "throttle_start called before ending previous throttle")]
#[cfg(debug_assertions)]
async fn test_double_start_panic() {
let cobalt_metrics = CobaltMetricsImpl::new();
cobalt_metrics.log_throttle_start(Nanoseconds(0));
cobalt_metrics.log_throttle_start(Nanoseconds(0));
}
/// Tests that a negative throttling elapsed time metric panics in the debug configuration.
#[fasync::run_singlethreaded(test)]
#[should_panic(expected = "Elapsed time must not be negative")]
#[cfg(debug_assertions)]
async fn test_negative_elapsed_time() {
let cobalt_metrics = CobaltMetricsImpl::new();
cobalt_metrics.log_throttle_start(Nanoseconds(10));
cobalt_metrics.log_throttle_end_mitigated(Nanoseconds(9));
}
/// Tests that two events for thermal_limiting_elapsed_time and thermal_limit_result are
/// dispatched after log calls corresponding to a single throttling event.
#[test]
fn test_throttle_elapsed_time() {
let (sender, mut receiver) = futures::channel::mpsc::channel(10);
let cobalt_metrics = CobaltMetricsImpl::new_with_cobalt_sender(CobaltSender::new(sender));
cobalt_metrics.log_throttle_start(Seconds(0.0).into());
cobalt_metrics.log_throttle_end_mitigated(Seconds(1.0).into());
// Verify the expected Cobalt event for the thermal_limiting_elapsed_time metric
assert_eq!(
receiver.try_next().unwrap().unwrap(),
CobaltEvent {
metric_id: power_metrics_registry::THERMAL_LIMITING_ELAPSED_TIME_METRIC_ID,
event_codes: vec![],
component: None,
payload: EventPayload::ElapsedMicros(Microseconds::from(Seconds(1.0)).0)
}
);
// Verify the expected Cobalt event for the thermal_limit_result metric
assert_eq!(
receiver.try_next().unwrap().unwrap(),
CobaltEvent {
metric_id: power_metrics_registry::THERMAL_LIMIT_RESULT_METRIC_ID,
event_codes: vec![thermal_limit_result::Mitigated as u32],
component: None,
payload: EventPayload::Event(Event),
}
);
// Verify there were no more dispatched Cobalt events
assert!(receiver.try_next().is_err());
}
/// Tests that an event for thermal_limit_result is dispatched after a log call corresponding to
/// a thermal shutdown.
#[test]
fn test_throttle_end_shutdown() {
let (sender, mut receiver) = futures::channel::mpsc::channel(10);
let cobalt_metrics = CobaltMetricsImpl::new_with_cobalt_sender(CobaltSender::new(sender));
cobalt_metrics.log_throttle_end_shutdown(Nanoseconds(0));
// Verify the expected Cobalt event for the thermal_limit_result metric
assert_eq!(
receiver.try_next().unwrap().unwrap(),
CobaltEvent {
metric_id: power_metrics_registry::THERMAL_LIMIT_RESULT_METRIC_ID,
event_codes: vec![thermal_limit_result::Shutdown as u32],
component: None,
payload: EventPayload::Event(Event),
}
);
// Verify there were no more dispatched Cobalt events
assert!(receiver.try_next().is_err());
}
/// Tests that a Cobalt event for raw_temperature is dispatched after log calls for the expected
/// number of raw temperature samples.
#[test]
fn test_raw_temperature() {
let (sender, mut receiver) = futures::channel::mpsc::channel(10);
let cobalt_metrics = CobaltMetricsImpl::new_with_cobalt_sender(CobaltSender::new(sender));
let num_temperature_readings = CobaltMetricsImpl::NUM_TEMPERATURE_READINGS + 1;
for _ in 0..num_temperature_readings {
cobalt_metrics.log_raw_temperature(Celsius(50.0));
}
// Generate the expected raw_temperature Cobalt event
let mut expected_histogram = CobaltIntHistogram::new(CobaltIntHistogramConfig {
floor: power_metrics_registry::RAW_TEMPERATURE_INT_BUCKETS_FLOOR,
num_buckets: power_metrics_registry::RAW_TEMPERATURE_INT_BUCKETS_NUM_BUCKETS,
step_size: power_metrics_registry::RAW_TEMPERATURE_INT_BUCKETS_STEP_SIZE,
});
for _ in 0..num_temperature_readings - 1 {
expected_histogram.add_data(Celsius(50.0).0 as i64);
}
let expected_cobalt_event = CobaltEvent {
metric_id: power_metrics_registry::RAW_TEMPERATURE_METRIC_ID,
event_codes: vec![],
component: None,
payload: EventPayload::IntHistogram(expected_histogram.get_data()),
};
// Verify that the expected Cobalt event was received, and there were no extra events
assert_eq!(receiver.try_next().unwrap().unwrap(), expected_cobalt_event);
assert!(receiver.try_next().is_err());
}
}
#[cfg(test)]
pub mod mock_cobalt_metrics {
use super::*;
use anyhow::Context;
use std::cell::RefCell;
use std::cell::RefMut;
use std::collections::VecDeque;
use std::rc::Rc;
/// Mock implementation of CobaltMetrics that is used for verifying a user of CobaltMetricsImpl
/// calls the expected logging functions.
#[derive(Clone)]
pub struct MockCobaltMetrics {
expected_logs: Rc<RefCell<VecDeque<LogType>>>,
}
/// Log types that the mock supports.
#[derive(PartialEq, Debug)]
pub enum LogType {
LogRawTemperature(Celsius),
LogThrottleStart(Nanoseconds),
LogThrottleEndMitigated(Nanoseconds),
LogThrottleEndShutdown(Nanoseconds),
}
impl MockCobaltMetrics {
pub fn new() -> Self {
Self { expected_logs: Rc::new(RefCell::new(VecDeque::new())) }
}
/// Returns a mutable borrow of `expected_logs`.
fn expected_logs_mut(&self) -> RefMut<'_, VecDeque<LogType>> {
self.expected_logs.borrow_mut()
}
/// Asserts that all expected log calls have been made.
pub fn verify(&self, err_str: &str) {
assert!(
self.expected_logs.borrow().is_empty(),
"{}. Leftover expected calls: {:?}",
err_str,
self.expected_logs.borrow()
);
}
pub fn expect_log_raw_temperature(&self, temperature: Celsius) {
self.expected_logs_mut().push_back(LogType::LogRawTemperature(temperature));
}
pub fn expect_log_throttle_start(&self, timestamp: Nanoseconds) {
self.expected_logs_mut().push_back(LogType::LogThrottleStart(timestamp));
}
pub fn expect_log_throttle_end_mitigated(&self, timestamp: Nanoseconds) {
self.expected_logs_mut().push_back(LogType::LogThrottleEndMitigated(timestamp));
}
pub fn expect_log_throttle_end_shutdown(&self, timestamp: Nanoseconds) {
self.expected_logs_mut().push_back(LogType::LogThrottleEndShutdown(timestamp));
}
}
impl CobaltMetrics for MockCobaltMetrics {
fn log_raw_temperature(&self, temperature: Celsius) {
assert_eq!(
self.expected_logs_mut()
.pop_front()
.context("Received unexpected call to `log_raw_temperature`")
.unwrap(),
LogType::LogRawTemperature(temperature),
);
}
fn log_throttle_start(&self, timestamp: Nanoseconds) {
assert_eq!(
self.expected_logs_mut()
.pop_front()
.context("Received unexpected call to `log_throttle_start`")
.unwrap(),
LogType::LogThrottleStart(timestamp)
);
}
fn log_throttle_end_mitigated(&self, timestamp: Nanoseconds) {
assert_eq!(
self.expected_logs_mut()
.pop_front()
.context("Received unexpected call to `log_throttle_end_mitigated`")
.unwrap(),
LogType::LogThrottleEndMitigated(timestamp)
);
}
fn log_throttle_end_shutdown(&self, timestamp: Nanoseconds) {
assert_eq!(
self.expected_logs_mut()
.pop_front()
.context("Received unexpected call to `log_throttle_end_shutdown`")
.unwrap(),
LogType::LogThrottleEndShutdown(timestamp)
);
}
}
}