All of lore.kernel.org
 help / color / mirror / Atom feed
From: "David E. Box" <david.e.box@linux.intel.com>
To: linux-kernel@vger.kernel.org, david.e.box@linux.intel.com,
	ilpo.jarvinen@linux.intel.com, andriy.shevchenko@linux.intel.com,
	platform-driver-x86@vger.kernel.org
Subject: [PATCH 14/17] tools/arch/x86/pmtctl: Add pmtctl 'stat' command
Date: Mon, 25 May 2026 18:47:12 -0700	[thread overview]
Message-ID: <20260526014719.2248380-15-david.e.box@linux.intel.com> (raw)
In-Reply-To: <20260526014719.2248380-1-david.e.box@linux.intel.com>

Register the 'stat' subcommand, giving pmtctl users a perf stat-like mode
for repeatedly sampling Intel PMT telemetry metrics at a configurable
interval and iteration count. Where the earlier 'list' command shows what
the platform exposes, 'stat' read metrics over time.

Metrics can be selected by name or sampled in 'raw' mode at fixes bit
positions, the latter which doesn't require metric definitions. Output is
available in human-readable tabular form.

Assisted-by: GitHub-Copilot:claude-opus-4.7
Signed-off-by: David E. Box <david.e.box@linux.intel.com>
---
 tools/arch/x86/pmtctl/Makefile                |   3 +
 tools/arch/x86/pmtctl/include/cmd_stat.h      |  75 +++
 .../arch/x86/pmtctl/include/cmd_stat_format.h |  11 +
 tools/arch/x86/pmtctl/include/pmtctl_cli.h    |   2 +
 tools/arch/x86/pmtctl/src/cmd_stat.c          | 501 +++++++++++++++++
 tools/arch/x86/pmtctl/src/cmd_stat_format.c   | 205 +++++++
 tools/arch/x86/pmtctl/src/cmd_stat_run.c      | 528 ++++++++++++++++++
 tools/arch/x86/pmtctl/src/main.c              |   4 +
 8 files changed, 1329 insertions(+)
 create mode 100644 tools/arch/x86/pmtctl/include/cmd_stat.h
 create mode 100644 tools/arch/x86/pmtctl/include/cmd_stat_format.h
 create mode 100644 tools/arch/x86/pmtctl/src/cmd_stat.c
 create mode 100644 tools/arch/x86/pmtctl/src/cmd_stat_format.c
 create mode 100644 tools/arch/x86/pmtctl/src/cmd_stat_run.c

diff --git a/tools/arch/x86/pmtctl/Makefile b/tools/arch/x86/pmtctl/Makefile
index ee6633a6f435..52e50597b5c1 100644
--- a/tools/arch/x86/pmtctl/Makefile
+++ b/tools/arch/x86/pmtctl/Makefile
@@ -42,6 +42,9 @@ SAMPLE_TARGET := $(BUILDDIR)/samples/libpmtctl_sample
 SRC := \
 	$(SRCDIR)/main.c \
 	$(SRCDIR)/cmd_list.c \
+	$(SRCDIR)/cmd_stat.c \
+	$(SRCDIR)/cmd_stat_format.c \
+	$(SRCDIR)/cmd_stat_run.c \
 	$(SRCDIR)/pager.c
 
 OBJ := $(patsubst $(SRCDIR)/%.c,$(BUILDDIR)/%.o,$(SRC))
diff --git a/tools/arch/x86/pmtctl/include/cmd_stat.h b/tools/arch/x86/pmtctl/include/cmd_stat.h
new file mode 100644
index 000000000000..be4ddfe1e636
--- /dev/null
+++ b/tools/arch/x86/pmtctl/include/cmd_stat.h
@@ -0,0 +1,75 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef PMTCTL_CMD_STAT_H
+#define PMTCTL_CMD_STAT_H
+
+#include <stdbool.h>
+
+#include "lib/device.h"
+#include "lib/metrics_db.h"
+#include "lib/pmtctl.h"
+
+enum pmtctl_fmt {
+	PMTCTL_FMT_DEC,
+	PMTCTL_FMT_HEX,
+};
+
+struct stat_raw_spec {
+	int sample_id;
+	int lsb;
+	int msb;
+};
+
+struct stat_options {
+	char                 **events;
+	int                    nr_events;
+	int                    events_cap;
+
+	char                  *device_selector;
+
+	enum pmtctl_fmt        fmt;
+
+	unsigned int           interval_ms;
+	long                   count;
+
+	struct stat_raw_spec  *raw_specs;
+	int                    nr_raw_specs;
+	int                    raw_specs_cap;
+	bool                   raw;
+
+	bool                   once;
+	bool                   header;
+	bool                   hex;
+	bool                   vertical;
+	bool                   show_help;
+};
+
+/*
+ * stat_item: internal sampling representation
+ *
+ *  - STAT_ITEM_METRIC:
+ *      Uses def/bindings and metrics_db.
+ *  - STAT_ITEM_RAW:
+ *      Uses device + (sample_id, lsb, msb) only. No metrics_db.
+ */
+enum stat_item_kind {
+	STAT_ITEM_METRIC,
+	STAT_ITEM_RAW,
+};
+
+struct stat_item {
+	enum stat_item_kind    kind;
+
+	struct pmt_metric_desc desc;
+
+	/* Metric mode fields */
+	int                    event_idx;     /* index into opts->events[] */
+	struct pmt_device     *dev;
+
+	uint64_t               cur_raw;
+	double                 cur_value;
+	bool                   present;       /* false if metric not bound to this device */
+};
+
+int stat_run(const struct stat_options *opts);
+
+#endif
diff --git a/tools/arch/x86/pmtctl/include/cmd_stat_format.h b/tools/arch/x86/pmtctl/include/cmd_stat_format.h
new file mode 100644
index 000000000000..ad76c2b2bc0b
--- /dev/null
+++ b/tools/arch/x86/pmtctl/include/cmd_stat_format.h
@@ -0,0 +1,11 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef PMTCTL_CMD_STAT_FORMAT_H
+#define PMTCTL_CMD_STAT_FORMAT_H
+
+struct stat_options;
+struct stat_item;
+
+void stat_print_header(const struct stat_options *opts);
+void stat_print_rows(const struct stat_options *opts, const struct stat_item *items, int nitems,
+		     int num_devices, int sample_idx);
+#endif
diff --git a/tools/arch/x86/pmtctl/include/pmtctl_cli.h b/tools/arch/x86/pmtctl/include/pmtctl_cli.h
index eb5efb0b650f..76540c3340cb 100644
--- a/tools/arch/x86/pmtctl/include/pmtctl_cli.h
+++ b/tools/arch/x86/pmtctl/include/pmtctl_cli.h
@@ -11,4 +11,6 @@ void pmtctl_finish_pager(FILE *out);
 
 int cmd_list(int argc, char **argv, const struct pmt_global_opts *gopts);
 
+int cmd_stat(int argc, char **argv, const struct pmt_global_opts *gopts);
+
 #endif
diff --git a/tools/arch/x86/pmtctl/src/cmd_stat.c b/tools/arch/x86/pmtctl/src/cmd_stat.c
new file mode 100644
index 000000000000..7825d7c9a44e
--- /dev/null
+++ b/tools/arch/x86/pmtctl/src/cmd_stat.c
@@ -0,0 +1,501 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <errno.h>
+#include <getopt.h>
+#include <limits.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define LOG_PREFIX "cmd_stat"
+#include "lib/log.h"
+
+#include "lib/common.h"
+#include "lib/pmtctl.h"
+
+#include "cmd_stat.h"
+#include "pmtctl_cli.h"
+
+#define CMD_STAT_DEFAULT_INTERVAL_MS 1000
+
+static int stat_add_event(struct stat_options *opts, const char *spec)
+{
+	/* Grow the events array by doubling; start at 8 slots. */
+	if (opts->nr_events == opts->events_cap) {
+		int new_cap = opts->events_cap ? opts->events_cap * 2 : 8;
+		char **tmp = realloc(opts->events, new_cap * sizeof(char *));
+
+		if (!tmp)
+			return log_ret(-ENOMEM, "could not allocate");
+
+		opts->events = tmp;
+		opts->events_cap = new_cap;
+	}
+
+	opts->events[opts->nr_events++] = xstrdup(spec);
+
+	return 0;
+}
+
+static int stat_add_event_list(struct stat_options *opts, const char *arg)
+{
+	/* arg can be "a,b,c" */
+	char *copy = xstrdup(arg);
+	char *p = copy;
+	char *saveptr = NULL;
+	int ret = 0;
+
+	for (;;) {
+		char *tok = strtok_r(p, ",", &saveptr);
+
+		if (!tok)
+			break;
+
+		ret = stat_add_event(opts, tok);
+		if (ret < 0)
+			break;
+
+		p = NULL;
+	}
+
+	free(copy);
+
+	return ret;
+}
+
+/*
+ * Strict decimal-int parser used by stat_parse_raw_arg().
+ *
+ * Unlike `strtol(s, NULL, 10)`, this:
+ *   - rejects NULL or empty input (e.g. "id=" with no value),
+ *   - rejects partial conversions ("foo", "12x"),
+ *   - accepts an optional leading '+' or '-'.
+ * On success, *out receives the parsed value and 0 is returned.
+ */
+static int parse_decimal_int(const char *s, int *out)
+{
+	char *end;
+	long v;
+
+	if (!s || *s == '\0')
+		return -1;
+
+	errno = 0;
+	v = strtol(s, &end, 10);
+	if (errno || end == s || *end != '\0')
+		return -1;
+	if (v < INT_MIN || v > INT_MAX)
+		return -1;
+
+	*out = (int)v;
+	return 0;
+}
+
+/*
+ * Parse one --raw argument of the form:
+ *     id=<num>[,lsb=<num>][,msb=<num>]
+ *
+ * Duplicate keys are rejected.
+ */
+static int stat_parse_raw_arg(struct stat_options *opts, const char *arg)
+{
+	auto_free char *copy = xstrdup(arg);
+	char *p = copy;
+	char *tok, *saveptr = NULL;
+	bool seen_id  = false;
+	bool seen_lsb = false;
+	bool seen_msb = false;
+	int id  = -1;
+	int lsb = -1;
+	int msb = -1;
+
+	/*
+	 * Reject empty input and structurally invalid lists *before*
+	 * strtok_r(), which silently collapses consecutive and
+	 * leading/trailing separators.
+	 */
+	if (*arg == '\0' || *arg == ',' || arg[strlen(arg) - 1] == ',' ||
+	    strstr(arg, ",,") != NULL) {
+		return log_ret(PMTCTL_ERR_CMD_PARSE,
+			       "invalid --raw argument '%s' (empty token)", arg);
+	}
+
+	while ((tok = strtok_r(p, ",", &saveptr)) != NULL) {
+		char *eq;
+
+		p = NULL;
+
+		eq = strchr(tok, '=');
+		if (!eq) {
+			return log_ret(PMTCTL_ERR_CMD_PARSE,
+				       "invalid --raw token '%s' (expected key=value)", tok);
+		}
+
+		*eq = '\0';
+		const char *key = tok;
+		const char *val = eq + 1;
+
+		if (!strcmp(key, "id")) {
+			if (seen_id) {
+				return log_ret(PMTCTL_ERR_CMD_PARSE,
+					       "duplicate key 'id' in --raw argument");
+			}
+
+			seen_id = true;
+
+			if (parse_decimal_int(val, &id))
+				return log_ret(PMTCTL_ERR_CMD_PARSE,
+					       "id must be a decimal integer, got '%s'", val);
+			if (id < 0)
+				return log_ret(PMTCTL_ERR_CMD_PARSE, "id must be >= 0");
+
+		} else if (!strcmp(key, "lsb")) {
+			if (seen_lsb) {
+				return log_ret(PMTCTL_ERR_CMD_PARSE,
+					       "duplicate key 'lsb' in --raw argument");
+			}
+
+			seen_lsb = true;
+
+			if (parse_decimal_int(val, &lsb))
+				return log_ret(PMTCTL_ERR_CMD_PARSE,
+					       "lsb must be a decimal integer, got '%s'", val);
+			if (lsb < 0 || lsb >= 64)
+				return log_ret(PMTCTL_ERR_CMD_PARSE, "lsb must be in [0,63]");
+
+		} else if (!strcmp(key, "msb")) {
+			if (seen_msb) {
+				return log_ret(PMTCTL_ERR_CMD_PARSE,
+					       "duplicate key 'msb' in --raw argument");
+			}
+
+			seen_msb = true;
+
+			if (parse_decimal_int(val, &msb))
+				return log_ret(PMTCTL_ERR_CMD_PARSE,
+					       "msb must be a decimal integer, got '%s'", val);
+			if (msb < 0 || msb >= 64)
+				return log_ret(PMTCTL_ERR_CMD_PARSE, "msb must be in [0,63]");
+
+		} else {
+			return log_ret(PMTCTL_ERR_CMD_PARSE,
+				       "unknown key '%s' in --raw argument", key);
+		}
+	}
+
+	/* Required: id */
+	if (id < 0)
+		return log_ret(PMTCTL_ERR_CMD_PARSE, "--raw requires at least 'id=<num>'");
+
+	/* Defaults */
+	if (lsb < 0)
+		lsb = 0;
+	if (msb < 0)
+		msb = 63;
+
+	if (lsb > msb) {
+		return log_ret(PMTCTL_ERR_CMD_PARSE,
+			       "lsb (%d) cannot be greater than msb (%d)", lsb, msb);
+	}
+
+	/* Append new raw spec */
+	if (opts->nr_raw_specs == opts->raw_specs_cap) {
+		int new_cap = opts->raw_specs_cap ? opts->raw_specs_cap * 2 : 8;
+		struct stat_raw_spec *tmp = realloc(opts->raw_specs, new_cap * sizeof(*tmp));
+
+		if (!tmp)
+			return log_ret(-ENOMEM, "could not prepare raw collection");
+
+		opts->raw_specs = tmp;
+		opts->raw_specs_cap = new_cap;
+	}
+
+	opts->raw_specs[opts->nr_raw_specs].sample_id = id;
+	opts->raw_specs[opts->nr_raw_specs].lsb = lsb;
+	opts->raw_specs[opts->nr_raw_specs].msb = msb;
+	opts->nr_raw_specs++;
+
+	return 0;
+}
+
+static void stat_usage_full(FILE *out)
+{
+	fprintf(out,
+		"Usage:\n"
+		"    pmtctl stat [options] -e <metric>[,<metric>...] ...\n"
+		"    pmtctl stat [options] --raw id=<n>[,lsb=<n>][,msb=<n>] ...\n"
+		"\n"
+		"Purpose:\n"
+		"    Collect metric values over time (similar to 'perf stat').\n"
+		"\n"
+		"Options:\n"
+		"    -h, --help\n"
+		"        Show help.\n"
+		"\n"
+		"    -e, --event <spec>\n"
+		"        Metric name or comma-separated list; may be repeated.\n"
+		"        More than one metric requires --vertical.\n"
+		"\n"
+		"    Device selection:\n"
+		"        -d, --device <selector>\n"
+		"            Restrict to a device/endpoint. Can be supplied globally before\n"
+		"            the command (recommended) or locally after the command as a\n"
+		"            fallback. Global value takes precedence when both are given.\n"
+		"            Selector forms: guid=<hex>, ep=<endpoint_name>\n"
+		"\n"
+		"    -i, --interval <ms>\n"
+		"        Sampling interval (default 1000 ms).\n"
+		"\n"
+		"    -c, --count <N>\n"
+		"        Number of samples (default infinite).\n"
+		"\n"
+		"    --once\n"
+		"        Single snapshot, exit.\n"
+		"\n"
+		"    --raw id=<n>[,lsb=<n>][,msb=<n>]\n"
+		"        Read raw sample values by sample id with optional bit slicing.\n"
+		"        May be specified multiple times.\n"
+		"          id     : required, sample id (>= 0)\n"
+		"          lsb    : optional, default 0 if omitted\n"
+		"          msb    : optional, default 63 if omitted\n"
+		"        Constraints:\n"
+		"          0 <= lsb <= msb <= 63\n"
+		"        -e/--event and --raw are mutually exclusive.\n"
+		"\n"
+		"    --header / --no-header\n"
+		"        Control header printing.\n"
+		"\n"
+		"    --hex\n"
+		"        Output values in hex.\n"
+		"\n"
+		"    --vertical\n"
+		"        One line per metric per sample: time_ms metric value.\n"
+		"\n"
+		"On read failure, the value prints as NaN.\n"
+		"\n"
+		"Examples:\n"
+		"    pmtctl stat -e temp_socket\n"
+		"    pmtctl stat -e socket_power --interval 500\n"
+		"    pmtctl stat -e fw_version_0 --once\n"
+		"    pmtctl stat -e temp_core0 -d guid=27971628\n"
+		"    pmtctl stat --raw id=2\n"
+		"    pmtctl stat --raw id=2,lsb=16,msb=31\n"
+		"    pmtctl stat --raw id=2,msb=7 --raw id=3,lsb=8,msb=15\n"
+	);
+}
+
+static void stat_usage_short(FILE *out)
+{
+	fprintf(out,
+		"Usage:\n"
+		"    pmtctl stat [options] -e <metric>[,<metric>...] ...\n"
+		"    pmtctl stat [options] --raw id=<n>[,lsb=<n>][,msb=<n>] ...\n"
+		"\n"
+		"Run 'pmtctl stat --help' for full help.\n"
+	);
+}
+
+static int stat_parse_options(int argc, char **argv, struct stat_options *opts)
+{
+	enum {
+		OPT_ONCE = 1000,
+		OPT_RAW,
+		OPT_HEADER,
+		OPT_NO_HEADER,
+		OPT_HEX,
+		OPT_VERTICAL,
+	};
+
+	static const struct option long_opts[] = {
+		{ "help",      no_argument,       NULL, 'h' },
+		{ "event",     required_argument, NULL, 'e' },
+		{ "device",    required_argument, NULL, 'd' },
+		{ "interval",  required_argument, NULL, 'i' },
+		{ "count",     required_argument, NULL, 'c' },
+
+		{ "once",      no_argument,       NULL, OPT_ONCE },
+		{ "raw",       required_argument, NULL, OPT_RAW },
+		{ "header",    no_argument,       NULL, OPT_HEADER },
+		{ "no-header", no_argument,       NULL, OPT_NO_HEADER },
+		{ "hex",       no_argument,       NULL, OPT_HEX },
+		{ "vertical",  no_argument,       NULL, OPT_VERTICAL },
+
+		{ 0, 0, 0, 0 }
+	};
+
+	int c;
+	int ret;
+
+	optind = 1;
+
+	while ((c = getopt_long(argc, argv, "he:d:i:c:", long_opts, NULL)) != -1) {
+		switch (c) {
+		case 'h':
+			opts->show_help = true;
+			return 0;
+
+		case 'e':
+			ret = stat_add_event_list(opts, optarg);
+			if (ret < 0)
+				return ret;
+			break;
+
+		case 'd':
+			if (opts->device_selector) {
+				return log_ret(PMTCTL_ERR_CMD_PARSE,
+					       "multiple --device options are not allowed");
+			}
+
+			opts->device_selector = xstrdup(optarg);
+			break;
+
+		case 'i': {
+			long v = strtol(optarg, NULL, 10);
+
+			if (v <= 0)
+				return log_ret(PMTCTL_ERR_CMD_PARSE, "interval must be > 0");
+
+			opts->interval_ms = (unsigned int)v;
+			break;
+		}
+
+		case 'c': {
+			long v = strtol(optarg, NULL, 10);
+
+			if (v <= 0)
+				return log_ret(PMTCTL_ERR_CMD_PARSE, "count must be > 0");
+
+			opts->count = v;
+			break;
+		}
+
+		case OPT_ONCE:
+			opts->once = true;
+			opts->count = 1;
+			opts->interval_ms = 0; /* oneshot, no sleep */
+			break;
+
+		case OPT_RAW:
+			ret = stat_parse_raw_arg(opts, optarg);
+			if (ret < 0)
+				return ret;
+			break;
+
+		case OPT_HEADER:
+			opts->header = true;
+			break;
+
+		case OPT_NO_HEADER:
+			opts->header = false;
+			break;
+
+		case OPT_HEX:
+			opts->hex = true;
+			break;
+
+		case OPT_VERTICAL:
+			opts->vertical = true;
+			break;
+
+		default:
+			stat_usage_short(stderr);
+			return PMTCTL_ERR_CMD_PARSE;
+		}
+	}
+
+	/* If the user explicitly requested hex output, set the format accordingly */
+	if (opts->hex)
+		opts->fmt = PMTCTL_FMT_HEX;
+	else
+		opts->fmt = PMTCTL_FMT_DEC;
+
+	if (opts->nr_events == 0 && opts->nr_raw_specs == 0) {
+		log_err(PMTCTL_ERR_CMD_PARSE, "requires -e/--event or --raw");
+		stat_usage_short(stderr);
+		return PMTCTL_ERR_CMD_PARSE;
+	}
+
+	if (optind < argc) {
+		log_err(PMTCTL_ERR_CMD_PARSE, "unexpected extra arguments");
+		stat_usage_short(stderr);
+		return PMTCTL_ERR_CMD_PARSE;
+	}
+
+	if (opts->nr_events > 0 && opts->nr_raw_specs > 0) {
+		log_err(PMTCTL_ERR_CMD_PARSE, "-e/--event and --raw are mutually exclusive");
+		return PMTCTL_ERR_CMD_PARSE;
+	}
+
+	/* Horizontal mode (default) supports only one metric */
+	if (!opts->vertical && opts->nr_events > 1) {
+		log_err(PMTCTL_ERR_CMD_PARSE,
+			"horizontal mode supports only one metric at a time (use --vertical for multiple metrics)");
+		return PMTCTL_ERR_CMD_PARSE;
+	}
+
+	return 0;
+}
+
+static void stat_opts_init(struct stat_options *opts)
+{
+	memset(opts, 0, sizeof(*opts));
+
+	opts->interval_ms = CMD_STAT_DEFAULT_INTERVAL_MS;
+	opts->count       = -1;    /* infinite */
+	opts->header      = true;
+	opts->fmt         = PMTCTL_FMT_DEC;
+}
+
+static void stat_opts_finalize(struct stat_options *opts)
+{
+	int i;
+
+	for (i = 0; i < opts->nr_events; i++)
+		free(opts->events[i]);
+
+	free(opts->events);
+	free(opts->device_selector);
+	free(opts->raw_specs);
+}
+
+int cmd_stat(int argc, char **argv, const struct pmt_global_opts *gopts)
+{
+	struct stat_options opts;
+	int ret;
+
+	stat_opts_init(&opts);
+
+	ret = stat_parse_options(argc, argv, &opts);
+	if (ret != 0) {
+		stat_opts_finalize(&opts);
+		return ret;
+	}
+
+	if (opts.show_help) {
+		stat_usage_full(stdout);
+		stat_opts_finalize(&opts);
+		return 0;
+	}
+
+	ret = pmtctl_init(gopts);
+	if (ret)
+		return ret;
+
+	if (opts.device_selector && gopts && gopts->device_selector) {
+		log_err(PMTCTL_ERR_CMD_PARSE,
+			"multiple --device options are not allowed (global and command-local)");
+		stat_opts_finalize(&opts);
+
+		return PMTCTL_ERR_CMD_PARSE;
+	}
+
+	/* If no command-local selector, inherit the global one (if any). */
+	if (!opts.device_selector && gopts && gopts->device_selector)
+		opts.device_selector = xstrdup(gopts->device_selector);
+
+	ret = stat_run(&opts);
+
+	stat_opts_finalize(&opts);
+
+	return ret;
+}
diff --git a/tools/arch/x86/pmtctl/src/cmd_stat_format.c b/tools/arch/x86/pmtctl/src/cmd_stat_format.c
new file mode 100644
index 000000000000..4c0ca21c32c4
--- /dev/null
+++ b/tools/arch/x86/pmtctl/src/cmd_stat_format.c
@@ -0,0 +1,205 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Output format helpers for pmtctl stat.
+ * Implements text output for the header and per-sample rows.
+ */
+
+#include <inttypes.h>
+#include <stdio.h>
+#include <math.h>
+
+#include "cmd_stat.h"
+#include "cmd_stat_format.h"
+
+/* Column widths (characters) shared by header and row formatters. */
+#define STAT_COL_W_SAMPLE	6
+#define STAT_COL_W_TIME		7
+#define STAT_COL_W_DEV_H	18	/* horizontal-mode device column */
+#define STAT_COL_W_DEV_V	20	/* vertical-mode device column */
+#define STAT_COL_W_METRIC_V	20	/* vertical-mode metric column */
+#define STAT_COL_W_VALUE_V	40	/* vertical-mode value column (header underline) */
+#define STAT_COL_W_EVENT	28	/* horizontal-mode per-event value column */
+
+/* 4-space gap between the device column and the per-event value columns. */
+#define STAT_COL_GAP		"    "
+
+/* Long enough dash string to satisfy any column width above. */
+#define STAT_DASHES \
+	"----------------------------------------------------------------"
+
+/*
+ * Buffer size for a stringified uint64 sample value.
+ * Worst case: decimal 18446744073709551615 (20 chars) or "0x" + 16 hex digits;
+ * 32 leaves headroom for a NUL and any future prefix/suffix.
+ */
+#define STAT_VALUE_BUF_SIZE	32
+
+static void print_u64_field(FILE *out, enum pmtctl_fmt fmt, const struct stat_item *it)
+{
+	if (!it->present || isnan(it->cur_value)) {
+		fprintf(out, STAT_COL_GAP "%*s", STAT_COL_W_EVENT, "NaN");
+		return;
+	}
+
+	switch (fmt) {
+	case PMTCTL_FMT_HEX: {
+		char buf[STAT_VALUE_BUF_SIZE];
+
+		snprintf(buf, sizeof(buf), "0x%" PRIx64, it->cur_raw);
+		fprintf(out, STAT_COL_GAP "%*s", STAT_COL_W_EVENT, buf);
+		break;
+	}
+	case PMTCTL_FMT_DEC:
+	default:
+		fprintf(out, STAT_COL_GAP "%*" PRIu64, STAT_COL_W_EVENT, it->cur_raw);
+		break;
+	}
+}
+
+void stat_print_header(const struct stat_options *opts)
+{
+	if (!opts->header)
+		return;
+
+	if (opts->vertical) {
+		fprintf(stdout, "%-*s  %-*s  %-*s  %-*s  %s\n",
+			STAT_COL_W_SAMPLE,   "sample",
+			STAT_COL_W_TIME,     "time_ms",
+			STAT_COL_W_DEV_V,    "device",
+			STAT_COL_W_METRIC_V, "metric",
+			"value");
+		fprintf(stdout, "%.*s  %.*s  %.*s  %.*s  %.*s\n",
+			STAT_COL_W_SAMPLE,   STAT_DASHES,
+			STAT_COL_W_TIME,     STAT_DASHES,
+			STAT_COL_W_DEV_V,    STAT_DASHES,
+			STAT_COL_W_METRIC_V, STAT_DASHES,
+			STAT_COL_W_VALUE_V,  STAT_DASHES);
+	} else {
+		int nr_value_cols = opts->nr_raw_specs > 0
+			? opts->nr_raw_specs : opts->nr_events;
+
+		fprintf(stdout, "%-*s  %-*s  %-*s",
+			STAT_COL_W_SAMPLE, "sample",
+			STAT_COL_W_TIME,   "time_ms",
+			STAT_COL_W_DEV_H,  "device");
+
+		if (opts->nr_raw_specs > 0) {
+			for (int r = 0; r < opts->nr_raw_specs; r++) {
+				const struct stat_raw_spec *rs = &opts->raw_specs[r];
+				char label[STAT_COL_W_EVENT + 1];
+
+				snprintf(label, sizeof(label),
+					 "raw[id=%d,lsb=%d,msb=%d]",
+					 rs->sample_id, rs->lsb, rs->msb);
+				fprintf(stdout, STAT_COL_GAP "%*.*s",
+					STAT_COL_W_EVENT, STAT_COL_W_EVENT, label);
+			}
+		} else {
+			for (int e = 0; e < opts->nr_events; e++)
+				fprintf(stdout, STAT_COL_GAP "%*.*s",
+					STAT_COL_W_EVENT, STAT_COL_W_EVENT, opts->events[e]);
+		}
+
+		fputc('\n', stdout);
+
+		fprintf(stdout, "%.*s  %.*s  %.*s",
+			STAT_COL_W_SAMPLE, STAT_DASHES,
+			STAT_COL_W_TIME,   STAT_DASHES,
+			STAT_COL_W_DEV_H,  STAT_DASHES);
+
+		for (int c = 0; c < nr_value_cols; c++)
+			fprintf(stdout, STAT_COL_GAP "%.*s", STAT_COL_W_EVENT, STAT_DASHES);
+
+		fputc('\n', stdout);
+	}
+}
+
+static const char *dev_name_or_unknown(const struct pmt_device *dev)
+{
+	return dev && dev->name ? dev->name : "(unknown)";
+}
+
+static void stat_print_rows_vertical(const struct stat_options *opts,
+				     const struct stat_item *items, int cols_per_dev,
+				     int num_devices, int sample_idx, unsigned long time_ms)
+{
+	for (int d = 0; d < num_devices; d++) {
+		const struct stat_item *row = &items[d * cols_per_dev];
+		const char *dev_name = dev_name_or_unknown(row->dev);
+
+		for (int c = 0; c < cols_per_dev; c++) {
+			const struct stat_item *it = &row[c];
+			char metric_buf[64];
+			char value_buf[STAT_VALUE_BUF_SIZE];
+
+			if (it->kind == STAT_ITEM_RAW) {
+				snprintf(metric_buf, sizeof(metric_buf),
+					 "raw[id=%u,lsb=%u,msb=%u]",
+					 it->desc.raw_sample_id,
+					 it->desc.raw_lsb,
+					 it->desc.raw_msb);
+			} else {
+				snprintf(metric_buf, sizeof(metric_buf), "%.*s",
+					 STAT_COL_W_METRIC_V, opts->events[c]);
+			}
+
+			if (!it->present || isnan(it->cur_value)) {
+				snprintf(value_buf, sizeof(value_buf), "NaN");
+			} else {
+				switch (opts->fmt) {
+				case PMTCTL_FMT_HEX:
+					snprintf(value_buf, sizeof(value_buf),
+						 "0x%" PRIx64, it->cur_raw);
+					break;
+				case PMTCTL_FMT_DEC:
+				default:
+					snprintf(value_buf, sizeof(value_buf),
+						 "%" PRIu64, it->cur_raw);
+					break;
+				}
+			}
+
+			fprintf(stdout, "%-*d  %-*lu  %-*s  %-*s  %s\n",
+				STAT_COL_W_SAMPLE,   sample_idx,
+				STAT_COL_W_TIME,     time_ms,
+				STAT_COL_W_DEV_V,    dev_name,
+				STAT_COL_W_METRIC_V, metric_buf,
+				value_buf);
+		}
+	}
+}
+
+static void stat_print_rows_horizontal(const struct stat_options *opts,
+				       const struct stat_item *items, int cols_per_dev,
+				       int num_devices, int sample_idx, unsigned long time_ms)
+{
+	for (int d = 0; d < num_devices; d++) {
+		const struct stat_item *row = &items[d * cols_per_dev];
+
+		fprintf(stdout, "%-*d  %-*lu  %-*s",
+			STAT_COL_W_SAMPLE, sample_idx,
+			STAT_COL_W_TIME,   time_ms,
+			STAT_COL_W_DEV_H,  dev_name_or_unknown(row->dev));
+
+		for (int c = 0; c < cols_per_dev; c++)
+			print_u64_field(stdout, opts->fmt, &row[c]);
+
+		fputc('\n', stdout);
+	}
+}
+
+void stat_print_rows(const struct stat_options *opts, const struct stat_item *items, int nitems,
+		     int num_devices, int sample_idx)
+{
+	unsigned long time_ms = (unsigned long)sample_idx * opts->interval_ms;
+	int cols_per_dev = nitems / num_devices;
+
+	if (opts->vertical)
+		stat_print_rows_vertical(opts, items, cols_per_dev, num_devices,
+					 sample_idx, time_ms);
+	else
+		stat_print_rows_horizontal(opts, items, cols_per_dev, num_devices,
+					   sample_idx, time_ms);
+
+	fflush(stdout);
+}
diff --git a/tools/arch/x86/pmtctl/src/cmd_stat_run.c b/tools/arch/x86/pmtctl/src/cmd_stat_run.c
new file mode 100644
index 000000000000..bd461f30cb68
--- /dev/null
+++ b/tools/arch/x86/pmtctl/src/cmd_stat_run.c
@@ -0,0 +1,528 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <errno.h>
+#include <fcntl.h>
+#include <math.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#define LOG_PREFIX "cmd_stat_run"
+#include "lib/log.h"
+
+#include "lib/common.h"
+#include "lib/device.h"
+#include "lib/metrics_db.h"
+#include "lib/pmtctl.h"
+#include "lib/pmtctl_context.h"
+
+#include "cmd_stat.h"
+#include "cmd_stat_format.h"
+
+static volatile sig_atomic_t stat_stop;
+
+static void stat_sigint(int sig)
+{
+	(void)sig;
+	stat_stop = 1;
+}
+
+/*
+ * Build raw-mode items:
+ *   - Devices: given by dev_indices[0..num_sel_devices-1]
+ *   - Raw specs: opts->raw_specs[0..nr_raw_specs-1]
+ *
+ * One item per (device, raw_spec).
+ */
+static int stat_build_raw_items(const struct stat_options *opts, const struct pmtctl_context *ctx,
+				const int *dev_indices, int num_sel_devices,
+				struct stat_item **out_items, int *out_nitems)
+{
+	struct stat_item *items;
+	int nitems;
+	int d, r;
+
+	/* Initialize output parameters */
+	*out_items = NULL;
+	*out_nitems = 0;
+
+	nitems = num_sel_devices * opts->nr_raw_specs;
+
+	items = calloc(nitems, sizeof(*items));
+	if (!items)
+		return log_ret(-ENOMEM, "could not create item list");
+
+	/* Grid layout: device-major, raw-spec-minor */
+	for (d = 0; d < num_sel_devices; d++) {
+		int dev_idx = dev_indices[d];
+		struct pmt_device *dev = &ctx->devices[dev_idx];
+
+		for (r = 0; r < opts->nr_raw_specs; r++) {
+			const struct stat_raw_spec *rs = &opts->raw_specs[r];
+			struct stat_item *it = &items[d * opts->nr_raw_specs + r];
+
+			it->kind      = STAT_ITEM_RAW;
+			it->event_idx = -1;
+			it->dev       = dev;
+			it->present   = true; /* raw is device-only; no bindings */
+
+			it->desc.def           = NULL;
+			it->desc.dev           = dev;
+			it->desc.name          = NULL;      /* formatter can synthesize a label */
+			it->desc.guid_inst     = dev->guid_inst;
+			it->desc.raw_sample_id = rs->sample_id;
+			it->desc.raw_lsb       = rs->lsb;
+			it->desc.raw_msb       = rs->msb;
+		}
+	}
+
+	*out_items  = items;
+	*out_nitems = nitems;
+
+	return 0;
+}
+
+static int build_present_guid_list(const struct pmtctl_context *ctx, const int *dev_indices,
+				   int num_sel_devices, uint32_t **out_guids, int *out_nr_guids)
+{
+	uint32_t *guids;
+	int nguids = 0;
+
+	guids = calloc(num_sel_devices, sizeof(*guids));
+	if (!guids)
+		return log_ret(-ENOMEM, "could not create guid list");
+
+	for (int d = 0; d < num_sel_devices; d++) {
+		int di = dev_indices[d];
+		uint32_t g;
+		bool seen = false;
+
+		if (di < 0 || di >= ctx->num_devices)
+			continue;
+
+		g = ctx->devices[di].guid ? ctx->devices[di].guid->guid : 0;
+
+		for (int i = 0; i < nguids; i++) {
+			if (guids[i] == g) {
+				seen = true;
+				break;
+			}
+		}
+		if (seen)
+			continue;
+
+		guids[nguids++] = g;
+	}
+
+	*out_guids = guids;
+	*out_nr_guids = nguids;
+
+	return 0;
+}
+
+static int find_metric_for_guid(const struct pmt_metrics_db *db, uint32_t guid, const char *name)
+{
+	for (int i = 0; i < db->total; i++) {
+		const struct pmt_metric_def *def = pmt_metrics_at(db, i);
+
+		if (!def || !def->event_name)
+			continue;
+
+		if (!def->guid || def->guid->guid != guid)
+			continue;
+
+		if (strcmp(def->event_name, name) == 0)
+			return i;
+	}
+
+	return -1;
+}
+
+static int stat_build_metric_items(const struct stat_options *opts,
+				   const struct pmtctl_context *ctx, const int *dev_indices,
+				   int num_sel_devices, struct stat_item **out_items,
+				   int *out_nitems)
+{
+	const struct pmt_metrics_db *db;
+	struct stat_item *items = NULL;
+	auto_free uint32_t *guids = NULL;
+	auto_free int *metric_by_guid = NULL;
+	int nguids = 0;
+	int nitems;
+	int ret;
+
+	/* Initialize output parameters */
+	*out_items = NULL;
+	*out_nitems = 0;
+
+	/* Build deduplicated list of GUIDs for the selected devices */
+	ret = build_present_guid_list(ctx, dev_indices, num_sel_devices, &guids, &nguids);
+	if (ret)
+		return ret;
+
+	if (nguids == 0)
+		return log_ret(PMTCTL_ERR_CMD_STAT, "no GUIDs found for selected devices");
+
+	/*
+	 * metric_by_guid[g * nr_events + e] gives the metric_idx for
+	 * GUID guids[g] and event opts->events[e], or -1 if not present.
+	 */
+	metric_by_guid = malloc(sizeof(int) * nguids * opts->nr_events);
+	if (!metric_by_guid)
+		return log_ret(-ENOMEM, "could not create event list");
+
+	db = &ctx->metrics;
+
+	for (int g = 0; g < nguids; g++) {
+		for (int e = 0; e < opts->nr_events; e++) {
+			metric_by_guid[g * opts->nr_events + e] =
+				find_metric_for_guid(db, guids[g], opts->events[e]);
+		}
+	}
+
+	/* Enforce: each requested event must be present on at least one GUID */
+	for (int e = 0; e < opts->nr_events; e++) {
+		bool found = false;
+
+		for (int g = 0; g < nguids; g++) {
+			if (metric_by_guid[g * opts->nr_events + e] >= 0) {
+				found = true;
+				break;
+			}
+		}
+
+		if (!found)
+			return log_ret(PMTCTL_ERR_BAD_ARG, "unknown event '%s'", opts->events[e]);
+	}
+
+	nitems = num_sel_devices * opts->nr_events;
+	items = calloc(nitems, sizeof(*items));
+	if (!items)
+		return log_ret(-ENOMEM, "could not create item list");
+
+	/* Grid layout: selected-device-major, event-minor */
+	for (int d = 0; d < num_sel_devices; d++) {
+		int dev_idx = dev_indices[d];
+		struct pmt_device *dev;
+		uint32_t guid;
+		int guid_idx = -1;
+
+		if (dev_idx < 0 || dev_idx >= ctx->num_devices)
+			continue;
+
+		dev = &ctx->devices[dev_idx];
+		guid = dev->guid ? dev->guid->guid : 0;
+
+		/* Find this device's GUID index */
+		for (int g = 0; g < nguids; g++) {
+			if (guids[g] == guid) {
+				guid_idx = g;
+				break;
+			}
+		}
+		if (guid_idx < 0)
+			continue; /* should not happen if build_present_guid_list is correct */
+
+		for (int e = 0; e < opts->nr_events; e++) {
+			int metric_idx = metric_by_guid[guid_idx * opts->nr_events + e];
+			const struct pmt_metric_def *def;
+			struct stat_item *it = &items[d * opts->nr_events + e];
+
+			it->kind      = STAT_ITEM_METRIC;
+			it->event_idx = e;
+			it->dev       = dev;
+			it->present   = false; /* default */
+
+			if (metric_idx < 0)
+				continue; /* metric not defined for this GUID */
+
+			def = pmt_metrics_at(db, metric_idx);
+			if (!def)
+				continue;
+
+			it->desc.def       = def;
+			it->desc.dev       = dev;
+			it->desc.name      = def->event_name;
+			it->desc.guid_inst = dev->guid_inst;
+			it->present        = true;
+		}
+	}
+
+	*out_items  = items;
+	*out_nitems = nitems;
+
+	return 0;
+}
+
+/*
+ * Drop devices that do not contain any of the requested events.
+ * This keeps the output rows limited to devices where at least one metric
+ * binding is present, avoiding rows of all-NaN values.
+ */
+static int filter_metric_items_present(const struct stat_options *opts, struct stat_item **items,
+				       int *nitems, int *num_devices)
+{
+	struct stat_item *old;
+	struct stat_item *filtered;
+	size_t row_bytes;
+	int cols_per_dev;
+	int keep = 0;
+	int devs;
+
+	cols_per_dev = opts->nr_events;
+	devs = *num_devices;
+
+	old = *items;
+	row_bytes = (size_t)cols_per_dev * sizeof(*old);
+
+	/* First pass: count devices that have at least one present metric. */
+	for (int d = 0; d < devs; d++) {
+		struct stat_item *row = &old[d * cols_per_dev];
+		bool any = false;
+
+		for (int c = 0; c < cols_per_dev; c++) {
+			if (row[c].present) {
+				any = true;
+				break;
+			}
+		}
+
+		if (any)
+			keep++;
+	}
+
+	if (keep == devs)
+		return 0; /* nothing to filter */
+
+	if (keep == 0)
+		return log_ret(PMTCTL_ERR_CMD_STAT, "no devices contain requested event(s)");
+
+	filtered = calloc((size_t)keep * cols_per_dev, sizeof(*filtered));
+	if (!filtered)
+		return log_ret(-ENOMEM, "could not filter device list");
+
+	/* Second pass: copy kept rows in order. */
+	int out_idx = 0;
+
+	for (int d = 0; d < devs; d++) {
+		struct stat_item *row = &old[d * cols_per_dev];
+		bool any = false;
+
+		for (int c = 0; c < cols_per_dev; c++) {
+			if (row[c].present) {
+				any = true;
+				break;
+			}
+		}
+
+		if (!any)
+			continue;
+
+		memcpy(&filtered[out_idx * cols_per_dev], row, row_bytes);
+		out_idx++;
+	}
+
+	free(old);
+	*items = filtered;
+	*nitems = keep * cols_per_dev;
+	*num_devices = keep;
+
+	return 0;
+}
+
+/*
+ * Check if all devices needed for stat can be opened.
+ * Returns 0 if all devices are readable, otherwise returns error code.
+ */
+static int stat_check_device_access(const struct stat_item *items, int nitems)
+{
+	/* Track which devices we've already checked to avoid redundant opens */
+	const struct pmt_device *last_dev = NULL;
+
+	for (int i = 0; i < nitems; i++) {
+		const struct stat_item *it = &items[i];
+
+		if (!it->dev || !it->present)
+			continue;
+
+		/* Skip if we just checked this device */
+		if (last_dev == it->dev)
+			continue;
+
+		last_dev = it->dev;
+
+		int fd;
+
+		if (!it->dev->data_path)
+			return log_ret(-EINVAL, "missing data path for %s", it->dev->name);
+
+		fd = open(it->dev->data_path, O_RDONLY | O_CLOEXEC);
+		if (fd == -1) {
+			if (errno == EACCES || errno == EPERM) {
+				return log_ret(-EACCES,
+					       "permission denied opening device %s (requires elevated privileges)",
+					       it->dev->name);
+			}
+
+			return log_ret(-errno, "cannot open device %s", it->dev->name);
+		}
+
+		close(fd);
+	}
+
+	return 0;
+}
+
+static int stat_loop_metrics(const struct stat_options *opts, const struct pmtctl_context *ctx,
+			     struct stat_item *items, int nitems, int num_devices)
+{
+	long max_samples = opts->count;   /* -1 = infinite */
+	struct sigaction sa;
+	int sample_idx = 0;
+	int last_read_err = 0;
+	bool any_read_ok = false;
+
+	memset(&sa, 0, sizeof(sa));
+	sa.sa_handler = stat_sigint;
+	sigaction(SIGINT, &sa, NULL);
+
+	stat_stop = 0;
+
+	stat_print_header(opts);
+
+	for (sample_idx = 0;
+	     !stat_stop && (max_samples < 0 || sample_idx < max_samples);
+	     sample_idx++) {
+		/* Read all present items */
+		for (int i = 0; i < nitems; i++) {
+			struct stat_item *it = &items[i];
+			uint64_t raw = 0;
+			int rc;
+
+			if (!it->present) {
+				it->cur_value = NAN;
+				continue;
+			}
+
+			rc = ctx->ops->read(&it->desc, &raw);
+			if (rc < 0) {
+				it->cur_value = NAN;
+				last_read_err = rc;
+				continue;
+			}
+
+			any_read_ok = true;
+			it->cur_raw = raw;
+			it->cur_value = (double)raw;
+		}
+
+		/* Print sample rows */
+		stat_print_rows(opts, items, nitems, num_devices, sample_idx);
+
+		if (opts->once)
+			break;
+
+		if (max_samples > 0 && sample_idx + 1 >= max_samples)
+			break;
+
+		if (opts->interval_ms > 0) {
+			struct timespec ts = {
+				.tv_sec  = opts->interval_ms / 1000,
+				.tv_nsec = (opts->interval_ms % 1000) * 1000000UL,
+			};
+			nanosleep(&ts, NULL);
+		}
+	}
+
+	/*
+	 * If no item ever produced a valid sample, propagate the last
+	 * read error so the CLI exits with PMTCTL_EXIT_SYSTEM rather
+	 * than masking a hard I/O failure as success.  A stat run that
+	 * had at least one good sample stays rc=0 (transient device
+	 * misses surface as NaN in the printed output).
+	 */
+	if (!any_read_ok && last_read_err < 0)
+		return last_read_err;
+
+	return 0;
+}
+
+int stat_run(const struct stat_options *opts)
+{
+	const struct pmtctl_context *ctx;
+	auto_free struct stat_item *items = NULL;
+	auto_free int *dev_indices = NULL;
+	int num_sel_devices = 0;
+	int nitems = 0;
+	int ret;
+
+	ctx = pmtctl_get_ctx();
+	if (!ctx)
+		return log_ret(PMTCTL_ERR_CMD_STAT, "context not initialized");
+
+	if (ctx->num_devices <= 0)
+		return log_ret(PMTCTL_ERR_CMD_STAT, "no devices available");
+
+	dev_indices = calloc(ctx->num_devices, sizeof(*dev_indices));
+	if (!dev_indices)
+		return log_ret(-ENOMEM, "can't create device index");
+
+	/*
+	 * Device selection:
+	 *  - If a selector string is present, parse it and call pmt_select_devices()
+	 *    to get the subset of device indices.
+	 *  - Otherwise, use all devices.
+	 */
+	if (opts->device_selector) {
+		struct pmt_ep_selector sel;
+
+		ret = pmtctl_parse_ep_selector(opts->device_selector, &sel);
+		if (ret < 0)
+			return ret;
+
+		num_sel_devices = pmt_select_devices(ctx, &sel, dev_indices, ctx->num_devices);
+		if (num_sel_devices < 0)
+			return num_sel_devices;
+
+		if (num_sel_devices == 0) {
+			return log_ret(PMTCTL_ERR_CMD_STAT, "no devices match selector '%s'",
+				       opts->device_selector);
+		}
+	} else {
+		/* No selector: use all devices. */
+		for (int i = 0; i < ctx->num_devices; i++)
+			dev_indices[i] = i;
+
+		num_sel_devices = ctx->num_devices;
+	}
+
+	/*
+	 * Build items according to mode:
+	 *   - metric mode: use events + bindings
+	 *   - raw mode:    use raw_specs only
+	 *
+	 * stat_parse_options() already enforces mutual exclusion between them.
+	 */
+	if (opts->nr_raw_specs > 0) {
+		ret = stat_build_raw_items(opts, ctx, dev_indices, num_sel_devices, &items,
+					   &nitems);
+	} else {
+		ret = stat_build_metric_items(opts, ctx, dev_indices, num_sel_devices, &items,
+					      &nitems);
+		if (!ret)
+			ret = filter_metric_items_present(opts, &items, &nitems, &num_sel_devices);
+	}
+
+	if (ret)
+		return ret;
+
+	/* Check that we have permission to access the devices */
+	ret = stat_check_device_access(items, nitems);
+	if (ret)
+		return ret;
+
+	return stat_loop_metrics(opts, ctx, items, nitems, num_sel_devices);
+}
diff --git a/tools/arch/x86/pmtctl/src/main.c b/tools/arch/x86/pmtctl/src/main.c
index d9666956c27b..81117456252c 100644
--- a/tools/arch/x86/pmtctl/src/main.c
+++ b/tools/arch/x86/pmtctl/src/main.c
@@ -45,6 +45,7 @@ static void print_usage(FILE *out)
 		"\n"
 		"Commands:\n"
 		"  list      List available PMT devices and metrics\n"
+		"  stat      Sample metrics over time (perf stat-like)\n"
 		"\n"
 		"Run 'pmtctl <command> --help' for command-specific options.\n"
 	);
@@ -116,6 +117,9 @@ static int cmd_dispatch(int argc, char **argv)
 	if (!strcmp(cmd, "list"))
 		return cmd_list(cmd_argc, cmd_argv, &gopts);
 
+	if (!strcmp(cmd, "stat"))
+		return cmd_stat(cmd_argc, cmd_argv, &gopts);
+
 	if (!strcmp(cmd, "--help") || !strcmp(cmd, "help")) {
 		print_usage(stdout);
 		return 0;
-- 
2.43.0


  parent reply	other threads:[~2026-05-26  1:48 UTC|newest]

Thread overview: 25+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-05-26  1:46 [PATCH 00/17] tools/arch/x86/pmtctl: Add Intel PMT command-line tool David E. Box
2026-05-26  1:46 ` [PATCH 01/17] tools/arch/x86/pmtctl: Add MAINTAINERS entry David E. Box
2026-05-26  1:47 ` [PATCH 02/17] tools/arch/x86/pmtctl: Add libpmtctl shared type enumerations David E. Box
2026-05-26  9:20   ` Ilpo Järvinen
2026-05-26  1:47 ` [PATCH 03/17] tools/arch/x86/pmtctl: Add libpmtctl internal logging and utility functions David E. Box
2026-05-26  9:59   ` Ilpo Järvinen
2026-05-26  1:47 ` [PATCH 04/17] tools/arch/x86/pmtctl: Add libpmtctl metric definition database David E. Box
2026-05-26 10:06   ` Ilpo Järvinen
2026-05-26  1:47 ` [PATCH 05/17] tools/arch/x86/pmtctl: Add libpmtctl device enumeration backend David E. Box
2026-05-26 10:35   ` Ilpo Järvinen
2026-05-26  1:47 ` [PATCH 06/17] tools/arch/x86/pmtctl: Add libpmtctl built-in metric provider David E. Box
2026-05-26  1:47 ` [PATCH 07/17] tools/arch/x86/pmtctl: Add libpmtctl JSON " David E. Box
2026-05-26 11:04   ` Ilpo Järvinen
2026-05-26  1:47 ` [PATCH 08/17] tools/arch/x86/pmtctl: Add libpmtctl public API and context David E. Box
2026-05-26 11:25   ` Ilpo Järvinen
2026-05-26 17:44     ` David Box
2026-05-26  1:47 ` [PATCH 09/17] tools/arch/x86/pmtctl: Add libpmtctl Makefile + pc + README David E. Box
2026-05-26  1:47 ` [PATCH 10/17] tools/arch/x86/pmtctl: Add libpmtctl usage sample David E. Box
2026-05-26  1:47 ` [PATCH 11/17] tools/arch/x86/pmtctl: Add libpmtctl built-in metric definition support David E. Box
2026-05-26  1:47 ` [PATCH 12/17] tools/arch/x86/pmtctl: Add pmtctl CLI entry point and pager David E. Box
2026-05-26  1:47 ` [PATCH 13/17] tools/arch/x86/pmtctl: Add pmtctl 'list' command David E. Box
2026-05-26  1:47 ` David E. Box [this message]
2026-05-26  1:47 ` [PATCH 15/17] tools/arch/x86/pmtctl: Add pmtxml2json conversion tool David E. Box
2026-05-26  1:47 ` [PATCH 16/17] tools/arch/x86/pmtctl: Add README.md David E. Box
2026-05-26  1:47 ` [PATCH 17/17] tools/arch/x86/pmtctl: Add man page David E. Box

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260526014719.2248380-15-david.e.box@linux.intel.com \
    --to=david.e.box@linux.intel.com \
    --cc=andriy.shevchenko@linux.intel.com \
    --cc=ilpo.jarvinen@linux.intel.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=platform-driver-x86@vger.kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.