pumas Power Usage Monitor

repository·main·Indexed 42 days ago

https://github.com/graelo/pumas

A command-line power usage monitor for Apple Silicon Macs (M1, M2, etc.) inspired by nvtop. Version 0.5.0 provides real-time monitoring of CPU clusters, GPU, ANE, power consumption, frequency, and memory usage via an interactive TUI or a JSON streaming mode for data export.

Tokens
9.4K
Snippets
39
Records
50
Agent score
53%

What's inside pumas

  1. Pumas module structure overview

    main

    If you are extending Pumas or building tools that interact with its logic, understand the following module boundaries:

    Data Collection (src/modules/)

    UI-agnostic modules that gather raw data:

    • powermetrics/: Parses macOS powermetrics plist output (GPU, frequencies, power).
    • sysinfo.rs: Provides CPU utilization.
    • soc.rs: Provides SoC info via sysctl and system_profiler.
    • vm_stat.rs: Provides memory statistics via the vm_stat command.

    Backend & Data Plane (src/backend/)

    • mod.rs: Contains the collector thread loop and the JSON exporter loop.
    • frame.rs: Defines the Frame snapshot and per-tab sub-structs.
    • history.rs: Manages the Signal<T> ring buffer and metric history.

    Terminal UI (src/ui/)

    Built with iocraft:

    • app_root.rs: The main PumasApp component handling the render loop and keyboard events.
    • layout.rs: Handles pixel geometry derived from terminal size.
    • components/: Reusable widgets like gauge, line_gauge, sparkline, and panel.
    • views/: Per-tab views (Overview, CPU, GPU, Memory, SoC).
  2. The `Frame` data contract

    main

    The Frame (defined in src/backend/frame.rs) is the single snapshot type used to transfer data from the backend to the frontend.

    To keep the frontend lightweight, the Frame contains per-tab sub-structs (OverviewFrame, CpuFrame, GpuFrame, MemoryFrame) where all data is already prepared for display. This includes:

    • Formatted labels (strings)
    • Gauge ratios
    • Sparkline data and its scaling ceiling

    Note: Geometry (widths/heights) is not included in the Frame; terminal size is treated as a frontend concern derived from the terminal environment.

  3. How Pumas data flows between backend and frontend

    main

    Pumas uses a one-directional backend → frontend data plane.

    1. Backend: A dedicated collector thread runs on its own OS thread. It performs blocking I/O (shelling out to powermetrics, sysinfo, and vm_stat), merges the data, manages all metric history, and builds an owned Frame snapshot. This frame is sent over a bounded smol::channel::<Frame>(4).
    2. Frontend: The UI (PumasApp) receives these frames via the channel. The frontend is a pure rendering layer: it holds no history, performs no string formatting, and does no data scaling. It simply lays out and draws the pre-formatted data provided by the backend.

    This architecture ensures the UI never blocks on I/O and that the render path remains trivial and performant.

  4. Configure passwordless sudo for pumas

    main

    To avoid entering your password every time you run sudo pumas run, you can create a sudoers drop-in file.

    Security Warning: Only allow the specific pumas binary path to run without a password to minimize security risks.

    1. Create a drop-in file:
      sudo visudo -f /etc/sudoers.d/pumas
    2. Add the following line (replace username with your actual username and ensure the path to pumas is correct, e.g., /opt/homebrew/bin/pumas):
      username ALL=(ALL) NOPASSWD: /opt/homebrew/bin/pumas
    3. (Optional) Add a shell alias for convenience:
      alias pumas='sudo pumas run'
    sudo visudo -f /etc/sudoers.d/pumas
    
    # Add this line to the file:
    username ALL=(ALL) NOPASSWD: /opt/homebrew/bin/pumas
  5. Install pumas

    main

    You can install pumas using several methods depending on your preferred package manager or workflow:

    Homebrew

    brew install graelo/tap/pumas

    Cargo

    cargo install pumas

    x-cmd

    x install pumas

    Manual Build from Source

    1. Clone the repository: git clone https://github.com/graelo/pumas.git
    2. Enter the directory: cd pumas
    3. Build the release binary: cargo build --release
    4. Move target/release/pumas to a directory in your PATH.

    Prebuilt Binary

    1. Download the latest macOS archive from GitHub Releases.
    2. Extract the archive: unzip pumas-aarch64-apple-darwin.zip
    3. Copy the binary to your PATH: sudo cp pumas /usr/local/bin/
    brew install graelo/tap/pumas
  6. Run pumas in UI mode

    main

    To launch the interactive terminal UI for monitoring Apple Silicon metrics, run the run command with sudo. sudo is required because pumas utilizes Apple's powermetrics utility to access hardware performance counters.

    Use the arrow keys to switch between tabs (Overview, CPU, GPU, Memory, SoC). Press Esc, q, x, or Ctrl-C to quit.

    sudo pumas run
    sudo pumas run
  7. Use JSON mode for data export

    main

    Pumas can be run in JSON mode to output power usage metrics as a stream of JSON lines instead of rendering a terminal UI. This is useful for piping data to other tools or logging.

    When using the --json flag, Pumas bypasses the UI and the Frame channel, instead calling backend::run_exporter which prints one JSON line per sample directly to stdout.

    pumas --json
  8. Understand the structure of parsed powermetrics output

    main

    The Metrics struct represents the top-level data structure parsed from Apple's powermetrics plist output. It aggregates metrics for the CPU, GPU, ANE (Apple Neural Engine), and thermal pressure.

    Key components include:

    • elapsed_ns: The sampling period in nanoseconds.
    • processor: Contains ProcessorMetrics (energy consumption for ANE, CPU, and GPU, and package power).
    • thermal_pressure: A string indicating the current thermal state (e.g., "Nominal", "Light", "Moderate", "Heavy", or "Critical").
    • gpu: Contains GpuMetrics (frequency, idle ratio, and DVFM states).
    // Conceptual representation of the Metrics hierarchy
    struct Metrics {
        elapsed_ns: u64,
        processor: ProcessorMetrics,
        thermal_pressure: String,
        gpu: GpuMetrics,
    }
  9. How the backend collector and exporter work

    main

    The backend operates in two primary modes:

    1. Collector Mode (run_collector): Designed for the TUI (Terminal User Interface). It produces high-level Frame objects that contain pre-formatted strings, sparkline data, and calculated ratios. It uses a smol::channel to ship these frames to the frontend. It maintains a History of previous samples to provide sparkline/trend data.

    2. Exporter Mode (run_exporter): Designed for data streaming. It bypasses the complex Frame construction and instead serializes the raw SocInfo and Metrics directly into a compact JSON line per sample. This mode is more efficient for external consumption as it avoids the overhead of UI-centric data structures.

  10. How Pumas UI rendering primitives work

    main

    The Pumas UI uses a grid-based rendering model for terminal output. Instead of rendering individual characters, components build a 2D grid of Cell objects (rows and columns).

    To optimize rendering, these grids are passed to render_grid, which performs a 'run-length encoding' style optimization: it collapses consecutive Cells that share the same foreground (fg) and background (bg) colors into single Segments. This allows the UI to use efficient Text spans rather than individual character elements, significantly reducing the number of elements in the iocraft tree.

  11. Understand the `Frame` data-plane contract

    main

    A Frame is a single, owned, and Clone-able snapshot of system metrics shipped from the backend collector to the frontend per sample.

    Key characteristics of the Frame model:

    • Pre-formatted Data: All strings (labels, values, units) are pre-formatted by the backend using crate::units. The frontend does not perform string formatting.
    • Pre-computed Visuals: Sparkline data vectors (spark) and their scaling ceilings (spark_max) are pre-computed. The frontend is responsible only for layout and rendering the provided data within its allocated geometry.
    • Separation of Concerns: The backend handles all 'history math' and scaling logic; the frontend handles pairing, layout, and geometry-specific trimming/scaling.
    • Statelessness: While the Frame contains per-sample snapshots, session-static information (like SoC details and the program header) is handled separately via RenderedHeader and SocRows to avoid redundant per-frame computation.
  12. Understand the data flow from powermetrics to metrics

    main

    The backend collects data by spawning the /usr/bin/powermetrics binary with specific samplers (cpu_power,gpu_power,thermal) in plist format.

    The pipeline works as follows:

    1. Stream: Reads plist messages from powermetrics stdout.
    2. Parse: Converts the plist text into a Metrics struct.
    3. Merge: Merges the powermetrics data with sysinfo data (to provide more accurate per-core CPU/memory usage, especially on M2 chips).
    4. History: Updates the History signals (e.g., CpuPowerW, RamUsageBytes) to allow for trend visualization.
    5. Frame/Export: Either builds a UI Frame or exports a JSON line.