use crate::Component;
use crate::common::ApiSender;
use crate::monitor::MonitorSample;
use crate::ringbuffer::{RingBufferStruct, RingBufferWriter};
use std::fmt::Write as FmtWrite;
use std::fs::{File, OpenOptions};
use std::io::{Result, Seek, SeekFrom, Write, stdout};
pub enum OutputMode {
Terminal,
CsvFile(File),
NumericFile(File),
}
pub enum OutputModeKind {
Terminal,
CsvFile,
NumericFile,
}
pub struct OutputWriter {
mode: OutputMode,
overwrite: bool,
scratch: String,
}
pub trait OutputSink {
fn send(&mut self, sample: &MonitorSample) -> Result<()>;
}
pub struct OutputBundle {
writer: Option<OutputWriter>,
component: Option<Component>,
numeric_only: bool,
ringbuffer: Option<RingBufferWriter>,
#[cfg(feature = "api")]
api_sender: ApiSender,
}
impl OutputBundle {
pub fn new(
component: Option<Component>,
numeric_only: bool,
ringbuffer: Option<RingBufferWriter>,
api_sender: ApiSender,
) -> Self {
#[cfg(not(feature = "api"))]
let _ = api_sender;
Self {
writer: None,
component,
numeric_only,
ringbuffer,
#[cfg(feature = "api")]
api_sender,
}
}
pub fn set_writer(&mut self, writer: OutputWriter) {
self.writer = Some(writer);
}
pub fn clear_writer(&mut self) {
self.writer = None;
}
pub fn set_component(&mut self, component: Option<Component>) {
self.component = component;
}
pub fn has_ringbuffer(&self) -> bool {
self.ringbuffer.is_some()
}
pub fn set_ringbuffer(&mut self, ringbuffer: Option<RingBufferWriter>) {
self.ringbuffer = ringbuffer;
}
#[cfg(feature = "api")]
pub fn has_api_sender(&self) -> bool {
self.api_sender.is_some()
}
#[cfg(feature = "api")]
pub fn set_api_sender(&mut self, api_sender: ApiSender) {
self.api_sender = api_sender;
}
}
impl OutputSink for OutputBundle {
fn send(&mut self, sample: &MonitorSample) -> Result<()> {
if let Some(writer) = &mut self.writer {
match writer.mode_kind() {
OutputModeKind::Terminal => writer.write_terminal_sample(
sample,
self.component.as_ref(),
self.numeric_only,
)?,
OutputModeKind::CsvFile => {
writer.write_csv_sample(sample, self.component.as_ref())?
}
OutputModeKind::NumericFile => {
writer.write_numeric_sample(sample, self.component.as_ref())?
}
}
}
if let Some(ref rb) = self.ringbuffer {
let rb_data = RingBufferStruct::from(sample);
rb.write(rb_data);
}
#[cfg(feature = "api")]
if let Some(ref tx) = self.api_sender {
let api_data = crate::api::ApiData::from(sample);
let _ = tx.send(api_data);
}
Ok(())
}
}
impl OutputWriter {
pub fn mode_kind(&self) -> OutputModeKind {
match self.mode {
OutputMode::Terminal => OutputModeKind::Terminal,
OutputMode::CsvFile(_) => OutputModeKind::CsvFile,
OutputMode::NumericFile(_) => OutputModeKind::NumericFile,
}
}
pub fn new(file_path: Option<&str>, numeric_only: bool, overwrite: bool) -> Result<Self> {
let mode = match file_path {
Some(path) => {
let file = if overwrite {
OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)?
} else {
OpenOptions::new().create(true).append(true).open(path)?
};
if numeric_only {
OutputMode::NumericFile(file)
} else {
OutputMode::CsvFile(file)
}
}
None => OutputMode::Terminal,
};
Ok(Self {
mode,
overwrite,
scratch: String::with_capacity(256),
})
}
pub fn write_csv_header(
&mut self,
component: Option<&Component>,
has_process: bool,
has_app: bool,
) -> Result<()> {
if let OutputMode::CsvFile(ref mut file) = self.mode {
let header = match component {
Some(Component::Cpu) => "Timestamp,CPU Power (W)\n",
Some(Component::Gpu) => "Timestamp,GPU Power (W)\n",
None if has_process => {
"Timestamp,Total Power (W),CPU Power (W),GPU Power (W),CPU Usage (%),Process Power (W)\n"
}
None if has_app => {
"Timestamp,Total Power (W),CPU Power (W),GPU Power (W),CPU Usage (%),App Power (W),App PIDs\n"
}
None => "Timestamp,Total Power (W),CPU Power (W),GPU Power (W),CPU Usage (%)\n",
};
file.write_all(header.as_bytes())?;
}
Ok(())
}
pub fn write_terminal_sample(
&mut self,
sample: &MonitorSample,
component_filter: Option<&Component>,
numeric_only: bool,
) -> Result<()> {
self.write_terminal_line(
sample.cpu_power,
sample.gpu_power,
sample.total_power,
sample.cpu_usage,
sample.process_power,
sample.app_power,
component_filter,
numeric_only,
)
}
pub fn write_csv_sample(
&mut self,
sample: &MonitorSample,
component: Option<&Component>,
) -> Result<()> {
self.write_csv_line(
sample.timestamp,
sample.cpu_power,
sample.gpu_power,
sample.total_power,
sample.cpu_usage,
sample.process_power,
sample.app_power,
component,
)
}
pub fn write_numeric_sample(
&mut self,
sample: &MonitorSample,
component: Option<&Component>,
) -> Result<()> {
self.write_numeric_line(
sample.cpu_power,
sample.gpu_power,
sample.total_power,
component,
)
}
#[allow(clippy::too_many_arguments)]
pub fn write_terminal_line(
&mut self,
cpu_power: f64,
gpu_power: f64,
total_power: f64,
cpu_usage: f64,
process_power: Option<f64>,
app_power: Option<(f64, usize)>,
component_filter: Option<&Component>,
numeric_only: bool,
) -> Result<()> {
let mut out = stdout();
if numeric_only {
let v = match component_filter {
Some(Component::Cpu) => cpu_power,
Some(Component::Gpu) => gpu_power,
None => total_power,
};
writeln!(out, "{:.2}", v)?;
out.flush()?;
return Ok(());
}
let buf = &mut self.scratch;
buf.clear();
match component_filter {
Some(Component::Cpu) => {
let _ = write!(buf, "\r\x1b[2K\x1b[1;36mCPU {cpu_power:.2} W\x1b[0m");
}
Some(Component::Gpu) => {
let _ = write!(buf, "\r\x1b[2K\x1b[1;35mGPU {gpu_power:.2} W\x1b[0m");
}
None => {
if let Some(p) = process_power {
let _ = write!(
buf,
"\r\x1b[2K\x1b[1;33m⚡ Total {total_power:.2} W\x1b[0m | \
\x1b[1;36mCPU {cpu_power:.2} W\x1b[0m | \
\x1b[1;35mGPU {gpu_power:.2} W\x1b[0m | \
\x1b[1;36mCPU Usage {cpu_usage:.2}%\x1b[0m | \
\x1b[1;32mPID {p:.2} W\x1b[0m"
);
} else if let Some((p, count)) = app_power {
let _ = write!(
buf,
"\r\x1b[2K\x1b[1;33m⚡ Total {total_power:.2} W\x1b[0m | \
\x1b[1;36mCPU {cpu_power:.2} W\x1b[0m | \
\x1b[1;35mGPU {gpu_power:.2} W\x1b[0m | \
\x1b[1;36mCPU Usage {cpu_usage:.2}%\x1b[0m | \
\x1b[1;32mApp {p:.2} W ({count} PIDs)\x1b[0m"
);
} else {
let _ = write!(
buf,
"\r\x1b[2K\x1b[1;33m⚡ Total {total_power:.2} W\x1b[0m | \
\x1b[1;36mCPU {cpu_power:.2} W\x1b[0m | \
\x1b[1;35mGPU {gpu_power:.2} W\x1b[0m | \
\x1b[1;36mCPU Usage {cpu_usage:.2}%\x1b[0m"
);
}
}
}
out.write_all(buf.as_bytes())?;
out.flush()?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn write_csv_line(
&mut self,
timestamp: u64,
cpu: f64,
gpu: f64,
total: f64,
cpu_usage: f64,
process_power: Option<f64>,
app_power: Option<(f64, usize)>,
component: Option<&Component>,
) -> Result<()> {
if let OutputMode::CsvFile(ref mut file) = self.mode {
if self.overwrite {
file.set_len(0)?;
file.seek(SeekFrom::Start(0))?;
}
let buf = &mut self.scratch;
buf.clear();
match component {
Some(Component::Cpu) => {
let _ = writeln!(buf, "{timestamp},{cpu:.2}");
}
Some(Component::Gpu) => {
let _ = writeln!(buf, "{timestamp},{gpu:.2}");
}
None => match (process_power, app_power) {
(Some(p), _) => {
let _ = writeln!(
buf,
"{timestamp},{total:.2},{cpu:.2},{gpu:.2},{cpu_usage:.2},{p:.2}"
);
}
(_, Some((p, c))) => {
let _ = writeln!(
buf,
"{timestamp},{total:.2},{cpu:.2},{gpu:.2},{cpu_usage:.2},{p:.2},{c}"
);
}
_ => {
let _ = writeln!(
buf,
"{timestamp},{total:.2},{cpu:.2},{gpu:.2},{cpu_usage:.2}"
);
}
},
}
file.write_all(buf.as_bytes())?;
}
Ok(())
}
pub fn write_numeric_line(
&mut self,
cpu: f64,
gpu: f64,
total: f64,
component: Option<&Component>,
) -> Result<()> {
if let OutputMode::NumericFile(ref mut file) = self.mode {
if self.overwrite {
file.set_len(0)?;
file.seek(SeekFrom::Start(0))?;
}
let v = match component {
Some(Component::Cpu) => cpu,
Some(Component::Gpu) => gpu,
None => total,
};
writeln!(file, "{:.2}", v)?;
}
Ok(())
}
}