summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDennis Kobert <dennis@kobert.dev>2025-04-02 16:47:34 +0200
committerDennis Kobert <dennis@kobert.dev>2025-04-02 16:49:43 +0200
commita56c2e8ab39d7247d2b4c8959c306ffa07520d01 (patch)
tree7564e8190174276e0ae78959e9512c0ee82055f3
parenteb32f2c998e1efc55edcb78899df7967bd531cc3 (diff)
Implement etop
-rw-r--r--Cargo.lock9
-rw-r--r--Cargo.toml3
-rw-r--r--energy-monitor/Cargo.toml10
-rw-r--r--energy-monitor/src/energy.rs71
-rw-r--r--energy-monitor/src/main.rs214
-rw-r--r--energy-monitor/src/process.rs97
-rw-r--r--energy-monitor/src/ui.rs391
-rw-r--r--src/energy.rs6
-rw-r--r--src/socket.rs39
9 files changed, 823 insertions, 17 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 778f188..a9d52a7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1664,6 +1664,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f878075b9794c1e4ac788c95b728f26aa6366d32eeb10c7051389f898f7d067"
[[package]]
+name = "energy-monitor"
+version = "0.1.0"
+dependencies = [
+ "crossterm",
+ "procfs",
+ "ratatui",
+]
+
+[[package]]
name = "enum-as-inner"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 902d3ed..55f3411 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,3 +1,6 @@
+[workspace]
+members = [ "energy-monitor"]
+
[package]
name = "power_sched"
version = "0.1.0"
diff --git a/energy-monitor/Cargo.toml b/energy-monitor/Cargo.toml
new file mode 100644
index 0000000..f67cfe5
--- /dev/null
+++ b/energy-monitor/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "energy-monitor"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+ratatui = "0.29.0"
+crossterm = "0.28.0"
+procfs = { version = "0.17.0", default-features = false }
+
diff --git a/energy-monitor/src/energy.rs b/energy-monitor/src/energy.rs
new file mode 100644
index 0000000..55b6e53
--- /dev/null
+++ b/energy-monitor/src/energy.rs
@@ -0,0 +1,71 @@
+use std::{
+ collections::HashMap,
+ io::{self, BufRead, BufReader, Write},
+};
+
+pub type Pid = i32;
+use crate::MAX_HISTORY_SIZE;
+
+#[derive(Debug, Clone)]
+pub struct ProcessInfo {
+ pub pid: Pid,
+ pub name: String,
+ pub energy: f64,
+ pub tree_energy: f64,
+ pub history: Vec<(f64, f64)>, // (timestamp, energy)
+}
+
+impl ProcessInfo {
+ pub fn new(pid: Pid, name: String) -> Self {
+ Self {
+ pid,
+ name,
+ energy: 0.,
+ tree_energy: 0.,
+ history: Vec::new(),
+ }
+ }
+
+ pub fn add_history_point(&mut self, timestamp: f64, energy: f64) {
+ self.history.push((timestamp, energy));
+
+ // Keep history bounded to a reasonable size
+ if self.history.len() > MAX_HISTORY_SIZE {
+ // Keep a minute of history at 1sec interval
+ self.history.remove(0);
+ }
+ }
+}
+
+pub fn connect_to_service(path: &str) -> io::Result<std::os::unix::net::UnixStream> {
+ std::os::unix::net::UnixStream::connect(path)
+}
+
+pub fn request_all_processes(
+ stream: &mut std::os::unix::net::UnixStream,
+) -> io::Result<HashMap<Pid, (f64, f64)>> {
+ // Write -1 to get all processes
+ stream.write_all(b"-1\n")?;
+
+ let mut reader = BufReader::new(stream);
+ let mut data = Vec::new();
+ let mut result = HashMap::new();
+
+ reader.read_until(b'#', &mut data)?;
+
+ // Parse each line of the format "pid,energy,tree_energy"
+ for process_line in std::str::from_utf8(data.as_slice()).unwrap().lines() {
+ let parts: Vec<&str> = process_line.split(',').collect();
+ if parts.len() >= 3 {
+ if let (Ok(pid), Ok(energy), Ok(tree_energy)) = (
+ parts[0].parse::<Pid>(),
+ parts[1].parse::<f64>(),
+ parts[2].parse::<f64>(),
+ ) {
+ result.insert(pid, (energy, tree_energy));
+ }
+ }
+ }
+
+ Ok(result)
+}
diff --git a/energy-monitor/src/main.rs b/energy-monitor/src/main.rs
new file mode 100644
index 0000000..b07bfc2
--- /dev/null
+++ b/energy-monitor/src/main.rs
@@ -0,0 +1,214 @@
+use std::{
+ collections::{HashMap, HashSet},
+ error::Error,
+ io,
+ time::{Duration, Instant},
+};
+
+use crossterm::{
+ event::{
+ self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, KeyModifiers,
+ },
+ execute,
+ terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
+};
+use ratatui::{backend::CrosstermBackend, style::Color, widgets::ListState, Terminal};
+
+mod energy;
+mod process;
+mod ui;
+
+use energy::Pid;
+use process::ProcessData;
+
+const SOCKET_PATH: &str = "/tmp/pm-sched";
+const UPDATE_INTERVAL: Duration = Duration::from_secs(1);
+const MAX_HISTORY_SIZE: usize = 60; // Keep a minute's worth of data (at 1 sec update interval)
+
+enum AppTab {
+ ProcessList,
+ DetailedView,
+}
+
+struct App {
+ process_data: ProcessData,
+ selected_tab: AppTab,
+ table_state: ListState,
+ selected_processes: HashSet<Pid>,
+ process_colors: HashMap<Pid, Color>,
+ available_colors: Vec<Color>,
+ color_index: usize,
+ last_update: Instant,
+ should_quit: bool,
+}
+
+impl App {
+ fn new() -> Self {
+ // Set up available colors for the graphs
+ let available_colors = vec![
+ Color::Red,
+ Color::Green,
+ Color::Yellow,
+ Color::Blue,
+ Color::Magenta,
+ Color::Cyan,
+ Color::LightRed,
+ Color::LightGreen,
+ Color::LightYellow,
+ Color::LightBlue,
+ Color::LightMagenta,
+ Color::LightCyan,
+ ];
+
+ let mut app = Self {
+ process_data: ProcessData::new(),
+ selected_tab: AppTab::ProcessList,
+ table_state: ListState::default(),
+ selected_processes: HashSet::new(),
+ process_colors: HashMap::new(),
+ available_colors,
+ color_index: 0,
+ last_update: Instant::now(),
+ should_quit: false,
+ };
+
+ // Start with the first process selected
+ app.table_state.select(Some(0));
+
+ app
+ }
+
+ fn on_tick(&mut self) {
+ if self.last_update.elapsed() >= UPDATE_INTERVAL {
+ self.process_data.update();
+ self.last_update = Instant::now();
+
+ // Make sure our selection is still valid after the update
+ if let Some(i) = self.table_state.selected() {
+ if i >= self.process_data.processes.len() {
+ self.table_state
+ .select(Some(self.process_data.processes.len().saturating_sub(1)));
+ }
+ }
+ }
+ }
+
+ fn on_up(&mut self) {
+ let i = match self.table_state.selected() {
+ Some(i) => {
+ if i == 0 {
+ self.process_data.processes.len() - 1
+ } else {
+ i - 1
+ }
+ }
+ None => 0,
+ };
+ self.table_state.select(Some(i));
+ }
+
+ fn on_down(&mut self) {
+ let i = match self.table_state.selected() {
+ Some(i) => {
+ if i >= self.process_data.processes.len() - 1 {
+ 0
+ } else {
+ i + 1
+ }
+ }
+ None => 0,
+ };
+ self.table_state.select(Some(i));
+ }
+
+ fn on_toggle_select(&mut self) {
+ if let Some(i) = self.table_state.selected() {
+ if i < self.process_data.processes.len() {
+ let pid = self.process_data.processes[i];
+
+ if self.selected_processes.contains(&pid) {
+ self.selected_processes.remove(&pid);
+ self.process_colors.remove(&pid);
+ } else {
+ self.selected_processes.insert(pid);
+ self.assign_color(pid);
+ }
+ }
+ }
+ }
+
+ fn assign_color(&mut self, pid: Pid) {
+ if let std::collections::hash_map::Entry::Vacant(e) = self.process_colors.entry(pid) {
+ let color = self.available_colors[self.color_index];
+ e.insert(color);
+ self.color_index = (self.color_index + 1) % self.available_colors.len();
+ }
+ }
+
+ fn on_tab(&mut self) {
+ self.selected_tab = match self.selected_tab {
+ AppTab::ProcessList => AppTab::DetailedView,
+ AppTab::DetailedView => AppTab::ProcessList,
+ };
+ }
+}
+
+fn main() -> Result<(), Box<dyn Error>> {
+ // Set up terminal
+ enable_raw_mode()?;
+ let mut stdout = io::stdout();
+ execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
+ let backend = CrosstermBackend::new(stdout);
+ let mut terminal = Terminal::new(backend)?;
+
+ // Create app and run event loop
+ let mut app = App::new();
+
+ app.process_data.update();
+
+ // Run the main application loop
+ let tick_rate = Duration::from_millis(100);
+ let mut last_tick = Instant::now();
+
+ while !app.should_quit {
+ terminal.draw(|f| ui::draw(f, &mut app))?;
+
+ let timeout = tick_rate
+ .checked_sub(last_tick.elapsed())
+ .unwrap_or_else(|| Duration::from_secs(0));
+
+ if crossterm::event::poll(timeout)? {
+ if let Event::Key(key) = event::read()? {
+ if key.kind == KeyEventKind::Press {
+ match key.code {
+ KeyCode::Char('q') => app.should_quit = true,
+ KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
+ app.should_quit = true;
+ }
+ KeyCode::Up | KeyCode::Char('k') => app.on_up(),
+ KeyCode::Down | KeyCode::Char('j') => app.on_down(),
+ KeyCode::Char(' ') => app.on_toggle_select(),
+ KeyCode::Tab => app.on_tab(),
+ _ => {}
+ }
+ }
+ }
+ }
+
+ if last_tick.elapsed() >= tick_rate {
+ app.on_tick();
+ last_tick = Instant::now();
+ }
+ }
+
+ // Restore terminal
+ disable_raw_mode()?;
+ execute!(
+ terminal.backend_mut(),
+ LeaveAlternateScreen,
+ DisableMouseCapture
+ )?;
+ terminal.show_cursor()?;
+
+ Ok(())
+}
diff --git a/energy-monitor/src/process.rs b/energy-monitor/src/process.rs
new file mode 100644
index 0000000..d35641e
--- /dev/null
+++ b/energy-monitor/src/process.rs
@@ -0,0 +1,97 @@
+use std::{collections::HashMap, os::unix::net::UnixStream, time::Instant};
+
+use procfs::process::Process;
+
+use crate::energy::{connect_to_service, request_all_processes, Pid, ProcessInfo};
+
+use crate::SOCKET_PATH;
+
+pub struct ProcessData {
+ pub processes: Vec<Pid>,
+ pub total_energy_history: Vec<(f64, f64)>,
+ pub started_at: Instant,
+ pub socket: UnixStream,
+ pub process_info: HashMap<Pid, ProcessInfo>,
+}
+
+impl ProcessData {
+ pub fn new() -> Self {
+ Self {
+ processes: Vec::new(),
+ total_energy_history: Vec::new(),
+ started_at: Instant::now(),
+ process_info: HashMap::new(),
+ socket: connect_to_service(SOCKET_PATH).expect("Failed to connect to socket"),
+ }
+ }
+
+ pub fn update(&mut self) {
+ self.fetch_processes();
+ }
+
+ pub fn fetch_processes(&mut self) {
+ let energy_data = match request_all_processes(&mut self.socket) {
+ Ok(data) => data,
+ Err(_) => return, // Can't get energy data
+ };
+
+ // Use process data with procfs to get process names
+ let mut updated_processes = Vec::new();
+ let mut total_energy = 0.0;
+
+ let elapsed = self.started_at.elapsed().as_secs_f64();
+ for (pid, (energy, tree_energy)) in energy_data {
+ let process_info = self.process_info.entry(pid).or_insert({
+ let name = get_process_name(pid).unwrap_or_else(|| format!("Process {}", pid));
+
+ ProcessInfo::new(pid, name)
+ });
+
+ let process_delta = energy - process_info.energy;
+ let delta = tree_energy - process_info.tree_energy;
+ process_info.energy = energy;
+ process_info.tree_energy = tree_energy;
+ process_info.add_history_point(elapsed, delta);
+
+ updated_processes.push(pid);
+ total_energy += process_delta;
+ }
+
+ // Sort by energy consumption (descending)
+ updated_processes.sort_by(|a, b| {
+ self.process_info[b]
+ .energy
+ .partial_cmp(&self.process_info[a].energy)
+ .unwrap()
+ });
+
+ // Update our local process list
+ self.processes = updated_processes;
+
+ // TODO: garbage collect process_info
+
+ // Add total energy to history
+ let elapsed = self.started_at.elapsed().as_secs_f64();
+ self.add_total_energy_point(elapsed, total_energy);
+ }
+
+ fn add_total_energy_point(&mut self, timestamp: f64, energy: f64) {
+ self.total_energy_history.push((timestamp, energy));
+
+ // Keep history bounded
+ if self.total_energy_history.len() > 60 {
+ self.total_energy_history.remove(0);
+ }
+ }
+}
+
+// Helper function to get process name using procfs
+fn get_process_name(pid: Pid) -> Option<String> {
+ match Process::new(pid) {
+ Ok(process) => match process.stat() {
+ Ok(stat) => Some(stat.comm),
+ Err(_) => None,
+ },
+ Err(_) => None,
+ }
+}
diff --git a/energy-monitor/src/ui.rs b/energy-monitor/src/ui.rs
new file mode 100644
index 0000000..276a3df
--- /dev/null
+++ b/energy-monitor/src/ui.rs
@@ -0,0 +1,391 @@
+use ratatui::{
+ layout::{Constraint, Direction, Layout, Rect},
+ style::{Color, Modifier, Style},
+ symbols,
+ text::{Line, Span},
+ widgets::{Axis, Block, Borders, Chart, Dataset, List, ListItem, Paragraph, Tabs},
+ Frame,
+};
+
+use crate::{App, AppTab};
+
+pub fn draw(f: &mut Frame, app: &mut App) {
+ let chunks = Layout::default()
+ .direction(Direction::Vertical)
+ .constraints([
+ Constraint::Length(3),
+ Constraint::Min(0),
+ Constraint::Length(3),
+ ])
+ .split(f.area());
+
+ // Draw header with tabs
+ let tab_items = vec![
+ Line::from(Span::styled(
+ "Process List",
+ Style::default().fg(Color::White),
+ )),
+ Line::from(Span::styled(
+ "Detailed View",
+ Style::default().fg(Color::White),
+ )),
+ ];
+
+ let tabs = Tabs::new(tab_items)
+ .block(
+ Block::default()
+ .borders(Borders::ALL)
+ .title("Energy Monitor"),
+ )
+ .select(match app.selected_tab {
+ AppTab::ProcessList => 0,
+ AppTab::DetailedView => 1,
+ })
+ .style(Style::default().fg(Color::White))
+ .highlight_style(Style::default().fg(Color::Yellow));
+ f.render_widget(tabs, chunks[0]);
+
+ // Draw content based on selected tab
+ match app.selected_tab {
+ AppTab::ProcessList => draw_process_list(f, app, chunks[1]),
+ AppTab::DetailedView => draw_detailed_view(f, app, chunks[1]),
+ }
+
+ // Draw footer with instructions
+ let footer_spans = vec![
+ Span::styled("q", Style::default().fg(Color::Yellow)),
+ Span::raw(" Quit | "),
+ Span::styled("↑/k ↓/j", Style::default().fg(Color::Yellow)),
+ Span::raw(" Navigate | "),
+ Span::styled("Space", Style::default().fg(Color::Yellow)),
+ Span::raw(" Select | "),
+ Span::styled("Tab", Style::default().fg(Color::Yellow)),
+ Span::raw(" Change Tab"),
+ ];
+
+ let footer =
+ Paragraph::new(Line::from(footer_spans)).block(Block::default().borders(Borders::ALL));
+ f.render_widget(footer, chunks[2]);
+}
+
+fn draw_process_list(f: &mut Frame, app: &mut App, area: Rect) {
+ let chunks = Layout::default()
+ .direction(Direction::Horizontal)
+ .constraints([Constraint::Percentage(60), Constraint::Percentage(40)])
+ .split(area);
+
+ // Draw process list
+ let processes: Vec<ListItem> = app
+ .process_data
+ .processes
+ .iter()
+ .map(|pid| {
+ let p = &app.process_data.process_info[pid];
+ let is_selected = app.selected_processes.contains(&p.pid);
+ let style = if is_selected {
+ Style::default().fg(app
+ .process_colors
+ .get(&p.pid)
+ .copied()
+ .unwrap_or(Color::White))
+ } else {
+ Style::default()
+ };
+
+ let content = vec![
+ Span::styled(
+ format!("{}{:>5} ", if is_selected { "* " } else { " " }, p.pid),
+ style,
+ ),
+ Span::styled(
+ format!("{:20}", p.name.chars().take(20).collect::<String>()),
+ style,
+ ),
+ Span::styled(format!(" {:8.2}J", p.energy), style),
+ Span::styled(format!(" {:8.2}J", p.tree_energy), style),
+ Span::styled(
+ format!(" {:8.2}W", p.history.last().map(|(_, x)| x).unwrap_or(&0.)),
+ style,
+ ),
+ ];
+
+ let line = Line::from(content);
+ ListItem::new(line)
+ })
+ .collect();
+
+ let processes_list = List::new(processes)
+ .block(
+ Block::default()
+ .borders(Borders::ALL)
+ .title("Processes by Energy"),
+ )
+ .highlight_style(Style::default().add_modifier(Modifier::REVERSED))
+ .highlight_symbol("> ");
+ f.render_stateful_widget(processes_list, chunks[0], &mut app.table_state);
+
+ // Draw energy graph
+ draw_energy_graph(f, app, chunks[1]);
+}
+
+fn draw_detailed_view(f: &mut Frame, app: &mut App, area: Rect) {
+ let selected_pid = app
+ .table_state
+ .selected()
+ .and_then(|i| app.process_data.processes.get(i));
+
+ if let Some(pid) = selected_pid {
+ let chunks = Layout::default()
+ .direction(Direction::Vertical)
+ .constraints([Constraint::Length(5), Constraint::Min(0)])
+ .split(area);
+
+ // Draw process details
+ if let Some(process) = app.process_data.process_info.get(pid) {
+ let details = vec![
+ Line::from(vec![
+ Span::raw("PID: "),
+ Span::styled(
+ format!("{}", process.pid),
+ Style::default().fg(Color::Yellow),
+ ),
+ ]),
+ Line::from(vec![
+ Span::raw("Name: "),
+ Span::styled(&process.name, Style::default().fg(Color::Yellow)),
+ ]),
+ Line::from(vec![
+ Span::raw("Energy: "),
+ Span::styled(
+ format!("{:.2}J", process.energy),
+ Style::default().fg(Color::Yellow),
+ ),
+ ]),
+ Line::from(vec![
+ Span::raw("Tree Energy: "),
+ Span::styled(
+ format!("{:.2}J", process.tree_energy),
+ Style::default().fg(Color::Yellow),
+ ),
+ ]),
+ ];
+
+ let details_widget = Paragraph::new(details).block(
+ Block::default()
+ .borders(Borders::ALL)
+ .title("Process Details"),
+ );
+ f.render_widget(details_widget, chunks[0]);
+
+ // Draw process energy history graph
+ let start_time = if let Some((first_time, _)) = process.history.first() {
+ *first_time
+ } else {
+ 0.0
+ };
+
+ let end_time = if let Some((last_time, _)) = process.history.last() {
+ *last_time
+ } else {
+ start_time + 60.0
+ };
+
+ let dataset = Dataset::default()
+ .name(process.name.clone())
+ .marker(symbols::Marker::Braille)
+ .style(
+ Style::default().fg(app
+ .process_colors
+ .get(&pid)
+ .copied()
+ .unwrap_or(Color::Yellow)),
+ )
+ .data(&process.history);
+
+ let chart = Chart::new(vec![dataset])
+ .block(
+ Block::default()
+ .borders(Borders::ALL)
+ .title("Energy Usage History"),
+ )
+ .x_axis(
+ Axis::default()
+ .title("Time (s)")
+ .style(Style::default().fg(Color::Gray))
+ .bounds([start_time, end_time])
+ .labels(vec![
+ Span::styled(
+ format!("{:.0}", start_time),
+ Style::default().fg(Color::White),
+ ),
+ Span::styled(
+ format!("{:.0}", (start_time + end_time) / 2.0),
+ Style::default().fg(Color::White),
+ ),
+ Span::styled(
+ format!("{:.0}", end_time),
+ Style::default().fg(Color::White),
+ ),
+ ]),
+ )
+ .y_axis(
+ Axis::default()
+ .title("Energy (J)")
+ .style(Style::default().fg(Color::Gray))
+ .bounds([
+ 0.0,
+ process
+ .history
+ .iter()
+ .map(|(_, e)| *e)
+ .fold(0.0f64, f64::max)
+ * 1.2,
+ ])
+ .labels(vec![
+ Span::styled("0", Style::default().fg(Color::White)),
+ Span::styled(
+ format!(
+ "{:.2}",
+ process
+ .history
+ .iter()
+ .map(|(_, e)| *e)
+ .fold(0.0f64, f64::max)
+ / 2.0
+ ),
+ Style::default().fg(Color::White),
+ ),
+ Span::styled(
+ format!(
+ "{:.2}",
+ process
+ .history
+ .iter()
+ .map(|(_, e)| *e)
+ .fold(0.0f64, f64::max)
+ ),
+ Style::default().fg(Color::White),
+ ),
+ ]),
+ );
+
+ f.render_widget(chart, chunks[1]);
+ }
+ } else {
+ let message = Paragraph::new("No process selected")
+ .block(Block::default().borders(Borders::ALL))
+ .alignment(ratatui::layout::Alignment::Center);
+ f.render_widget(message, area);
+ }
+}
+
+fn draw_energy_graph(f: &mut Frame, app: &mut App, area: Rect) {
+ // Create datasets for each selected process and the total
+ let mut datasets = Vec::new();
+
+ // Add total system energy dataset
+ if !app.process_data.total_energy_history.is_empty() {
+ datasets.push(
+ Dataset::default()
+ .name("Total System Energy")
+ .marker(symbols::Marker::Braille)
+ .style(Style::default().fg(Color::White))
+ .data(&app.process_data.total_energy_history),
+ );
+ }
+
+ // Add selected processes datasets
+ for pid in &app.selected_processes {
+ if let Some(process) = app.process_data.process_info.get(pid) {
+ if !process.history.is_empty() {
+ datasets.push(
+ Dataset::default()
+ .name(process.name.clone())
+ .marker(symbols::Marker::Braille)
+ .style(
+ Style::default().fg(app
+ .process_colors
+ .get(pid)
+ .copied()
+ .unwrap_or(Color::White)),
+ )
+ .data(&process.history),
+ );
+ }
+ }
+ }
+
+ // Get time bounds
+ let start_time = app
+ .process_data
+ .total_energy_history
+ .first()
+ .map(|(t, _)| *t)
+ .unwrap_or(0.0);
+
+ let end_time = app
+ .process_data
+ .total_energy_history
+ .last()
+ .map(|(t, _)| *t)
+ .unwrap_or(start_time + 60.0);
+
+ let max_total = app
+ .process_data
+ .total_energy_history
+ .iter()
+ .map(|(_, e)| *e)
+ .fold(0.0f64, f64::max);
+
+ let max_processes = app
+ .selected_processes
+ .iter()
+ .filter_map(|pid| app.process_data.process_info.get(pid))
+ .flat_map(|process| process.history.iter().map(|(_, e)| *e))
+ .fold(0.0f64, f64::max);
+
+ let max_energy = f64::max(max_total, max_processes);
+
+ // Create chart widget
+ let chart = Chart::new(datasets)
+ .block(Block::default().borders(Borders::ALL).title("Energy Usage"))
+ .x_axis(
+ Axis::default()
+ .title("Time (s)")
+ .style(Style::default().fg(Color::Gray))
+ .bounds([start_time, end_time])
+ .labels(vec![
+ Span::styled(
+ format!("{:.0}", start_time),
+ Style::default().fg(Color::White),
+ ),
+ Span::styled(
+ format!("{:.0}", (start_time + end_time) / 2.0),
+ Style::default().fg(Color::White),
+ ),
+ Span::styled(
+ format!("{:.0}", end_time),
+ Style::default().fg(Color::White),
+ ),
+ ]),
+ )
+ .y_axis(
+ Axis::default()
+ .title("Power (W)")
+ .style(Style::default().fg(Color::Gray))
+ .bounds([0.0, max_energy * 1.2])
+ .labels(vec![
+ Span::styled("0", Style::default().fg(Color::White)),
+ Span::styled(
+ format!("{:.2}", max_energy / 2.0),
+ Style::default().fg(Color::White),
+ ),
+ Span::styled(
+ format!("{:.2}", max_energy),
+ Style::default().fg(Color::White),
+ ),
+ ]),
+ );
+
+ f.render_widget(chart, area);
+}
diff --git a/src/energy.rs b/src/energy.rs
index 9c90ab9..3f6931d 100644
--- a/src/energy.rs
+++ b/src/energy.rs
@@ -19,7 +19,7 @@ pub use budget::BudgetPolicy;
pub use trackers::{KernelDriver, PerfEstimator};
const IDLE_CONSUMPTION_W: f64 = 7.;
-const UPDATE_INTERVAL: Duration = Duration::from_millis(3);
+const UPDATE_INTERVAL: Duration = Duration::from_millis(10);
pub enum Request {
NewTask(Pid, Arc<TaskInfo>),
@@ -122,6 +122,7 @@ pub struct EnergyService {
}
impl EnergyService {
+ #[allow(clippy::too_many_arguments)]
pub fn new(
estimator: Box<dyn Estimator>,
budget_policy: Box<dyn BudgetPolicy>,
@@ -244,7 +245,7 @@ impl EnergyService {
.unwrap_or(0.);
for pid in &self.active_processes {
let mut process_info = self.process_info.write().unwrap();
- if let Some(info) = process_info.get_mut(&pid) {
+ if let Some(info) = process_info.get_mut(pid) {
if info
.task_info
.read_time_since_last_schedule()
@@ -255,6 +256,7 @@ impl EnergyService {
}
if let Some(energy) = self.estimator.read_consumption(*pid as u64) {
info.energy += energy * self.bias;
+ info.tree_energy += energy * self.bias;
self.estimator.update_information(
*pid as u64,
info.task_info.read_cpu(),
diff --git a/src/socket.rs b/src/socket.rs
index 316c662..e2ecdbf 100644
--- a/src/socket.rs
+++ b/src/socket.rs
@@ -35,21 +35,9 @@ impl LoggingSocketService {
break;
}
if let Ok(pid) = line.trim().parse::<Pid>() {
- if let Some(info) = self.process_info.read().unwrap().get(&pid).clone() {
- socket
- .write_all(
- format!(
- "pid: {pid} process: {}J process tree: {}J\n",
- info.energy, info.tree_energy
- )
- .as_bytes(),
- )
- .unwrap();
- } else {
- socket
- .write_all(format!("Unknown pid: {pid}\n").as_bytes())
- .unwrap();
- }
+ socket
+ .write_all(self.handle_request(pid).as_bytes())
+ .unwrap();
} else {
socket
.write_all(
@@ -63,6 +51,27 @@ impl LoggingSocketService {
}
});
}
+
+ fn handle_request(&self, pid: i32) -> String {
+ let mut output = String::new();
+ use std::fmt::Write;
+ if pid == -1 {
+ for (pid, info) in self.process_info.read().unwrap().iter() {
+ writeln!(&mut output, "{pid},{},{}", info.energy, info.tree_energy).unwrap();
+ }
+ writeln!(&mut output, "#",).unwrap();
+ return output;
+ }
+
+ if let Some(info) = self.process_info.read().unwrap().get(&pid) {
+ format!(
+ "pid: {pid} process: {}J process tree: {}J\n",
+ info.energy, info.tree_energy
+ )
+ } else {
+ format!("Unknown pid: {pid}\n")
+ }
+ }
}
pub fn start_logging_socket_service(