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); }