Skip to content

Harden zenmonitor, add non-blocking sampling, headless CLI, and rolling averages#1

Open
HonsW wants to merge 4 commits into
masterfrom
claude/zenmonitor-improvements
Open

Harden zenmonitor, add non-blocking sampling, headless CLI, and rolling averages#1
HonsW wants to merge 4 commits into
masterfrom
claude/zenmonitor-improvements

Conversation

@HonsW

@HonsW HonsW commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Consolidated set of improvements, developed and verified incrementally. Bumps the version to 1.5.0.

Verification note: runtime sensor values require an AMD Zen host and couldn't be confirmed in the build environment. Everything builds clean (-O2 -Wall, both binaries), and the pure logic (averaging math, substring filter) was unit-checked against brute-force references. The streaming CSV's crash-safety was verified by kill -9 mid-run.

Correctness & safety

  • cpu_model(): fix a 1-byte stack buffer overflow (model[48]model[49]).
  • zenpower_init(): check the hwmon name read and reset the buffer after free — avoids a NULL deref / use-after-free.
  • get_core_fid(): guard against divide-by-zero when the FDID field is 0.
  • mallocg_malloc; sprintfsnprintf; drop dead code + a typo in get_core_count(); fix a signed/unsigned comparison and a shadowed counter in sysfs.c; free the leaked cpu_model() result in the GUI; convert K&R () prototypes to explicit/(void); make file-local sensor arrays static.

CPU support

  • Accept family 0x19 (Zen 3 / Zen 4) alongside 0x17, matching the established zenmonitor3 fork (RAPL/energy/FID logic unchanged).

UI responsiveness & refresh interval

  • Remove the blocking usleep(100ms) from msr_update(). RAPL power is now measured over the real elapsed time between refreshes (monotonic clock), so the GTK main loop is no longer frozen ~⅓ of each cycle.
  • Runtime-configurable refresh interval: a header-bar button opens an "Update Interval" dialog (50–60000 ms), and --interval/-i sets the initial value (default 1000 ms). Not persisted — all configuration stays on the command line. Changing the interval re-parses the average windows for the new cadence and resets the average history (samples from different cadences can't share a window).

Headless CLI (zenmonitor-cli)

  • Split the shared core into zenmonitor-lib.c; add a CLI build with stdout output, --delay (clamped to ≥ 0.05 s), --coreid, --refresh-in-place (ncurses), and --output-once. makefile gains build-cli/install-cli/all; also drops DESTDIR from the .desktop Exec path.
  • --file streams a CSV log: header once, one row appended + flushed per refresh. Memory use is constant regardless of run length or sample rate, tail -f works on the live file, and the log survives up to the last row on crash/SIGKILL — suitable for long high-frequency captures. Fails fast if the file can't be opened; warns that it's ignored in --daemon mode.
  • Signal handler only sets a flag (teardown runs in the main loop).

Rolling averages

  • Optional GUI columns via --average (e.g. 30s,1m,5m), backed by a central ring-buffer + running-sum engine (exact windowed mean, O(windows)/tick). --average-only restricts which sensors are averaged. Omitting either keeps the original behavior.
  • CLI --daemon keeps history resident and atomically rewrites a snapshot file each interval (default under $XDG_RUNTIME_DIR, i.e. tmpfs/RAM), so short-lived readers (e.g. an xfce4-genmon panel) can show a trailing average. --sensors limits which sensors are handled and skips a backend's per-tick reads when nothing matches. Example genmon script and systemd user unit under data/.

The averaging math and the substring filter live once in zenmonitor-lib.c, shared by the GUI and CLI.

Docs & housekeeping

  • README documents all new options; the clone URL and the About dialog now point at this repository (original-author credit unchanged); the AUR section (which packages the upstream project) was dropped.

🤖 Generated with Claude Code

claude added 4 commits July 17, 2026 22:27
…ng averages

This is a consolidated set of improvements to zenmonitor, developed and
verified incrementally. Runtime sensor values require an AMD Zen host to
confirm; everything was built clean (-O2 -Wall) and the pure logic
(averaging, filters) was unit-checked against brute-force references.

Correctness & safety:
- cpu_model(): fix a 1-byte stack buffer overflow (model[48] -> model[49]).
- zenpower_init(): check the hwmon "name" read and reset the buffer after
  free, avoiding a NULL deref / use-after-free.
- get_core_fid(): guard against divide-by-zero when the FDID field is 0.
- Replace unchecked malloc() with g_malloc(); sprintf -> snprintf; drop dead
  code and a typo in get_core_count(); fix a signed/unsigned comparison and a
  shadowed counter in sysfs.c; free the leaked cpu_model() result in the GUI;
  convert K&R () prototypes to explicit/(void) forms.

CPU support:
- Accept family 0x19 (Zen 3 / Zen 4) in addition to 0x17, matching the
  established zenmonitor3 fork (RAPL/energy/FID logic unchanged).

UI responsiveness:
- Remove the blocking usleep(100ms) from msr_update(); RAPL power is now
  measured over the real elapsed time between refreshes (monotonic clock), so
  the GTK main loop is no longer frozen ~1/3 of each cycle. Refresh interval
  lowered to 200ms now that ticks are cheap.

Headless CLI (zenmonitor-cli):
- Split the shared core into zenmonitor-lib.c; add a command-line build with
  stdout output, --delay, --coreid, --refresh-in-place (ncurses),
  --output-once, and --file (CSV export on exit). makefile gains
  build-cli/install-cli/all; also drops DESTDIR from the .desktop Exec path.
- Signal handler only sets a flag (flush/teardown happen in the main loop);
  fopen() is checked; the CSV writer iterates by length; the data store frees
  its per-series arrays.

Rolling averages:
- Optional GUI columns via --average (e.g. 30s,1m,5m) with a central
  ring-buffer + running-sum engine (exact windowed mean, O(windows)/tick).
  --average-only restricts which sensors are averaged. Omitting either keeps
  the original behaviour.
- CLI --daemon mode keeps history resident and atomically rewrites a snapshot
  file each interval (default under $XDG_RUNTIME_DIR), so short-lived readers
  (e.g. an xfce4-genmon panel) can show a trailing average. --sensors limits
  which sensors are handled and skips a backend's per-tick reads when nothing
  matches. Example genmon script and systemd user unit under data/.

The averaging math and the substring filter live once in zenmonitor-lib.c and
are shared by the GUI and CLI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BhfyG1chPR37duryk96VTa
Ports the update-interval control from the zenmonitor3 fork onto this
codebase: a header-bar button opens an "Update Interval" dialog (spin
button, 50-60000 ms) and a new --interval/-i option sets the initial value
(default now 1000 ms, previously a fixed 200 ms). The setting is
deliberately not persisted - all configuration stays on the command line.

Because the interval also converts average windows into sample counts,
changing it re-parses the windows against the new cadence and resets the
average history (samples taken at different cadences cannot share a
window); the Avg columns restart from "---". The window spec is retained
unparsed for that purpose.

Also fixed while reviewing the change:
- apply_interval() only reschedules the timer if one is running, so using
  the dialog on the "Zen CPU not detected" path cannot start a stray timer
  or leave a stale source ID.
- A negative --interval clamps to the minimum instead of wrapping through
  the guint cast to the maximum.
- CLI: --delay below 0.05s is clamped with a warning; 0 would busy-loop and
  a negative value wraps through the usleep()/window-math casts.

README: document --interval, point the clone URL at this repository, and
drop the AUR section (it packages the original upstream project).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BhfyG1chPR37duryk96VTa
- About dialog now links this repository (original-author credit unchanged).
- VERSION 1.4.2 -> 1.5.0, reflecting the non-blocking sampling, Zen 3/4
  detection, headless CLI, rolling averages, and configurable refresh
  interval added since 1.4.2.
- Make the msr/os sensor arrays and zenpower's sensor list static: they are
  only referenced within their own files (external access goes through
  SensorInit pointers), so they no longer leak into the global namespace.
- update_data() clears the stored timeout ID when it self-removes, so a
  later apply_interval() cannot g_source_remove() a dead source.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BhfyG1chPR37duryk96VTa
The CLI's --file mode previously accumulated every sample of every sensor
in memory and wrote the CSV only on a clean exit. That grows without bound
(hundreds of MB/day at high sample rates) and loses the entire log on any
unclean death - the exact runs a hardware monitor is usually logging to
diagnose.

Now the header is written once after sensor init and one row is appended
and flushed per refresh: memory use is constant, tail -f works on the live
file, and the log survives up to the last row on crash/SIGKILL (verified by
killing -9 mid-run). If the file cannot be opened the CLI fails fast at
startup instead of discovering it at exit, and --file now warns that it is
ignored in --daemon mode instead of being silently dropped.

The SensorDataStore machinery existed only to back the exit dump, so it is
removed from zenmonitor-lib.c and the header (along with the now-unneeded
time.h includes); the daemon takes its sensor count from the filter pass in
init_sensors instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BhfyG1chPR37duryk96VTa
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants