diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..32a5e3e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+/zenmonitor
+/zenmonitor-cli
+*.o
diff --git a/README.md b/README.md
index 23ecc99..048179b 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,60 @@ Alternatively, you can set capabilities to zenmonitor executable: `sudo setcap c
``--coreid`` - Display core_id instead of core index
+``--interval MS`` - Initial refresh interval in milliseconds (50-60000, default 1000). The interval is also adjustable while running via the "Update interval" button in the header bar; changing it resets the rolling averages.
+
+``--average WINDOWS`` - Show additional rolling-average columns for the given comma-separated time windows, e.g. ``--average 30s,1m,5m`` (suffixes: ``s`` seconds, ``m`` minutes, ``h`` hours; a bare number is seconds). Omit to show no average columns.
+
+``--average-only SUBSTRINGS`` - Only average sensors whose label contains one of these comma-separated substrings (case-insensitive), e.g. ``--average-only power,temp``. Non-matching rows still show Value/Min/Max but leave the average cells blank. Omit to average every sensor.
+
+## Command line interface (zenmonitor-cli)
+A headless build is available for terminals and panels:
+```
+make build-cli
+sudo make install-cli
+```
+It reuses the same sensor backends and supports ``--delay SECONDS`` (poll
+interval), ``--coreid``, ``--refresh-in-place`` (redraw in place), ``--output-once``,
+and ``--file FILE`` (stream readings to a CSV file, one row per refresh). The CSV
+is appended and flushed as it goes, so memory use stays constant, ``tail -f``
+works on it, and the log survives up to the last row if the machine crashes -
+which makes it suitable for long high-frequency captures.
+
+``--sensors SUBSTRINGS`` limits output to sensors whose label contains one of the
+given comma-separated substrings (case-insensitive), e.g.
+``--sensors "temperature,package power"``. Besides trimming the output and the
+per-sensor average buffers, if a whole backend has no matching sensors its
+per-tick read is skipped entirely (handy to avoid the MSR reads when you only
+want temperatures).
+
+### Rolling averages in a panel (daemon mode)
+A rolling average needs a process that has been sampling for the whole window,
+so short-lived panel commands can't compute one on their own. Run zenmonitor-cli
+as a small resident daemon instead:
+```
+zenmonitor-cli --daemon --delay 1 --average 1m,5m
+```
+It samples every ``--delay`` seconds and rewrites a snapshot file (default
+``$XDG_RUNTIME_DIR/zenmonitor.snapshot``, override with ``--snapshot FILE``)
+containing each sensor's current value and the configured rolling averages.
+The window→sample conversion follows ``--delay``, so ``5m`` is five minutes at
+any poll rate.
+
+``data/zenmonitor-cli.service`` is an example systemd *user* unit that keeps the
+daemon running. ``data/zenmonitor-genmon.sh`` reads the snapshot for the
+[xfce4-genmon-plugin](https://docs.xfce.org/panel-plugins/xfce4-genmon-plugin);
+point a genmon item at, for example:
+```
+zenmonitor-genmon.sh "CPU Temperature (tCtl)" "Avg 1m"
+```
+
+Note: temperature/SVI2 sensors (via the zenpower driver) work as a normal user,
+but RAPL package/core power needs MSR privileges. To include power in the daemon
+snapshot, grant capabilities to the binary:
+```
+sudo setcap cap_sys_rawio,cap_dac_read_search+ep /usr/local/bin/zenmonitor-cli
+```
+
## Installing
By default, Zenmonitor will be installed to /usr/local.
```
@@ -57,11 +111,9 @@ sudo modprobe msr
sudo bash -c 'echo "msr" > /etc/modules-load.d/msr.conf'
sudo apt install build-essential libgtk-3-dev git
cd ~
-git clone https://github.com/ocerman/zenmonitor
+git clone https://github.com/HonsW/zenmonitor
cd zenmonitor
make
sudo make install
sudo make install-polkit
```
-## Setup on Arch
-You may use the AUR package [zenmonitor-git](https://aur.archlinux.org/packages/zenmonitor-git/) to install via [traditional method](https://wiki.archlinux.org/index.php/Arch_User_Repository) or using an AUR helper (like yay)
diff --git a/data/zenmonitor-cli.service b/data/zenmonitor-cli.service
new file mode 100644
index 0000000..f696016
--- /dev/null
+++ b/data/zenmonitor-cli.service
@@ -0,0 +1,18 @@
+# Example systemd *user* service for the zenmonitor-cli snapshot daemon.
+# Install: cp data/zenmonitor-cli.service ~/.config/systemd/user/
+# systemctl --user enable --now zenmonitor-cli.service
+#
+# Note: SVI2/temperature sensors (zenpower) work as a normal user, but RAPL
+# package/core power comes from the MSR device and needs privileges. Grant them
+# to the binary if you want power in the snapshot:
+# sudo setcap cap_sys_rawio,cap_dac_read_search+ep /usr/local/bin/zenmonitor-cli
+
+[Unit]
+Description=Zenmonitor CLI snapshot daemon (rolling averages for panels)
+
+[Service]
+ExecStart=/usr/local/bin/zenmonitor-cli --daemon --delay 1 --average 1m,5m
+Restart=on-failure
+
+[Install]
+WantedBy=default.target
diff --git a/data/zenmonitor-genmon.sh b/data/zenmonitor-genmon.sh
new file mode 100755
index 0000000..6226a83
--- /dev/null
+++ b/data/zenmonitor-genmon.sh
@@ -0,0 +1,49 @@
+#!/bin/sh
+# xfce4-genmon-plugin consumer for the zenmonitor-cli daemon snapshot.
+#
+# Usage in a genmon panel item (Command field):
+# zenmonitor-genmon.sh "CPU Temperature (tCtl)" "Avg 1m"
+#
+# Arg 1: exact sensor label as written in the snapshot (default: first sensor)
+# Arg 2: column to display - "value" or one of the configured average
+# windows, e.g. "Avg 1m" (default: value)
+#
+# The snapshot is produced by: zenmonitor-cli --daemon --average 1m,5m
+# Path resolution: $ZENMONITOR_SNAPSHOT, else $XDG_RUNTIME_DIR/zenmonitor.snapshot,
+# else /tmp/zenmonitor.snapshot.
+
+snapshot="${ZENMONITOR_SNAPSHOT:-${XDG_RUNTIME_DIR:-/tmp}/zenmonitor.snapshot}"
+sensor="$1"
+column="${2:-value}"
+
+if [ ! -r "$snapshot" ]; then
+ echo "n/a"
+ echo "zenmonitor daemon not running ($snapshot)"
+ exit 0
+fi
+
+awk -F'\t' -v sensor="$sensor" -v col="$column" '
+ NR == 2 {
+ sub(/^# /, "", $0)
+ n = split($0, h, "\t")
+ for (i = 1; i <= n; i++)
+ if (h[i] == col) ci = i
+ if (sensor == "") skip_sensor = 1
+ next
+ }
+ NR > 2 && (skip_sensor || $1 == sensor) {
+ if (ci == "") { print "?col"; exit }
+ v = $ci
+ if (v == "") { print "--"; exit }
+ printf "%.1f\n", v
+ printf "%s — %s: %.2f\n", $1, col, v
+ found = 1
+ exit
+ }
+ END {
+ if (!found) {
+ print "n/a"
+ print "sensor not found: " sensor ""
+ }
+ }
+' "$snapshot"
diff --git a/makefile b/makefile
index dabe5d2..e57febc 100755
--- a/makefile
+++ b/makefile
@@ -1,25 +1,52 @@
+CC := cc
+
ifeq ($(PREFIX),)
PREFIX := /usr/local
endif
+BUILD_FILES_COMMON := \
+ src/ss/*.c \
+ src/sysfs.c \
+ src/zenmonitor-lib.c
+
+BUILD_FILES_GUI := \
+ $(BUILD_FILES_COMMON) \
+ src/gui.c \
+ src/zenmonitor.c
+
+BUILD_FILES_CLI := \
+ $(BUILD_FILES_COMMON) \
+ src/zenmonitor-cli.c
+
+.PHONY: build build-cli all install install-cli install-polkit uninstall uninstall-cli clean
+
build:
- cc -Isrc/include `pkg-config --cflags gtk+-3.0` src/*.c src/ss/*.c -o zenmonitor `pkg-config --libs gtk+-3.0` -lm -no-pie -Wall
+ $(CC) -Isrc/include `pkg-config --cflags gtk+-3.0` $(BUILD_FILES_GUI) -o zenmonitor `pkg-config --libs gtk+-3.0` -lm -no-pie -O2 -Wall $(CFLAGS)
+
+build-cli:
+ $(CC) -Isrc/include `pkg-config --cflags glib-2.0` $(BUILD_FILES_CLI) -o zenmonitor-cli `pkg-config --libs glib-2.0` -lm -lncurses -no-pie -O2 -Wall $(CFLAGS)
+
+all: build build-cli
install:
mkdir -p $(DESTDIR)$(PREFIX)/bin
install -m 755 zenmonitor $(DESTDIR)$(PREFIX)/bin
mkdir -p $(DESTDIR)$(PREFIX)/share/applications
- sed -e "s|@APP_EXEC@|${DESTDIR}${PREFIX}/bin/zenmonitor|" \
+ sed -e "s|@APP_EXEC@|${PREFIX}/bin/zenmonitor|" \
data/zenmonitor.desktop.in > \
$(DESTDIR)$(PREFIX)/share/applications/zenmonitor.desktop
+install-cli:
+ mkdir -p $(DESTDIR)$(PREFIX)/bin
+ install -m 755 zenmonitor-cli $(DESTDIR)$(PREFIX)/bin
+
install-polkit:
- sed -e "s|@APP_EXEC@|${DESTDIR}${PREFIX}/bin/zenmonitor|" \
+ sed -e "s|@APP_EXEC@|${PREFIX}/bin/zenmonitor|" \
data/zenmonitor-root.desktop.in > \
$(DESTDIR)$(PREFIX)/share/applications/zenmonitor-root.desktop
- sed -e "s|@APP_EXEC@|${DESTDIR}${PREFIX}/bin/zenmonitor|" \
+ sed -e "s|@APP_EXEC@|${PREFIX}/bin/zenmonitor|" \
data/org.pkexec.zenmonitor.policy.in > \
$(DESTDIR)/usr/share/polkit-1/actions/org.pkexec.zenmonitor.policy
@@ -29,5 +56,8 @@ uninstall:
rm -f $(DESTDIR)$(PREFIX)/share/applications/zenmonitor-root.desktop
rm -f $(DESTDIR)/usr/share/polkit-1/actions/org.pkexec.zenmonitor.policy
+uninstall-cli:
+ rm -f $(DESTDIR)$(PREFIX)/bin/zenmonitor-cli
+
clean:
- rm -f zenmonitor
+ rm -f zenmonitor zenmonitor-cli *.o
diff --git a/src/gui.c b/src/gui.c
index 11ef4f8..3d998fd 100644
--- a/src/gui.c
+++ b/src/gui.c
@@ -1,7 +1,15 @@
#include
#include
-#include "gui.h"
#include "zenmonitor.h"
+#include "gui.h"
+
+// Default sensor refresh cadence (ms). Overridable at startup via --interval
+// and at runtime via the "Update Interval" dialog. The interval also converts
+// the configured average windows (seconds) into a sample count, so changing it
+// re-parses the windows and resets the average history.
+#define DEFAULT_INTERVAL_MS 1000
+#define MIN_INTERVAL_MS 50
+#define MAX_INTERVAL_MS 60000
GtkWidget *window;
@@ -10,14 +18,63 @@ static guint timeout = 0;
static SensorSource *sensor_sources;
static const guint defaultHeight = 350;
-enum {
- COLUMN_NAME,
- COLUMN_HINT,
- COLUMN_VALUE,
- COLUMN_MIN,
- COLUMN_MAX,
- NUM_COLUMNS
-};
+// Fixed columns; any configured rolling-average columns follow at
+// COLUMN_AVG_BASE .. COLUMN_AVG_BASE + avg->count - 1.
+#define COLUMN_NAME 0
+#define COLUMN_HINT 1
+#define COLUMN_VALUE 2
+#define COLUMN_MIN 3
+#define COLUMN_MAX 4
+#define COLUMN_AVG_BASE 5
+
+// Current refresh cadence in ms (settable via --interval and the dialog).
+static guint refresh_interval_ms = DEFAULT_INTERVAL_MS;
+
+// Rolling-average configuration (count == 0 by default -> no average columns).
+static AvgWindows *avg = NULL;
+static AvgSeries *series = NULL; // one per tree row
+static gboolean *row_avg = NULL; // whether each row is averaged (--average-only)
+static gchar **avg_filter = NULL; // NULL => average every sensor
+static gchar *avg_spec = NULL; // retained window spec, re-parsed on interval change
+static guint n_rows = 0;
+
+static guint avg_count(void) {
+ return avg ? avg->count : 0;
+}
+
+// (Re)build the average windows from the retained spec and current interval.
+static void parse_averages(void) {
+ if (avg)
+ avg_windows_free(avg);
+ avg = avg_windows_parse(avg_spec, refresh_interval_ms);
+}
+
+// Set the initial refresh interval (clamped). Called before start_gui().
+void gui_set_interval(guint ms) {
+ if (ms < MIN_INTERVAL_MS)
+ ms = MIN_INTERVAL_MS;
+ if (ms > MAX_INTERVAL_MS)
+ ms = MAX_INTERVAL_MS;
+ refresh_interval_ms = ms;
+}
+
+// Retain the window spec (e.g. "30s,1m,5m"); parsing happens in start_gui once
+// the interval is known. NULL/empty leaves averaging disabled.
+void gui_set_averages(const gchar *spec) {
+ g_free(avg_spec);
+ avg_spec = (spec && *spec) ? g_strdup(spec) : NULL;
+}
+
+// Restrict which sensors get averaged to those whose label contains one of the
+// given comma-separated substrings. NULL/empty averages every sensor.
+void gui_set_average_filter(const gchar *spec) {
+ str_filter_free(avg_filter);
+ avg_filter = str_filter_parse(spec);
+}
+
+static guint window_width(void) {
+ return 500 + avg_count() * 100;
+}
static void init_sensors() {
GtkTreeIter iter;
@@ -25,7 +82,7 @@ static void init_sensors() {
GtkListStore *store;
SensorSource *source;
const SensorInit *data;
- guint i = 0;
+ guint i = 0, k;
store = GTK_LIST_STORE(model);
for (source = sensor_sources; source->drv; source++) {
@@ -45,17 +102,49 @@ static void init_sensors() {
COLUMN_MIN, " --- ",
COLUMN_MAX, " --- ",
-1);
+ for (k = 0; k < avg_count(); k++)
+ gtk_list_store_set(store, &iter, COLUMN_AVG_BASE + k, " --- ", -1);
sensor = sensor->next;
i++;
}
}
}
}
+
+ // Allocate per-row history once we know how many rows exist. A second pass
+ // (same iteration order) decides, per row, whether it is averaged, and only
+ // those rows get a ring buffer allocated.
+ n_rows = i;
+ if (avg_count() > 0 && n_rows > 0) {
+ guint r = 0;
+ series = g_new0(AvgSeries, n_rows);
+ row_avg = g_new0(gboolean, n_rows);
+
+ for (source = sensor_sources; source->drv; source++) {
+ if (!source->enabled)
+ continue;
+ for (sensor = source->sensors; sensor; sensor = sensor->next) {
+ data = (SensorInit *)sensor->data;
+ row_avg[r] = str_filter_match(avg_filter, data->label);
+ if (row_avg[r])
+ avg_series_init(&series[r], avg);
+ r++;
+ }
+ }
+ }
}
static GtkTreeModel* create_model (void) {
GtkListStore *store;
- store = gtk_list_store_new (NUM_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING);
+ guint n_columns = COLUMN_AVG_BASE + avg_count();
+ GType *types = g_new(GType, n_columns);
+ guint c;
+
+ for (c = 0; c < n_columns; c++)
+ types[c] = G_TYPE_STRING;
+
+ store = gtk_list_store_newv(n_columns, types);
+ g_free(types);
return GTK_TREE_MODEL (store);
}
@@ -69,22 +158,45 @@ static void set_list_column_value(float num, const gchar *printf_format, GtkTree
g_free(value);
}
+// Push a fresh reading into a row's history and refresh its average columns.
+static void update_averages(guint row, float value, const gchar *printf_format, GtkTreeIter *iter) {
+ guint k;
+
+ if (avg_count() == 0 || row >= n_rows || !row_avg[row])
+ return;
+
+ avg_series_push(&series[row], avg, value);
+ if (!series[row].valid)
+ return;
+
+ for (k = 0; k < avg->count; k++)
+ set_list_column_value((float)series[row].avg[k], printf_format, iter,
+ COLUMN_AVG_BASE + k);
+}
+
static gboolean update_data (gpointer data) {
GtkTreeIter iter;
GSList *node;
SensorSource *source;
const SensorInit *sensorData;
+ guint row = 0;
- if (model == NULL)
+ // On self-removal, clear the stored ID so apply_interval() won't
+ // g_source_remove() a source that no longer exists.
+ if (model == NULL) {
+ timeout = 0;
return G_SOURCE_REMOVE;
+ }
- if (!gtk_tree_model_get_iter_first (model, &iter))
+ if (!gtk_tree_model_get_iter_first (model, &iter)) {
+ timeout = 0;
return G_SOURCE_REMOVE;
+ }
for (source = sensor_sources; source->drv; source++) {
if (!source->enabled)
continue;
-
+
source->func_update();
if (source->sensors){
node = source->sensors;
@@ -94,7 +206,9 @@ static gboolean update_data (gpointer data) {
set_list_column_value(*(sensorData->value), sensorData->printf_format, &iter, COLUMN_VALUE);
set_list_column_value(*(sensorData->min), sensorData->printf_format, &iter, COLUMN_MIN);
set_list_column_value(*(sensorData->max), sensorData->printf_format, &iter, COLUMN_MAX);
+ update_averages(row, *(sensorData->value), sensorData->printf_format, &iter);
+ row++;
node = node->next;
if (!gtk_tree_model_iter_next(model, &iter))
break;
@@ -104,46 +218,33 @@ static gboolean update_data (gpointer data) {
return G_SOURCE_CONTINUE;
}
-static void add_columns (GtkTreeView *treeview) {
+static void append_text_column(GtkTreeView *treeview, const gchar *title, gint column) {
GtkCellRenderer *renderer;
- GtkTreeViewColumn *column;
+ GtkTreeViewColumn *col;
- // NAME
renderer = gtk_cell_renderer_text_new ();
- column = gtk_tree_view_column_new_with_attributes ("Sensor", renderer,
- "text", COLUMN_NAME,
- NULL);
+ col = gtk_tree_view_column_new_with_attributes (title, renderer,
+ "text", column,
+ NULL);
g_object_set(renderer, "family", "monotype", NULL);
- gtk_tree_view_append_column (treeview, column);
+ gtk_tree_view_append_column (treeview, col);
+}
- //VALUE
- renderer = gtk_cell_renderer_text_new ();
- column = gtk_tree_view_column_new_with_attributes ("Value", renderer,
- "text", COLUMN_VALUE,
- NULL);
- g_object_set(renderer, "family", "monotype", NULL);
- gtk_tree_view_append_column (treeview, column);
+static void add_columns (GtkTreeView *treeview) {
+ guint k;
- //MIN
- renderer = gtk_cell_renderer_text_new ();
- column = gtk_tree_view_column_new_with_attributes ("Min", renderer,
- "text", COLUMN_MIN,
- NULL);
- g_object_set(renderer, "family", "monotype", NULL);
- gtk_tree_view_append_column (treeview, column);
+ append_text_column(treeview, "Sensor", COLUMN_NAME);
+ append_text_column(treeview, "Value", COLUMN_VALUE);
+ append_text_column(treeview, "Min", COLUMN_MIN);
+ append_text_column(treeview, "Max", COLUMN_MAX);
- //MAX
- renderer = gtk_cell_renderer_text_new ();
- column = gtk_tree_view_column_new_with_attributes ("Max", renderer,
- "text", COLUMN_MAX,
- NULL);
- g_object_set(renderer, "family", "monotype", NULL);
- gtk_tree_view_append_column (treeview, column);
+ for (k = 0; k < avg_count(); k++)
+ append_text_column(treeview, avg->titles[k], COLUMN_AVG_BASE + k);
}
static void about_btn_clicked(GtkButton *button, gpointer user_data) {
GtkWidget *dialog;
- const gchar *website = "https://github.com/ocerman/zenmonitor";
+ const gchar *website = "https://github.com/HonsW/zenmonitor";
const gchar *msg = "Zen Monitor %s\n"
"Monitoring software for AMD Zen-based CPUs\n"
"%s\n\n"
@@ -169,6 +270,72 @@ static void clear_btn_clicked(GtkButton *button, gpointer user_data) {
}
}
+// Re-parse the average windows for the new interval and reset every averaged
+// row's history (samples taken at different cadences can't share a window).
+static void reset_averages(void) {
+ GtkTreeIter iter;
+ guint r = 0, k;
+
+ if (avg_count() == 0)
+ return;
+
+ parse_averages();
+
+ if (!series || !gtk_tree_model_get_iter_first(model, &iter))
+ return;
+
+ do {
+ if (r < n_rows && row_avg[r]) {
+ avg_series_free(&series[r]);
+ avg_series_init(&series[r], avg);
+ for (k = 0; k < avg->count; k++)
+ gtk_list_store_set(GTK_LIST_STORE(model), &iter,
+ COLUMN_AVG_BASE + k, " --- ", -1);
+ }
+ r++;
+ } while (gtk_tree_model_iter_next(model, &iter));
+}
+
+static void apply_interval(guint ms) {
+ if (ms == refresh_interval_ms)
+ return;
+
+ refresh_interval_ms = ms;
+ reset_averages(); // rescale windows and restart the average history
+
+ // Reschedule only if monitoring is actually running (timeout is 0 when no
+ // Zen CPU was detected and sensors were never initialised).
+ if (timeout) {
+ g_source_remove(timeout);
+ timeout = g_timeout_add(refresh_interval_ms, update_data, NULL);
+ }
+}
+
+static void interval_btn_clicked(GtkButton *button, gpointer user_data) {
+ GtkWidget *dialog, *content, *box, *label, *spin;
+
+ dialog = gtk_dialog_new_with_buttons("Update Interval", GTK_WINDOW(window),
+ GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
+ "_Cancel", GTK_RESPONSE_CANCEL,
+ "_OK", GTK_RESPONSE_OK, NULL);
+ content = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
+
+ box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
+ gtk_container_set_border_width(GTK_CONTAINER(box), 12);
+ label = gtk_label_new("Update interval (ms):");
+ spin = gtk_spin_button_new_with_range(MIN_INTERVAL_MS, MAX_INTERVAL_MS, 50);
+ gtk_spin_button_set_value(GTK_SPIN_BUTTON(spin), refresh_interval_ms);
+ gtk_box_pack_start(GTK_BOX(box), label, FALSE, FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(box), spin, FALSE, FALSE, 0);
+ gtk_container_add(GTK_CONTAINER(content), box);
+ gtk_widget_show_all(dialog);
+
+ if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_OK)
+ apply_interval((guint)gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(spin)));
+
+ gtk_widget_destroy(dialog);
+}
+
static gboolean mid_search_eq_func(GtkTreeModel *model, gint column, const gchar *key, GtkTreeIter *iter) {
gchar *iter_string = NULL, *lc_iter_string = NULL, *lc_key = NULL;
gboolean result;
@@ -201,12 +368,13 @@ static void resize_to_treeview(GtkWindow* window, GtkTreeView* treeview) {
gtk_tree_view_get_visible_rect(treeview, &r);
uiHeight = defaultHeight - r.height;
- gtk_window_resize(window, 500, uiHeight + (vSeparator + cellHeight) * rows);
+ gtk_window_resize(window, window_width(), uiHeight + (vSeparator + cellHeight) * rows);
}
int start_gui (SensorSource *ss) {
GtkWidget *about_btn;
GtkWidget *clear_btn;
+ GtkWidget *interval_btn;
GtkWidget *box;
GtkWidget *header;
GtkWidget *treeview;
@@ -216,14 +384,16 @@ int start_gui (SensorSource *ss) {
window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
- gtk_window_set_default_size(GTK_WINDOW(window), 500, defaultHeight);
+ gtk_window_set_default_size(GTK_WINDOW(window), window_width(), defaultHeight);
+ gchar *cpu_model_str = cpu_model();
header = gtk_header_bar_new();
gtk_header_bar_set_show_close_button(GTK_HEADER_BAR (header), TRUE);
gtk_header_bar_set_title(GTK_HEADER_BAR (header), "Zen monitor");
gtk_header_bar_set_has_subtitle(GTK_HEADER_BAR (header), TRUE);
- gtk_header_bar_set_subtitle(GTK_HEADER_BAR (header), cpu_model());
+ gtk_header_bar_set_subtitle(GTK_HEADER_BAR (header), cpu_model_str);
gtk_window_set_titlebar (GTK_WINDOW (window), header);
+ g_free(cpu_model_str);
box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
gtk_style_context_add_class (gtk_widget_get_style_context (box), "linked");
@@ -238,9 +408,15 @@ int start_gui (SensorSource *ss) {
gtk_container_add(GTK_CONTAINER(box), clear_btn);
gtk_widget_set_tooltip_text(clear_btn, "Clear Min/Max");
+ interval_btn = gtk_button_new();
+ gtk_container_add(GTK_CONTAINER(interval_btn), gtk_image_new_from_icon_name("preferences-system", GTK_ICON_SIZE_BUTTON));
+ gtk_container_add(GTK_CONTAINER(box), interval_btn);
+ gtk_widget_set_tooltip_text(interval_btn, "Update interval");
+
gtk_header_bar_pack_start(GTK_HEADER_BAR(header), box);
g_signal_connect(about_btn, "clicked", G_CALLBACK(about_btn_clicked), NULL);
g_signal_connect(clear_btn, "clicked", G_CALLBACK(clear_btn_clicked), NULL);
+ g_signal_connect(interval_btn, "clicked", G_CALLBACK(interval_btn_clicked), NULL);
g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL);
vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
@@ -251,6 +427,7 @@ int start_gui (SensorSource *ss) {
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW (sw), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
gtk_box_pack_start(GTK_BOX (vbox), sw, TRUE, TRUE, 0);
+ parse_averages(); // build windows from the spec + initial interval
model = create_model();
treeview = gtk_tree_view_new_with_model(model);
gtk_tree_view_set_tooltip_column(GTK_TREE_VIEW(treeview), COLUMN_HINT);
@@ -270,7 +447,7 @@ int start_gui (SensorSource *ss) {
init_sensors();
resize_to_treeview(GTK_WINDOW(window), GTK_TREE_VIEW(treeview));
- timeout = g_timeout_add(300, update_data, NULL);
+ timeout = g_timeout_add(refresh_interval_ms, update_data, NULL);
}
else{
dialog = gtk_message_dialog_new(GTK_WINDOW (window),
diff --git a/src/include/gui.h b/src/include/gui.h
index 0bc2501..7868956 100644
--- a/src/include/gui.h
+++ b/src/include/gui.h
@@ -1 +1,4 @@
-int start_gui();
+int start_gui(SensorSource *ss);
+void gui_set_interval(guint ms);
+void gui_set_averages(const gchar *spec);
+void gui_set_average_filter(const gchar *spec);
diff --git a/src/include/msr.h b/src/include/msr.h
index 664a5ee..dc0018c 100644
--- a/src/include/msr.h
+++ b/src/include/msr.h
@@ -1,4 +1,4 @@
-gboolean msr_init();
-void msr_update();
-void msr_clear_minmax();
-GSList* msr_get_sensors();
+gboolean msr_init(void);
+void msr_update(void);
+void msr_clear_minmax(void);
+GSList* msr_get_sensors(void);
diff --git a/src/include/zenmonitor.h b/src/include/zenmonitor.h
index 43a6e3f..adcf029 100644
--- a/src/include/zenmonitor.h
+++ b/src/include/zenmonitor.h
@@ -1,5 +1,10 @@
+#ifndef __ZENMONITOR_ZENMONITOR_H__
+#define __ZENMONITOR_ZENMONITOR_H__
+
+#include
+
#define ERROR_VALUE -999.0
-#define VERSION "1.4.2"
+#define VERSION "1.5.0"
typedef struct
{
@@ -14,17 +19,47 @@ SensorInit;
typedef struct {
const gchar *drv;
- gboolean (*func_init)();
- GSList* (*func_get_sensors)();
- void (*func_update)();
- void (*func_clear_minmax)();
+ gboolean (*func_init)(void);
+ GSList* (*func_get_sensors)(void);
+ void (*func_update)(void);
+ void (*func_clear_minmax)(void);
gboolean enabled;
GSList *sensors;
} SensorSource;
+// Rolling-average configuration, shared by the GUI columns and the CLI daemon.
+typedef struct {
+ gchar **titles; // count entries: column/label heading, e.g. "Avg 1m"
+ guint *samples; // count entries: window length expressed in samples
+ guint count; // number of windows (0 = averaging disabled)
+ guint cap; // ring capacity = largest window in samples
+} AvgWindows;
+
+// Per-series rolling state (one per GUI row / per CLI sensor).
+typedef struct {
+ float *buf; // ring buffer of the last AvgWindows.cap samples
+ gdouble *sum; // running sum per window
+ gdouble *avg; // last computed average per window
+ guint n; // number of valid samples pushed
+ gboolean valid; // TRUE once at least one sample is present
+} AvgSeries;
+
SensorInit* sensor_init_new(void);
void sensor_init_free(SensorInit *s);
-gboolean check_zen();
-gchar *cpu_model();
-guint get_core_count();
+gboolean check_zen(void);
+gchar *cpu_model(void);
+guint get_core_count(void);
+
+AvgWindows* avg_windows_parse(const gchar *spec, guint interval_ms);
+void avg_windows_free(AvgWindows *w);
+void avg_series_init(AvgSeries *s, const AvgWindows *w);
+void avg_series_free(AvgSeries *s);
+void avg_series_push(AvgSeries *s, const AvgWindows *w, float value);
+
+gchar** str_filter_parse(const gchar *spec);
+gboolean str_filter_match(gchar * const *filter, const gchar *text);
+void str_filter_free(gchar **filter);
+
extern gboolean display_coreid;
+
+#endif /* __ZENMONITOR_ZENMONITOR_H__ */
diff --git a/src/include/zenpower.h b/src/include/zenpower.h
index 54978d6..6b89727 100644
--- a/src/include/zenpower.h
+++ b/src/include/zenpower.h
@@ -1,4 +1,4 @@
-gboolean zenpower_init();
-GSList* zenpower_get_sensors();
-void zenpower_update();
-void zenpower_clear_minmax();
+gboolean zenpower_init(void);
+GSList* zenpower_get_sensors(void);
+void zenpower_update(void);
+void zenpower_clear_minmax(void);
diff --git a/src/ss/msr.c b/src/ss/msr.c
index 258d46c..8d3c33d 100644
--- a/src/ss/msr.c
+++ b/src/ss/msr.c
@@ -12,7 +12,6 @@
#define MSR_PWR_PRINTF_FORMAT " %8.3f W"
#define MSR_FID_PRINTF_FORMAT " %8.3f GHz"
-#define MESUREMENT_TIME 0.1
// AMD PPR = https://www.amd.com/system/files/TechDocs/54945_PPR_Family_17h_Models_00h-0Fh.pdf
// AMD OSRR = https://developer.amd.com/wp-content/resources/56255_3_03.PDF
@@ -21,6 +20,12 @@ static guint cores = 0;
static gdouble energy_unit = 0;
static struct cpudev *cpu_dev_ids;
+// Monotonic timestamp (microseconds) of the previous sample. RAPL energy is a
+// running counter, so power is the energy delta divided by the real elapsed
+// time between samples. Measuring over the natural refresh interval avoids a
+// blocking sleep that would otherwise freeze the GTK main loop each tick.
+static gint64 last_sample_time = 0;
+
static gint *msr_files = NULL;
static gulong package_eng_b = 0;
@@ -28,20 +33,20 @@ static gulong package_eng_a = 0;
static gulong *core_eng_b = NULL;
static gulong *core_eng_a = NULL;
-gfloat package_power;
-gfloat package_power_min;
-gfloat package_power_max;
-gfloat *core_power;
-gfloat *core_fid;
-gfloat *core_power_min;
-gfloat *core_power_max;
-gfloat *core_fid_min;
-gfloat *core_fid_max;
+static gfloat package_power;
+static gfloat package_power_min;
+static gfloat package_power_max;
+static gfloat *core_power;
+static gfloat *core_fid;
+static gfloat *core_power_min;
+static gfloat *core_power_max;
+static gfloat *core_fid_min;
+static gfloat *core_fid_max;
static gint open_msr(gshort devid) {
- gchar msr_path[20];
- sprintf(msr_path, "/dev/cpu/%d/msr", devid);
+ gchar msr_path[32];
+ snprintf(msr_path, sizeof msr_path, "/dev/cpu/%d/msr", devid);
return open(msr_path, O_RDONLY);
}
@@ -81,7 +86,7 @@ gulong get_core_energy(gint core) {
gdouble get_core_fid(gint core) {
gdouble ratio;
- gulong data;
+ gulong data, fdid;
// By reverse-engineering Ryzen Master, we know that
// this undocumented MSR is responsible for returning
@@ -93,13 +98,17 @@ gdouble get_core_fid(gint core) {
if (!read_msr(msr_files[core], 0xC0010293, &data))
return 0;
- ratio = (gdouble)(data & 0xff) / (gdouble)((data >> 8) & 0x3F);
+ fdid = (data >> 8) & 0x3F;
+ if (fdid == 0)
+ return 0;
+
+ ratio = (gdouble)(data & 0xff) / (gdouble)fdid;
// The effective ratio is based on increments of 200 MHz.
return ratio * 200.0 / 1000.0;
}
-gboolean msr_init() {
+gboolean msr_init(void) {
guint i;
if (!check_zen())
@@ -110,7 +119,7 @@ gboolean msr_init() {
return FALSE;
cpu_dev_ids = get_cpu_dev_ids();
- msr_files = malloc(cores * sizeof (gint));
+ msr_files = g_malloc(cores * sizeof (gint));
for (i = 0; i < cores; i++) {
msr_files[i] = open_msr(cpu_dev_ids[i].cpuid);
}
@@ -119,16 +128,26 @@ gboolean msr_init() {
if (energy_unit == 0)
return FALSE;
- core_eng_b = malloc(cores * sizeof (gulong));
- core_eng_a = malloc(cores * sizeof (gulong));
- core_power = malloc(cores * sizeof (gfloat));
- core_fid = malloc(cores * sizeof (gfloat));
- core_power_min = malloc(cores * sizeof (gfloat));
- core_power_max = malloc(cores * sizeof (gfloat));
- core_fid_min = malloc(cores * sizeof (gfloat));
- core_fid_max = malloc(cores * sizeof (gfloat));
+ core_eng_b = g_malloc(cores * sizeof (gulong));
+ core_eng_a = g_malloc(cores * sizeof (gulong));
+ core_power = g_malloc(cores * sizeof (gfloat));
+ core_fid = g_malloc(cores * sizeof (gfloat));
+ core_power_min = g_malloc(cores * sizeof (gfloat));
+ core_power_max = g_malloc(cores * sizeof (gfloat));
+ core_fid_min = g_malloc(cores * sizeof (gfloat));
+ core_fid_max = g_malloc(cores * sizeof (gfloat));
+
+ // Establish the energy/time baseline. Power stays at 0 until the first
+ // timer-driven msr_update() computes it over the elapsed interval.
+ last_sample_time = g_get_monotonic_time();
+ package_eng_b = get_package_energy();
+ package_power = 0;
+ for (i = 0; i < cores; i++) {
+ core_eng_b[i] = get_core_energy(i);
+ core_power[i] = 0;
+ core_fid[i] = get_core_fid(i);
+ }
- msr_update();
memcpy(core_power_min, core_power, cores * sizeof (gfloat));
memcpy(core_power_max, core_power, cores * sizeof (gfloat));
memcpy(core_fid_min, core_fid, cores * sizeof (gfloat));
@@ -139,40 +158,45 @@ gboolean msr_init() {
return TRUE;
}
-void msr_update() {
+void msr_update(void) {
guint i;
+ gint64 now;
+ gdouble elapsed;
- package_eng_b = get_package_energy();
- for (i = 0; i < cores; i++) {
- core_eng_b[i] = get_core_energy(i);
- }
-
- usleep(MESUREMENT_TIME*1000000);
+ // Read the current energy counters and compute power over the time that
+ // has actually elapsed since the previous sample. No blocking sleep, so
+ // the GTK main loop stays responsive between refreshes.
+ now = g_get_monotonic_time();
+ elapsed = (now - last_sample_time) / 1000000.0;
package_eng_a = get_package_energy();
for (i = 0; i < cores; i++) {
core_eng_a[i] = get_core_energy(i);
}
- if (package_eng_a >= package_eng_b) {
- package_power = (package_eng_a - package_eng_b) * energy_unit / MESUREMENT_TIME;
+ if (elapsed > 0) {
+ if (package_eng_a >= package_eng_b) {
+ package_power = (package_eng_a - package_eng_b) * energy_unit / elapsed;
- if (package_power < package_power_min)
- package_power_min = package_power;
- if (package_power > package_power_max)
- package_power_max = package_power;
- }
+ if (package_power < package_power_min)
+ package_power_min = package_power;
+ if (package_power > package_power_max)
+ package_power_max = package_power;
+ }
- for (i = 0; i < cores; i++) {
- if (core_eng_a[i] >= core_eng_b[i]) {
- core_power[i] = (core_eng_a[i] - core_eng_b[i]) * energy_unit / MESUREMENT_TIME;
+ for (i = 0; i < cores; i++) {
+ if (core_eng_a[i] >= core_eng_b[i]) {
+ core_power[i] = (core_eng_a[i] - core_eng_b[i]) * energy_unit / elapsed;
- if (core_power[i] < core_power_min[i])
- core_power_min[i] = core_power[i];
- if (core_power[i] > core_power_max[i])
- core_power_max[i] = core_power[i];
+ if (core_power[i] < core_power_min[i])
+ core_power_min[i] = core_power[i];
+ if (core_power[i] > core_power_max[i])
+ core_power_max[i] = core_power[i];
+ }
}
+ }
+ for (i = 0; i < cores; i++) {
core_fid[i] = get_core_fid(i);
if (core_fid[i] < core_fid_min[i])
@@ -180,9 +204,16 @@ void msr_update() {
if (core_fid[i] > core_fid_max[i])
core_fid_max[i] = core_fid[i];
}
+
+ // Current counters become the baseline for the next interval.
+ package_eng_b = package_eng_a;
+ for (i = 0; i < cores; i++) {
+ core_eng_b[i] = core_eng_a[i];
+ }
+ last_sample_time = now;
}
-void msr_clear_minmax() {
+void msr_clear_minmax(void) {
guint i;
package_power_min = package_power;
@@ -195,7 +226,7 @@ void msr_clear_minmax() {
}
}
-GSList* msr_get_sensors() {
+GSList* msr_get_sensors(void) {
GSList *list = NULL;
SensorInit *data;
guint i;
diff --git a/src/ss/os.c b/src/ss/os.c
index c2beb04..4d4ae71 100644
--- a/src/ss/os.c
+++ b/src/ss/os.c
@@ -11,9 +11,9 @@ static gchar **frq_files = NULL;
static guint cores;
static struct cpudev *cpu_dev_ids;
-gfloat *core_freq;
-gfloat *core_freq_min;
-gfloat *core_freq_max;
+static gfloat *core_freq;
+static gfloat *core_freq_min;
+static gfloat *core_freq_max;
static gdouble get_frequency(guint corei) {
gchar *data;
@@ -39,16 +39,16 @@ gboolean os_init(void) {
return FALSE;
cpu_dev_ids = get_cpu_dev_ids();
- frq_files = malloc(cores * sizeof (gchar*));
+ frq_files = g_malloc(cores * sizeof (gchar*));
for (i = 0; i < cores; i++) {
frq_files[i] = g_strdup_printf(
"/sys/devices/system/cpu/cpu%d/cpufreq/scaling_cur_freq",
cpu_dev_ids[i].cpuid);
}
- core_freq = malloc(cores * sizeof (gfloat));
- core_freq_min = malloc(cores * sizeof (gfloat));
- core_freq_max = malloc(cores * sizeof (gfloat));
+ core_freq = g_malloc(cores * sizeof (gfloat));
+ core_freq_min = g_malloc(cores * sizeof (gfloat));
+ core_freq_max = g_malloc(cores * sizeof (gfloat));
os_update();
memcpy(core_freq_min, core_freq, cores * sizeof (gfloat));
diff --git a/src/ss/zenpower.c b/src/ss/zenpower.c
index 2bf762b..dfb8614 100644
--- a/src/ss/zenpower.c
+++ b/src/ss/zenpower.c
@@ -4,7 +4,7 @@
#include "zenmonitor.h"
#include "zenpower.h"
-GSList *zp_sensors = NULL;
+static GSList *zp_sensors = NULL;
static int nodes = 0;
typedef struct
@@ -82,7 +82,7 @@ static HwmonSensor *hwmon_sensor_new(HwmonSensorType *type, const gchar *dir, gi
return s;
}
-gboolean zenpower_init() {
+gboolean zenpower_init(void) {
GDir *hwmon;
const gchar *entry;
gchar *name = NULL;
@@ -93,7 +93,8 @@ gboolean zenpower_init() {
return FALSE;
while ((entry = g_dir_read_name(hwmon))) {
- read_raw_hwmon_value(entry, "name", &name);
+ if (!read_raw_hwmon_value(entry, "name", &name))
+ continue;
if (strcmp(g_strchomp(name), "zenpower") == 0) {
@@ -106,6 +107,7 @@ gboolean zenpower_init() {
}
g_free(name);
+ name = NULL;
}
if (zp_sensors == NULL)
@@ -114,7 +116,7 @@ gboolean zenpower_init() {
return TRUE;
}
-void zenpower_update() {
+void zenpower_update(void) {
gchar *tmp = NULL;
GSList *node;
HwmonSensor *sensor;
@@ -141,7 +143,7 @@ void zenpower_update() {
}
}
-void zenpower_clear_minmax() {
+void zenpower_clear_minmax(void) {
HwmonSensor *sensor;
GSList *node;
node = zp_sensors;
@@ -153,7 +155,7 @@ void zenpower_clear_minmax() {
}
}
-GSList* zenpower_get_sensors() {
+GSList* zenpower_get_sensors(void) {
GSList *list = NULL;
HwmonSensor *sensor;
GSList *node;
diff --git a/src/sysfs.c b/src/sysfs.c
index a6cc675..d5d7b09 100644
--- a/src/sysfs.c
+++ b/src/sysfs.c
@@ -35,16 +35,16 @@ struct cpudev* get_cpu_dev_ids(void) {
guint cores;
gboolean found;
struct bitset seen = { 0 };
- int i;
+ guint i;
cores = get_core_count();
- cpu_dev_ids = malloc(cores * sizeof (*cpu_dev_ids));
+ cpu_dev_ids = g_malloc(cores * sizeof (*cpu_dev_ids));
for (i=0;i
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "zenmonitor.h"
+#include "zenpower.h"
+#include "msr.h"
+#include "os.h"
+
+gboolean display_coreid = 0;
+static gdouble delay = 0.5;
+static gchar *file = "";
+static gchar *average_spec = NULL;
+static gchar *snapshot_path = NULL;
+static gchar *sensor_spec = NULL;
+static gchar **sensor_filter = NULL; // NULL => every sensor is selected
+static gint refresh_in_place = 0;
+static gint output_once = 0;
+static gint daemon_mode = 0;
+
+static FILE *csv = NULL; // streaming CSV log (--file), NULL when off
+static guint n_selected = 0; // sensors passing the --sensors filter
+static volatile sig_atomic_t stop_requested = 0;
+
+static GOptionEntry options[] = {
+ {"file", 'f', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &file,
+ "Append readings to a CSV file (one row per refresh, crash-safe)", "FILE"},
+ {"delay", 'd', G_OPTION_FLAG_NONE, G_OPTION_ARG_DOUBLE, &delay,
+ "Interval between refreshes in seconds (default 0.5)", "SECONDS"},
+ {"coreid", 'c', 0, G_OPTION_ARG_NONE, &display_coreid,
+ "Display core_id instead of core index", NULL},
+ {"average", 'a', 0, G_OPTION_ARG_STRING, &average_spec,
+ "Rolling-average windows for --daemon output, e.g. 30s,1m,5m", "WINDOWS"},
+ {"sensors", 'S', 0, G_OPTION_ARG_STRING, &sensor_spec,
+ "Only handle sensors whose label contains one of these comma-separated substrings (case-insensitive)", "SUBSTRINGS"},
+ {"daemon", 'D', 0, G_OPTION_ARG_NONE, &daemon_mode,
+ "Run resident, writing a snapshot file each interval (keeps history for averages)", NULL},
+ {"snapshot", 's', 0, G_OPTION_ARG_STRING, &snapshot_path,
+ "Snapshot file path for --daemon (default $XDG_RUNTIME_DIR/zenmonitor.snapshot)", "FILE"},
+ {"refresh-in-place", 'r', 0, G_OPTION_ARG_NONE, &refresh_in_place,
+ "Redraw the readings in place instead of scrolling", NULL},
+ {"output-once", 'o', 0, G_OPTION_ARG_NONE, &output_once,
+ "Output CPU information once and quit", NULL},
+ {NULL}};
+
+static SensorSource sensor_sources[] = {
+ {
+ "zenpower",
+ zenpower_init, zenpower_get_sensors, zenpower_update, zenpower_clear_minmax,
+ FALSE, NULL
+ },
+ {
+ "msr",
+ msr_init, msr_get_sensors, msr_update, msr_clear_minmax,
+ FALSE, NULL
+ },
+ {
+ "os",
+ os_init, os_get_sensors, os_update, os_clear_minmax,
+ FALSE, NULL
+ },
+ {
+ NULL
+ }
+};
+
+// Async-signal-safe: just request a clean shutdown. The ncurses teardown,
+// CSV close, and daemon snapshot removal happen back in the main loop.
+static void request_stop(int signum) {
+ (void)signum;
+ stop_requested = 1;
+}
+
+#define sensor_selected(label) str_filter_match(sensor_filter, (label))
+
+static void init_sensors(void) {
+ GSList *sensor;
+ SensorSource *source;
+ const SensorInit *data;
+
+ for (source = sensor_sources; source->drv; source++) {
+ if (source->func_init()) {
+ source->sensors = source->func_get_sensors();
+ if (source->sensors != NULL) {
+ guint selected = 0;
+ sensor = source->sensors;
+ while (sensor) {
+ data = (SensorInit *)sensor->data;
+ if (sensor_selected(data->label))
+ selected++;
+ sensor = sensor->next;
+ }
+ // Skip the whole source at update time when nothing matched,
+ // avoiding its per-tick sensor reads (e.g. the MSR preads).
+ source->enabled = (selected > 0);
+ n_selected += selected;
+ }
+ }
+ }
+}
+
+// Open the CSV log and write the header. Rows are appended one per refresh in
+// update_data() and flushed immediately, so the file is valid mid-run
+// (tail -f works) and survives a crash up to the last row - unlike the old
+// dump-at-exit design, which also held every sample in memory.
+static gboolean csv_open(const gchar *path) {
+ SensorSource *source;
+ GSList *node;
+ const SensorInit *data;
+
+ csv = fopen(path, "w");
+ if (!csv) {
+ fprintf(stderr, "zenmonitor-cli: cannot open '%s': %s\n",
+ path, g_strerror(errno));
+ return FALSE;
+ }
+
+ // Column order matches the iteration order used in update_data().
+ fprintf(csv, "time(epoch)");
+ for (source = sensor_sources; source->drv; source++) {
+ if (!source->enabled)
+ continue;
+ for (node = source->sensors; node; node = node->next) {
+ data = (SensorInit *)node->data;
+ if (sensor_selected(data->label))
+ fprintf(csv, ",%s", data->label);
+ }
+ }
+ fprintf(csv, "\n");
+ fflush(csv);
+ return TRUE;
+}
+
+static void update_data(void) {
+ SensorSource *source;
+ GSList *node;
+ const SensorInit *sensorData;
+ int row = 1; // ncurses uses 1-based row indexing
+
+ if (csv) {
+ struct timespec ts;
+ timespec_get(&ts, TIME_UTC);
+ fprintf(csv, "%ld.%.9ld", (long)ts.tv_sec, (long)ts.tv_nsec);
+ }
+
+ for (source = sensor_sources; source->drv; source++) {
+ if (!source->enabled)
+ continue;
+
+ source->func_update();
+ if (!source->sensors)
+ continue;
+
+ node = source->sensors;
+ while (node) {
+ sensorData = (SensorInit *)node->data;
+ if (!sensor_selected(sensorData->label)) {
+ node = node->next;
+ continue;
+ }
+ if (csv)
+ fprintf(csv, ",%f", *sensorData->value);
+
+ if (refresh_in_place) {
+ mvprintw(row++, 0, "%s\t%f", sensorData->label, *sensorData->value);
+ } else {
+ printf("%s\t%f\n", sensorData->label, *sensorData->value);
+ }
+ node = node->next;
+ }
+ }
+
+ if (csv) {
+ fprintf(csv, "\n");
+ fflush(csv);
+ }
+
+ if (refresh_in_place)
+ refresh();
+ else
+ printf("\v");
+}
+
+static void start_watching(void) {
+ while (!stop_requested) {
+ update_data();
+ if (output_once)
+ break;
+ usleep(delay * 1000 * 1000);
+ }
+}
+
+// ---- Daemon mode ----------------------------------------------------------
+// Keeps rolling-average history in memory and rewrites a snapshot file each
+// interval, so short-lived readers (e.g. an xfce4-genmon panel) can display a
+// trailing average without holding any history themselves.
+
+static gchar *default_snapshot_path(void) {
+ const gchar *runtime = g_getenv("XDG_RUNTIME_DIR");
+ if (runtime && *runtime)
+ return g_build_filename(runtime, "zenmonitor.snapshot", NULL);
+ return g_strdup("/tmp/zenmonitor.snapshot");
+}
+
+// Write the snapshot atomically: fill a temp file, then rename over the target.
+static void write_snapshot(const gchar *path, const AvgWindows *avg, AvgSeries *series) {
+ SensorSource *source;
+ GSList *node;
+ const SensorInit *sensorData;
+ gchar *tmp;
+ FILE *out;
+ guint idx = 0, k;
+
+ tmp = g_strdup_printf("%s.tmp", path);
+ out = fopen(tmp, "w");
+ if (!out) {
+ fprintf(stderr, "zenmonitor-cli: cannot open '%s': %s\n", tmp, g_strerror(errno));
+ g_free(tmp);
+ return;
+ }
+
+ fprintf(out, "# updated %ld\n", (long)time(NULL));
+ fprintf(out, "# sensor\tvalue");
+ for (k = 0; k < avg->count; k++)
+ fprintf(out, "\t%s", avg->titles[k]);
+ fprintf(out, "\n");
+
+ for (source = sensor_sources; source->drv; source++) {
+ if (!source->enabled)
+ continue;
+
+ source->func_update();
+ if (!source->sensors)
+ continue;
+
+ node = source->sensors;
+ while (node) {
+ sensorData = (SensorInit *)node->data;
+ if (!sensor_selected(sensorData->label)) {
+ node = node->next;
+ continue;
+ }
+ float value = *sensorData->value;
+
+ avg_series_push(&series[idx], avg, value);
+
+ fprintf(out, "%s\t%f", sensorData->label, value);
+ for (k = 0; k < avg->count; k++) {
+ if (series[idx].valid)
+ fprintf(out, "\t%f", series[idx].avg[k]);
+ else
+ fprintf(out, "\t");
+ }
+ fprintf(out, "\n");
+
+ idx++;
+ node = node->next;
+ }
+ }
+
+ fclose(out);
+
+ if (rename(tmp, path) != 0)
+ fprintf(stderr, "zenmonitor-cli: cannot update '%s': %s\n", path, g_strerror(errno));
+
+ g_free(tmp);
+}
+
+static int run_daemon(void) {
+ AvgWindows *avg;
+ AvgSeries *series;
+ guint n_sensors, i;
+ guint interval_ms;
+
+ if (!snapshot_path)
+ snapshot_path = default_snapshot_path();
+
+ interval_ms = (guint)(delay * 1000.0 + 0.5);
+ avg = avg_windows_parse(average_spec, interval_ms);
+
+ n_sensors = n_selected;
+ series = g_new0(AvgSeries, n_sensors ? n_sensors : 1);
+ for (i = 0; i < n_sensors; i++)
+ avg_series_init(&series[i], avg);
+
+ fprintf(stderr, "zenmonitor-cli: daemon writing '%s' every %.3gs\n",
+ snapshot_path, delay);
+
+ while (!stop_requested) {
+ write_snapshot(snapshot_path, avg, series);
+ if (output_once)
+ break;
+ usleep(delay * 1000 * 1000);
+ }
+
+ if (!output_once)
+ unlink(snapshot_path);
+
+ for (i = 0; i < n_sensors; i++)
+ avg_series_free(&series[i]);
+ g_free(series);
+ avg_windows_free(avg);
+ return EXIT_SUCCESS;
+}
+
+int main(int argc, char *argv[]) {
+ GError *error = NULL;
+ GOptionContext *context;
+ gboolean write_csv;
+ int ret;
+
+ context = g_option_context_new("- Zenmonitor command line interface");
+ g_option_context_add_main_entries(context, options, NULL);
+ if (!g_option_context_parse(context, &argc, &argv, &error)) {
+ g_print("option parsing failed: %s\n", error->message);
+ exit(1);
+ }
+
+ write_csv = (strcmp(file, "") != 0);
+ sensor_filter = str_filter_parse(sensor_spec);
+
+ // Guard against a zero/negative interval: 0 would busy-loop, and a
+ // negative value wraps through the usleep() and window-math casts.
+ if (delay < 0.05) {
+ fprintf(stderr, "zenmonitor-cli: --delay too small, using 0.05s\n");
+ delay = 0.05;
+ }
+
+ // Handle Ctrl-C/termination ourselves so ncurses is torn down, the CSV is
+ // closed, and the daemon snapshot is cleaned up.
+ signal(SIGINT, request_stop);
+ signal(SIGTERM, request_stop);
+
+ init_sensors();
+
+ if (daemon_mode) {
+ if (write_csv)
+ fprintf(stderr, "zenmonitor-cli: --file is ignored in --daemon mode\n");
+ ret = run_daemon();
+ return ret;
+ }
+
+ // Fail fast if the log can't be opened, instead of sampling for hours and
+ // discovering it at exit.
+ if (write_csv && !csv_open(file))
+ return EXIT_FAILURE;
+
+ if (refresh_in_place) {
+ initscr();
+ curs_set(0);
+ }
+
+ start_watching();
+
+ if (refresh_in_place)
+ endwin();
+
+ if (csv)
+ fclose(csv);
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/zenmonitor-lib.c b/src/zenmonitor-lib.c
new file mode 100644
index 0000000..22dbe49
--- /dev/null
+++ b/src/zenmonitor-lib.c
@@ -0,0 +1,267 @@
+#define _GNU_SOURCE /* for strcasestr */
+#include
+#include
+#include "zenmonitor.h"
+
+#define AMD_STRING "AuthenticAMD"
+#define ZEN_FAMILY 0x17
+#define ZEN3_FAMILY 0x19
+
+// AMD PPR = https://www.amd.com/system/files/TechDocs/54945_PPR_Family_17h_Models_00h-0Fh.pdf
+
+gboolean check_zen(void) {
+ guint32 eax = 0, ebx = 0, ecx = 0, edx = 0, ext_family;
+ char vendor[13];
+
+ __get_cpuid(0, &eax, &ebx, &ecx, &edx);
+
+ memcpy(vendor, &ebx, 4);
+ memcpy(vendor+4, &edx, 4);
+ memcpy(vendor+8, &ecx, 4);
+ vendor[12] = 0;
+
+ if (strcmp(vendor, AMD_STRING) != 0){
+ return FALSE;
+ }
+
+ __get_cpuid(1, &eax, &ebx, &ecx, &edx);
+
+ ext_family = ((eax >> 8) & 0xF) + ((eax >> 20) & 0xFF);
+ if (ext_family != ZEN_FAMILY && ext_family != ZEN3_FAMILY){
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+gchar *cpu_model(void) {
+ guint32 eax = 0, ebx = 0, ecx = 0, edx = 0;
+ char model[49];
+
+ // AMD PPR: page 65-68 - CPUID_Fn80000002_EAX-CPUID_Fn80000004_EDX
+ __get_cpuid(0x80000002, &eax, &ebx, &ecx, &edx);
+ memcpy(model, &eax, 4);
+ memcpy(model+4, &ebx, 4);
+ memcpy(model+8, &ecx, 4);
+ memcpy(model+12, &edx, 4);
+
+ __get_cpuid(0x80000003, &eax, &ebx, &ecx, &edx);
+ memcpy(model+16, &eax, 4);
+ memcpy(model+20, &ebx, 4);
+ memcpy(model+24, &ecx, 4);
+ memcpy(model+28, &edx, 4);
+
+ __get_cpuid(0x80000004, &eax, &ebx, &ecx, &edx);
+ memcpy(model+32, &eax, 4);
+ memcpy(model+36, &ebx, 4);
+ memcpy(model+40, &ecx, 4);
+ memcpy(model+44, &edx, 4);
+
+ model[48] = 0;
+ return g_strdup(g_strchomp(model));
+}
+
+guint get_core_count(void) {
+ guint eax = 0, ebx = 0, ecx = 0, edx = 0;
+ guint logical_cpus, threads_per_core;
+
+ // AMD PPR: page 57 - CPUID_Fn00000001_EBX
+ __get_cpuid(1, &eax, &ebx, &ecx, &edx);
+ logical_cpus = (ebx >> 16) & 0xFF;
+
+ // AMD PPR: page 82 - CPUID_Fn8000001E_EBX
+ __get_cpuid(0x8000001E, &eax, &ebx, &ecx, &edx);
+ // ThreadsPerCore is zero-based, so add 1 (always >= 1).
+ threads_per_core = ((ebx >> 8) & 0xF) + 1;
+
+ return logical_cpus / threads_per_core;
+}
+
+SensorInit *sensor_init_new(void) {
+ return g_new0(SensorInit, 1);
+}
+
+void sensor_init_free(SensorInit *s) {
+ if (s) {
+ g_free(s->label);
+ g_free(s->hint);
+ g_free(s);
+ }
+}
+
+// ---- Rolling averages -----------------------------------------------------
+// A window given in seconds is converted to a sample count using the caller's
+// sampling interval, so the same "5m" means 5 minutes regardless of how often
+// the GUI or CLI polls. Each series keeps a ring buffer sized to the largest
+// window plus a running sum per window, yielding an exact windowed mean in
+// O(windows) per push with no re-summing.
+
+static gboolean parse_window_seconds(const gchar *tok, gdouble *out_seconds) {
+ gchar *end = NULL;
+ gdouble val, mult;
+
+ val = g_ascii_strtod(tok, &end);
+ if (end == tok)
+ return FALSE;
+
+ while (*end == ' ')
+ end++;
+
+ switch (*end) {
+ case '\0':
+ case 's': case 'S': mult = 1.0; break;
+ case 'm': case 'M': mult = 60.0; break;
+ case 'h': case 'H': mult = 3600.0; break;
+ default: return FALSE;
+ }
+
+ *out_seconds = val * mult;
+ return TRUE;
+}
+
+AvgWindows *avg_windows_parse(const gchar *spec, guint interval_ms) {
+ AvgWindows *w;
+ gchar **tokens;
+ guint i;
+
+ w = g_new0(AvgWindows, 1);
+ if (!spec || *spec == '\0' || interval_ms == 0)
+ return w; // count == 0: averaging disabled
+
+ tokens = g_strsplit(spec, ",", -1);
+ w->titles = g_new0(gchar *, g_strv_length(tokens));
+ w->samples = g_new0(guint, g_strv_length(tokens));
+
+ for (i = 0; tokens[i]; i++) {
+ gchar *tok = g_strstrip(tokens[i]);
+ gdouble seconds;
+ guint samples;
+
+ if (*tok == '\0')
+ continue;
+
+ if (!parse_window_seconds(tok, &seconds) || seconds <= 0) {
+ g_printerr("zenmonitor: ignoring invalid average window '%s'\n", tok);
+ continue;
+ }
+
+ samples = (guint)((seconds * 1000.0) / interval_ms + 0.5);
+ if (samples < 1)
+ samples = 1;
+
+ w->titles[w->count] = g_strdup_printf("Avg %s", tok);
+ w->samples[w->count] = samples;
+ if (samples > w->cap)
+ w->cap = samples;
+ w->count++;
+ }
+
+ g_strfreev(tokens);
+ return w;
+}
+
+void avg_windows_free(AvgWindows *w) {
+ guint i;
+ if (!w)
+ return;
+ for (i = 0; i < w->count; i++)
+ g_free(w->titles[i]);
+ g_free(w->titles);
+ g_free(w->samples);
+ g_free(w);
+}
+
+void avg_series_init(AvgSeries *s, const AvgWindows *w) {
+ s->n = 0;
+ s->valid = FALSE;
+ if (w->count == 0) {
+ s->buf = NULL;
+ s->sum = NULL;
+ s->avg = NULL;
+ return;
+ }
+ s->buf = g_new0(float, w->cap);
+ s->sum = g_new0(gdouble, w->count);
+ s->avg = g_new0(gdouble, w->count);
+}
+
+void avg_series_free(AvgSeries *s) {
+ if (!s)
+ return;
+ g_free(s->buf);
+ g_free(s->sum);
+ g_free(s->avg);
+ s->buf = NULL;
+ s->sum = NULL;
+ s->avg = NULL;
+}
+
+// ---- Substring filter -----------------------------------------------------
+// Parse a comma-separated list of substrings into a NULL-terminated array
+// (empty tokens dropped). Returns NULL for an empty/unset spec, meaning
+// "match everything". Free with str_filter_free.
+
+gchar **str_filter_parse(const gchar *spec) {
+ gchar **raw;
+ GPtrArray *arr;
+ guint i;
+
+ if (!spec || *spec == '\0')
+ return NULL;
+
+ raw = g_strsplit(spec, ",", -1);
+ arr = g_ptr_array_new();
+ for (i = 0; raw[i]; i++) {
+ gchar *tok = g_strstrip(raw[i]);
+ if (*tok)
+ g_ptr_array_add(arr, g_strdup(tok));
+ }
+ g_strfreev(raw);
+
+ if (arr->len == 0) {
+ g_ptr_array_free(arr, TRUE);
+ return NULL;
+ }
+
+ g_ptr_array_add(arr, NULL);
+ return (gchar **)g_ptr_array_free(arr, FALSE);
+}
+
+gboolean str_filter_match(gchar * const *filter, const gchar *text) {
+ guint i;
+ if (!filter)
+ return TRUE;
+ for (i = 0; filter[i]; i++)
+ if (strcasestr(text, filter[i]))
+ return TRUE;
+ return FALSE;
+}
+
+void str_filter_free(gchar **filter) {
+ if (filter)
+ g_strfreev(filter);
+}
+
+void avg_series_push(AvgSeries *s, const AvgWindows *w, float value) {
+ guint k;
+
+ // Skip error readings so a transient failure doesn't skew the window.
+ if (w->count == 0 || value == ERROR_VALUE)
+ return;
+
+ for (k = 0; k < w->count; k++) {
+ guint window = w->samples[k];
+ guint denom;
+
+ s->sum[k] += value;
+ if (s->n >= window)
+ s->sum[k] -= s->buf[(s->n - window) % w->cap];
+
+ denom = (s->n + 1 < window) ? (s->n + 1) : window;
+ s->avg[k] = s->sum[k] / denom;
+ }
+
+ s->buf[s->n % w->cap] = value;
+ s->n++;
+ s->valid = TRUE;
+}
diff --git a/src/zenmonitor.c b/src/zenmonitor.c
index d02c510..cea5b05 100644
--- a/src/zenmonitor.c
+++ b/src/zenmonitor.c
@@ -1,6 +1,4 @@
#include
-#include
-#include
#include
#include "zenmonitor.h"
#include "zenpower.h"
@@ -8,81 +6,6 @@
#include "os.h"
#include "gui.h"
-#define AMD_STRING "AuthenticAMD"
-#define ZEN_FAMILY 0x17
-
-// AMD PPR = https://www.amd.com/system/files/TechDocs/54945_PPR_Family_17h_Models_00h-0Fh.pdf
-
-gboolean check_zen() {
- guint32 eax = 0, ebx = 0, ecx = 0, edx = 0, ext_family;
- char vendor[13];
-
- __get_cpuid(0, &eax, &ebx, &ecx, &edx);
-
- memcpy(vendor, &ebx, 4);
- memcpy(vendor+4, &edx, 4);
- memcpy(vendor+8, &ecx, 4);
- vendor[12] = 0;
-
- if (strcmp(vendor, AMD_STRING) != 0){
- return FALSE;
- }
-
- __get_cpuid(1, &eax, &ebx, &ecx, &edx);
-
- ext_family = ((eax >> 8) & 0xF) + ((eax >> 20) & 0xFF);
- if (ext_family != ZEN_FAMILY){
- return FALSE;
- }
-
- return TRUE;
-}
-
-gchar *cpu_model() {
- guint32 eax = 0, ebx = 0, ecx = 0, edx = 0;
- char model[48];
-
- // AMD PPR: page 65-68 - CPUID_Fn80000002_EAX-CPUID_Fn80000004_EDX
- __get_cpuid(0x80000002, &eax, &ebx, &ecx, &edx);
- memcpy(model, &eax, 4);
- memcpy(model+4, &ebx, 4);
- memcpy(model+8, &ecx, 4);
- memcpy(model+12, &edx, 4);
-
- __get_cpuid(0x80000003, &eax, &ebx, &ecx, &edx);
- memcpy(model+16, &eax, 4);
- memcpy(model+20, &ebx, 4);
- memcpy(model+24, &ecx, 4);
- memcpy(model+28, &edx, 4);
-
- __get_cpuid(0x80000004, &eax, &ebx, &ecx, &edx);
- memcpy(model+32, &eax, 4);
- memcpy(model+36, &ebx, 4);
- memcpy(model+40, &ecx, 4);
- memcpy(model+44, &edx, 4);
-
- model[48] = 0;
- return g_strdup(g_strchomp(model));
-}
-
-guint get_core_count() {
- guint eax = 0, ebx = 0, ecx = 0, edx = 0;
- guint logical_cpus, threads_per_code;
-
- // AMD PPR: page 57 - CPUID_Fn00000001_EBX
- __get_cpuid(1, &eax, &ebx, &ecx, &edx);
- logical_cpus = (ebx >> 16) & 0xFF;
-
- // AMD PPR: page 82 - CPUID_Fn8000001E_EBX
- __get_cpuid(0x8000001E, &eax, &ebx, &ecx, &edx);
- threads_per_code = ((ebx >> 8) & 0xF) + 1;
-
- if (threads_per_code == 0)
- return logical_cpus;
-
- return logical_cpus / threads_per_code;
-}
-
static SensorSource sensor_sources[] = {
{
"zenpower",
@@ -104,23 +27,17 @@ static SensorSource sensor_sources[] = {
}
};
-SensorInit *sensor_init_new() {
- return g_new0(SensorInit, 1);
-}
-
-void sensor_init_free(SensorInit *s) {
- if (s) {
- g_free(s->label);
- g_free(s->hint);
- g_free(s);
- }
-}
-
gboolean display_coreid = 0;
+static gint interval_ms = 1000;
+static gchar *average_spec = NULL;
+static gchar *average_only_spec = NULL;
static GOptionEntry options[] =
{
{ "coreid", 'c', 0, G_OPTION_ARG_NONE, &display_coreid, "Display core_id instead of core index", NULL },
+ { "interval", 'i', 0, G_OPTION_ARG_INT, &interval_ms, "Initial refresh interval in ms (50-60000, default 1000; adjustable at runtime)", "MS" },
+ { "average", 'a', 0, G_OPTION_ARG_STRING, &average_spec, "Show rolling-average columns for the given comma-separated windows (e.g. 30s,1m,5m)", "WINDOWS" },
+ { "average-only", 'A', 0, G_OPTION_ARG_STRING, &average_only_spec, "Only average sensors whose label contains one of these comma-separated substrings (e.g. power,temp)", "SUBSTRINGS" },
{ NULL }
};
@@ -137,5 +54,9 @@ int main (int argc, char *argv[])
exit (1);
}
+ // A negative value must clamp to the minimum, not wrap around the cast.
+ gui_set_interval(interval_ms > 0 ? (guint)interval_ms : 0);
+ gui_set_averages(average_spec);
+ gui_set_average_filter(average_only_spec);
start_gui(sensor_sources);
}