blob: 348f983d93ee0cadfe40ffcfcd83688d27fb465c [file] [log] [blame]
// Copyright 2022 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! Implements support for collecting and sending hibernate metrics.
use std::io::{BufRead, BufReader, Cursor, Write};
use std::process::Command;
use std::time::Duration;
use anyhow::{Context, Result};
use log::warn;
use serde::{Deserialize, Serialize};
use crate::diskfile::BouncedDiskFile;
use crate::files::{
increment_file_counter, metrics_file_exists, open_attempts_file, open_hiber_fails_file,
open_metrics_file, open_resume_failures_file,
};
use crate::hiberutil::HibernateError;
use crate::mmapbuf::MmapBuffer;
/// Bytes per MB float value.
pub const BYTES_PER_MB_F64: f64 = 1048576.0;
/// Max expected IO size for IO metrics.
pub const MAX_IO_SIZE_KB: isize = 9437000;
/// Size of the metrics buffer, 4k aligned to be compatible with BouncedDiskFile writes.
pub const METRICS_BUFFER_SIZE: usize = 4096;
/// A MetricSample represents a sample point for a Hibernate histogram in UMA.
/// It requires the histogram name, the sample value, the minimum value,
/// the maximum value, and the number of buckets
#[derive(Serialize, Deserialize)]
pub struct MetricsSample<'a> {
pub name: &'a str,
pub value: isize,
pub min: isize,
pub max: isize,
pub buckets: usize,
}
/// Define the known metrics file types.
pub enum MetricsFile {
Suspend,
Resume,
}
/// Define the hibernate metrics logger.
pub struct MetricsLogger {
pub file: Option<BouncedDiskFile>,
buf: MmapBuffer,
offset: usize,
}
impl MetricsLogger {
pub fn new() -> Result<Self> {
let buf = MmapBuffer::new(METRICS_BUFFER_SIZE)?;
Ok(Self {
file: None,
buf,
offset: 0,
})
}
/// Log a metric to the MetricsLogger buffer, flush if full.
pub fn log_metric(&mut self, metric: MetricsSample) {
let log = match serde_json::to_string(&metric) {
Ok(s) => s,
Err(e) => {
warn!("Failed to make metric string, {}", e);
return;
}
};
let log_str = format!("{}\n", log);
let metric_bytes = log_str.as_bytes();
assert!(metric_bytes.len() < self.buf.len());
let remaining = self.buf.len() - self.offset;
let copy_size = std::cmp::min(remaining, metric_bytes.len());
let end = self.offset + copy_size;
self.buf.u8_slice_mut()[self.offset..end].copy_from_slice(&metric_bytes[0..copy_size]);
self.offset += copy_size;
if self.offset == self.buf.len() {
if let Err(e) = self.flush() {
warn!("Failed to flush metrics buf to file {:?}", e);
}
let remainder = metric_bytes.len() - copy_size;
self.buf.u8_slice_mut()[0..remainder].copy_from_slice(&metric_bytes[copy_size..]);
self.offset = remainder;
}
}
/// Write the MetricsLogger buffer to the MetricsLogger file.
pub fn flush(&mut self) -> Result<()> {
let remaining = self.buf.len() - self.offset;
if remaining > 0 {
let zero = [0u8; METRICS_BUFFER_SIZE];
self.buf.u8_slice_mut()[self.offset..].copy_from_slice(&zero[..remaining]);
}
self.offset = 0;
match &mut self.file {
Some(f) => f
.write_all(self.buf.u8_slice())
.context("Failed to write metrics file"),
None => Err(HibernateError::MetricsSendFailure(
"No metrics file set.".to_string(),
))
.context("Failed to write to metrics file"),
}
}
pub fn metrics_send_io_sample(&mut self, histogram: &str, io_bytes: i64, duration: Duration) {
let rate = ((io_bytes as f64) / duration.as_secs_f64()) / BYTES_PER_MB_F64;
let base_name = "Platform.Hibernate.IO.";
// Convert the bytes to KiB for more manageable metric values.
let io_kbytes = io_bytes / 1024;
let size_metric = MetricsSample {
name: &format!("{}{}.Size", base_name, histogram),
value: io_kbytes as isize,
min: 0,
max: MAX_IO_SIZE_KB,
buckets: 50,
};
let rate_metric = MetricsSample {
name: &format!("{}{}.Rate", base_name, histogram),
value: rate as isize,
min: 0,
max: 1024,
buckets: 50,
};
let duration_metric = MetricsSample {
name: &format!("{}{}.Duration", base_name, histogram),
value: duration.as_secs() as isize,
min: 0,
max: 120,
buckets: 50,
};
self.log_metric(size_metric);
self.log_metric(rate_metric);
self.log_metric(duration_metric);
}
pub fn metrics_send_duration_sample(
&mut self,
histogram: &str,
duration: Duration,
max: isize,
) {
let mut num_buckets = 50;
if max < 50 {
num_buckets = max + 1;
}
let base_name = "Platform.Hibernate.Duration.";
let duration_metric = MetricsSample {
name: &format!("{}{}", base_name, histogram),
value: duration.as_secs() as isize,
min: 0,
max,
buckets: num_buckets as usize,
};
self.log_metric(duration_metric);
}
}
/// Send metrics_client sample.
fn metrics_send_sample(sample: &MetricsSample) -> Result<()> {
let status = Command::new("metrics_client")
.arg("--")
.arg(sample.name)
.arg(sample.value.to_string())
.arg(sample.min.to_string())
.arg(sample.max.to_string())
.arg(sample.buckets.to_string())
.status()?;
if !status.success() {
warn!(
"Failed to send metric {} {} {} {} {}",
sample.name,
sample.value.to_string(),
sample.min.to_string(),
sample.max.to_string(),
sample.buckets.to_string(),
);
return Err(HibernateError::MetricsSendFailure(format!(
"Metrics failed to send with exit code: {:?}",
status.code()
)))
.context("Failed to send metrics");
}
Ok(())
}
pub fn log_hibernate_attempt() -> Result<()> {
let mut f = open_attempts_file()?;
increment_file_counter(&mut f)
}
pub fn log_hibernate_failure() -> Result<()> {
let mut f = open_hiber_fails_file()?;
increment_file_counter(&mut f)
}
pub fn log_resume_failure() -> Result<()> {
let mut f = open_resume_failures_file()?;
increment_file_counter(&mut f)
}
fn read_and_send_metrics_file(name: MetricsFile) -> Result<()> {
if !metrics_file_exists(&name) {
return Ok(());
}
let mut metrics_file = open_metrics_file(name)?;
let mut reader = BufReader::new(&mut metrics_file);
let mut buf = Vec::<u8>::new();
reader.read_until(0, &mut buf)?;
// Now split that big buffer into lines.
let len_without_delim = buf.len() - 1;
let cursor = Cursor::new(&buf[..len_without_delim]);
for line in cursor.lines() {
let line = match line {
Ok(l) => l,
Err(e) => {
warn!("Failed to read metrics line, {}", e);
continue;
}
};
let sample: MetricsSample = match serde_json::from_str(&line) {
Ok(s) => s,
Err(e) => {
warn!("Failed to make metric string, {}", e);
continue;
}
};
let _ = metrics_send_sample(&sample);
}
// Overwrite the metrics file with zeros to avoid stale metrics in next iteration.
metrics_file.rewind()?;
let zero = [0u8; METRICS_BUFFER_SIZE];
metrics_file
.write_all(&zero)
.context("Failed to zero-out metrics file")
}
pub fn read_and_send_metrics() {
if let Err(e) = read_and_send_metrics_file(MetricsFile::Suspend) {
warn!("Failed to read suspend metrics, {}", e);
}
if let Err(e) = read_and_send_metrics_file(MetricsFile::Resume) {
warn!("Failed to read resume metrics, {}", e);
}
}