feat: configurable graphs period (#431)

* chore: update test snapshot

* feat: configurable graphs period
This commit is contained in:
Ilya Zlobintsev
2024-12-22 15:13:46 +02:00
committed by GitHub
parent 7d5d6b96e5
commit f0a878909c
7 changed files with 358 additions and 877 deletions

View File

@@ -46,84 +46,19 @@ expression: device_info
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
1
]
},
{
"clock_type": null,
"values": [
4
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
800
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
3
]
},
{
"clock_type": null,
"values": [
1
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0,
0,
0,
1,
4,
0,
800,
0,
0,
0,
3,
1,
0,
0
]
}
@@ -135,84 +70,19 @@ expression: device_info
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
2
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
1
]
},
{
"clock_type": null,
"values": [
4
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
650
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
1
]
},
{
"clock_type": null,
"values": [
1
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0,
2,
0,
1,
4,
0,
650,
0,
0,
0,
1,
1,
0,
0
]
}
@@ -224,84 +94,19 @@ expression: device_info
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
1
]
},
{
"clock_type": null,
"values": [
3
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
1
]
},
{
"clock_type": null,
"values": [
1
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0,
0,
0,
1,
3,
0,
0,
0,
0,
0,
1,
1,
0,
0
]
}
@@ -313,84 +118,19 @@ expression: device_info
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
1
]
},
{
"clock_type": null,
"values": [
4
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
1
]
},
{
"clock_type": null,
"values": [
1
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0,
0,
0,
1,
4,
0,
0,
0,
0,
0,
1,
1,
0,
0
]
}
@@ -402,84 +142,19 @@ expression: device_info
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
1
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
4
]
},
{
"clock_type": null,
"values": [
1
]
},
{
"clock_type": null,
"values": [
1000
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
1
]
},
{
"clock_type": null,
"values": [
1
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0,
1,
0,
4,
1,
1000,
0,
0,
0,
0,
1,
1,
0,
0
]
}
@@ -491,84 +166,19 @@ expression: device_info
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
1
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
4
]
},
{
"clock_type": null,
"values": [
1
]
},
{
"clock_type": null,
"values": [
1000
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
1
]
},
{
"clock_type": null,
"values": [
1
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0,
1,
0,
4,
1,
1000,
0,
0,
0,
0,
1,
1,
0,
0
]
}
@@ -580,84 +190,19 @@ expression: device_info
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
1
]
},
{
"clock_type": null,
"values": [
4
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
800
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
3
]
},
{
"clock_type": null,
"values": [
1
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0,
0,
0,
1,
4,
0,
800,
0,
0,
0,
3,
1,
0,
0
]
}
@@ -669,84 +214,19 @@ expression: device_info
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
2
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
1
]
},
{
"clock_type": null,
"values": [
4
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
650
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
3
]
},
{
"clock_type": null,
"values": [
1
]
},
{
"clock_type": null,
"values": [
0
]
},
{
"clock_type": null,
"values": [
0,
2,
0,
1,
4,
0,
650,
0,
0,
0,
3,
1,
0,
0
]
}
@@ -787,6 +267,7 @@ expression: device_info
"speed_min": 0
},
"memory_power_state": 0,
"performance_level": "auto",
"power": {
"average": 1.0,
"cap_current": 100.0,

View File

@@ -11,7 +11,7 @@ use crate::{APP_ID, GUI_VERSION};
use anyhow::{anyhow, Context};
use apply_revealer::{ApplyRevealer, ApplyRevealerMsg};
use confirmation_dialog::ConfirmationDialog;
use graphs_window::GraphsWindow;
use graphs_window::{GraphsWindow, GraphsWindowMsg};
use gtk::{
glib::{self, clone, ControlFlow},
prelude::{
@@ -46,7 +46,7 @@ const STATS_POLL_INTERVAL_MS: u64 = 250;
pub struct AppModel {
daemon_client: DaemonClient,
graphs_window: GraphsWindow,
graphs_window: relm4::Controller<GraphsWindow>,
info_page: relm4::Controller<InformationPage>,
oc_page: OcPage,
@@ -212,7 +212,7 @@ impl AsyncComponent for AppModel {
));
}
let graphs_window = GraphsWindow::new();
let graphs_window = GraphsWindow::builder().launch(()).detach();
let model = AppModel {
daemon_client,
@@ -297,7 +297,8 @@ impl AppModel {
self.thermals_page.set_stats(&stats, false);
self.oc_page.set_stats(&stats, false);
self.graphs_window.set_stats(&stats);
self.graphs_window.emit(GraphsWindowMsg::Stats(stats));
}
AppMsg::ApplyChanges => {
self.apply_settings(self.current_gpu_id()?, root, &sender)
@@ -328,7 +329,7 @@ impl AppModel {
sender.input(AppMsg::ReloadData { full: false });
}
AppMsg::ShowGraphsWindow => {
self.graphs_window.show();
self.graphs_window.emit(GraphsWindowMsg::Show);
}
AppMsg::DumpVBios => {
self.dump_vbios(&self.current_gpu_id()?, root).await;
@@ -405,13 +406,14 @@ impl AppModel {
.as_ref()
.map(|info| info.vram_clock_ratio)
.unwrap_or(1.0);
self.graphs_window.set_vram_clock_ratio(vram_clock_ratio);
self.graphs_window
.emit(GraphsWindowMsg::VramClockRatio(vram_clock_ratio));
self.update_gpu_data(gpu_id, sender).await?;
self.thermals_page.set_info(&info);
self.graphs_window.clear();
self.graphs_window.emit(GraphsWindowMsg::Clear);
Ok(())
}

View File

@@ -1,185 +1,239 @@
pub(crate) mod plot;
use self::plot::PlotData;
use glib::Object;
use gtk::{
glib::{self, subclass::types::ObjectSubclassIsExt},
prelude::WidgetExt,
};
use lact_client::schema::DeviceStats;
use gtk::prelude::*;
use lact_schema::DeviceStats;
use plot::{Plot, PlotData};
use relm4::{ComponentParts, ComponentSender, RelmWidgetExt};
use std::rc::Rc;
const GRAPH_WIDTH_SECONDS: i64 = 60;
pub struct GraphsWindow {
time_period_seconds_adj: gtk::Adjustment,
vram_clock_ratio: f64,
}
glib::wrapper! {
pub struct GraphsWindow(ObjectSubclass<imp::GraphsWindow>)
@extends gtk::Box, gtk::Widget, gtk::Window,
@implements gtk::Orientable, gtk::Accessible, gtk::Buildable;
#[derive(Debug)]
pub enum GraphsWindowMsg {
Stats(Rc<DeviceStats>),
VramClockRatio(f64),
Refresh,
Show,
Clear,
}
#[relm4::component(pub)]
impl relm4::Component for GraphsWindow {
type Init = ();
type Input = GraphsWindowMsg;
type Output = ();
type CommandOutput = ();
view! {
gtk::Window {
set_default_height: 400,
set_default_width: 1200,
set_title: Some("Historical data"),
set_hide_on_close: true,
gtk::Grid {
set_margin_all: 10,
set_row_spacing: 20,
set_column_spacing: 20,
attach[0, 0, 1, 1]: temperature_plot = &Plot {
set_title: "Temperature",
set_hexpand: true,
set_value_suffix: "°C",
set_y_label_area_relative_size: 0.15,
#[watch]
set_time_period_seconds: model.time_period_seconds_adj.value() as i64,
},
attach[0, 1, 1, 1]: fan_plot = &Plot {
set_title: "Fan speed",
set_hexpand: true,
set_value_suffix: "RPM",
set_y_label_area_relative_size: 0.25,
set_secondary_y_label_area_relative_size: 0.15,
#[watch]
set_time_period_seconds: model.time_period_seconds_adj.value() as i64,
},
attach[1, 0, 1, 1]: clockspeed_plot = &Plot {
set_title: "Clockspeed",
set_hexpand: true,
set_value_suffix: "MHz",
set_y_label_area_relative_size: 0.3,
#[watch]
set_time_period_seconds: model.time_period_seconds_adj.value() as i64,
},
attach[1, 1, 1, 1]: power_plot = &Plot {
set_title: "Power usage",
set_hexpand: true,
set_value_suffix: "W",
set_y_label_area_relative_size: 0.2,
#[watch]
set_time_period_seconds: model.time_period_seconds_adj.value() as i64,
},
attach[1, 2, 1, 1] = &gtk::Box {
set_halign: gtk::Align::End,
set_spacing: 5,
gtk::Label {
set_label: "Time period (seconds):"
},
gtk::SpinButton {
set_adjustment: &model.time_period_seconds_adj,
},
},
},
}
}
fn init(
_init: Self::Init,
root: Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let time_period_seconds_adj = gtk::Adjustment::new(60.0, 15.0, 3601.0, 1.0, 1.0, 1.0);
time_period_seconds_adj.connect_value_changed(move |_| {
sender.input(GraphsWindowMsg::Refresh);
});
let model = Self {
time_period_seconds_adj,
vram_clock_ratio: 1.0,
};
let widgets = view_output!();
ComponentParts { model, widgets }
}
fn update_with_view(
&mut self,
widgets: &mut Self::Widgets,
msg: Self::Input,
sender: ComponentSender<Self>,
root: &Self::Root,
) {
match msg {
GraphsWindowMsg::Refresh => {}
GraphsWindowMsg::Show => {
root.show();
}
GraphsWindowMsg::VramClockRatio(ratio) => {
self.vram_clock_ratio = ratio;
}
GraphsWindowMsg::Stats(stats) => {
let mut temperature_plot = widgets.temperature_plot.data_mut();
let mut clockspeed_plot = widgets.clockspeed_plot.data_mut();
let mut power_plot = widgets.power_plot.data_mut();
let mut fan_plot = widgets.fan_plot.data_mut();
let throttling_plots =
[&mut temperature_plot, &mut clockspeed_plot, &mut power_plot];
match &stats.throttle_info {
Some(throttle_info) => {
if throttle_info.is_empty() {
for plot in throttling_plots {
plot.push_throttling("No", false);
}
} else {
let type_text: Vec<String> = throttle_info
.iter()
.map(|(throttle_type, details)| {
format!("{throttle_type} ({})", details.join(", "))
})
.collect();
let text = type_text.join(", ");
for plot in throttling_plots {
plot.push_throttling(&text, true);
}
}
}
None => {
for plot in throttling_plots {
plot.push_throttling("Unknown", false);
}
}
}
for (name, value) in &stats.temps {
temperature_plot.push_line_series(name, value.current.unwrap_or(0.0) as f64);
}
if let Some(average) = stats.power.average {
power_plot.push_line_series("Average", average);
}
if let Some(current) = stats.power.current {
power_plot.push_line_series("Current", current);
}
if let Some(limit) = stats.power.cap_current {
power_plot.push_line_series("Limit", limit);
}
if let Some(point) = stats.clockspeed.gpu_clockspeed {
clockspeed_plot.push_line_series("GPU (Avg)", point as f64);
}
if let Some(point) = stats.clockspeed.current_gfxclk {
clockspeed_plot.push_line_series("GPU (Trgt)", point as f64);
}
if let Some(point) = stats.clockspeed.vram_clockspeed {
clockspeed_plot.push_line_series("VRAM", point as f64 * self.vram_clock_ratio);
}
if let Some(max_speed) = stats.fan.speed_max {
fan_plot.push_line_series("Maximum", max_speed as f64);
}
if let Some(min_speed) = stats.fan.speed_min {
fan_plot.push_line_series("Minimum", min_speed as f64);
}
if let Some(current_speed) = stats.fan.speed_current {
fan_plot.push_line_series("Current", current_speed as f64);
}
if let Some(pwm) = stats.fan.pwm_current {
fan_plot.push_secondary_line_series(
"Percentage",
(pwm as f64 / u8::MAX as f64) * 100.0,
);
}
let time_period_seconds = self.time_period_seconds_adj.value() as i64;
temperature_plot.trim_data(time_period_seconds);
clockspeed_plot.trim_data(time_period_seconds);
power_plot.trim_data(time_period_seconds);
fan_plot.trim_data(time_period_seconds);
Self::queue_plots_draw(widgets);
}
GraphsWindowMsg::Clear => {
*widgets.temperature_plot.data_mut() = PlotData::default();
*widgets.clockspeed_plot.data_mut() = PlotData::default();
*widgets.power_plot.data_mut() = PlotData::default();
*widgets.fan_plot.data_mut() = PlotData::default();
Self::queue_plots_draw(widgets);
}
}
self.update_view(widgets, sender);
}
}
impl GraphsWindow {
pub fn new() -> Self {
Object::builder().property("vram_clock_ratio", 1.0).build()
}
pub fn set_stats(&self, stats: &DeviceStats) {
let imp = self.imp();
let mut temperature_plot = imp.temperature_plot.data_mut();
let mut clockspeed_plot = imp.clockspeed_plot.data_mut();
let mut power_plot = imp.power_plot.data_mut();
let mut fan_plot = imp.fan_plot.data_mut();
let throttling_plots = [&mut temperature_plot, &mut clockspeed_plot, &mut power_plot];
match &stats.throttle_info {
Some(throttle_info) => {
if throttle_info.is_empty() {
for plot in throttling_plots {
plot.push_throttling("No", false);
}
} else {
let type_text: Vec<String> = throttle_info
.iter()
.map(|(throttle_type, details)| {
format!("{throttle_type} ({})", details.join(", "))
})
.collect();
let text = type_text.join(", ");
for plot in throttling_plots {
plot.push_throttling(&text, true);
}
}
}
None => {
for plot in throttling_plots {
plot.push_throttling("Unknown", false);
}
}
}
for (name, value) in &stats.temps {
temperature_plot.push_line_series(name, value.current.unwrap_or(0.0) as f64);
}
if let Some(average) = stats.power.average {
power_plot.push_line_series("Average", average);
}
if let Some(current) = stats.power.current {
power_plot.push_line_series("Current", current);
}
if let Some(limit) = stats.power.cap_current {
power_plot.push_line_series("Limit", limit);
}
if let Some(point) = stats.clockspeed.gpu_clockspeed {
clockspeed_plot.push_line_series("GPU (Avg)", point as f64);
}
if let Some(point) = stats.clockspeed.current_gfxclk {
clockspeed_plot.push_line_series("GPU (Trgt)", point as f64);
}
if let Some(point) = stats.clockspeed.vram_clockspeed {
clockspeed_plot.push_line_series("VRAM", point as f64 * self.vram_clock_ratio());
}
if let Some(max_speed) = stats.fan.speed_max {
fan_plot.push_line_series("Maximum", max_speed as f64);
}
if let Some(min_speed) = stats.fan.speed_min {
fan_plot.push_line_series("Minimum", min_speed as f64);
}
if let Some(current_speed) = stats.fan.speed_current {
fan_plot.push_line_series("Current", current_speed as f64);
}
if let Some(pwm) = stats.fan.pwm_current {
fan_plot
.push_secondary_line_series("Percentage", (pwm as f64 / u8::MAX as f64) * 100.0);
}
temperature_plot.trim_data(GRAPH_WIDTH_SECONDS);
clockspeed_plot.trim_data(GRAPH_WIDTH_SECONDS);
power_plot.trim_data(GRAPH_WIDTH_SECONDS);
fan_plot.trim_data(GRAPH_WIDTH_SECONDS);
imp.temperature_plot.queue_draw();
imp.clockspeed_plot.queue_draw();
imp.power_plot.queue_draw();
imp.fan_plot.queue_draw();
}
pub fn clear(&self) {
let imp = self.imp();
*imp.temperature_plot.data_mut() = PlotData::default();
*imp.clockspeed_plot.data_mut() = PlotData::default();
*imp.power_plot.data_mut() = PlotData::default();
*imp.fan_plot.data_mut() = PlotData::default();
imp.temperature_plot.queue_draw();
imp.clockspeed_plot.queue_draw();
imp.power_plot.queue_draw();
imp.fan_plot.queue_draw();
fn queue_plots_draw(widgets: &<Self as relm4::Component>::Widgets) {
widgets.temperature_plot.queue_draw();
widgets.clockspeed_plot.queue_draw();
widgets.power_plot.queue_draw();
widgets.fan_plot.queue_draw();
}
}
impl Default for GraphsWindow {
fn default() -> Self {
Self::new()
}
}
mod imp {
use super::plot::Plot;
use gtk::{
glib::{self, subclass::InitializingObject, Properties},
prelude::*,
subclass::{
prelude::*,
widget::{CompositeTemplateClass, WidgetImpl},
},
CompositeTemplate,
};
use std::cell::Cell;
#[derive(CompositeTemplate, Default, Properties)]
#[properties(wrapper_type = super::GraphsWindow)]
#[template(file = "ui/graphs_window.blp")]
pub struct GraphsWindow {
#[template_child]
pub(super) temperature_plot: TemplateChild<Plot>,
#[template_child]
pub(super) clockspeed_plot: TemplateChild<Plot>,
#[template_child]
pub(super) power_plot: TemplateChild<Plot>,
#[template_child]
pub(super) fan_plot: TemplateChild<Plot>,
#[property(get, set)]
pub vram_clock_ratio: Cell<f64>,
}
#[glib::object_subclass]
impl ObjectSubclass for GraphsWindow {
const NAME: &'static str = "GraphsWindow";
type Type = super::GraphsWindow;
type ParentType = gtk::Window;
fn class_init(class: &mut Self::Class) {
Plot::ensure_type();
class.bind_template();
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
#[glib::derived_properties]
impl ObjectImpl for GraphsWindow {}
impl WidgetImpl for GraphsWindow {}
impl WindowImpl for GraphsWindow {}
impl ApplicationWindowImpl for GraphsWindow {}
}

View File

@@ -25,6 +25,8 @@ pub struct Plot {
pub(super) data: RefCell<PlotData>,
pub(super) dirty: Cell<bool>,
render_thread: RenderThread,
#[property(get, set)]
time_period_seconds: Cell<i64>,
}
#[glib::object_subclass]
@@ -74,6 +76,7 @@ impl WidgetImpl for Plot {
.secondary_y_label_area_relative_size
.get(),
supersample_factor: 4,
time_period_seconds: self.time_period_seconds.get(),
});
}

View File

@@ -7,13 +7,19 @@ use std::cell::RefMut;
pub use imp::PlotData;
use gtk::glib::{self, subclass::types::ObjectSubclassIsExt};
use gtk::glib::{self, subclass::types::ObjectSubclassIsExt, Object};
glib::wrapper! {
pub struct Plot(ObjectSubclass<imp::Plot>)
@extends gtk::Widget;
}
impl Default for Plot {
fn default() -> Self {
Object::builder().build()
}
}
impl Plot {
pub fn data_mut(&self) -> RefMut<'_, PlotData> {
self.imp().dirty.set(true);

View File

@@ -35,6 +35,8 @@ pub struct RenderRequest {
pub height: u32,
pub supersample_factor: u32,
pub time_period_seconds: i64,
}
#[derive(Default)]
@@ -259,11 +261,11 @@ impl RenderRequest {
("sans-serif", RelativeSize::Smaller(0.08)),
)
.build_cartesian_2d(
start_date..max(end_date, start_date + 60 * 1000),
start_date..max(end_date, start_date + self.time_period_seconds * 1000),
0f64..maximum_value,
)?
.set_secondary_coord(
start_date..max(end_date, start_date + 60 * 1000),
start_date..max(end_date, start_date + self.time_period_seconds * 1000),
0.0..100.0,
);

View File

@@ -1,67 +0,0 @@
using Gtk 4.0;
template $GraphsWindow: Window {
default-height: 400;
default-width: 1200;
title: "Historical data";
hide-on-close: true;
Grid {
margin-top: 10;
margin-bottom: 10;
margin-start: 10;
margin-end: 10;
row-spacing: 20;
column-spacing: 20;
$Plot temperature_plot {
title: "Temperature";
hexpand: true;
value-suffix: "°C";
y-label-area-relative-size: 0.15;
layout {
column: 0;
row: 0;
}
}
$Plot fan_plot {
title: "Fan speed";
hexpand: true;
value-suffix: "RPM";
secondary-value-suffix: "%";
y-label-area-relative-size: 0.25;
secondary-y-label-area-relative-size: 0.15;
layout {
column: 0;
row: 1;
}
}
$Plot clockspeed_plot {
title: "Clockspeed";
hexpand: true;
value-suffix: "MHz";
y-label-area-relative-size: 0.3;
layout {
column: 1;
row: 0;
}
}
$Plot power_plot {
title: "Power usage";
hexpand: true;
value-suffix: "W";
y-label-area-relative-size: 0.2;
layout {
column: 1;
row: 1;
}
}
}
}