All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH 00/17] tools/arch/x86/pmtctl: Add Intel PMT command-line tool
@ 2026-05-26  1:46 David E. Box
  2026-05-26  1:46 ` [PATCH 01/17] tools/arch/x86/pmtctl: Add MAINTAINERS entry David E. Box
                   ` (16 more replies)
  0 siblings, 17 replies; 25+ messages in thread
From: David E. Box @ 2026-05-26  1:46 UTC (permalink / raw)
  To: linux-kernel, david.e.box, ilpo.jarvinen, andriy.shevchenko,
	platform-driver-x86

Intel Platform Monitoring Technology (PMT) exposes per-component telemetry
counters through sysfs via the pmt_telemetry auxiliary bus driver. Each
telemetry device provides a data file that can be read to sample the
underlying hardware telemetry data.  The meaning of each field within a
sample is described by platform-specific metric definitions.

This series adds pmtctl, a command-line tool for querying PMT metrics on
x86 Linux systems.  It lives under tools/arch/x86/pmtctl/ alongside other
platform-specific userspace tools.

The series introduces approximately 7k lines across the library, CLI
frontend, code generation scripts, and documentation. The changes are
intentionally split into small logical components so the architecture and
dependencies can be reviewed incrementally.

The implementation is split into a reusable library (libpmtctl_core) and a
thin CLI frontend:

  libpmtctl_core (lib/)
  - Device enumeration: scans /sys/bus/auxiliary/drivers/pmt_telemetry
    and reads GUID and telem data path from sysfs attributes.
  - Metric definitions: supports two loading modes:
      built-in  -- a C struct array compiled in at build time, generated
                   from perf-style JSON by scripts/gen_builtin_defs.py.
      runtime   -- loaded at invocation via -J/--json-file (requires
                   libjansson).
  - Metric DB: block-based container with flat-index accessors.
  - GUID intern table: canonical struct pmt_guid pointers shared between
    the built-in and JSON providers.

  CLI frontend (src/)
  - list: enumerate discovered PMT devices and/or metric definitions,
    with optional GUID intersection report (--guids) and device-only
    mode (--devices).
  - stat: perf-stat-like metric sampling with configurable interval,
    count, event selection, and raw-register mode.

Metric JSON files are not included in the tree. Users obtain them from
the Intel-PMT repository using Makefile targets that fetch XML metric
definitions over the network and convert them into the JSON format
expected by the tool.

Testing: functional testing requires a system with PMT-capable hardware
and /sys/bus/auxiliary/drivers/pmt_telemetry populated.  Reading telemetry
data (stat) requires elevated privileges (CAP_SYS_ADMIN / sudo); listing
metrics and devices (list) does not.

Summary

1. Infrastructure and meta:
   0001: Adds the MAINTAINERS entry for the new tool.

2. Library (libpmtctl_core, under lib/):
   0002–0008: Introduce the core library, including shared types, logging,
              metric definitions, device enumeration, built-in and JSON
              metric providers, public API, and core logic.
   0009:      Adds the libpmtctl Makefile, pkg-config file, and README for
              the library.
   0010–0011: Add usage samples and further built-in metric support for the
              library.

3. CLI tool (under src/):
   0012:      Introduces the pmtctl CLI entry point, option parsing, and
              pager helpers.
   0013:      Adds the 'list' command.
   0014:      Adds the 'stat' command.

4. Scripts/codegen:
   0015:      Adds the pmtxml2json.py conversion tool for XML→JSON metric
              definitions.

5. Documentation and other:
   0016:      Adds README.md.
   0017:      Adds the man page.


David E. Box (17):
  tools/arch/x86/pmtctl: Add MAINTAINERS entry
  tools/arch/x86/pmtctl: Add libpmtctl shared type enumerations
  tools/arch/x86/pmtctl: Add libpmtctl internal logging and utility
    functions
  tools/arch/x86/pmtctl: Add libpmtctl metric definition database
  tools/arch/x86/pmtctl: Add libpmtctl device enumeration backend
  tools/arch/x86/pmtctl: Add libpmtctl built-in metric provider
  tools/arch/x86/pmtctl: Add libpmtctl JSON metric provider
  tools/arch/x86/pmtctl: Add libpmtctl public API and context
  tools/arch/x86/pmtctl: Add libpmtctl Makefile + pc + README
  tools/arch/x86/pmtctl: Add libpmtctl usage sample
  tools/arch/x86/pmtctl: Add libpmtctl built-in metric definition
    support
  tools/arch/x86/pmtctl: Add pmtctl CLI entry point and pager
  tools/arch/x86/pmtctl: Add pmtctl 'list' command
  tools/arch/x86/pmtctl: Add pmtctl 'stat' command
  tools/arch/x86/pmtctl: Add pmtxml2json conversion tool
  tools/arch/x86/pmtctl: Add README.md
  tools/arch/x86/pmtctl: Add man page

 MAINTAINERS                                   |   1 +
 tools/arch/x86/pmtctl/Makefile                | 204 ++++
 tools/arch/x86/pmtctl/README.md               | 279 ++++++
 tools/arch/x86/pmtctl/include/cmd_stat.h      |  75 ++
 .../arch/x86/pmtctl/include/cmd_stat_format.h |  11 +
 .../x86/pmtctl/include/lib/builtin_defs.h     |  14 +
 tools/arch/x86/pmtctl/include/lib/common.h    |  34 +
 tools/arch/x86/pmtctl/include/lib/device.h    |  53 ++
 tools/arch/x86/pmtctl/include/lib/log.h       |  80 ++
 .../arch/x86/pmtctl/include/lib/metrics_db.h  |  69 ++
 .../x86/pmtctl/include/lib/metrics_provider.h |  21 +
 tools/arch/x86/pmtctl/include/lib/pmt_guid.h  |  63 ++
 tools/arch/x86/pmtctl/include/lib/pmtctl.h    |  90 ++
 .../x86/pmtctl/include/lib/pmtctl_context.h   |  21 +
 .../x86/pmtctl/include/lib/pmtctl_types.h     |  16 +
 tools/arch/x86/pmtctl/include/pmtctl_cli.h    |  16 +
 tools/arch/x86/pmtctl/lib/Makefile            | 138 +++
 tools/arch/x86/pmtctl/lib/README              | 116 +++
 .../arch/x86/pmtctl/lib/builtin_defs_empty.c  |  13 +
 tools/arch/x86/pmtctl/lib/common.c            | 178 ++++
 tools/arch/x86/pmtctl/lib/device_telem.c      | 371 ++++++++
 tools/arch/x86/pmtctl/lib/log.c               |  80 ++
 tools/arch/x86/pmtctl/lib/metrics_db.c        |  62 ++
 tools/arch/x86/pmtctl/lib/metrics_provider.c  |  42 +
 .../x86/pmtctl/lib/metrics_provider_json.c    | 459 +++++++++
 tools/arch/x86/pmtctl/lib/pmt_guid.c          | 200 ++++
 tools/arch/x86/pmtctl/lib/pmtctl.c            | 327 +++++++
 tools/arch/x86/pmtctl/libpmtctl-core.pc.in    |  11 +
 tools/arch/x86/pmtctl/pmtctl.8                | 317 +++++++
 .../x86/pmtctl/samples/libpmtctl_sample.c     |  30 +
 .../x86/pmtctl/scripts/gen_builtin_defs.py    | 405 ++++++++
 tools/arch/x86/pmtctl/scripts/pmtxml2json.md  | 158 ++++
 tools/arch/x86/pmtctl/scripts/pmtxml2json.py  | 883 ++++++++++++++++++
 tools/arch/x86/pmtctl/src/cmd_list.c          | 786 ++++++++++++++++
 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              | 151 +++
 tools/arch/x86/pmtctl/src/pager.c             | 140 +++
 39 files changed, 7148 insertions(+)
 create mode 100644 tools/arch/x86/pmtctl/Makefile
 create mode 100644 tools/arch/x86/pmtctl/README.md
 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/include/lib/builtin_defs.h
 create mode 100644 tools/arch/x86/pmtctl/include/lib/common.h
 create mode 100644 tools/arch/x86/pmtctl/include/lib/device.h
 create mode 100644 tools/arch/x86/pmtctl/include/lib/log.h
 create mode 100644 tools/arch/x86/pmtctl/include/lib/metrics_db.h
 create mode 100644 tools/arch/x86/pmtctl/include/lib/metrics_provider.h
 create mode 100644 tools/arch/x86/pmtctl/include/lib/pmt_guid.h
 create mode 100644 tools/arch/x86/pmtctl/include/lib/pmtctl.h
 create mode 100644 tools/arch/x86/pmtctl/include/lib/pmtctl_context.h
 create mode 100644 tools/arch/x86/pmtctl/include/lib/pmtctl_types.h
 create mode 100644 tools/arch/x86/pmtctl/include/pmtctl_cli.h
 create mode 100644 tools/arch/x86/pmtctl/lib/Makefile
 create mode 100644 tools/arch/x86/pmtctl/lib/README
 create mode 100644 tools/arch/x86/pmtctl/lib/builtin_defs_empty.c
 create mode 100644 tools/arch/x86/pmtctl/lib/common.c
 create mode 100644 tools/arch/x86/pmtctl/lib/device_telem.c
 create mode 100644 tools/arch/x86/pmtctl/lib/log.c
 create mode 100644 tools/arch/x86/pmtctl/lib/metrics_db.c
 create mode 100644 tools/arch/x86/pmtctl/lib/metrics_provider.c
 create mode 100644 tools/arch/x86/pmtctl/lib/metrics_provider_json.c
 create mode 100644 tools/arch/x86/pmtctl/lib/pmt_guid.c
 create mode 100644 tools/arch/x86/pmtctl/lib/pmtctl.c
 create mode 100644 tools/arch/x86/pmtctl/libpmtctl-core.pc.in
 create mode 100644 tools/arch/x86/pmtctl/pmtctl.8
 create mode 100644 tools/arch/x86/pmtctl/samples/libpmtctl_sample.c
 create mode 100755 tools/arch/x86/pmtctl/scripts/gen_builtin_defs.py
 create mode 100644 tools/arch/x86/pmtctl/scripts/pmtxml2json.md
 create mode 100755 tools/arch/x86/pmtctl/scripts/pmtxml2json.py
 create mode 100644 tools/arch/x86/pmtctl/src/cmd_list.c
 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
 create mode 100644 tools/arch/x86/pmtctl/src/main.c
 create mode 100644 tools/arch/x86/pmtctl/src/pager.c


base-commit: 5200f5f493f79f14bbdc349e402a40dfb32f23c8
-- 
2.43.0


^ permalink raw reply	[flat|nested] 25+ messages in thread

* [PATCH 01/17] tools/arch/x86/pmtctl: Add MAINTAINERS entry
  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 ` David E. Box
  2026-05-26  1:47 ` [PATCH 02/17] tools/arch/x86/pmtctl: Add libpmtctl shared type enumerations David E. Box
                   ` (15 subsequent siblings)
  16 siblings, 0 replies; 25+ messages in thread
From: David E. Box @ 2026-05-26  1:46 UTC (permalink / raw)
  To: linux-kernel, david.e.box, ilpo.jarvinen, andriy.shevchenko,
	platform-driver-x86

Add tools/arch/x86/pmtctl/ to the INTEL PLATFORM MONITORING TECHNOLOGY
(PMT) MAINTAINERS section so patches against the pmtctl userspace tool
and its libpmtctl_core library are routed alongside the kernel PMT
driver.

Assisted-by: GitHub-Copilot:claude-opus-4.7
Signed-off-by: David E. Box <david.e.box@linux.intel.com>
---
 MAINTAINERS | 1 +
 1 file changed, 1 insertion(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index c2c6d79275c6..5bd59c044676 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -13198,6 +13198,7 @@ S:	Supported
 F:	Documentation/ABI/testing/sysfs-class-intel_pmt
 F:	Documentation/ABI/testing/sysfs-class-intel_pmt-features
 F:	drivers/platform/x86/intel/pmt/
+F:	tools/arch/x86/pmtctl/
 
 INTEL PRO/WIRELESS 2100, 2200BG, 2915ABG NETWORK CONNECTION SUPPORT
 M:	Stanislav Yakovlev <stas.yakovlev@gmail.com>
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [PATCH 02/17] tools/arch/x86/pmtctl: Add libpmtctl shared type enumerations
  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 ` 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
                   ` (14 subsequent siblings)
  16 siblings, 1 reply; 25+ messages in thread
From: David E. Box @ 2026-05-26  1:47 UTC (permalink / raw)
  To: linux-kernel, david.e.box, ilpo.jarvinen, andriy.shevchenko,
	platform-driver-x86

Add pmtctl_types.h to centralize shared libpmtctl type definitions used
across both the public and internal library interfaces.

This provides a common definition point for core library-facing types,
including device backend classification and logging verbosity levels,
avoiding duplication across API boundaries.

Assisted-by: GitHub-Copilot:claude-sonnet-4.6
Signed-off-by: David E. Box <david.e.box@linux.intel.com>
---
 tools/arch/x86/pmtctl/include/lib/pmtctl_types.h | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)
 create mode 100644 tools/arch/x86/pmtctl/include/lib/pmtctl_types.h

diff --git a/tools/arch/x86/pmtctl/include/lib/pmtctl_types.h b/tools/arch/x86/pmtctl/include/lib/pmtctl_types.h
new file mode 100644
index 000000000000..8e3bf5a173c4
--- /dev/null
+++ b/tools/arch/x86/pmtctl/include/lib/pmtctl_types.h
@@ -0,0 +1,16 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef PMTCTL_TYPES_H
+#define PMTCTL_TYPES_H
+
+enum pmt_device_type {
+	PMT_DEVICE_TELEM
+};
+
+enum pmtctl_log_level {
+	PMTCTL_LOG_ERROR = 0,
+	PMTCTL_LOG_WARN,
+	PMTCTL_LOG_INFO,
+	PMTCTL_LOG_DEBUG,
+};
+
+#endif
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [PATCH 03/17] tools/arch/x86/pmtctl: Add libpmtctl internal logging and utility functions
  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  1:47 ` 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
                   ` (13 subsequent siblings)
  16 siblings, 1 reply; 25+ messages in thread
From: David E. Box @ 2026-05-26  1:47 UTC (permalink / raw)
  To: linux-kernel, david.e.box, ilpo.jarvinen, andriy.shevchenko,
	platform-driver-x86

Add the internal logging infrastructure and shared utility helpers used by
libpmtctl_core.

The logging layer provides consistent level-filtered diagnostics across the
library, while the utility helpers centralize common sysfs access, string
handling, and scoped resource cleanup functionality used across backend
implementations.

Logging levels mirror the public pmtctl_log_level enum and can be used to
control library verbosity.

Assisted-by: GitHub-Copilot:claude-sonnet-4.6
Signed-off-by: David E. Box <david.e.box@linux.intel.com>
---
 tools/arch/x86/pmtctl/include/lib/common.h   |  34 ++++
 tools/arch/x86/pmtctl/include/lib/log.h      |  80 ++++++++
 tools/arch/x86/pmtctl/include/lib/pmt_guid.h |  63 ++++++
 tools/arch/x86/pmtctl/lib/common.c           | 178 +++++++++++++++++
 tools/arch/x86/pmtctl/lib/log.c              |  80 ++++++++
 tools/arch/x86/pmtctl/lib/pmt_guid.c         | 200 +++++++++++++++++++
 6 files changed, 635 insertions(+)
 create mode 100644 tools/arch/x86/pmtctl/include/lib/common.h
 create mode 100644 tools/arch/x86/pmtctl/include/lib/log.h
 create mode 100644 tools/arch/x86/pmtctl/include/lib/pmt_guid.h
 create mode 100644 tools/arch/x86/pmtctl/lib/common.c
 create mode 100644 tools/arch/x86/pmtctl/lib/log.c
 create mode 100644 tools/arch/x86/pmtctl/lib/pmt_guid.c

diff --git a/tools/arch/x86/pmtctl/include/lib/common.h b/tools/arch/x86/pmtctl/include/lib/common.h
new file mode 100644
index 000000000000..cf1540326ec6
--- /dev/null
+++ b/tools/arch/x86/pmtctl/include/lib/common.h
@@ -0,0 +1,34 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef PMTCTL_COMMON_H
+#define PMTCTL_COMMON_H
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <stdint.h>
+
+#ifndef __GNUC__
+#error "pmtctl: cleanup helpers require GCC/Clang (__GNUC__)."
+#endif
+
+static inline void freep(void *p)
+{
+	void **ptr = (void **)p;
+
+	if (*ptr) {
+		free(*ptr);
+		*ptr = NULL;
+	}
+}
+
+#define auto_free __attribute__((cleanup(freep)))
+
+#ifndef ARRAY_SIZE
+#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
+#endif
+
+char *xstrdup(const char *s);
+int read_attr_text(const char *dir, const char *name, char *buf, size_t buflen);
+int read_optional_int_attr(const char *devpath, const char *attr, int *out_id);
+int read_int_attr(const char *devpath, const char *attr, int *out_id);
+int read_u32_hex_attr(const char *devpath, const char *attr, uint32_t *out, int err_invalid);
+#endif
diff --git a/tools/arch/x86/pmtctl/include/lib/log.h b/tools/arch/x86/pmtctl/include/lib/log.h
new file mode 100644
index 000000000000..32f4de05f9de
--- /dev/null
+++ b/tools/arch/x86/pmtctl/include/lib/log.h
@@ -0,0 +1,80 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef PMTCTL_LOG_H
+#define PMTCTL_LOG_H
+
+#include <linux/compiler.h>
+#include "pmtctl_types.h"
+
+#define PMTCTL_EXIT_USER    1
+#define PMTCTL_EXIT_SYSTEM  2
+#define PMTCTL_EXIT_BUG     3
+
+enum {
+	PMTCTL_ERR_PARSE = 1,   /* malformed JSON, invalid numbers, missing fields */
+	PMTCTL_ERR_NOTFOUND,   /* metric/device/group not found */
+	PMTCTL_ERR_BAD_ARG,
+	PMTCTL_ERR_CMD_PARSE,
+	PMTCTL_ERR_CMD_LIST,
+	PMTCTL_ERR_CMD_STAT,
+	PMTCTL_ERR_NOMETRICS,
+	PMTCTL_ERR_METRICS,
+	PMTCTL_ERR_INVALID,   /* bad selector, bad arguments, bad config */
+	PMTCTL_ERR_DEVICE,   /* device mismatch or device internal failure */
+	PMTCTL_ERR_BINDING,   /* binding table construction failed */
+	PMTCTL_ERR_UNSUPPORTED,   /* metric/feature not supported on this system */
+};
+
+#ifdef LOG_PREFIX
+#define _LOG_PREFIXED(fmt) LOG_PREFIX ": " fmt
+#else
+#define _LOG_PREFIXED(fmt) fmt
+#endif
+
+#define LOG_ERROR PMTCTL_LOG_ERROR
+#define LOG_WARN PMTCTL_LOG_WARN
+#define LOG_INFO PMTCTL_LOG_INFO
+#define LOG_DEBUG PMTCTL_LOG_DEBUG
+
+void log_impl(enum pmtctl_log_level lvl, int err, const char *fmt, ...)
+	__printf(3, 4);
+void log_set_level(enum pmtctl_log_level lvl);
+
+#define log_bug_and_exit(fmt, ...) \
+	log_bug_impl(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
+
+#define log_err(err, fmt, ...) \
+	log_impl(LOG_ERROR, (err), _LOG_PREFIXED(fmt), ##__VA_ARGS__)
+
+#define log_warn(fmt, ...) \
+	log_impl(LOG_WARN, 0, _LOG_PREFIXED(fmt), ##__VA_ARGS__)
+
+#define log_info(fmt, ...) \
+	log_impl(LOG_INFO, 0, _LOG_PREFIXED(fmt), ##__VA_ARGS__)
+
+#ifdef DEBUG
+#define log_debug(fmt, ...) \
+	log_impl(LOG_DEBUG, 0, _LOG_PREFIXED(fmt), ##__VA_ARGS__)
+#else
+#define log_debug(fmt, ...) \
+	((void)(0 ? log_info(fmt, ##__VA_ARGS__) : 0))
+#endif
+
+int log_ret(int ret, const char *fmt, ...);
+
+__noreturn
+void log_bug_impl(const char *file, int line, const char *fmt, ...);
+
+#define LOG_ONCE(level, fmt, ...)                             \
+	do {                                                  \
+		static int __done_##__LINE__;                 \
+		if (!__done_##__LINE__) {                     \
+			level(fmt, ##__VA_ARGS__);            \
+			__done_##__LINE__ = 1;                \
+		}                                             \
+	} while (0)
+
+#define log_err_once(fmt, ...)  LOG_ONCE(log_err,  fmt, ##__VA_ARGS__)
+#define log_warn_once(fmt, ...) LOG_ONCE(log_warn, fmt, ##__VA_ARGS__)
+#define log_info_once(fmt, ...) LOG_ONCE(log_info, fmt, ##__VA_ARGS__)
+
+#endif
diff --git a/tools/arch/x86/pmtctl/include/lib/pmt_guid.h b/tools/arch/x86/pmtctl/include/lib/pmt_guid.h
new file mode 100644
index 000000000000..d21bda5bc71b
--- /dev/null
+++ b/tools/arch/x86/pmtctl/include/lib/pmt_guid.h
@@ -0,0 +1,63 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef PMTCTL_PMT_GUID_H
+#define PMTCTL_PMT_GUID_H
+
+#include <stdbool.h>
+#include <stdint.h>
+
+/*
+ * Per-GUID metadata derived from the Intel-PMT pmt.xml top-level mapping
+ * table. The compiled-in built-in table (builtin_guids[]) is generated by
+ * scripts/gen_builtin_defs.py. Runtime-loaded providers may register
+ * additional entries via pmt_guid_register().
+ *
+ * A global intern registry ensures pointer-equality across pmt_device and
+ * pmt_metric_def: two references to the same GUID always yield the same
+ * struct pmt_guid pointer.
+ */
+struct pmt_guid {
+	uint32_t    guid;
+	const char *name;        /* basedir lowercased with '/' -> '_'; may be NULL */
+	const char *description; /* may be NULL */
+};
+
+/*
+ * Register an array of pmt_guid entries with the global registry. The
+ * pointer must remain valid for the registry's lifetime. Entries with a
+ * GUID already present in the registry are skipped (first wins).
+ *
+ * Returns 0 on success, negative errno on failure.
+ */
+int pmt_guid_register(const struct pmt_guid *entries, int count);
+
+/*
+ * Same as pmt_guid_register(), but the registry takes ownership of `block`
+ * (typically the heap allocation that backs `entries` and any pooled
+ * strings the entries point into). The block is freed by pmt_guid_cleanup().
+ *
+ * `block` may equal `entries` when the array sits at the head of the
+ * allocation. Passing a NULL block degrades to pmt_guid_register().
+ *
+ * Returns 0 on success, negative errno on failure. On failure the caller
+ * retains ownership of `block` and must free it.
+ */
+int pmt_guid_register_owned(void *block, const struct pmt_guid *entries, int count);
+
+/*
+ * Lookup the registered pmt_guid for the given numeric GUID. Returns NULL
+ * if no entry has been registered or interned for that GUID.
+ */
+const struct pmt_guid *pmt_guid_lookup(uint32_t guid);
+
+/*
+ * Resolve numeric GUID to a stable struct pmt_guid pointer. If no entry is
+ * registered, a synthetic entry (name = NULL, description = NULL) is
+ * allocated and returned; subsequent calls with the same GUID return the
+ * same pointer. Returns NULL only on allocation failure.
+ */
+const struct pmt_guid *pmt_guid_intern(uint32_t guid);
+
+/* Free all synthetic (interned) entries and clear registrations. */
+void pmt_guid_cleanup(void);
+
+#endif
diff --git a/tools/arch/x86/pmtctl/lib/common.c b/tools/arch/x86/pmtctl/lib/common.c
new file mode 100644
index 000000000000..42931bf79480
--- /dev/null
+++ b/tools/arch/x86/pmtctl/lib/common.c
@@ -0,0 +1,178 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "lib/common.h"
+#include "lib/log.h"
+
+char *xstrdup(const char *s)
+{
+	char *p;
+
+	if (!s)
+		return NULL;
+
+	p = strdup(s);
+	if (!p) {
+		log_err(errno, "out of memory");
+		exit(EXIT_FAILURE);
+	}
+
+	return p;
+}
+
+static void trim_newline(char *s)
+{
+	size_t len;
+	char *end;
+
+	if (!s)
+		return;
+
+	len = strlen(s);
+	if (!len)
+		return;
+
+	end = s + len - 1;
+
+	while (end >= s && (*end == '\n' || *end == '\r')) {
+		*end = '\0';
+		end--;
+	}
+}
+
+int read_attr_text(const char *dir, const char *name, char *buf, size_t buflen)
+{
+	char path[PATH_MAX];
+	int fd;
+	ssize_t n;
+
+	if (!dir || !name || !buf || buflen == 0)
+		return -EINVAL;
+
+	if (snprintf(path, sizeof(path), "%s/%s", dir, name) >= (int)sizeof(path))
+		return -ENAMETOOLONG;
+
+	fd = open(path, O_RDONLY | O_CLOEXEC);
+	if (fd == -1)
+		return -errno;
+
+	n = read(fd, buf, buflen - 1);
+	close(fd);
+
+	if (n == -1)
+		return -errno;
+
+	buf[n] = '\0';
+	trim_newline(buf);
+
+	return 0;
+}
+
+int read_optional_int_attr(const char *devpath, const char *attr, int *out_id)
+{
+	char buf[64];
+	char *end;
+	long v;
+	int ret;
+
+	if (!out_id || !devpath || !attr)
+		return -EINVAL;
+
+	ret = read_attr_text(devpath, attr, buf, sizeof(buf));
+	if (ret != 0) {
+		if (ret == -ENOENT) {
+			/* file not found */
+			*out_id = -1;
+			return 0;
+		}
+
+		return ret;
+	}
+
+	errno = 0;
+
+	/* accept decimal or 0x-prefixed hex */
+	v = strtol(buf, &end, 0);
+	if (errno)
+		return -errno;
+
+	if (end == buf || *end != '\0')
+		return -EINVAL;
+
+	if (v < INT_MIN || v > INT_MAX)
+		return -ERANGE;
+
+	*out_id = (int)v;
+
+	return 0;
+}
+
+int read_int_attr(const char *devpath, const char *attr, int *out_id)
+{
+	char buf[64];
+	char *end;
+	long v;
+	int ret;
+
+	if (!out_id || !devpath || !attr)
+		return -EINVAL;
+
+	ret = read_attr_text(devpath, attr, buf, sizeof(buf));
+	if (ret != 0)
+		return ret;
+
+	errno = 0;
+
+	/* accept decimal or 0x-prefixed hex */
+	v = strtol(buf, &end, 0);
+	if (errno)
+		return -errno;
+
+	if (end == buf || *end != '\0')
+		return -EINVAL;
+
+	if (v < INT_MIN || v > INT_MAX)
+		return -ERANGE;
+
+	*out_id = (int)v;
+
+	return 0;
+}
+
+int read_u32_hex_attr(const char *devpath, const char *attr, uint32_t *out, int err_invalid)
+{
+	char buf[64];
+	char *end;
+	unsigned long v;
+	int ret;
+
+	if (!out || !devpath || !attr)
+		return -EINVAL;
+
+	ret = read_attr_text(devpath, attr, buf, sizeof(buf));
+	if (ret != 0)
+		return ret;
+
+	errno = 0;
+	v = strtoul(buf, &end, 16);
+	if (errno)
+		return -errno;
+
+	if (end == buf || *end != '\0')
+		return err_invalid;
+
+	if (v > UINT32_MAX)
+		return err_invalid;
+
+	*out = (uint32_t)v;
+
+	return 0;
+}
diff --git a/tools/arch/x86/pmtctl/lib/log.c b/tools/arch/x86/pmtctl/lib/log.c
new file mode 100644
index 000000000000..2ce9edacc97a
--- /dev/null
+++ b/tools/arch/x86/pmtctl/lib/log.c
@@ -0,0 +1,80 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+
+#include "lib/log.h"
+
+static enum pmtctl_log_level g_log_level = LOG_INFO;
+
+void log_set_level(enum pmtctl_log_level lvl)
+{
+	if (lvl < LOG_ERROR || lvl > LOG_DEBUG)
+		g_log_level = LOG_INFO;
+	else
+		g_log_level = lvl;
+}
+
+static void log_vmsg(enum pmtctl_log_level lvl, int err, const char *fmt,
+		     va_list ap)
+{
+	if (lvl > g_log_level)
+		return;
+
+	const char *tag =
+		(lvl == LOG_ERROR) ? "error"  :
+		(lvl == LOG_WARN)  ? "warning" :
+		(lvl == LOG_INFO)  ? "info"   :
+				     "debug";
+
+	fprintf(stderr, "%s: ", tag);
+	vfprintf(stderr, fmt, ap);
+
+	if (err != 0) {
+		int e = (err < 0) ? -err : err;
+
+		if (err < 0)
+			fprintf(stderr, ": %s", strerror(e));
+	}
+
+	fputc('\n', stderr);
+}
+
+void log_impl(enum pmtctl_log_level lvl, int err, const char *fmt, ...)
+{
+	va_list ap;
+
+	va_start(ap, fmt);
+	log_vmsg(lvl, err, fmt, ap);
+	va_end(ap);
+}
+
+int log_ret(int ret, const char *fmt, ...)
+{
+	if (ret == 0)
+		return 0;
+
+	va_list ap;
+
+	va_start(ap, fmt);
+	log_vmsg(LOG_ERROR, ret, fmt, ap);
+	va_end(ap);
+
+	return ret;
+}
+
+__noreturn
+void log_bug_impl(const char *file, int line, const char *fmt, ...)
+{
+	va_list ap;
+
+	fprintf(stderr, "pmtctl: BUG at %s:%d: ", file, line);
+	va_start(ap, fmt);
+	vfprintf(stderr, fmt, ap);
+	va_end(ap);
+	fputc('\n', stderr);
+
+	exit(PMTCTL_EXIT_BUG);
+}
diff --git a/tools/arch/x86/pmtctl/lib/pmt_guid.c b/tools/arch/x86/pmtctl/lib/pmt_guid.c
new file mode 100644
index 000000000000..f4d74fc7a977
--- /dev/null
+++ b/tools/arch/x86/pmtctl/lib/pmt_guid.c
@@ -0,0 +1,200 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <errno.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "lib/pmt_guid.h"
+
+/*
+ * Global intern registry for struct pmt_guid.
+ *
+ * Two sources feed this table:
+ *   1. Pre-registered blocks (e.g. builtin_guids[] from generated code,
+ *      or JSON-loaded pmt_guids.json from the runtime provider). Pointers
+ *      to those entries are stored verbatim and assumed to outlive the
+ *      registry's use.
+ *   2. Synthetic entries allocated on demand by pmt_guid_intern() for
+ *      GUIDs that no provider knows about. These have name = NULL and
+ *      description = NULL.
+ *
+ * A simple linear-scan array is used; the registry holds at most a few
+ * hundred entries on any realistic system.
+ */
+struct guid_slot {
+	const struct pmt_guid *entry; /* points into registered block or synth_storage */
+	struct pmt_guid       *synth; /* non-NULL iff this slot owns the entry */
+};
+
+static struct guid_slot *g_slots;
+static int               g_nslots;
+static int               g_cap;
+
+/*
+ * Heap blocks handed to pmt_guid_register_owned(). The registry frees
+ * them in pmt_guid_cleanup() after disposing of the slot array, so any
+ * pointers the slots held into these blocks are gone by then.
+ */
+static void **g_owned_blocks;
+static int    g_nowned;
+static int    g_owned_cap;
+
+static int reserve_one(void)
+{
+	struct guid_slot *tmp;
+	int new_cap;
+
+	if (g_nslots < g_cap)
+		return 0;
+
+	new_cap = g_cap ? g_cap * 2 : 32;
+	tmp = realloc(g_slots, (size_t)new_cap * sizeof(*tmp));
+	if (!tmp)
+		return -ENOMEM;
+
+	g_slots = tmp;
+	g_cap = new_cap;
+	return 0;
+}
+
+const struct pmt_guid *pmt_guid_lookup(uint32_t guid)
+{
+	for (int i = 0; i < g_nslots; i++) {
+		if (g_slots[i].entry && g_slots[i].entry->guid == guid)
+			return g_slots[i].entry;
+	}
+	return NULL;
+}
+
+int pmt_guid_register(const struct pmt_guid *entries, int count)
+{
+	if (!entries || count <= 0)
+		return -EINVAL;
+
+	for (int i = 0; i < count; i++) {
+		const struct pmt_guid *e = &entries[i];
+		bool already_known = false;
+		int ret;
+
+		/*
+		 * If a slot already exists for this GUID, upgrade it when the
+		 * existing entry is a synthetic placeholder (created earlier
+		 * by pmt_guid_intern() before any provider was loaded) and the
+		 * new entry carries real metadata. The synthetic struct must
+		 * be kept alive because earlier callers (e.g. devices) hold
+		 * pointers to it; patch its fields in place instead of
+		 * swapping the slot entry.
+		 */
+		for (int s = 0; s < g_nslots; s++) {
+			struct guid_slot *slot = &g_slots[s];
+
+			if (!slot->entry || slot->entry->guid != e->guid)
+				continue;
+
+			if (slot->synth && e->name && *e->name) {
+				slot->synth->name = e->name;
+				slot->synth->description = e->description;
+			}
+			already_known = true;
+			break;
+		}
+
+		if (already_known)
+			continue;
+
+		ret = reserve_one();
+		if (ret < 0)
+			return ret;
+
+		g_slots[g_nslots].entry = e;
+		g_slots[g_nslots].synth = NULL;
+		g_nslots++;
+	}
+
+	return 0;
+}
+
+int pmt_guid_register_owned(void *block, const struct pmt_guid *entries, int count)
+{
+	void **tmp;
+	int ret;
+
+	/*
+	 * Grow g_owned_blocks first, before registering anything. If this
+	 * fails, no entries have been added to g_slots yet, so the caller
+	 * can safely free(block) on error without leaving dangling slot
+	 * pointers behind.
+	 */
+	if (block && g_nowned == g_owned_cap) {
+		int new_cap = g_owned_cap ? g_owned_cap * 2 : 8;
+
+		tmp = realloc(g_owned_blocks, (size_t)new_cap * sizeof(*tmp));
+		if (!tmp)
+			return -ENOMEM;
+
+		g_owned_blocks = tmp;
+		g_owned_cap = new_cap;
+	}
+
+	ret = pmt_guid_register(entries, count);
+	if (ret < 0)
+		return ret;
+
+	if (block)
+		g_owned_blocks[g_nowned++] = block;
+
+	return 0;
+}
+
+const struct pmt_guid *pmt_guid_intern(uint32_t guid)
+{
+	const struct pmt_guid *existing;
+	struct pmt_guid *synth;
+	int ret;
+
+	existing = pmt_guid_lookup(guid);
+	if (existing)
+		return existing;
+
+	ret = reserve_one();
+	if (ret < 0)
+		return NULL;
+
+	synth = calloc(1, sizeof(*synth));
+	if (!synth)
+		return NULL;
+
+	synth->guid = guid;
+	synth->name = NULL;
+	synth->description = NULL;
+
+	g_slots[g_nslots].entry = synth;
+	g_slots[g_nslots].synth = synth;
+	g_nslots++;
+
+	return synth;
+}
+
+void pmt_guid_cleanup(void)
+{
+	for (int i = 0; i < g_nslots; i++)
+		free(g_slots[i].synth);
+
+	free(g_slots);
+	g_slots = NULL;
+	g_nslots = 0;
+	g_cap = 0;
+
+	/*
+	 * Owned blocks are freed last: slot entries may have pointed into
+	 * them, but those pointers are gone now that g_slots is released.
+	 */
+	for (int i = 0; i < g_nowned; i++)
+		free(g_owned_blocks[i]);
+
+	free(g_owned_blocks);
+	g_owned_blocks = NULL;
+	g_nowned = 0;
+	g_owned_cap = 0;
+}
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [PATCH 04/17] tools/arch/x86/pmtctl: Add libpmtctl metric definition database
  2026-05-26  1:46 [PATCH 00/17] tools/arch/x86/pmtctl: Add Intel PMT command-line tool David E. Box
                   ` (2 preceding siblings ...)
  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  1:47 ` 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
                   ` (12 subsequent siblings)
  16 siblings, 1 reply; 25+ messages in thread
From: David E. Box @ 2026-05-26  1:47 UTC (permalink / raw)
  To: linux-kernel, david.e.box, ilpo.jarvinen, andriy.shevchenko,
	platform-driver-x86

Add a growable container for PMT metric definitions to support loading and
querying hardware metric descriptors at runtime.

PMT metrics vary by platform and are discovered dynamically from sysfs. A
block-based database allows metric definitions to be appended as they are
parsed, without requiring the final size to be known in advance, while
bounding individual memory allocations.

Assisted-by: GitHub-Copilot:claude-sonnet-4.6
Signed-off-by: David E. Box <david.e.box@linux.intel.com>
---
 .../arch/x86/pmtctl/include/lib/metrics_db.h  | 69 +++++++++++++++++++
 tools/arch/x86/pmtctl/lib/metrics_db.c        | 62 +++++++++++++++++
 2 files changed, 131 insertions(+)
 create mode 100644 tools/arch/x86/pmtctl/include/lib/metrics_db.h
 create mode 100644 tools/arch/x86/pmtctl/lib/metrics_db.c

diff --git a/tools/arch/x86/pmtctl/include/lib/metrics_db.h b/tools/arch/x86/pmtctl/include/lib/metrics_db.h
new file mode 100644
index 000000000000..9dff27d8785a
--- /dev/null
+++ b/tools/arch/x86/pmtctl/include/lib/metrics_db.h
@@ -0,0 +1,69 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef PMTCTL_METRICS_DB_H
+#define PMTCTL_METRICS_DB_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#include "lib/pmt_guid.h"
+
+/* Metric definition from builtin or JSON */
+struct pmt_metric_def {
+	const char *event_name;
+	const char *description;
+	const char *group;
+	const char *platform_group;
+
+	const struct pmt_guid *guid;
+	uint32_t sample_id;
+	uint8_t  lsb;
+	uint8_t  msb;
+};
+
+struct pmt_metrics_block {
+	const struct pmt_metric_def *defs;
+	int count;
+	bool is_builtin;
+};
+
+struct pmt_metrics_db {
+	struct pmt_metrics_block *blocks;
+	int nblocks;
+	int total;   /* sum of all block->count */
+};
+
+/* Treat DB as a flat array [0..total-1] */
+static inline const struct pmt_metric_def *pmt_metrics_at(const struct pmt_metrics_db *db, int idx)
+{
+	if (!db || idx < 0 || idx >= db->total)
+		return NULL;
+
+	for (int i = 0; i < db->nblocks; i++) {
+		if (idx < db->blocks[i].count)
+			return &db->blocks[i].defs[idx];
+		idx -= db->blocks[i].count;
+	}
+	return NULL;
+}
+
+static inline const struct pmt_metrics_block *
+pmt_metrics_block_for(const struct pmt_metrics_db *db, int idx)
+{
+	if (!db || idx < 0 || idx >= db->total)
+		return NULL;
+
+	for (int i = 0; i < db->nblocks; i++) {
+		if (idx < db->blocks[i].count)
+			return &db->blocks[i];
+		idx -= db->blocks[i].count;
+	}
+
+	return NULL;
+}
+
+int pmt_metrics_add_block(struct pmt_metrics_db *db, const struct pmt_metric_def *defs,
+			  int count, bool is_builtin);
+void pmt_metrics_free(struct pmt_metrics_db *db);
+
+#endif
diff --git a/tools/arch/x86/pmtctl/lib/metrics_db.c b/tools/arch/x86/pmtctl/lib/metrics_db.c
new file mode 100644
index 000000000000..82e8121a1b98
--- /dev/null
+++ b/tools/arch/x86/pmtctl/lib/metrics_db.c
@@ -0,0 +1,62 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <errno.h>
+#include <limits.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "lib/metrics_db.h"
+
+int pmt_metrics_add_block(struct pmt_metrics_db *db, const struct pmt_metric_def *defs,
+			  int count, bool is_builtin)
+{
+	struct pmt_metrics_block *blocks;
+	size_t alloc_size;
+	int new_cap;
+
+	if (!db || !defs || count <= 0)
+		return -EINVAL;
+
+	if (db->nblocks > INT_MAX - 1)
+		return -EOVERFLOW;
+
+	if (db->total > INT_MAX - count)
+		return -EOVERFLOW;
+
+	new_cap = db->nblocks + 1;
+
+	if ((size_t)new_cap > SIZE_MAX / sizeof(*blocks))
+		return -EOVERFLOW;
+
+	alloc_size = (size_t)new_cap * sizeof(*blocks);
+
+	blocks = realloc(db->blocks, alloc_size);
+	if (!blocks)
+		return -ENOMEM;
+
+	db->blocks = blocks;
+
+	db->blocks[db->nblocks].defs       = defs;
+	db->blocks[db->nblocks].count      = count;
+	db->blocks[db->nblocks].is_builtin = is_builtin;
+
+	db->nblocks++;
+	db->total += count;
+
+	return 0;
+}
+
+void pmt_metrics_free(struct pmt_metrics_db *db)
+{
+	if (!db)
+		return;
+
+	if (db->blocks) {
+		for (int i = 0; i < db->nblocks; i++) {
+			if (!db->blocks[i].is_builtin && db->blocks[i].defs)
+				free((void *)db->blocks[i].defs);
+		}
+		free(db->blocks);
+	}
+
+	memset(db, 0, sizeof(*db));
+}
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [PATCH 05/17] tools/arch/x86/pmtctl: Add libpmtctl device enumeration backend
  2026-05-26  1:46 [PATCH 00/17] tools/arch/x86/pmtctl: Add Intel PMT command-line tool David E. Box
                   ` (3 preceding siblings ...)
  2026-05-26  1:47 ` [PATCH 04/17] tools/arch/x86/pmtctl: Add libpmtctl metric definition database David E. Box
@ 2026-05-26  1:47 ` 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
                   ` (11 subsequent siblings)
  16 siblings, 1 reply; 25+ messages in thread
From: David E. Box @ 2026-05-26  1:47 UTC (permalink / raw)
  To: linux-kernel, david.e.box, ilpo.jarvinen, andriy.shevchenko,
	platform-driver-x86

Add a sysfs-based backend for enumerating Intel PMT telemetry devices,
enabling libpmtctl to discover and access hardware telemetry data at
runtime without hardcoded device paths.

Intel PMT telemetry devices are exposed under
/sys/bus/auxiliary/drivers/pmt_telemetry. This backend walks those sysfs
entries to discover device metadata and data paths, populating per-device
descriptors used by the metric access layer.

Introduce a pmt_device_ops vtable to allow additional enumeration backends
or PMT transport types to be added without changing the calling code.

Assisted-by: GitHub-Copilot:claude-sonnet-4.6
Signed-off-by: David E. Box <david.e.box@linux.intel.com>
---
 tools/arch/x86/pmtctl/include/lib/device.h |  53 +++
 tools/arch/x86/pmtctl/lib/device_telem.c   | 371 +++++++++++++++++++++
 2 files changed, 424 insertions(+)
 create mode 100644 tools/arch/x86/pmtctl/include/lib/device.h
 create mode 100644 tools/arch/x86/pmtctl/lib/device_telem.c

diff --git a/tools/arch/x86/pmtctl/include/lib/device.h b/tools/arch/x86/pmtctl/include/lib/device.h
new file mode 100644
index 000000000000..5bee487e9b99
--- /dev/null
+++ b/tools/arch/x86/pmtctl/include/lib/device.h
@@ -0,0 +1,53 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef PMTCTL_DEVICES_H
+#define PMTCTL_DEVICES_H
+
+#include <stdint.h>
+
+#include "metrics_db.h"
+#include "pmtctl_types.h"
+
+#define PMT_SAMPLE_SIZE 8
+
+/* --- Device representation returned by any device --- */
+struct pmt_device {
+	const struct pmt_guid *guid;   /* per-GUID metadata (interned) */
+	int        guid_inst;    /* global guid instance */
+	int        dev_inst;     /* local device instance */
+
+	char      *name;         /* e.g. "pmt_ep_27971628_0" or "telem0" */
+	char      *path;         /* e.g. "/sys/class/intel_pmt/telem0" */
+	char      *data_path;    /* readable metric data path for this device */
+	int        instance;     /* 0, 1, etc. */
+
+	int        pkg_id;       /* Package/Socket ID if known, else -1 */
+	int        die_id;       /* Die ID if known, else -1 */
+
+	int        fd;           /* telem file fd */
+};
+
+struct pmt_metric_desc {
+	const struct pmt_metric_def *def;
+	struct pmt_device *dev;
+
+	const char *name; /* JSON name */
+	int guid_inst; /* global GUID instance (matches pmt_device.guid_inst) */
+
+	/* raw mode fields */
+	uint32_t raw_sample_id;
+	uint8_t  raw_lsb;
+	uint8_t  raw_msb;
+
+};
+
+struct pmt_device_ops {
+	enum pmt_device_type dev_type;
+	int  (*init)(void);
+	void (*cleanup)(void);
+	int  (*read)(struct pmt_metric_desc *m, uint64_t *val);
+	struct pmt_device *(*device_list)(int *count);
+};
+
+extern struct pmt_device_ops device_telem_ops;
+
+#endif
diff --git a/tools/arch/x86/pmtctl/lib/device_telem.c b/tools/arch/x86/pmtctl/lib/device_telem.c
new file mode 100644
index 000000000000..4c90dc95890f
--- /dev/null
+++ b/tools/arch/x86/pmtctl/lib/device_telem.c
@@ -0,0 +1,371 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#define LOG_PREFIX "telem"
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "lib/common.h"
+#include "lib/device.h"
+#include "lib/log.h"
+
+#define PMT_TELEM_AUX_DIR    "/sys/bus/auxiliary/drivers/pmt_telemetry"
+#define PMT_TELEM_PMT_DIR    "intel_pmt"
+#define PMT_TELEM_DATA_FILE  "telem"
+
+static struct pmt_device *devices;
+static int device_count;
+static int device_initialized;
+
+static int telem_read(struct pmt_metric_desc *m, uint64_t *val)
+{
+	uint64_t raw;
+	uint64_t mask;
+	uint8_t width;
+	ssize_t n;
+	off_t off;
+	uint32_t sample_id;
+	uint8_t  lsb, msb;
+
+	if (!m || !val || !m->dev)
+		log_bug_and_exit("unexpected NULL parameters");
+
+	/* Open the telem file on first read */
+	if (m->dev->fd < 0) {
+		int fd;
+
+		if (!m->dev->data_path)
+			return log_ret(-EINVAL, "missing data path for %s", m->dev->name);
+
+		fd = open(m->dev->data_path, O_RDONLY | O_CLOEXEC);
+		if (fd == -1)
+			return log_ret(-errno, "could not open telem file %s", m->dev->data_path);
+
+		m->dev->fd = fd;
+	}
+
+	if (m->def) {
+		sample_id = m->def->sample_id;
+		lsb       = m->def->lsb;
+		msb       = m->def->msb;
+	} else {
+		sample_id = m->raw_sample_id;
+		lsb       = m->raw_lsb;
+		msb       = m->raw_msb;
+	}
+
+	if (msb > 63 || lsb > msb)
+		return PMTCTL_ERR_DEVICE;
+
+	off = (off_t)sample_id * PMT_SAMPLE_SIZE;
+
+	n = pread(m->dev->fd, &raw, sizeof(raw), off);
+	if (n == -1) {
+		log_err_once(-errno, "unable to read telem %s", m->dev->name);
+		return -errno;
+	}
+
+	if (n != (ssize_t)sizeof(raw)) {
+		log_err_once(-EIO, "read expected %zu bytes, got %zd (dev=%s)",
+			     sizeof(raw), n, m->dev->name);
+		return -EIO;
+	}
+
+	if (lsb == 0 && msb == 63) {
+		*val = raw;
+		return 0;
+	}
+
+	width = msb - lsb + 1;
+	if (width >= 64)
+		mask = ~0ULL;
+	else
+		mask = (1ULL << width) - 1ULL;
+
+	*val = (raw >> lsb) & mask;
+
+	return 0;
+}
+
+static struct pmt_device *telem_device_list(int *count)
+{
+	if (!count || !device_initialized)
+		return NULL;
+
+	*count = device_count;
+
+	return devices;
+}
+
+/* ----------------- per-GUID instance tracking ----------------- */
+struct guid_counter {
+	uint32_t guid;
+	int      next_guid_inst;
+};
+
+struct dev_guid_counter {
+	int      dev_index;
+	uint32_t guid;
+	int      next_dev_inst;
+};
+
+static struct guid_counter     *guid_counters;
+static int                      guid_counter_cnt;
+
+static struct dev_guid_counter *dev_guid_counters;
+static int                      dev_guid_counter_cnt;
+/* -------------------------------------------------------------- */
+
+static int next_guid_instance_global(uint32_t guid)
+{
+	struct guid_counter *tmp;
+	int inst = 0;
+	int i;
+
+	for (i = 0; i < guid_counter_cnt; i++) {
+		if (guid_counters[i].guid == guid) {
+			inst = guid_counters[i].next_guid_inst;
+			guid_counters[i].next_guid_inst++;
+			return inst;
+		}
+	}
+
+	/* new guid, start at 0 */
+	tmp = realloc(guid_counters, (guid_counter_cnt + 1) * sizeof(*guid_counters));
+	if (!tmp)
+		return -ENOMEM;
+
+	guid_counters = tmp;
+	guid_counters[guid_counter_cnt].guid = guid;
+	guid_counters[guid_counter_cnt].next_guid_inst = 1;
+	guid_counter_cnt++;
+
+	return inst;
+}
+
+static int next_guid_instance_device(int dev_index, uint32_t guid)
+{
+	struct dev_guid_counter *tmp;
+	int inst = 0;
+	int i;
+
+	for (i = 0; i < dev_guid_counter_cnt; i++) {
+		if (dev_guid_counters[i].dev_index == dev_index &&
+		    dev_guid_counters[i].guid == guid) {
+			inst = dev_guid_counters[i].next_dev_inst;
+			dev_guid_counters[i].next_dev_inst++;
+			return inst;
+		}
+	}
+
+	/* new (dev_index, guid) pair */
+	tmp = realloc(dev_guid_counters, (dev_guid_counter_cnt + 1) * sizeof(*dev_guid_counters));
+	if (!tmp)
+		return -ENOMEM;
+
+	dev_guid_counters = tmp;
+	dev_guid_counters[dev_guid_counter_cnt].dev_index = dev_index;
+	dev_guid_counters[dev_guid_counter_cnt].guid = guid;
+	dev_guid_counters[dev_guid_counter_cnt].next_dev_inst = 1;
+	dev_guid_counter_cnt++;
+
+	return inst;
+}
+
+static int telem_add_device(const char *devpath, const char *devname, int dev_index)
+{
+	struct pmt_device dev = { 0 };
+
+	auto_free char *temp_name = NULL;
+	auto_free char *temp_path = NULL;
+	auto_free char *temp_data_path = NULL;
+	char data_path[PATH_MAX];
+	uint32_t raw_guid;
+	int len;
+	int ret;
+
+	if (!devpath || !devname)
+		return -EINVAL;
+
+	temp_name = xstrdup(devname);
+	temp_path = xstrdup(devpath);
+
+	len = snprintf(data_path, sizeof(data_path), "%s/" PMT_TELEM_DATA_FILE, devpath);
+	if (len < 0 || (size_t)len >= sizeof(data_path))
+		return log_ret(-EINVAL, "path too long for %s", devname);
+
+	temp_data_path = xstrdup(data_path);
+
+	/* Defaults */
+	dev.pkg_id = -1; // not availeble in /sys/class/intel_pmt
+	dev.die_id = -1; // not available in /sys/class/intel_pmt
+
+	/* Get guid */
+	ret = read_u32_hex_attr(devpath, "guid", &raw_guid, PMTCTL_ERR_DEVICE);
+	if (ret)
+		return log_ret(ret, "unable to read guid for %s", devpath);
+
+	dev.guid = pmt_guid_intern(raw_guid);
+	if (!dev.guid)
+		return log_ret(-ENOMEM, "could not intern guid 0x%08x", raw_guid);
+
+	/* Compute instances */
+	ret = next_guid_instance_global(raw_guid);
+	if (ret < 0)
+		return ret;
+
+	dev.guid_inst = ret;
+
+	ret = next_guid_instance_device(dev_index, raw_guid);
+	if (ret < 0)
+		return ret;
+
+	dev.dev_inst = ret;
+
+	/* Don't open telem file yet - defer until first read */
+	dev.fd = -1;
+
+	/* Append to global array */
+	struct pmt_device *tmp;
+
+	tmp = realloc(devices, (device_count + 1) * sizeof(*devices));
+	if (!tmp)
+		return log_ret(-ENOMEM, "could not add telem device %s", devpath);
+
+	dev.name = temp_name;
+	dev.path = temp_path;
+	dev.data_path = temp_data_path;
+
+	temp_name = NULL;
+	temp_path = NULL;
+	temp_data_path = NULL;
+
+	devices = tmp;
+	devices[device_count++] = dev;
+
+	return 0;
+}
+
+static int telem_scan_intel_pmt(void)
+{
+	DIR *aux_dir = opendir(PMT_TELEM_AUX_DIR);
+	struct dirent *aux_de;
+	int dev_index = 0;
+
+	if (!aux_dir) {
+		log_debug("error opening %s: %s", PMT_TELEM_AUX_DIR, strerror(errno));
+		return -errno;
+	}
+
+	while ((aux_de = readdir(aux_dir))) {
+		char intel_pmt_path[PATH_MAX];
+		struct dirent *pmt_de;
+		DIR *pmt_dir;
+		ssize_t len;
+
+		/* skip . and .. */
+		if (aux_de->d_name[0] == '.')
+			continue;
+
+		len = snprintf(intel_pmt_path, sizeof(intel_pmt_path),
+			       "%s/%s/" PMT_TELEM_PMT_DIR,
+			       PMT_TELEM_AUX_DIR, aux_de->d_name);
+
+		if (len < 0 || (size_t)len >= sizeof(intel_pmt_path))
+			continue;
+
+		pmt_dir = opendir(intel_pmt_path);
+		if (!pmt_dir)
+			continue;
+
+		while ((pmt_de = readdir(pmt_dir)) != NULL) {
+			char telem_path[PATH_MAX];
+			int ret;
+
+			/* only telem* directories */
+			if (strncmp(pmt_de->d_name, PMT_TELEM_DATA_FILE,
+				    strlen(PMT_TELEM_DATA_FILE)) != 0)
+				continue;
+
+			len = snprintf(telem_path, sizeof(telem_path),
+				       "%s/%s", intel_pmt_path, pmt_de->d_name);
+			if (len < 0 || (size_t)len >= sizeof(telem_path))
+				continue;
+
+			ret = telem_add_device(telem_path, pmt_de->d_name, dev_index);
+			if (ret)
+				log_warn("unable to add telem device %s (ret=%d)",
+					 telem_path, ret);
+		}
+
+		closedir(pmt_dir);
+		dev_index++;
+	}
+	closedir(aux_dir);
+
+	if (device_count == 0)
+		return PMTCTL_ERR_NOTFOUND;
+
+	return 0;
+}
+
+static int telem_init(void)
+{
+	int ret;
+
+	if (device_initialized)
+		return 0;
+
+	ret = telem_scan_intel_pmt();
+	if (ret)
+		return ret;
+
+	device_initialized = 1;
+
+	return 0;
+}
+
+static void telem_cleanup(void)
+{
+	int i;
+
+	if (!device_initialized)
+		return;
+
+	for (i = 0; i < device_count; i++) {
+		if (devices[i].fd >= 0)
+			close(devices[i].fd);
+
+		free(devices[i].name);
+		free(devices[i].path);
+		free(devices[i].data_path);
+	}
+
+	free(guid_counters);
+	guid_counters = NULL;
+	guid_counter_cnt = 0;
+
+	free(dev_guid_counters);
+	dev_guid_counters = NULL;
+	dev_guid_counter_cnt = 0;
+
+	free(devices);
+	devices = NULL;
+	device_count = 0;
+	device_initialized = 0;
+}
+
+struct pmt_device_ops device_telem_ops = {
+	.dev_type    = PMT_DEVICE_TELEM,
+	.init        = telem_init,
+	.cleanup     = telem_cleanup,
+	.read        = telem_read,
+	.device_list = telem_device_list,
+};
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [PATCH 06/17] tools/arch/x86/pmtctl: Add libpmtctl built-in metric provider
  2026-05-26  1:46 [PATCH 00/17] tools/arch/x86/pmtctl: Add Intel PMT command-line tool David E. Box
                   ` (4 preceding siblings ...)
  2026-05-26  1:47 ` [PATCH 05/17] tools/arch/x86/pmtctl: Add libpmtctl device enumeration backend David E. Box
@ 2026-05-26  1:47 ` David E. Box
  2026-05-26  1:47 ` [PATCH 07/17] tools/arch/x86/pmtctl: Add libpmtctl JSON " David E. Box
                   ` (10 subsequent siblings)
  16 siblings, 0 replies; 25+ messages in thread
From: David E. Box @ 2026-05-26  1:47 UTC (permalink / raw)
  To: linux-kernel, david.e.box, ilpo.jarvinen, andriy.shevchenko,
	platform-driver-x86

Add a unified metric definition provider interface to libpmtctl_core,
giving callers a single entry point for loading PMT metric definitions
regardless of their source.

pmt_metrics_load() abstracts whether definitions come from a built-in
table or an external JSON file. When no path is provided, the built-in
definitions are loaded. When a path is provided, loading is delegated to
the JSON provider when built with HAVE_JANSSON, otherwise
PMTCTL_ERR_UNSUPPORTED is returned.

Provide a stub built-in definition table so the library can build and run
before platform-specific definitions are generated. In that case,
pmt_metrics_load() returns PMTCTL_ERR_NOMETRICS, allowing callers to
report a meaningful error.

Add the remaining build-system integration in a later patch to keep the
library split into logically separate changes.

Assisted-by: GitHub-Copilot:claude-sonnet-4.6
Signed-off-by: David E. Box <david.e.box@linux.intel.com>
---
 .../x86/pmtctl/include/lib/builtin_defs.h     | 14 +++++++
 .../x86/pmtctl/include/lib/metrics_provider.h | 21 ++++++++++
 .../arch/x86/pmtctl/lib/builtin_defs_empty.c  | 13 ++++++
 tools/arch/x86/pmtctl/lib/metrics_provider.c  | 42 +++++++++++++++++++
 4 files changed, 90 insertions(+)
 create mode 100644 tools/arch/x86/pmtctl/include/lib/builtin_defs.h
 create mode 100644 tools/arch/x86/pmtctl/include/lib/metrics_provider.h
 create mode 100644 tools/arch/x86/pmtctl/lib/builtin_defs_empty.c
 create mode 100644 tools/arch/x86/pmtctl/lib/metrics_provider.c

diff --git a/tools/arch/x86/pmtctl/include/lib/builtin_defs.h b/tools/arch/x86/pmtctl/include/lib/builtin_defs.h
new file mode 100644
index 000000000000..432e4d87d0ac
--- /dev/null
+++ b/tools/arch/x86/pmtctl/include/lib/builtin_defs.h
@@ -0,0 +1,14 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef PMTCTL_BUILTIN_DEFS_H
+#define PMTCTL_BUILTIN_DEFS_H
+
+#include "lib/metrics_db.h"
+#include "lib/pmt_guid.h"
+
+extern const struct pmt_metric_def builtin_defs[];
+extern const int builtin_defs_count;
+
+extern const struct pmt_guid builtin_guids[];
+extern const int builtin_guids_count;
+
+#endif
diff --git a/tools/arch/x86/pmtctl/include/lib/metrics_provider.h b/tools/arch/x86/pmtctl/include/lib/metrics_provider.h
new file mode 100644
index 000000000000..efe9bea817c8
--- /dev/null
+++ b/tools/arch/x86/pmtctl/include/lib/metrics_provider.h
@@ -0,0 +1,21 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef PMTCTL_METRICS_PROVIDER_H
+#define PMTCTL_METRICS_PROVIDER_H
+
+#include "lib/metrics_db.h"
+
+/*
+ * Populate @db with metric definitions.
+ *
+ * If @json_path is NULL, the built-in provider is used (definitions linked
+ * into the library via builtin_defs[]). If @json_path is non-NULL, the JSON
+ * provider is used; that provider is only available when the library is
+ * built with HAVE_JANSSON.
+ */
+int pmt_metrics_load(const char *json_path, struct pmt_metrics_db *db);
+
+#ifdef HAVE_JANSSON
+int pmt_metrics_load_json(const char *json_path, struct pmt_metrics_db *db);
+#endif
+
+#endif
diff --git a/tools/arch/x86/pmtctl/lib/builtin_defs_empty.c b/tools/arch/x86/pmtctl/lib/builtin_defs_empty.c
new file mode 100644
index 000000000000..4496d85b230c
--- /dev/null
+++ b/tools/arch/x86/pmtctl/lib/builtin_defs_empty.c
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include "lib/builtin_defs.h"
+
+/*
+ * Empty stub used when no generated/builtin_defs.c is present.
+ * The build system will pick this file unless a generated one exists.
+ */
+
+const struct pmt_guid builtin_guids[1];
+const int builtin_guids_count;
+
+const struct pmt_metric_def builtin_defs[1];
+const int builtin_defs_count;
diff --git a/tools/arch/x86/pmtctl/lib/metrics_provider.c b/tools/arch/x86/pmtctl/lib/metrics_provider.c
new file mode 100644
index 000000000000..6673145d4a9a
--- /dev/null
+++ b/tools/arch/x86/pmtctl/lib/metrics_provider.c
@@ -0,0 +1,42 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#define LOG_PREFIX "metrics_provider"
+#include <string.h>
+
+#include "lib/builtin_defs.h"
+#include "lib/log.h"
+#include "lib/metrics_provider.h"
+
+static int pmt_metrics_load_builtin(struct pmt_metrics_db *db)
+{
+	if (builtin_defs_count == 0)
+		return PMTCTL_ERR_NOMETRICS;
+
+	/*
+	 * Register the compiled-in GUID metadata table so device_telem and
+	 * any runtime providers can resolve raw GUIDs to the same intern'd
+	 * struct pmt_guid pointers that builtin_defs[] already references.
+	 */
+	if (builtin_guids_count > 0)
+		pmt_guid_register(builtin_guids, builtin_guids_count);
+
+	return pmt_metrics_add_block(db, builtin_defs, builtin_defs_count, true);
+}
+
+int pmt_metrics_load(const char *json_path, struct pmt_metrics_db *db)
+{
+	if (!db)
+		log_bug_and_exit("invalid metric db pointer");
+
+	memset(db, 0, sizeof(*db));
+
+	if (!json_path)
+		return pmt_metrics_load_builtin(db);
+
+#ifdef HAVE_JANSSON
+	return pmt_metrics_load_json(json_path, db);
+#else
+	(void)json_path;
+	return log_ret(PMTCTL_ERR_UNSUPPORTED,
+		       "JSON metric definitions requested but library built without jansson support");
+#endif
+}
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [PATCH 07/17] tools/arch/x86/pmtctl: Add libpmtctl JSON metric provider
  2026-05-26  1:46 [PATCH 00/17] tools/arch/x86/pmtctl: Add Intel PMT command-line tool David E. Box
                   ` (5 preceding siblings ...)
  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 ` 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
                   ` (9 subsequent siblings)
  16 siblings, 1 reply; 25+ messages in thread
From: David E. Box @ 2026-05-26  1:47 UTC (permalink / raw)
  To: linux-kernel, david.e.box, ilpo.jarvinen, andriy.shevchenko,
	platform-driver-x86

Add an optional JSON-based metric definition provider to libpmtctl_core,
allowing PMT metric definitions to be supplied at runtime from external
files rather than compiled into the library.

This allows platform-specific metric definitions to be maintained
separately from the kernel tree and updated without rebuilding the library.
The provider accepts either a single JSON file or a directory of JSON
files, loading discovered definitions into the metric database.

When built with HAVE_JANSSON, pmt_metrics_load() dispatches non-NULL path
arguments to the JSON provider. Otherwise, PMTCTL_ERR_UNSUPPORTED is
returned so callers can detect that JSON-backed loading is unavailable.

Build-system wiring is added in a later patch of the series.

Assisted-by: GitHub-Copilot:claude-sonnet-4.6
Signed-off-by: David E. Box <david.e.box@linux.intel.com>
---
 .../x86/pmtctl/lib/metrics_provider_json.c    | 459 ++++++++++++++++++
 1 file changed, 459 insertions(+)
 create mode 100644 tools/arch/x86/pmtctl/lib/metrics_provider_json.c

diff --git a/tools/arch/x86/pmtctl/lib/metrics_provider_json.c b/tools/arch/x86/pmtctl/lib/metrics_provider_json.c
new file mode 100644
index 000000000000..9c0a7e55407d
--- /dev/null
+++ b/tools/arch/x86/pmtctl/lib/metrics_provider_json.c
@@ -0,0 +1,459 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#define LOG_PREFIX "metrics_provider_json"
+#include <dirent.h>
+#include <errno.h>
+#include <jansson.h>
+#include <limits.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+
+#include "lib/log.h"
+#include "lib/metrics_db.h"
+#include "lib/metrics_provider.h"
+
+static bool is_regular(const char *path)
+{
+	struct stat st;
+
+	if (stat(path, &st) < 0)
+		return false;
+
+	return !!S_ISREG(st.st_mode);
+}
+
+static bool is_dir(const char *path)
+{
+	struct stat st;
+
+	if (stat(path, &st) < 0)
+		return false;
+
+	return !!S_ISDIR(st.st_mode);
+}
+
+static const char *pool_copy(char **pool, const char *s)
+{
+	char *dst;
+	size_t len;
+
+	if (!pool || !s)
+		return NULL;
+
+	len = strlen(s) + 1;
+	dst = *pool;
+
+	memcpy(dst, s, len);
+	*pool += len;
+
+	return dst;
+}
+
+/*
+ * Example PMU name: "pmt_ep_3d4bb41a"
+ * Extract GUID = 0x3d4bb41a
+ */
+static uint32_t parse_guid_from_pmu(const char *pmu)
+{
+	unsigned long val;
+	const char *last;
+	const char *p;
+	char *end;
+
+	if (!pmu)
+		return 0;
+
+	last = strrchr(pmu, '_');
+	p = last ? last + 1 : pmu;
+
+	end = NULL;
+	val = strtoul(p, &end, 16);
+
+	if (end == p || val > 0xffffffffUL)
+		return 0;
+
+	return (uint32_t)val;
+}
+
+/*
+ * Decode a perf-style ConfigCode string into (sample_id, lsb, msb).
+ *
+ * Bit layout (matches scripts/gen_builtin_defs.py::decode_config_code()
+ * and scripts/pmtxml2json.py::pack_config()):
+ *
+ *   bits[15:0]   sample_id
+ *   bits[23:16]  lsb
+ *   bits[31:24]  msb
+ *
+ * Returns 0 on success, or -EINVAL if the string is malformed or the
+ * decoded range is out of bounds. A missing ConfigCode (NULL) decodes
+ * to (0, 0, 0) successfully so callers don't need to special-case it.
+ */
+static int parse_config_code(const char *s, uint32_t *sample_id,
+			     uint8_t *lsb, uint8_t *msb)
+{
+	unsigned long val;
+	char *end;
+	uint8_t l, m;
+
+	*sample_id = 0;
+	*lsb = 0;
+	*msb = 0;
+
+	if (!s)
+		return 0;
+
+	end = NULL;
+	val = strtoul(s, &end, 0);
+
+	if (end == s || *end != '\0' || val > 0xffffffffUL)
+		return -EINVAL;
+
+	l = (val >> 16) & 0xff;
+	m = (val >> 24) & 0xff;
+
+	if (m > 63 || l > m)
+		return -EINVAL;
+
+	*sample_id = val & 0xffff;
+	*lsb = l;
+	*msb = m;
+	return 0;
+}
+
+static int json_count_root(json_t *root, size_t *metric_count, size_t *string_bytes)
+{
+	size_t n;
+
+	if (!json_is_array(root))
+		return -EINVAL;
+
+	n = json_array_size(root);
+
+	for (size_t i = 0; i < n; i++) {
+		const char *name, *brief, *group, *platform_group;
+		json_t *ev;
+
+		ev = json_array_get(root, i);
+
+		if (!json_is_object(ev))
+			continue;
+
+		name = json_string_value(json_object_get(ev, "EventName"));
+		brief = json_string_value(json_object_get(ev, "BriefDescription"));
+		group = json_string_value(json_object_get(ev, "MetricGroup"));
+		platform_group = json_string_value(json_object_get(ev, "PlatformGroup"));
+
+		if (name)
+			*string_bytes += strlen(name) + 1;
+		if (brief)
+			*string_bytes += strlen(brief) + 1;
+		if (group)
+			*string_bytes += strlen(group) + 1;
+		if (platform_group)
+			*string_bytes += strlen(platform_group) + 1;
+
+		(*metric_count)++;
+	}
+
+	if (*metric_count == 0)
+		return PMTCTL_ERR_NOMETRICS;
+
+	return 0;
+}
+
+static int json_fill_root(json_t *root, struct pmt_metric_def *defs, char **pool)
+{
+	int idx = 0;
+	size_t n;
+
+	if (!json_is_array(root))
+		return -EINVAL;
+
+	n = json_array_size(root);
+
+	for (size_t i = 0; i < n; i++) {
+		const char *pmu, *name, *brief, *group, *platform_group, *cfg;
+		json_t *ev;
+		struct pmt_metric_def *m;
+		int ret;
+
+		ev = json_array_get(root, i);
+
+		if (!json_is_object(ev))
+			continue;
+
+		m = &defs[idx++];
+
+		pmu = json_string_value(json_object_get(ev, "PMU"));
+		name = json_string_value(json_object_get(ev, "EventName"));
+		brief = json_string_value(json_object_get(ev, "BriefDescription"));
+		group = json_string_value(json_object_get(ev, "MetricGroup"));
+		platform_group = json_string_value(json_object_get(ev, "PlatformGroup"));
+		cfg = json_string_value(json_object_get(ev, "ConfigCode"));
+
+		m->event_name = pool_copy(pool, name);
+		m->description = pool_copy(pool, brief);
+		m->group = pool_copy(pool, group);
+		m->platform_group = pool_copy(pool, platform_group);
+		m->guid = pmt_guid_intern(parse_guid_from_pmu(pmu));
+		if (!m->guid)
+			return -ENOMEM;
+
+		ret = parse_config_code(cfg, &m->sample_id, &m->lsb, &m->msb);
+		if (ret)
+			return log_ret(ret, "metric \"%s\" (PMU %s): invalid ConfigCode \"%s\"",
+				       name ? name : "(null)",
+				       pmu ? pmu : "(null)",
+				       cfg ? cfg : "(null)");
+	}
+
+	return 0;
+}
+
+static struct pmt_metric_def *load_metrics_from_json_single(const char *path, int *count)
+{
+	json_error_t error;
+	json_t *root;
+	struct pmt_metric_def *defs;
+	void *block;
+	char *pool;
+	size_t metric_count = 0;
+	size_t string_bytes = 0;
+	size_t def_bytes;
+	size_t total_bytes;
+	int ret;
+
+	if (!count)
+		return NULL;
+
+	*count = 0;
+
+	root = json_load_file(path, 0, &error);
+	if (!root) {
+		log_err(PMTCTL_ERR_METRICS, "JSON parse error in %s: %s\n", path, error.text);
+		return NULL;
+	}
+
+	ret = json_count_root(root, &metric_count, &string_bytes);
+	if (ret < 0) {
+		json_decref(root);
+		return NULL;
+	}
+
+	def_bytes = metric_count * sizeof(struct pmt_metric_def);
+	total_bytes = def_bytes + string_bytes;
+
+	block = malloc(total_bytes);
+	if (!block) {
+		json_decref(root);
+		return NULL;
+	}
+
+	defs = block;
+	memset(defs, 0, def_bytes);
+	pool = (char *)block + def_bytes;
+
+	ret = json_fill_root(root, defs, &pool);
+	json_decref(root);
+	if (ret < 0) {
+		free(block);
+		return NULL;
+	}
+
+	*count = (int)metric_count;
+
+	return defs;
+}
+
+static int load_pmt_guids_sidecar(const char *path)
+{
+	json_error_t error;
+	json_t *root;
+	struct pmt_guid *table;
+	char *pool;
+	size_t string_bytes = 0;
+	size_t n;
+	int ret = -EINVAL;
+
+	root = json_load_file(path, 0, &error);
+	if (!root) {
+		log_warn("pmt_guids JSON parse error in %s: %s\n", path, error.text);
+		return -EINVAL;
+	}
+
+	if (!json_is_array(root)) {
+		json_decref(root);
+		return -EINVAL;
+	}
+
+	n = json_array_size(root);
+	if (n == 0) {
+		json_decref(root);
+		return 0;
+	}
+
+	for (size_t i = 0; i < n; i++) {
+		json_t *e = json_array_get(root, i);
+		const char *s;
+
+		if (!json_is_object(e))
+			continue;
+		s = json_string_value(json_object_get(e, "name"));
+		if (s)
+			string_bytes += strlen(s) + 1;
+		s = json_string_value(json_object_get(e, "description"));
+		if (s)
+			string_bytes += strlen(s) + 1;
+	}
+
+	table = calloc(1, n * sizeof(*table) + string_bytes);
+	if (!table) {
+		json_decref(root);
+		return -ENOMEM;
+	}
+
+	pool = (char *)table + n * sizeof(*table);
+
+	for (size_t i = 0; i < n; i++) {
+		json_t *e = json_array_get(root, i);
+		const char *guid_s, *name, *desc;
+		unsigned long guid_val;
+		char *end;
+
+		if (!json_is_object(e))
+			continue;
+
+		guid_s = json_string_value(json_object_get(e, "guid"));
+		if (!guid_s)
+			continue;
+
+		errno = 0;
+		guid_val = strtoul(guid_s, &end, 0);
+		if (errno || end == guid_s || *end != '\0' || guid_val > UINT32_MAX)
+			continue;
+
+		name = json_string_value(json_object_get(e, "name"));
+		desc = json_string_value(json_object_get(e, "description"));
+
+		table[i].guid = (uint32_t)guid_val;
+		table[i].name = name ? pool_copy(&pool, name) : NULL;
+		table[i].description = desc ? pool_copy(&pool, desc) : NULL;
+	}
+
+	ret = pmt_guid_register_owned(table, table, (int)n);
+
+	json_decref(root);
+	if (ret < 0)
+		free(table);
+
+	return ret;
+}
+
+static int load_metrics_from_json_dir(const char *dir_path, struct pmt_metrics_db *db)
+{
+	DIR *d = opendir(dir_path);
+	struct dirent *de;
+	char guids_path[PATH_MAX];
+	int len;
+	int ret = 0;
+
+	if (!d)
+		return log_ret(-errno, "could not open %s", dir_path);
+
+	/*
+	 * Load the optional pmt_guids.json sidecar first so any subsequent
+	 * intern calls in json_fill_root() resolve to the registered
+	 * struct pmt_guid entries (with name/description populated).
+	 */
+	len = snprintf(guids_path, sizeof(guids_path), "%s/%s", dir_path, "pmt_guids.json");
+	if (len > 0 && (size_t)len < sizeof(guids_path) && is_regular(guids_path))
+		(void)load_pmt_guids_sidecar(guids_path);
+
+	while ((de = readdir(d)) != NULL) {
+		struct pmt_metric_def *defs;
+		const char *name = de->d_name;
+		const char *dot = strrchr(name, '.');
+		char pathbuf[PATH_MAX];
+		int count = 0;
+
+		if (name[0] == '.')
+			continue;
+
+		if (!dot || strcmp(dot, ".json"))
+			continue;
+
+		/* Sidecar already handled above. */
+		if (!strcmp(name, "pmt_guids.json"))
+			continue;
+
+		if (snprintf(pathbuf, sizeof(pathbuf), "%s/%s", dir_path, name) >=
+		    (int)sizeof(pathbuf))
+			continue;
+		if (!is_regular(pathbuf))
+			continue;
+
+		defs = load_metrics_from_json_single(pathbuf, &count);
+		if (!defs) {
+			log_warn("unable to load JSON %s\n", pathbuf);
+			ret = -EINVAL;
+			continue;
+		}
+
+		ret = pmt_metrics_add_block(db, defs, count, false);
+		if (ret < 0) {
+			free(defs);
+			log_ret(ret, "unable to add json %s", pathbuf);
+			break;
+		}
+	}
+	closedir(d);
+
+	if (ret < 0) {
+		pmt_metrics_free(db);
+		return ret;
+	}
+
+	/*
+	 * Successfully scanned the directory.  A zero count is reported via
+	 * db->total == 0 to the caller; the dir itself was not broken, so
+	 * do not invent an error rc here.  pmtctl_init() distinguishes
+	 * "broken source" (negative rc) from "empty source" (rc == 0 with
+	 * db->total == 0) and only fails the init in the former case.
+	 */
+	return 0;
+}
+
+int pmt_metrics_load_json(const char *json_path, struct pmt_metrics_db *db)
+{
+	int ret;
+
+	if (!db)
+		log_bug_and_exit("invalid metric db pointer");
+
+	if (!json_path)
+		return PMTCTL_ERR_METRICS;
+
+	if (is_regular(json_path)) {
+		struct pmt_metric_def *defs;
+		int count = 0;
+
+		defs = load_metrics_from_json_single(json_path, &count);
+		if (!defs)
+			return log_ret(PMTCTL_ERR_METRICS, "unable to load json %s", json_path);
+
+		ret = pmt_metrics_add_block(db, defs, count, false);
+		if (ret < 0) {
+			free(defs);
+			return log_ret(ret, "unable to add json %s", json_path);
+		}
+
+		return 0;
+	}
+
+	if (is_dir(json_path))
+		return load_metrics_from_json_dir(json_path, db);
+
+	return PMTCTL_ERR_METRICS;
+}
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [PATCH 08/17] tools/arch/x86/pmtctl: Add libpmtctl public API and context
  2026-05-26  1:46 [PATCH 00/17] tools/arch/x86/pmtctl: Add Intel PMT command-line tool David E. Box
                   ` (6 preceding siblings ...)
  2026-05-26  1:47 ` [PATCH 07/17] tools/arch/x86/pmtctl: Add libpmtctl JSON " David E. Box
@ 2026-05-26  1:47 ` David E. Box
  2026-05-26 11:25   ` Ilpo Järvinen
  2026-05-26  1:47 ` [PATCH 09/17] tools/arch/x86/pmtctl: Add libpmtctl Makefile + pc + README David E. Box
                   ` (8 subsequent siblings)
  16 siblings, 1 reply; 25+ messages in thread
From: David E. Box @ 2026-05-26  1:47 UTC (permalink / raw)
  To: linux-kernel, david.e.box, ilpo.jarvinen, andriy.shevchenko,
	platform-driver-x86

Add the public API for libpmtctl_core and the context object that backs it,
providing callers with a stable, opaque interface for discovering Intel PMT
telemetry devices and querying their metrics without depending on internal
library structures.

This patch adds the primary lifecycle and query interfaces used by library
consumers. pmtctl_init() performs device enumeration and metric database
loading once during initialization so subsequent queries operate on cached
state rather than re-scanning sysfs. pmtctl_cleanup() releases associated
resources.

The library context (struct pmtctl_context) remains opaque to callers.
pmtctl_get_ctx() provides controlled access for consumers that need to pass
library context across translation units.

Add PMTCTL_SCOPE_GUARD() as a convenience macro for deterministic cleanup
of library-managed resources in single-exit C code paths.

Build-system wiring is added a later patch of the series.

Assisted-by: GitHub-Copilot:claude-sonnet-4.6
Signed-off-by: David E. Box <david.e.box@linux.intel.com>
---
 tools/arch/x86/pmtctl/include/lib/pmtctl.h    |  90 +++++
 .../x86/pmtctl/include/lib/pmtctl_context.h   |  21 ++
 tools/arch/x86/pmtctl/lib/pmtctl.c            | 327 ++++++++++++++++++
 3 files changed, 438 insertions(+)
 create mode 100644 tools/arch/x86/pmtctl/include/lib/pmtctl.h
 create mode 100644 tools/arch/x86/pmtctl/include/lib/pmtctl_context.h
 create mode 100644 tools/arch/x86/pmtctl/lib/pmtctl.c

diff --git a/tools/arch/x86/pmtctl/include/lib/pmtctl.h b/tools/arch/x86/pmtctl/include/lib/pmtctl.h
new file mode 100644
index 000000000000..b243a48e8d72
--- /dev/null
+++ b/tools/arch/x86/pmtctl/include/lib/pmtctl.h
@@ -0,0 +1,90 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef PMTCTL_H
+#define PMTCTL_H
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include <linux/compiler.h>
+#include "pmtctl_types.h"
+
+enum pmt_selector_kind {
+	PMT_SEL_ANY = 0,   /* no selector provided */
+	PMT_SEL_GUID,      /* guid=27971628 */
+	PMT_SEL_EP_NAME,   /* ep=pmt_ep_27971628_0 OR ep=telem1 */
+};
+
+struct pmt_ep_selector {
+	enum pmt_selector_kind kind;
+
+	uint32_t guid;
+
+	/* For SEL_EP_NAME */
+	const char *str;
+};
+
+struct pmt_binding {
+	int metric_idx;   /* index into defs[] */
+	int device_idx;   /* index into devices[] */
+};
+
+struct pmtctl_context;
+
+struct pmt_global_opts {
+	const char            *json_path;        /* -J / --json-file */
+	const char            *device_selector;  /* -d / --device (raw) */
+	bool                   quiet;            /* -q / --quiet */
+	bool                   debug;            /* --debug */
+};
+
+/*
+ * Initialize library-global PMT state.
+ *
+ * This may return success even if no metric definitions are available.
+ * In that case, device enumeration is still usable and raw mode remains
+ * supported (raw reads do not require metric definitions).
+ */
+int pmtctl_init(const struct pmt_global_opts *gopts);
+const struct pmtctl_context *pmtctl_get_ctx(void);
+enum pmt_device_type pmtctl_get_device_type(void);
+int pmtctl_get_num_devices(void);
+int pmtctl_get_num_metrics(void);
+int pmtctl_get_num_bindings(void);
+
+/*
+ * Set process-global library logging verbosity.
+ *
+ * Invalid values are clamped to PMTCTL_LOG_INFO.
+ */
+void pmtctl_set_log_level(enum pmtctl_log_level level);
+
+/*
+ * Thread-safety note:
+ *
+ * libpmtctl_core is generally not thread-safe. Callers should serialize
+ * pmtctl_init()/pmtctl_cleanup() and API usage around shared library state.
+ */
+void pmtctl_cleanup(void);
+
+static inline __always_unused void pmtctl_scope_cleanup(int *unused)
+{
+	(void)unused;
+	pmtctl_cleanup();
+}
+
+#define PMTCTL_SCOPE_GUARD                              \
+	__attribute__((cleanup(pmtctl_scope_cleanup)))  \
+	int _pmtctl_scope_guard __always_unused \
+
+int pmt_select_devices(const struct pmtctl_context *ctx, const struct pmt_ep_selector *sel,
+		       int *out_idx, int max_out);
+
+/*
+ * Parse an ep selector string like "guid=27971628" or "ep=pmt_ep_...".
+ * For selectors that set a string (ep/name) the implementation will
+ * allocate a copy into sel->str; the caller is responsible for freeing
+ * sel->str if non-NULL.
+ */
+int pmtctl_parse_ep_selector(const char *s, struct pmt_ep_selector *out);
+
+#endif
diff --git a/tools/arch/x86/pmtctl/include/lib/pmtctl_context.h b/tools/arch/x86/pmtctl/include/lib/pmtctl_context.h
new file mode 100644
index 000000000000..6f3e8563f7eb
--- /dev/null
+++ b/tools/arch/x86/pmtctl/include/lib/pmtctl_context.h
@@ -0,0 +1,21 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef PMTCTL_CONTEXT_H
+#define PMTCTL_CONTEXT_H
+
+#include "lib/device.h"
+#include "lib/metrics_db.h"
+#include "lib/pmtctl.h"
+
+struct pmtctl_context {
+	const struct pmt_device_ops *ops;
+
+	struct pmt_device *devices;
+	int num_devices;
+
+	struct pmt_metrics_db metrics;
+
+	struct pmt_binding *bindings;
+	int num_bindings;
+};
+
+#endif
diff --git a/tools/arch/x86/pmtctl/lib/pmtctl.c b/tools/arch/x86/pmtctl/lib/pmtctl.c
new file mode 100644
index 000000000000..5ee2f576316a
--- /dev/null
+++ b/tools/arch/x86/pmtctl/lib/pmtctl.c
@@ -0,0 +1,327 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <errno.h>
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "lib/common.h"
+#include "lib/device.h"
+#include "lib/log.h"
+#include "lib/metrics_db.h"
+#include "lib/metrics_provider.h"
+#include "lib/pmtctl.h"
+#include "lib/pmtctl_context.h"
+
+static struct pmtctl_context g_pmtctl_ctx;
+
+const struct pmtctl_context *pmtctl_get_ctx(void)
+{
+	return &g_pmtctl_ctx;
+}
+
+enum pmt_device_type pmtctl_get_device_type(void)
+{
+	const struct pmt_device_ops *ops = g_pmtctl_ctx.ops;
+
+	return ops ? ops->dev_type : PMT_DEVICE_TELEM;
+}
+
+int pmtctl_get_num_devices(void)
+{
+	return g_pmtctl_ctx.num_devices;
+}
+
+int pmtctl_get_num_metrics(void)
+{
+	return g_pmtctl_ctx.metrics.total;
+}
+
+int pmtctl_get_num_bindings(void)
+{
+	return g_pmtctl_ctx.num_bindings;
+}
+
+void pmtctl_set_log_level(enum pmtctl_log_level level)
+{
+	log_set_level(level);
+}
+
+int pmt_select_devices(const struct pmtctl_context *ctx,
+		       const struct pmt_ep_selector *sel,
+		       int *out_idx, int max_out)
+{
+	int i, n = 0;
+
+	if (!ctx || !sel || !out_idx || max_out <= 0)
+		return log_ret(PMTCTL_ERR_INVALID, "bad argument");
+
+	if (!ctx->devices || ctx->num_devices <= 0)
+		return log_ret(PMTCTL_ERR_INVALID, "no devices");
+
+	switch (sel->kind) {
+	case PMT_SEL_ANY:
+		/* No filter: return all devices */
+		for (i = 0; i < ctx->num_devices && n < max_out; i++)
+			out_idx[n++] = i;
+		break;
+
+	case PMT_SEL_GUID:
+		for (i = 0; i < ctx->num_devices && n < max_out; i++) {
+			const struct pmt_device *dev = &ctx->devices[i];
+
+			if (dev->guid && dev->guid->guid == sel->guid)
+				out_idx[n++] = i;
+		}
+		break;
+
+	case PMT_SEL_EP_NAME:
+		if (!sel->str || !sel->str[0])
+			return log_ret(PMTCTL_ERR_CMD_PARSE, "empty ep selector");
+
+		for (i = 0; i < ctx->num_devices && n < max_out; i++) {
+			const struct pmt_device *dev = &ctx->devices[i];
+
+			if (dev->name && strcmp(dev->name, sel->str) == 0)
+				out_idx[n++] = i;
+		}
+		break;
+
+	default:
+		return log_ret(PMTCTL_ERR_CMD_PARSE, "unknown selector kind %d", sel->kind);
+	}
+
+	/* n == 0 is "no matches", not an API error. Caller decides what to do. */
+	return n;
+}
+
+int pmtctl_parse_ep_selector(const char *s, struct pmt_ep_selector *out)
+{
+	auto_free char *copy = NULL;
+	char *end;
+	char *eq;
+	char *key = NULL;
+	const char *val = NULL;
+
+	if (!out)
+		return -EINVAL;
+
+	if (!s || !*s) {
+		out->kind = PMT_SEL_ANY;
+		out->str = NULL;
+		out->guid = 0;
+		return 0;
+	}
+
+	/* Expect a single key=value selector: guid=..., ep=..., name=... */
+	copy = strdup(s);
+
+	if (!copy)
+		return -ENOMEM;
+
+	eq = strchr(copy, '=');
+	if (!eq)
+		return -EINVAL;
+
+	*eq = '\0';
+	key = copy;
+	val = eq + 1;
+
+	if (!strcmp(key, "guid")) {
+		errno = 0;
+		unsigned long v = strtoul(val, &end, 16);
+
+		if (errno || end == val || *end != '\0' || v > UINT32_MAX)
+			return errno ? -errno : -EINVAL;
+		out->kind = PMT_SEL_GUID;
+		out->guid = (uint32_t)v;
+		out->str = NULL;
+	} else if (!strcmp(key, "ep") || !strcmp(key, "name")) {
+		out->kind = PMT_SEL_EP_NAME;
+		out->str = strdup(val);
+		if (!out->str)
+			return -ENOMEM;
+		out->guid = 0;
+	} else {
+		log_err(PMTCTL_ERR_CMD_PARSE, "unknown device selector %s", key);
+		return -EINVAL;
+	}
+	return 0;
+}
+
+static bool
+metric_matches_device(const struct pmt_metric_def *def, const struct pmt_device *dev)
+{
+	/*
+	 * Compare the underlying numeric GUID rather than the pmt_guid pointer:
+	 * builtin metric defs reference &builtin_guids[idx] directly, while
+	 * devices intern through the registry. These may resolve to different
+	 * pmt_guid entries for the same GUID depending on init order.
+	 */
+	return def->guid->guid == dev->guid->guid;
+}
+
+static int pmt_bind_build(struct pmtctl_context *ctx)
+{
+	struct pmt_binding *bindings;
+	size_t count = 0;
+	int i, j, k;
+
+	/*
+	 * -----------------------------
+	 * Pass 1: count bindings
+	 * -----------------------------
+	 */
+	for (i = 0; i < ctx->metrics.total; i++) {
+		const struct pmt_metric_def *def = pmt_metrics_at(&ctx->metrics, i);
+
+		if (!def)
+			continue;
+
+		for (j = 0; j < ctx->num_devices; j++) {
+			const struct pmt_device *dev = &ctx->devices[j];
+
+			if (!metric_matches_device(def, dev))
+				continue;
+
+			count++;
+		}
+	}
+
+	if (count == 0) {
+		log_warn("no metric/device bindings found");
+		return 0;
+	}
+
+	bindings = calloc(count, sizeof(*bindings));
+	if (!bindings)
+		return log_ret(-ENOMEM, "count not allocate bindings");
+
+	/*
+	 * -----------------------------
+	 * Pass 2: fill bindings
+	 * -----------------------------
+	 */
+	k = 0;
+	for (i = 0; i < ctx->metrics.total; i++) {
+		const struct pmt_metric_def *def = pmt_metrics_at(&ctx->metrics, i);
+
+		if (!def)
+			continue;
+
+		for (j = 0; j < ctx->num_devices; j++) {
+			const struct pmt_device *dev = &ctx->devices[j];
+
+			if (!metric_matches_device(def, dev))
+				continue;
+
+			bindings[k].metric_idx = i;
+			bindings[k].device_idx = j;
+			k++;
+		}
+	}
+
+	ctx->bindings     = bindings;
+	ctx->num_bindings = k;
+
+	return 0;
+}
+
+int pmtctl_init(const struct pmt_global_opts *gopts)
+{
+	const struct pmt_device_ops *ops = NULL;
+	int num_devices;
+	int ret;
+
+	if (!gopts)
+		return log_ret(PMTCTL_ERR_INVALID, "bad argument");
+
+	memset(&g_pmtctl_ctx, 0, sizeof(g_pmtctl_ctx));
+
+	/*
+	 * 1) Initialize device backend.
+	 *    Only the telem backend is currently supported; PMU support is
+	 *    pending upstream driver availability.
+	 */
+	ret = device_telem_ops.init();
+	if (ret != 0)
+		return log_ret(ret, "failed to find PMT source");
+	ops = &device_telem_ops;
+	log_debug("Selecting from /sys/class/intel_pmt");
+	g_pmtctl_ctx.ops = ops;
+
+	/*
+	 * 2) Enumerate devices from the chosen device
+	 */
+	g_pmtctl_ctx.devices = ops->device_list(&num_devices);
+	if (!g_pmtctl_ctx.devices) {
+		pmtctl_cleanup();
+		log_bug_and_exit("unexpected NULL device context");
+	}
+
+	if (num_devices <= 0) {
+		pmtctl_cleanup();
+		log_bug_and_exit("unexpected zero device count");
+	}
+	g_pmtctl_ctx.num_devices = num_devices;
+
+	/*
+	 * 3) Load metric definitions from JSON or built-in
+	 *
+	 * If metric load fails or returns zero metrics, we intentionally keep init as a
+	 * degraded success (return 0) with a warning. This allows raw mode operation,
+	 * which does not require metric definitions.
+	 */
+	ret = pmt_metrics_load(gopts->json_path, &g_pmtctl_ctx.metrics);
+	/*
+	 * Any nonzero rc here means the metric source itself was broken
+	 * (e.g. -J pointed at a nonexistent path, or a JSON file failed to
+	 * parse).  Treat that as a hard init failure so the CLI exits with
+	 * PMTCTL_EXIT_SYSTEM instead of silently degrading to a metric-less
+	 * session.  Empty source (ret == 0 but metrics.total == 0) stays a
+	 * degraded success -- raw mode and `list --devices` are still usable.
+	 *
+	 * Some provider paths return positive PMTCTL_ERR_* codes; normalize
+	 * those to -EIO so main()'s mapping selects EXIT_SYSTEM.
+	 */
+	if (ret != 0) {
+		pmtctl_cleanup();
+		if (ret > 0)
+			ret = -EIO;
+		return log_ret(ret, "failed to load metrics from %s",
+			       gopts->json_path ? gopts->json_path : "<built-in>");
+	}
+	if (g_pmtctl_ctx.metrics.total == 0) {
+		log_warn("no metrics from %s", gopts->json_path ? gopts->json_path : "<built-in>");
+		return 0;
+	}
+
+	/*
+	 * 4) Build metric ↔ device bindings
+	 */
+	ret = pmt_bind_build(&g_pmtctl_ctx);
+	if (ret != 0) {
+		pmtctl_cleanup();
+		return ret;
+	}
+
+	return 0;
+}
+
+void pmtctl_cleanup(void)
+{
+	if (g_pmtctl_ctx.bindings) {
+		free(g_pmtctl_ctx.bindings);
+		g_pmtctl_ctx.bindings = NULL;
+		g_pmtctl_ctx.num_bindings = 0;
+	}
+
+	if (g_pmtctl_ctx.ops && g_pmtctl_ctx.ops->cleanup)
+		g_pmtctl_ctx.ops->cleanup();
+
+	pmt_metrics_free(&g_pmtctl_ctx.metrics);
+	pmt_guid_cleanup();
+
+	g_pmtctl_ctx.ops         = NULL;
+	g_pmtctl_ctx.devices     = NULL;
+	g_pmtctl_ctx.num_devices = 0;
+}
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [PATCH 09/17] tools/arch/x86/pmtctl: Add libpmtctl Makefile + pc + README
  2026-05-26  1:46 [PATCH 00/17] tools/arch/x86/pmtctl: Add Intel PMT command-line tool David E. Box
                   ` (7 preceding siblings ...)
  2026-05-26  1:47 ` [PATCH 08/17] tools/arch/x86/pmtctl: Add libpmtctl public API and context David E. Box
@ 2026-05-26  1:47 ` David E. Box
  2026-05-26  1:47 ` [PATCH 10/17] tools/arch/x86/pmtctl: Add libpmtctl usage sample David E. Box
                   ` (7 subsequent siblings)
  16 siblings, 0 replies; 25+ messages in thread
From: David E. Box @ 2026-05-26  1:47 UTC (permalink / raw)
  To: linux-kernel, david.e.box, ilpo.jarvinen, andriy.shevchenko,
	platform-driver-x86

Add build infrastructure for libpmtctl_core, allowing the library to be
built standalone and consumed by downstream tools via pkg-config.

Provide separate release and debug build variants so optimized and
development builds can coexist within the same checkout without
overwriting each other.

JSON metric definition support is optional at build time. When built with
HAVE_JANSSON, the JSON metric provider is enabled and linked against
libjansson. Otherwise, JSON-backed metric loading remains unavailable and
pmt_metrics_load() returns PMTCTL_ERR_UNSUPPORTED for non-NULL path
arguments.

Add a pkg-config template for discovering the library's compile and link
flags, along with a README describing the library purpose and build
options.

Assisted-by: GitHub-Copilot:claude-sonnet-4.6
Signed-off-by: David E. Box <david.e.box@linux.intel.com>
---
 tools/arch/x86/pmtctl/lib/Makefile         | 108 +++++++++++++++++++
 tools/arch/x86/pmtctl/lib/README           | 116 +++++++++++++++++++++
 tools/arch/x86/pmtctl/libpmtctl-core.pc.in |  11 ++
 3 files changed, 235 insertions(+)
 create mode 100644 tools/arch/x86/pmtctl/lib/Makefile
 create mode 100644 tools/arch/x86/pmtctl/lib/README
 create mode 100644 tools/arch/x86/pmtctl/libpmtctl-core.pc.in

diff --git a/tools/arch/x86/pmtctl/lib/Makefile b/tools/arch/x86/pmtctl/lib/Makefile
new file mode 100644
index 000000000000..e2aeff1935cf
--- /dev/null
+++ b/tools/arch/x86/pmtctl/lib/Makefile
@@ -0,0 +1,108 @@
+# SPDX-License-Identifier: GPL-2.0-only
+
+CC      := gcc
+AR      := ar
+RANLIB  := ranlib
+
+BUILD ?= release
+
+CPPFLAGS += -I../include -I../../../../include -D_GNU_SOURCE
+CFLAGS   += -std=gnu11 -Wall -Wextra
+
+WEXTRA_WARN := -Wpedantic -Wmissing-prototypes -Wstrict-prototypes \
+               -Wunused-function -Wunused-variable -Wunused-parameter \
+               -Wunused-but-set-variable -Wunreachable-code
+
+ifeq ($(BUILD),debug)
+  CFLAGS += -O0 -g3 -fno-omit-frame-pointer
+  CFLAGS += $(WEXTRA_WARN)
+  CPPFLAGS += -DDEBUG
+else ifeq ($(BUILD),release)
+  CFLAGS += -O2 -g0 -DNDEBUG
+else
+  $(error unknown BUILD '$(BUILD)' (use release or debug))
+endif
+
+BUILDDIR := ../build/$(BUILD)/lib
+CORE_TARGET := $(BUILDDIR)/libpmtctl_core.a
+
+CORE_SRC := \
+	log.c \
+	common.c \
+	device_telem.c \
+	metrics_db.c \
+	pmt_guid.c \
+	pmtctl.c \
+	metrics_provider.c \
+	builtin_defs_empty.c
+
+HAVE_JANSSON ?= 0
+ifeq ($(HAVE_JANSSON),1)
+  CORE_SRC  += metrics_provider_json.c
+  CPPFLAGS  += -DHAVE_JANSSON
+  LDLIBS    += -ljansson
+endif
+
+CORE_OBJ := $(patsubst %.c,$(BUILDDIR)/%.o,$(CORE_SRC))
+
+PREFIX ?= /usr/local
+DESTDIR ?=
+INCLUDEDIR ?= $(PREFIX)/include/pmtctl
+LIBINSTALLDIR ?= $(PREFIX)/lib
+PKGCONFIGDIR ?= $(LIBINSTALLDIR)/pkgconfig
+
+PC_CORE := ../libpmtctl-core.pc
+PC_FILES := $(PC_CORE)
+
+PUBLIC_HEADERS := \
+	../include/lib/pmtctl.h \
+	../include/lib/metrics_db.h \
+	../include/lib/pmt_guid.h \
+	../include/lib/device.h
+
+.PHONY: all clean install install-lib install-headers install-pkgconfig uninstall uninstall-lib uninstall-headers uninstall-pkgconfig
+
+all: $(CORE_TARGET)
+
+$(CORE_TARGET): $(CORE_OBJ)
+	$(AR) rcs $@ $(CORE_OBJ)
+	$(RANLIB) $@
+
+$(PC_CORE): ../libpmtctl-core.pc.in
+	@sed -e 's|@PREFIX@|$(PREFIX)|g' \
+	     -e 's|@JANSSON_LIBS@|$(if $(filter 1,$(HAVE_JANSSON)), -ljansson,)|g' \
+	     $< > $@
+
+$(BUILDDIR)/%.o: %.c
+	@mkdir -p $(BUILDDIR)
+	$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
+
+install: all install-lib install-headers install-pkgconfig
+
+install-lib: all
+	install -d $(DESTDIR)$(LIBINSTALLDIR)
+	install -m 0644 $(CORE_TARGET) $(DESTDIR)$(LIBINSTALLDIR)
+
+install-headers:
+	install -d $(DESTDIR)$(INCLUDEDIR)
+	install -m 0644 $(PUBLIC_HEADERS) $(DESTDIR)$(INCLUDEDIR)
+
+install-pkgconfig: $(PC_FILES)
+	install -d $(DESTDIR)$(PKGCONFIGDIR)
+	install -m 0644 $(PC_FILES) $(DESTDIR)$(PKGCONFIGDIR)
+
+uninstall: uninstall-lib uninstall-headers uninstall-pkgconfig
+
+uninstall-lib:
+	rm -f $(DESTDIR)$(LIBINSTALLDIR)/libpmtctl_core.a
+
+uninstall-headers:
+	rm -f $(DESTDIR)$(INCLUDEDIR)/pmtctl.h
+	rm -f $(DESTDIR)$(INCLUDEDIR)/metrics_db.h
+	rm -f $(DESTDIR)$(INCLUDEDIR)/device.h
+
+uninstall-pkgconfig:
+	rm -f $(DESTDIR)$(PKGCONFIGDIR)/libpmtctl-core.pc
+
+clean:
+	rm -rf $(BUILDDIR) $(PC_FILES)
diff --git a/tools/arch/x86/pmtctl/lib/README b/tools/arch/x86/pmtctl/lib/README
new file mode 100644
index 000000000000..60f7de8c2256
--- /dev/null
+++ b/tools/arch/x86/pmtctl/lib/README
@@ -0,0 +1,116 @@
+libpmtctl_core usage notes
+==========================
+
+Overview
+--------
+libpmtctl_core provides PMT device enumeration, metric loading, and
+metric-to-device binding services used by pmtctl and other PMT tools.
+
+Public headers installed by lib/Makefile:
+- include/lib/pmtctl.h
+- include/lib/metrics_db.h
+- include/lib/device.h
+
+Typical flow
+------------
+1) Prepare options (struct pmt_global_opts).
+2) Optionally set log verbosity with pmtctl_set_log_level().
+3) Call pmtctl_init(&opts).
+4) Query context/counts and perform selection/reads through public APIs.
+5) Call pmtctl_cleanup() before exit.
+
+API reference
+-------------
+Initialization and lifecycle:
+- int pmtctl_init(const struct pmt_global_opts *gopts)
+	Initializes library-global PMT context. Enumerates devices,
+	and loads metric definitions. Returns 0 on success or a non-zero error code.
+
+- void pmtctl_cleanup(void)
+	Releases library-global resources allocated by pmtctl_init(). Safe to call
+	as part of normal shutdown after successful or partially successful init.
+
+- PMTCTL_SCOPE_GUARD
+	Cleanup helper macro that arranges pmtctl_cleanup() to run automatically
+	when the current scope exits.
+
+Context and counts:
+- const struct pmtctl_context *pmtctl_get_ctx(void)
+	Returns pointer to current library-global context.
+
+- enum pmt_device_type pmtctl_get_device_type(void)
+	Returns selected backend/device type for current context.
+
+- int pmtctl_get_num_devices(void)
+	Returns number of enumerated devices in current context.
+
+- int pmtctl_get_num_metrics(void)
+	Returns number of loaded metric definitions.
+
+- int pmtctl_get_num_bindings(void)
+	Returns number of metric-to-device bindings currently available.
+
+Logging:
+- void pmtctl_set_log_level(enum pmtctl_log_level level)
+	Sets process-global logging verbosity used by libpmtctl_core.
+	Clamp behavior: invalid/out-of-range values are clamped to
+	PMTCTL_LOG_INFO.
+
+Device selection helpers:
+- int pmtctl_parse_ep_selector(const char *s,
+                              struct pmt_ep_selector *out)
+       Parses a selector string (for example: guid=27971628, ep=pmt_ep_...)
+       into structured selector fields.
+
+- int pmt_select_devices(const struct pmtctl_context *ctx,
+                        const struct pmt_ep_selector *sel,
+                        int *out_idx, int max_out)
+	Filters context devices using selector criteria and writes matching device
+	indexes into out_idx. Returns number of matches (0 means no matches).
+
+Log level behavior
+------------------
+pmtctl_set_log_level(enum pmtctl_log_level level)
+- Accepted levels:
+  PMTCTL_LOG_ERROR
+  PMTCTL_LOG_WARN
+  PMTCTL_LOG_INFO
+  PMTCTL_LOG_DEBUG
+- Clamp policy:
+  Invalid/out-of-range values are clamped to PMTCTL_LOG_INFO.
+
+Thread-safety
+-------------
+libpmtctl_core is generally not thread-safe.
+
+In particular, library-global state is used for:
+- PMT context/session ownership
+- logging verbosity
+
+Callers should serialize calls that operate on shared library state,
+including pmtctl_init(), pmtctl_cleanup(), and other APIs that consume
+or mutate global context.
+
+Minimal example
+---------------
+#include "lib/pmtctl.h"
+
+int run(void)
+{
+	struct pmt_global_opts opts = {
+		.json_path = NULL,
+		.device_selector = NULL,
+		.quiet = false,
+		.debug = false,
+	};
+
+	pmtctl_set_log_level(PMTCTL_LOG_INFO);
+
+	if (pmtctl_init(&opts) != 0)
+		return -1;
+
+	/* ... use library APIs ... */
+
+	pmtctl_cleanup();
+	return 0;
+}
diff --git a/tools/arch/x86/pmtctl/libpmtctl-core.pc.in b/tools/arch/x86/pmtctl/libpmtctl-core.pc.in
new file mode 100644
index 000000000000..0cbc9127d686
--- /dev/null
+++ b/tools/arch/x86/pmtctl/libpmtctl-core.pc.in
@@ -0,0 +1,11 @@
+# SPDX-License-Identifier: GPL-2.0-only
+prefix=@PREFIX@
+exec_prefix=${prefix}
+libdir=${exec_prefix}/lib
+includedir=${prefix}/include
+
+Name: libpmtctl-core
+Description: Intel PMT core access library (transport/enumeration/bindings)
+Version: 0.1
+Libs: -L${libdir} -lpmtctl_core @JANSSON_LIBS@
+Cflags: -I${includedir}/pmtctl
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [PATCH 10/17] tools/arch/x86/pmtctl: Add libpmtctl usage sample
  2026-05-26  1:46 [PATCH 00/17] tools/arch/x86/pmtctl: Add Intel PMT command-line tool David E. Box
                   ` (8 preceding siblings ...)
  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 ` 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
                   ` (6 subsequent siblings)
  16 siblings, 0 replies; 25+ messages in thread
From: David E. Box @ 2026-05-26  1:47 UTC (permalink / raw)
  To: linux-kernel, david.e.box, ilpo.jarvinen, andriy.shevchenko,
	platform-driver-x86

Add a small sample program under samples/ that demonstrates how an
out-of-tree consumer links against libpmtctl, plus the corresponding
'sample' Makefile target.

Assisted-by: GitHub-Copilot:claude-opus-4.7
Signed-off-by: David E. Box <david.e.box@linux.intel.com>
---
 .../x86/pmtctl/samples/libpmtctl_sample.c     | 30 +++++++++++++++++++
 1 file changed, 30 insertions(+)
 create mode 100644 tools/arch/x86/pmtctl/samples/libpmtctl_sample.c

diff --git a/tools/arch/x86/pmtctl/samples/libpmtctl_sample.c b/tools/arch/x86/pmtctl/samples/libpmtctl_sample.c
new file mode 100644
index 000000000000..88a12d5c9ddb
--- /dev/null
+++ b/tools/arch/x86/pmtctl/samples/libpmtctl_sample.c
@@ -0,0 +1,30 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <stdio.h>
+
+#include "pmtctl.h"
+
+int main(void)
+{
+	struct pmt_global_opts opts = {
+		.json_path = NULL,
+		.device_selector = NULL,
+		.quiet = false,
+		.debug = false,
+	};
+	int ret;
+
+	ret = pmtctl_init(&opts);
+	if (ret != 0) {
+		fprintf(stderr, "pmtctl_init failed: %d\n", ret);
+		return 1;
+	}
+
+	printf("devices=%d metrics=%d bindings=%d\n",
+	       pmtctl_get_num_devices(),
+	       pmtctl_get_num_metrics(),
+	       pmtctl_get_num_bindings());
+
+	pmtctl_cleanup();
+
+	return 0;
+}
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [PATCH 11/17] tools/arch/x86/pmtctl: Add libpmtctl built-in metric definition support
  2026-05-26  1:46 [PATCH 00/17] tools/arch/x86/pmtctl: Add Intel PMT command-line tool David E. Box
                   ` (9 preceding siblings ...)
  2026-05-26  1:47 ` [PATCH 10/17] tools/arch/x86/pmtctl: Add libpmtctl usage sample David E. Box
@ 2026-05-26  1:47 ` 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
                   ` (5 subsequent siblings)
  16 siblings, 0 replies; 25+ messages in thread
From: David E. Box @ 2026-05-26  1:47 UTC (permalink / raw)
  To: linux-kernel, david.e.box, ilpo.jarvinen, andriy.shevchenko,
	platform-driver-x86

Add support for compiling PMT metric definitions directly into
libpmtctl_core so tools can operate without requiring external JSON
definition files at runtime.

Built-in definitions avoid a runtime dependency on host filesystem data by
embedding generated metric definition tables directly into the library.
When no path argument is provided, pmt_metrics_load() serves definitions
from the built-in table.

Add a Python generator script to convert defs/*.json into a generated C
definition table used by the library build. An empty stub table remains
available for builds where generation is skipped, preserving the existing
runtime contract where pmt_metrics_load() returns PMTCTL_ERR_NOMETRICS when
no definitions are present.

Add a top-level 'make defs' target and update the README and CLI
documentation to describe the built-in definition workflow.

Assisted-by: GitHub-Copilot:claude-sonnet-4.6
Signed-off-by: David E. Box <david.e.box@linux.intel.com>
---
 tools/arch/x86/pmtctl/lib/Makefile            |  34 +-
 .../x86/pmtctl/scripts/gen_builtin_defs.py    | 405 ++++++++++++++++++
 2 files changed, 437 insertions(+), 2 deletions(-)
 create mode 100755 tools/arch/x86/pmtctl/scripts/gen_builtin_defs.py

diff --git a/tools/arch/x86/pmtctl/lib/Makefile b/tools/arch/x86/pmtctl/lib/Makefile
index e2aeff1935cf..2dbd4e5b95a0 100644
--- a/tools/arch/x86/pmtctl/lib/Makefile
+++ b/tools/arch/x86/pmtctl/lib/Makefile
@@ -1,5 +1,9 @@
 # SPDX-License-Identifier: GPL-2.0-only
 
+# Remove targets whose recipe exited non-zero so a failed codegen step
+# does not leave a truncated $@ behind that fools the next build.
+.DELETE_ON_ERROR:
+
 CC      := gcc
 AR      := ar
 RANLIB  := ranlib
@@ -33,8 +37,7 @@ CORE_SRC := \
 	metrics_db.c \
 	pmt_guid.c \
 	pmtctl.c \
-	metrics_provider.c \
-	builtin_defs_empty.c
+	metrics_provider.c
 
 HAVE_JANSSON ?= 0
 ifeq ($(HAVE_JANSSON),1)
@@ -45,6 +48,17 @@ endif
 
 CORE_OBJ := $(patsubst %.c,$(BUILDDIR)/%.o,$(CORE_SRC))
 
+# Built-in metric definitions: use the generated table when available,
+# otherwise fall back to the empty stub. Both sources export builtin_defs[]
+# and builtin_defs_count and are compiled to a fixed object name so the
+# choice is transparent to the rest of the library.
+# Use 'find' (recursive) to match the top-level Makefile; $(wildcard) would
+# silently miss any JSON files placed in defs/ subdirectories.
+DEFS_JSON ?= $(shell find ../defs -name '*.json' 2>/dev/null)
+BUILTIN_DEFS_SRC := $(if $(wildcard ../generated/builtin_defs.c),../generated/builtin_defs.c,builtin_defs_empty.c)
+BUILTIN_DEFS_OBJ := $(BUILDDIR)/builtin_defs.o
+CORE_OBJ += $(BUILTIN_DEFS_OBJ)
+
 PREFIX ?= /usr/local
 DESTDIR ?=
 INCLUDEDIR ?= $(PREFIX)/include/pmtctl
@@ -77,6 +91,22 @@ $(BUILDDIR)/%.o: %.c
 	@mkdir -p $(BUILDDIR)
 	$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
 
+$(BUILTIN_DEFS_OBJ): $(BUILTIN_DEFS_SRC)
+	@mkdir -p $(BUILDDIR)
+	$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
+
+../generated/builtin_defs.c: ../scripts/gen_builtin_defs.py $(DEFS_JSON)
+	@mkdir -p ../generated
+	@if [ -z "$(DEFS_JSON)" ]; then \
+		echo "No JSON files in defs/. Nothing to generate."; \
+	else \
+		if ! command -v python3 >/dev/null 2>&1; then \
+			echo "python3 is required to generate builtin defs but was not found." >&2; \
+			exit 1; \
+		fi; \
+		python3 ../scripts/gen_builtin_defs.py $(DEFS_JSON) > $@; \
+	fi
+
 install: all install-lib install-headers install-pkgconfig
 
 install-lib: all
diff --git a/tools/arch/x86/pmtctl/scripts/gen_builtin_defs.py b/tools/arch/x86/pmtctl/scripts/gen_builtin_defs.py
new file mode 100755
index 000000000000..5e34c8f9a1b3
--- /dev/null
+++ b/tools/arch/x86/pmtctl/scripts/gen_builtin_defs.py
@@ -0,0 +1,405 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Auto-generate builtin_defs.c from PMT perf-style JSON files.
+#
+"""Auto-generate builtin_defs.c from PMT perf-style JSON event files."""
+
+import sys
+import re
+import json
+from pathlib import Path
+
+# We only sanity-check that these fields exist in struct pmt_metric_def.
+EXPECTED_FIELDS = [
+    "event_name",
+    "description",
+    "group",
+    "platform_group",
+    "guid",
+    "sample_id",
+    "lsb",
+    "msb",
+]
+
+PMT_GUIDS_JSON_NAME = "pmt_guids.json"
+
+
+def find_project_root(start: Path) -> Path:
+    """
+    Walk upward until we find 'include/lib/metrics_db.h'.
+    This allows running the generator from anywhere:
+    - project root
+    - tools/
+    - build system
+    - editor integration
+    """
+    cur = start.resolve()
+    root = cur.anchor
+
+    while True:
+        candidate = cur / "include" / "lib" / "metrics_db.h"
+        if candidate.exists():
+            return cur
+        if str(cur) == root:
+            break
+        cur = cur.parent
+
+    sys.stderr.write(
+        "ERROR: Could not locate 'include/lib/metrics_db.h'. "
+        "Run from inside the project tree.\n"
+    )
+    sys.exit(1)
+
+
+PROJECT_ROOT = find_project_root(Path(__file__).parent)
+METRICS_DB_H = PROJECT_ROOT / "include" / "lib" / "metrics_db.h"
+
+
+def parse_struct_fields(header_path: Path):
+    """Parse struct pmt_metric_def from metrics_db.h and return [(type, name), ...]."""
+
+    sys.stderr.write(f"Using metrics_db.h at: {header_path}\n")
+
+    text = header_path.read_text()
+
+    m = re.search(r"struct\s+pmt_metric_def\s*\{([^}]*)\}", text, re.S)
+    if not m:
+        sys.stderr.write(
+            "ERROR: could not find struct pmt_metric_def in metrics_db.h\n"
+        )
+        sys.exit(1)
+
+    block = m.group(1)
+    fields: list[tuple[str, str]] = []
+
+    for raw in block.splitlines():
+        line = raw.strip()
+        if not line:
+            continue
+        if line.startswith("/*") or line.startswith("//"):
+            continue
+        if ";" not in line:
+            continue
+
+        # Take text before ';'
+        before = line.split(";", 1)[0]
+
+        # Strip trailing // and /* ... */ comments crudely
+        before = before.split("//", 1)[0]
+        before = before.split("/*", 1)[0]
+        before = before.rstrip()
+        if not before:
+            continue
+
+        # Find the trailing identifier (the field name)
+        m_name = re.search(r"([A-Za-z_][A-Za-z0-9_]*)$", before)
+        if not m_name:
+            continue
+
+        name = m_name.group(1)
+        type_part = before[: m_name.start()].rstrip()
+        if not type_part:
+            continue
+
+        fields.append((type_part, name))
+
+    sys.stderr.write("Parsed fields in struct pmt_metric_def:\n")
+    for t, n in fields:
+        sys.stderr.write(f"  {t} {n}\n")
+
+    return fields
+
+
+def check_expected_fields(fields):
+    """Verify that all EXPECTED_FIELDS are present in the parsed struct fields."""
+    names = [name for (_ty, name) in fields]
+    missing = [f for f in EXPECTED_FIELDS if f not in names]
+    if missing:
+        sys.stderr.write(
+            "ERROR: metrics_db.h struct pmt_metric_def is missing fields: "
+            + ", ".join(missing)
+            + "\n"
+        )
+        sys.exit(1)
+
+
+def parse_guid_from_pmu(pmu_str: str) -> int:
+    """
+    Example PMU strings:
+        "pmt_ep_22806802"
+        "pmt_ep_22806802_s0_r0" (future)
+    We take the last underscore-separated token and interpret it as hex.
+    """
+
+    if not pmu_str:
+        return 0
+
+    parts = pmu_str.split("_")
+    tail = parts[-1]
+    try:
+        return int(tail, 16)
+    except ValueError:
+        # Fallback: unknown format, just 0 it for now
+        return 0
+
+
+def decode_config_code(cfg: int):
+    """
+    ConfigCode mapping (from your spec):
+
+        bits[15:0]   = sample_id
+        bits[23:16]  = lsb
+        bits[31:24]  = msb
+        bits[63:32]  = reserved/0
+    """
+
+    sample_id = cfg & 0xFFFF
+    lsb = (cfg >> 16) & 0xFF
+    msb = (cfg >> 24) & 0xFF
+    return sample_id, lsb, msb
+
+
+def load_events_from_json(path: Path):
+    """Load and return the list of event dicts from a JSON file."""
+    with path.open("r", encoding="utf-8") as f:
+        data = json.load(f)
+
+    if not isinstance(data, list):
+        raise ValueError(f"{path}: expected top-level JSON array")
+
+    return data
+
+
+def emit_header():
+    """Print the C file header and includes."""
+    print("/* Auto-generated by tools/gen_builtin_defs.py; do not edit. */")
+    print('#include "lib/metrics_db.h"')
+    print('#include "lib/pmt_guid.h"')
+    print()
+
+
+def emit_guid_table(guids: list[dict]):
+    """Emit the builtin_guids[] array and its count."""
+    print("const struct pmt_guid builtin_guids[] = {")
+    for g in guids:
+        print("    {")
+        print(f"        .guid        = 0x{g['guid']:08x},")
+        print(f"        .name        = {c_str(g.get('name') or None)},")
+        print(f"        .description = {c_str(g.get('description') or None)},")
+        print("    },")
+    print("};")
+    print()
+    print(f"const int builtin_guids_count = {len(guids)};")
+    print()
+
+
+def emit_defs_open():
+    """Print the opening brace for builtin_defs[]."""
+    print("const struct pmt_metric_def builtin_defs[] = {")
+    print()
+
+
+def emit_footer(total: int):
+    """Print the closing brace and builtin_defs_count definition."""
+    print("};")
+    print()
+    print(f"const int builtin_defs_count = {total};")
+
+
+def c_str(s):
+    """Return s as a quoted C string literal, or NULL for None."""
+    if s is None:
+        return "NULL"
+    s = str(s)
+    s = s.replace("\\", "\\\\").replace('"', '\\"')
+    return f'"{s}"'
+
+
+def emit_entry(ev: dict, guid_index: dict[int, int]):
+    """Print one C struct initializer for the given event dict."""
+    pmu = ev.get("PMU", "")
+    name = ev.get("EventName", "")
+    brief = ev.get("BriefDescription", "")
+    group = ev.get("MetricGroup", "")
+    platform_group = ev.get("PlatformGroup", "")
+
+    cfg_s = ev.get("ConfigCode", "0")
+
+    try:
+        cfg = int(cfg_s, 0)
+    except (ValueError, TypeError):
+        cfg = 0
+
+    sample_id, lsb, msb = decode_config_code(cfg)
+    guid = parse_guid_from_pmu(pmu)
+    idx = guid_index[guid]
+
+    print("    {")
+    print(f"        .event_name = {c_str(name)},")
+    print(f"        .description = {c_str(brief)},")
+    print(f"        .group       = {c_str(group)},")
+    print(f"        .platform_group = {c_str(platform_group)},")
+    print(f"        .guid        = &builtin_guids[{idx}], /* 0x{guid:08x} */")
+    print(f"        .sample_id   = {sample_id},")
+    print(f"        .lsb         = {lsb},")
+    print(f"        .msb         = {msb},")
+    print("    },")
+    print()
+
+
+def expand_paths(paths: list[str]) -> tuple[list[Path], Path | None]:
+    """
+    Expand arguments into JSON file paths.
+    If an argument is a directory, find all .json files in it.
+    If an argument is a file, include it directly.
+
+    Returns (event_json_paths, pmt_guids_json_path_or_None). The
+    pmt_guids.json sidecar (if encountered) is routed separately and
+    is never treated as a metric-event file.
+    """
+    json_paths: list[Path] = []
+    pmt_guids_path: Path | None = None
+
+    def _consider(p: Path) -> None:
+        nonlocal pmt_guids_path
+        if p.name == PMT_GUIDS_JSON_NAME:
+            if pmt_guids_path is not None and pmt_guids_path != p:
+                sys.stderr.write(
+                    f"WARNING: multiple {PMT_GUIDS_JSON_NAME} found; "
+                    f"using {pmt_guids_path}, ignoring {p}\n"
+                )
+                return
+            pmt_guids_path = p
+            return
+        json_paths.append(p)
+
+    for p_str in paths:
+        p = Path(p_str)
+        if p.is_dir():
+            json_files = sorted(p.glob("*.json"))
+            if not json_files:
+                sys.stderr.write(f"WARNING: no .json files found in directory {p}\n")
+            for jp in json_files:
+                _consider(jp)
+        elif p.is_file():
+            _consider(p)
+        else:
+            sys.stderr.write(f"WARNING: path does not exist: {p}\n")
+
+    return json_paths, pmt_guids_path
+
+
+def load_pmt_guids(path: Path) -> list[dict]:
+    """Load and validate the pmt_guids.json sidecar."""
+    with path.open("r", encoding="utf-8") as f:
+        data = json.load(f)
+    if not isinstance(data, list):
+        raise ValueError(f"{path}: expected top-level JSON array")
+
+    out: list[dict] = []
+    for entry in data:
+        guid_s = entry.get("guid")
+        if guid_s is None:
+            raise ValueError(f"{path}: entry missing 'guid'")
+        try:
+            guid = int(guid_s, 0) if isinstance(guid_s, str) else int(guid_s)
+        except (TypeError, ValueError) as ex:
+            raise ValueError(f"{path}: invalid guid {guid_s!r}: {ex}") from ex
+        out.append(
+            {
+                "guid": guid,
+                "name": entry.get("name") or "",
+                "description": entry.get("description") or "",
+            }
+        )
+    return out
+
+
+def _load_guid_meta(pmt_guids_path: Path | None) -> dict[int, dict]:
+    """Load GUID metadata (name + description per GUID) if available."""
+    guid_meta: dict[int, dict] = {}
+    if pmt_guids_path is not None:
+        sys.stderr.write(f"Loading GUID metadata from {pmt_guids_path}\n")
+        for g in load_pmt_guids(pmt_guids_path):
+            guid_meta[g["guid"]] = g
+    else:
+        sys.stderr.write(
+            f"WARNING: {PMT_GUIDS_JSON_NAME} not provided; "
+            "builtin_guids[] will have empty name/description.\n"
+        )
+    return guid_meta
+
+
+def _collect_events(
+    json_paths: list[Path],
+) -> tuple[list[tuple[Path, list[dict]]], list[int], dict[int, int]]:
+    """Load events from JSON paths and assign a stable GUID index."""
+    loaded: list[tuple[Path, list[dict]]] = []
+    seen_guids: list[int] = []
+    guid_index: dict[int, int] = {}
+    for jp in json_paths:
+        sys.stderr.write(f"Processing {jp}\n")
+        events = load_events_from_json(jp)
+        loaded.append((jp, events))
+        for ev in events:
+            guid = parse_guid_from_pmu(ev.get("PMU", ""))
+            if guid not in guid_index:
+                guid_index[guid] = len(seen_guids)
+                seen_guids.append(guid)
+    return loaded, seen_guids, guid_index
+
+
+def _build_guid_table(seen_guids: list[int], guid_meta: dict[int, dict]) -> list[dict]:
+    """Build the ordered guid table; merge in metadata."""
+    guid_table: list[dict] = []
+    for guid in seen_guids:
+        meta = guid_meta.get(guid)
+        if meta is None:
+            sys.stderr.write(
+                f"WARNING: no metadata for GUID 0x{guid:08x}; "
+                "emitting empty name/description.\n"
+            )
+            meta = {"guid": guid, "name": "", "description": ""}
+        guid_table.append(meta)
+    return guid_table
+
+
+def main(argv: list[str]) -> int:
+    """Entry point: parse arguments, emit C source to stdout."""
+    if len(argv) < 2:
+        sys.stderr.write(
+            "Usage: gen_builtin_defs.py <json|folder> [<json|folder> ...]\n"
+        )
+        return 1
+
+    # Sanity-check metrics_db.h
+    check_expected_fields(parse_struct_fields(METRICS_DB_H))
+
+    json_paths, pmt_guids_path = expand_paths(argv[1:])
+
+    if not json_paths:
+        sys.stderr.write("ERROR: no JSON files found to process\n")
+        return 1
+
+    guid_meta = _load_guid_meta(pmt_guids_path)
+    loaded, seen_guids, guid_index = _collect_events(json_paths)
+    guid_table = _build_guid_table(seen_guids, guid_meta)
+
+    # Emit C source.
+    emit_header()
+    emit_guid_table(guid_table)
+    emit_defs_open()
+
+    total = 0
+    for _jp, events in loaded:
+        for ev in events:
+            emit_entry(ev, guid_index)
+            total += 1
+
+    emit_footer(total)
+    return 0
+
+
+if __name__ == "__main__":
+    raise SystemExit(main(sys.argv))
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [PATCH 12/17] tools/arch/x86/pmtctl: Add pmtctl CLI entry point and pager
  2026-05-26  1:46 [PATCH 00/17] tools/arch/x86/pmtctl: Add Intel PMT command-line tool David E. Box
                   ` (10 preceding siblings ...)
  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 ` David E. Box
  2026-05-26  1:47 ` [PATCH 13/17] tools/arch/x86/pmtctl: Add pmtctl 'list' command David E. Box
                   ` (4 subsequent siblings)
  16 siblings, 0 replies; 25+ messages in thread
From: David E. Box @ 2026-05-26  1:47 UTC (permalink / raw)
  To: linux-kernel, david.e.box, ilpo.jarvinen, andriy.shevchenko,
	platform-driver-x86

Add the pmtctl command-line frontend, a user-facing binary built on top of
libpmtctl_core for discovering and inspecting Intel PMT telemetry devices.

Add a built-in pager so wide or lengthy telemetry output remains readable
without requiring users to pipe output through an external pager. The pager
is automatically disabled when output is redirected, preserving
scriptability.

This patch establishes the shared CLI infrastructure and global option
handling used by later subcommands. Subsequent patches add the 'list' and
'stat' commands for device enumeration and live telemetry sampling.

Add top-level Makefile integration so a single 'make' invocation builds
both the library and CLI.

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             | 114 +++++++++++++++++
 tools/arch/x86/pmtctl/include/pmtctl_cli.h |  12 ++
 tools/arch/x86/pmtctl/src/main.c           | 134 ++++++++++++++++++++
 tools/arch/x86/pmtctl/src/pager.c          | 140 +++++++++++++++++++++
 4 files changed, 400 insertions(+)
 create mode 100644 tools/arch/x86/pmtctl/Makefile
 create mode 100644 tools/arch/x86/pmtctl/include/pmtctl_cli.h
 create mode 100644 tools/arch/x86/pmtctl/src/main.c
 create mode 100644 tools/arch/x86/pmtctl/src/pager.c

diff --git a/tools/arch/x86/pmtctl/Makefile b/tools/arch/x86/pmtctl/Makefile
new file mode 100644
index 000000000000..83bca8c312e7
--- /dev/null
+++ b/tools/arch/x86/pmtctl/Makefile
@@ -0,0 +1,114 @@
+# SPDX-License-Identifier: GPL-2.0-only
+
+CC      ?= gcc
+
+BUILD	?= release
+
+CPPFLAGS += -Iinclude -Ilib -I../../../include -D_GNU_SOURCE
+CFLAGS   += -std=gnu11 -Wall -Wextra
+
+WEXTRA_WARN := -Wpedantic -Wmissing-prototypes -Wstrict-prototypes \
+               -Wunused-function -Wunused-variable -Wunused-parameter \
+               -Wunused-but-set-variable -Wunreachable-code
+
+ifeq ($(BUILD),debug)
+  CFLAGS += -O0 -g3 -fno-omit-frame-pointer
+  CFLAGS += $(WEXTRA_WARN)
+  CPPFLAGS += -DDEBUG
+else ifeq ($(BUILD),release)
+  CFLAGS += -O2 -g0 -DNDEBUG
+else
+  $(error unknown BUILD '$(BUILD)' (use release or debug))
+endif
+
+JANSSON_CFLAGS := $(shell pkg-config --cflags jansson 2>/dev/null)
+JANSSON_LIBS   := $(shell pkg-config --libs   jansson 2>/dev/null)
+ifeq ($(JANSSON_LIBS),)
+  $(error jansson not found: install libjansson-dev (Debian/Ubuntu) or jansson-devel (Fedora/RHEL))
+endif
+CPPFLAGS += $(JANSSON_CFLAGS)
+LDLIBS   := $(JANSSON_LIBS) -lm
+
+SRCDIR  := src
+BUILDDIR:= build/$(BUILD)
+TARGET  := pmtctl
+LIBDIR  := lib
+LIBPMTCTL_CORE      := $(BUILDDIR)/lib/libpmtctl_core.a
+LIBPMTCTL_ARTIFACTS := $(LIBPMTCTL_CORE)
+LIBPMTCTL_STAMP := $(BUILDDIR)/lib/.built
+SAMPLE_SRC := samples/libpmtctl_sample.c
+SAMPLE_TARGET := $(BUILDDIR)/samples/libpmtctl_sample
+
+SRC := \
+	$(SRCDIR)/main.c \
+	$(SRCDIR)/pager.c
+
+OBJ := $(patsubst $(SRCDIR)/%.c,$(BUILDDIR)/%.o,$(SRC))
+CLEAN_BUILDS := release debug
+
+.PHONY: all clean libpmtctl_core sample FORCE
+
+all: $(TARGET)
+
+$(TARGET): $(OBJ) $(LIBPMTCTL_ARTIFACTS)
+	$(CC) $(CFLAGS) -o $@ $(OBJ) $(LIBPMTCTL_ARTIFACTS) $(LDLIBS)
+
+libpmtctl_core: $(LIBPMTCTL_CORE)
+
+sample: $(SAMPLE_TARGET)
+
+# Build the sample with the public-header search path only, so its
+# `#include "pmtctl.h"` resolves the same way it would for an external
+# consumer linking against the installed libpmtctl_core.
+$(SAMPLE_TARGET): CPPFLAGS += -Iinclude/lib
+$(SAMPLE_TARGET): $(SAMPLE_SRC) $(LIBPMTCTL_ARTIFACTS)
+	@mkdir -p $(dir $@)
+	$(CC) $(CPPFLAGS) $(CFLAGS) -o $@ $< $(LIBPMTCTL_ARTIFACTS) $(LDLIBS)
+
+$(LIBPMTCTL_ARTIFACTS): $(LIBPMTCTL_STAMP)
+
+$(LIBPMTCTL_STAMP): FORCE
+	$(MAKE) -C $(LIBDIR) BUILD=$(BUILD)
+	@mkdir -p $(dir $@)
+	@touch $@
+
+FORCE:
+
+# Install settings
+PREFIX ?= /usr/local
+DESTDIR ?=
+
+
+.PHONY: install uninstall install-lib install-headers install-pkgconfig uninstall-lib uninstall-headers uninstall-pkgconfig
+
+install: $(TARGET) install-lib install-headers install-pkgconfig
+	install -d $(DESTDIR)$(PREFIX)/bin
+	install -m 0755 $(TARGET) $(DESTDIR)$(PREFIX)/bin
+	@echo "Installed $(TARGET) to $(DESTDIR)$(PREFIX)/bin/"
+
+install-lib:
+	$(MAKE) -C $(LIBDIR) BUILD=$(BUILD) PREFIX=$(PREFIX) DESTDIR=$(DESTDIR) install-lib
+
+install-headers:
+	$(MAKE) -C $(LIBDIR) BUILD=$(BUILD) PREFIX=$(PREFIX) DESTDIR=$(DESTDIR) install-headers
+
+install-pkgconfig:
+	$(MAKE) -C $(LIBDIR) BUILD=$(BUILD) PREFIX=$(PREFIX) DESTDIR=$(DESTDIR) install-pkgconfig
+
+uninstall:
+	rm -f $(DESTDIR)$(PREFIX)/bin/$(TARGET)
+	$(MAKE) -C $(LIBDIR) BUILD=$(BUILD) PREFIX=$(PREFIX) DESTDIR=$(DESTDIR) uninstall-lib
+	$(MAKE) -C $(LIBDIR) BUILD=$(BUILD) PREFIX=$(PREFIX) DESTDIR=$(DESTDIR) uninstall-headers
+	$(MAKE) -C $(LIBDIR) BUILD=$(BUILD) PREFIX=$(PREFIX) DESTDIR=$(DESTDIR) uninstall-pkgconfig
+	@echo "Removed $(DESTDIR)$(PREFIX)/bin/$(TARGET) (if present)"
+
+$(BUILDDIR)/%.o: $(SRCDIR)/%.c
+	@mkdir -p $(BUILDDIR)
+	$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
+
+clean:
+	@for build_type in $(CLEAN_BUILDS); do \
+		$(MAKE) -C $(LIBDIR) BUILD=$$build_type clean; \
+		rm -rf build/$$build_type; \
+	done
+	rm -rf $(BUILDDIR) $(TARGET)
diff --git a/tools/arch/x86/pmtctl/include/pmtctl_cli.h b/tools/arch/x86/pmtctl/include/pmtctl_cli.h
new file mode 100644
index 000000000000..0b99dfe0ed64
--- /dev/null
+++ b/tools/arch/x86/pmtctl/include/pmtctl_cli.h
@@ -0,0 +1,12 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef PMTCTL_CLI_H
+#define PMTCTL_CLI_H
+
+#include <stdio.h>
+
+#include "lib/pmtctl.h"
+
+FILE *pmtctl_start_pager(const struct pmt_global_opts *gopts);
+void pmtctl_finish_pager(FILE *out);
+
+#endif
diff --git a/tools/arch/x86/pmtctl/src/main.c b/tools/arch/x86/pmtctl/src/main.c
new file mode 100644
index 000000000000..e93b544d9343
--- /dev/null
+++ b/tools/arch/x86/pmtctl/src/main.c
@@ -0,0 +1,134 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdbool.h>
+#include <getopt.h>
+
+#include "lib/log.h"
+
+#include "pmtctl_cli.h"
+
+#define PMTCTL_VERSION	"2026.05.19"
+#define OPT_DEBUG 256
+
+static struct pmt_global_opts gopts = {
+	.device_selector = NULL,
+	.json_path	= NULL,
+	.quiet		= false,
+	.debug		= false,
+};
+
+static void print_usage(FILE *out)
+{
+	fprintf(out,
+		"Usage: pmtctl [global options] <command> [command options] ...\n"
+		"\n"
+		"Query Intel Platform Monitoring Technology (PMT) metrics.\n"
+		"\n"
+		"Global options:\n"
+		"  -h, --help		   Show this help and exit\n"
+		"  -V, --version	   Show version and exit\n"
+		"\n"
+		"  -J, --json-file <path>  Metrics JSON file\n"
+		"                          If omitted, no metric definitions are loaded\n"
+		"                          or search a default path (e.g. $PMTCTL_JSON_PATH).\n"
+		"\n"
+		"  -d, --device <selector> Restrict to a single endpoint.\n"
+		"                          Can be supplied either before the command (global)\n"
+		"                          or after the command as a command-local fallback.\n"
+		"                          Global value takes precedence when both are given.\n"
+		"                          Selectors: guid=<hex>, ep=<endpoint_name>\n"
+		"\n"
+		"  -q, --quiet             Suppress non-essential messages\n"
+		"      --debug             Enable debug logging\n"
+	);
+}
+
+static void print_version(void)
+{
+	printf("pmtctl version %s - David E. Box <david.e.box@linux.intel.com>\n", PMTCTL_VERSION);
+}
+
+static int cmd_dispatch(int argc, char **argv)
+{
+	const struct option long_options[] = {
+		{ "help",        no_argument,       0, 'h' },
+		{ "version",     no_argument,       0, 'V' },
+		{ "json-file",   required_argument, 0, 'J' },
+		{ "device",      required_argument, 0, 'd' },
+		{ "quiet",       no_argument,       0, 'q' },
+		{ "debug",       no_argument,       0,  OPT_DEBUG },
+		{ 0,             0,                 0,  0  }
+	};
+	const char *cmd;
+	int option_index = 0;
+	int opt;
+
+	while ((opt = getopt_long(argc, argv, "+hVJ:qd:", long_options, &option_index)) != -1) {
+		switch (opt) {
+		case 'h':
+			print_usage(stdout);
+			return 0;
+		case 'V':
+			print_version();
+			return 0;
+		case 'J':
+			gopts.json_path = optarg;
+			break;
+		case 'd':
+			if (gopts.device_selector)
+				return log_ret(PMTCTL_ERR_CMD_PARSE,
+					   "multiple --device options are not allowed");
+			gopts.device_selector = optarg;
+			break;
+		case 'q':
+			gopts.quiet = true;
+			break;
+		case OPT_DEBUG:
+			gopts.debug = true;
+			break;
+		case '?':
+		default:
+			/* getopt_long already printed an error */
+			fprintf(stderr, "Try 'pmtctl --help' for usage.\n");
+			return PMTCTL_ERR_CMD_PARSE;
+		}
+	}
+
+	if (optind >= argc) {
+		fprintf(stderr, "pmtctl: missing command\n");
+		print_usage(stderr);
+		return PMTCTL_ERR_CMD_PARSE;
+	}
+
+	cmd = argv[optind];
+	if (!strcmp(cmd, "--help") || !strcmp(cmd, "help")) {
+		print_usage(stdout);
+		return 0;
+	}
+
+	fprintf(stderr, "pmtctl: unknown command '%s'\n", cmd);
+	fprintf(stderr, "Run 'pmtctl --help' for a list of commands.\n");
+	return PMTCTL_ERR_CMD_PARSE;
+}
+
+int main(int argc, char **argv)
+{
+	int ret = cmd_dispatch(argc, argv);
+
+	pmtctl_cleanup();
+
+	/*
+	 * Collapse internal error codes to conventional exit values.
+	 * Granular error details are printed to stderr by log_ret().
+	 * 0 = success, PMTCTL_EXIT_USER (1) = usage/user error,
+	 * PMTCTL_EXIT_SYSTEM (2) = device/system error.
+	 */
+	if (ret == 0)
+		return 0;
+	else if (ret > 0)
+		return PMTCTL_EXIT_USER;
+	else
+		return PMTCTL_EXIT_SYSTEM;
+}
diff --git a/tools/arch/x86/pmtctl/src/pager.c b/tools/arch/x86/pmtctl/src/pager.c
new file mode 100644
index 000000000000..a9329293da84
--- /dev/null
+++ b/tools/arch/x86/pmtctl/src/pager.c
@@ -0,0 +1,140 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#define LOG_PREFIX "pager"
+#define _XOPEN_SOURCE 700
+#include <errno.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#include "lib/pmtctl.h"
+#include "lib/common.h"
+#include "lib/log.h"
+
+#include "pmtctl_cli.h"
+
+static FILE *pager_fp;
+static pid_t pager_pid;
+
+/* Maximum number of whitespace-separated tokens parsed from $PAGER. */
+#define PAGER_ARGV_MAX	16
+
+static bool env_false(const char *v)
+{
+	return !*v || !strcmp(v, "0") || !strcasecmp(v, "false") || !strcasecmp(v, "no") ||
+	       !strcasecmp(v, "off");
+}
+
+FILE *pmtctl_start_pager(const struct pmt_global_opts *gopts)
+{
+	(void)gopts;
+
+	/* Ignore SIGPIPE to handle pager closing early */
+	signal(SIGPIPE, SIG_IGN);
+
+	/* Already using a pager? Just reuse it. */
+	if (pager_fp)
+		return pager_fp;
+
+	/* Don't page if stdout is not a TTY. */
+	if (!isatty(STDOUT_FILENO))
+		return stdout;
+
+	/* Opt-out env: PMTCTL_NOPAGER=1 */
+	const char *nopager = getenv("PMTCTL_NOPAGER");
+	const char *pager_env;
+	int fds[2];
+	pid_t pid;
+	FILE *fp;
+
+	if (nopager && !env_false(nopager))
+		return stdout;
+
+	/* Choose pager: PMTCTL_PAGER > PAGER > "less" */
+	pager_env = getenv("PMTCTL_PAGER");
+	if (!pager_env || !*pager_env)
+		pager_env = getenv("PAGER");
+	if (!pager_env || !*pager_env)
+		pager_env = "less";
+
+	if (pipe(fds) < 0) {
+		log_err(errno, "pipe for pager failed");
+		return stdout;
+	}
+
+	pid = fork();
+	if (pid < 0) {
+		log_err(errno, "fork for pager failed");
+		close(fds[0]);
+		close(fds[1]);
+		return stdout;
+	}
+
+	if (pid == 0) {
+		/* Child: exec pager, reading from pipe */
+		close(fds[1]);
+
+		if (fds[0] != STDIN_FILENO) {
+			if (dup2(fds[0], STDIN_FILENO) < 0)
+				_exit(1);
+			close(fds[0]);
+		}
+
+		/* build argv from pager_env (very simple split on spaces) */
+		char *argv[PAGER_ARGV_MAX];
+		char *cmd = strdup(pager_env);
+		char *tok;
+		int argc = 0;
+
+		if (!cmd)
+			_exit(1);
+
+		tok = strtok(cmd, " ");
+		while (tok && argc < (int)ARRAY_SIZE(argv) - 1) {
+			argv[argc++] = tok;
+			tok = strtok(NULL, " ");
+		}
+		argv[argc] = NULL;
+
+		execvp(argv[0], argv);
+		/* If exec fails */
+		_exit(1);
+	}
+
+	/* Parent */
+	close(fds[0]);
+	fp = fdopen(fds[1], "w");
+	if (!fp) {
+		log_err(errno, "fdopen for pager failed");
+		close(fds[1]);
+		/* We spawned a child; best effort wait and fall back */
+		int status;
+		(void)waitpid(pid, &status, 0);
+		return stdout;
+	}
+
+	pager_pid = pid;
+	pager_fp  = fp;
+	return pager_fp;
+}
+
+void pmtctl_finish_pager(FILE *out)
+{
+	if (!pager_fp || out != pager_fp) {
+		/* No pager or not ours */
+		return;
+	}
+
+	fclose(pager_fp);
+	pager_fp = NULL;
+
+	if (pager_pid > 0) {
+		int status;
+		(void)waitpid(pager_pid, &status, 0);
+		pager_pid = 0;
+	}
+}
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [PATCH 13/17] tools/arch/x86/pmtctl: Add pmtctl 'list' command
  2026-05-26  1:46 [PATCH 00/17] tools/arch/x86/pmtctl: Add Intel PMT command-line tool David E. Box
                   ` (11 preceding siblings ...)
  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 ` David E. Box
  2026-05-26  1:47 ` [PATCH 14/17] tools/arch/x86/pmtctl: Add pmtctl 'stat' command David E. Box
                   ` (3 subsequent siblings)
  16 siblings, 0 replies; 25+ messages in thread
From: David E. Box @ 2026-05-26  1:47 UTC (permalink / raw)
  To: linux-kernel, david.e.box, ilpo.jarvinen, andriy.shevchenko,
	platform-driver-x86

Register the 'list' subcommand in the CLI dispatcher and add its
implementation in src/cmd_list.c. The command enumerates PMT devices
and metrics, with optional filtering by GUID or endpoint name.

Per-command --help, JSON output, and tabular formatting are handled
locally inside cmd_list.

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             |   1 +
 tools/arch/x86/pmtctl/include/pmtctl_cli.h |   2 +
 tools/arch/x86/pmtctl/src/cmd_list.c       | 786 +++++++++++++++++++++
 tools/arch/x86/pmtctl/src/main.c           |  13 +
 4 files changed, 802 insertions(+)
 create mode 100644 tools/arch/x86/pmtctl/src/cmd_list.c

diff --git a/tools/arch/x86/pmtctl/Makefile b/tools/arch/x86/pmtctl/Makefile
index 83bca8c312e7..ee6633a6f435 100644
--- a/tools/arch/x86/pmtctl/Makefile
+++ b/tools/arch/x86/pmtctl/Makefile
@@ -41,6 +41,7 @@ SAMPLE_TARGET := $(BUILDDIR)/samples/libpmtctl_sample
 
 SRC := \
 	$(SRCDIR)/main.c \
+	$(SRCDIR)/cmd_list.c \
 	$(SRCDIR)/pager.c
 
 OBJ := $(patsubst $(SRCDIR)/%.c,$(BUILDDIR)/%.o,$(SRC))
diff --git a/tools/arch/x86/pmtctl/include/pmtctl_cli.h b/tools/arch/x86/pmtctl/include/pmtctl_cli.h
index 0b99dfe0ed64..eb5efb0b650f 100644
--- a/tools/arch/x86/pmtctl/include/pmtctl_cli.h
+++ b/tools/arch/x86/pmtctl/include/pmtctl_cli.h
@@ -9,4 +9,6 @@
 FILE *pmtctl_start_pager(const struct pmt_global_opts *gopts);
 void pmtctl_finish_pager(FILE *out);
 
+int cmd_list(int argc, char **argv, const struct pmt_global_opts *gopts);
+
 #endif
diff --git a/tools/arch/x86/pmtctl/src/cmd_list.c b/tools/arch/x86/pmtctl/src/cmd_list.c
new file mode 100644
index 000000000000..a4164bf75dfb
--- /dev/null
+++ b/tools/arch/x86/pmtctl/src/cmd_list.c
@@ -0,0 +1,786 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <ctype.h>
+#include <errno.h>
+#include <getopt.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <unistd.h>
+
+#define LIST_BASE_INDENT    2
+#define LIST_CONT_INDENT    6
+#define LIST_DEFAULT_COLS  80
+
+/* Column widths for the --guids table */
+#define GCOL_GUID   8
+#define GCOL_COUNT  5
+#define GCOL_DEF   10
+#define GCOL_PLAT   8
+
+#define LOG_PREFIX "cmd_list"
+#include "lib/log.h"
+
+#include "lib/common.h"
+#include "lib/device.h"
+#include "lib/metrics_db.h"
+#include "lib/pmt_guid.h"
+#include "lib/pmtctl_context.h"
+
+#include "pmtctl_cli.h"
+
+struct list_opts {
+	bool show_help;
+	bool devices_only;
+	bool guids_only;
+	bool keep_screen;
+	const char *device_selector;   /* raw "guid=...,ep=..." string (command-local fallback) */
+};
+
+struct list_metric_view {
+	const char *device_name;       /* GUID: "22806802" */
+	const char *platform_group;    /* platform group name */
+	const char *group_name;        /* pmt_guid->name */
+	const char *group_description; /* pmt_guid->description (may be NULL) */
+	const char *name;              /* metric name */
+	const char *desc;              /* metric description (may be NULL) */
+};
+
+struct guid_defs {
+	const char *platform_group;
+	const char *name;
+	const struct pmt_guid *pg;  /* registry entry (may be NULL) */
+	uint32_t guid;
+	bool from_builtin;
+	bool from_json;
+};
+
+struct guid_sys {
+	uint32_t guid;
+	int endpoints;
+};
+
+static int guid_defs_find(struct guid_defs *arr, int n, uint32_t guid)
+{
+	for (int i = 0; i < n; i++) {
+		if (arr[i].guid == guid)
+			return i;
+	}
+
+	return -1;
+}
+
+static int guid_defs_cmp_platform_group(const void *a, const void *b)
+{
+	const struct guid_defs *ga = (const struct guid_defs *)a;
+	const struct guid_defs *gb = (const struct guid_defs *)b;
+	const char *pg_a = ga->platform_group;
+	const char *pg_b = gb->platform_group;
+
+	if (pg_a && pg_b) {
+		int cmp = strcmp(pg_a, pg_b);
+
+		if (cmp)
+			return cmp;
+	} else if (pg_a) {
+		return -1;
+	} else if (pg_b) {
+		return 1;
+	}
+
+	if (ga->guid < gb->guid)
+		return -1;
+	if (ga->guid > gb->guid)
+		return 1;
+
+	return 0;
+}
+
+static int guid_sys_find(struct guid_sys *arr, int n, uint32_t guid)
+{
+	for (int i = 0; i < n; i++) {
+		if (arr[i].guid == guid)
+			return i;
+	}
+
+	return -1;
+}
+
+static bool is_blank_string(const char *s)
+{
+	while (*s) {
+		if (!isspace((unsigned char)*s))
+			return false;
+		s++;
+	}
+
+	return true;
+}
+
+static int do_list_guids(const int *dev_indices, int num_sel_devices)
+{
+	const struct pmtctl_context *ctx = pmtctl_get_ctx();
+	auto_free struct guid_defs *defs = NULL;
+	auto_free struct guid_sys  *sys  = NULL;
+	int num_defs = 0;
+	int n_sys = 0;
+
+	/* ----- 1) collect GUIDs from metric definitions ----- */
+	defs = calloc(ctx->metrics.total, sizeof(*defs));
+	if (!defs)
+		return log_ret(-ENOMEM, "could not build guid list");
+
+	for (int i = 0; i < ctx->metrics.total; i++) {
+		const struct pmt_metric_def *md;
+		const struct pmt_metrics_block *block;
+		uint32_t guid;
+		int idx;
+
+		md = pmt_metrics_at(&ctx->metrics, i);
+		if (!md)
+			continue;
+
+		block = pmt_metrics_block_for(&ctx->metrics, i);
+		if (!block)
+			continue;
+
+		/*
+		 * Fold every metric def into a single per-GUID row in defs[].
+		 * Many defs share the same GUID, so look up an existing row
+		 * and append a new one only the first time we see this GUID.
+		 * The metric-level fields below (from_builtin/from_json,
+		 * platform_group, name) then accumulate onto that row.
+		 *
+		 * A NULL md->guid means a provider failed to intern (or parse)
+		 * the GUID for this def; skip it rather than fold every such
+		 * def into a bogus "00000000" bucket.
+		 */
+		if (!md->guid) {
+			log_warn("metric def %d (%s) has no GUID; skipping",
+				 i, md->event_name ? md->event_name : "?");
+			continue;
+		}
+
+		guid = md->guid->guid;
+		idx = guid_defs_find(defs, num_defs, guid);
+		if (idx == -1) {
+			idx = num_defs++;
+			defs[idx].guid = guid;
+			defs[idx].pg = md->guid;
+		}
+
+		if (block->is_builtin)
+			defs[idx].from_builtin = true;
+		else
+			defs[idx].from_json = true;
+
+		defs[idx].platform_group = md->platform_group;
+		defs[idx].name = md->group;
+	}
+
+	if (num_defs > 1)
+		qsort(defs, num_defs, sizeof(*defs), guid_defs_cmp_platform_group);
+
+	/* ----- 2) collect GUIDs from system devices (filtered) ----- */
+	sys = calloc(num_sel_devices, sizeof(*sys));
+	if (!sys)
+		return log_ret(-ENOMEM, "could not build guid list");
+
+	for (int i = 0; i < num_sel_devices; i++) {
+		const struct pmt_device *dev = &ctx->devices[dev_indices[i]];
+		uint32_t guid;
+		int idx;
+
+		if (!dev->guid) {
+			log_warn("device %s has no GUID; skipping",
+				 dev->name ? dev->name : "?");
+			continue;
+		}
+
+		guid = dev->guid->guid;
+		idx = guid_sys_find(sys, n_sys, guid);
+
+		if (idx < 0) {
+			idx = n_sys++;
+			sys[idx].guid = guid;
+		}
+		sys[idx].endpoints++;
+	}
+
+	/* ----- 3) print combined system + defs ----- */
+	printf("GUIDs on this system (TELEM):\n\n");
+	printf("  %-*s   %-*s   %-*s   %-*s   %s\n",
+	       GCOL_GUID, "GUID", GCOL_COUNT, "Count",
+	       GCOL_DEF, "Definition", GCOL_PLAT, "Platform",
+	       "PMT Name");
+	printf("  %.*s   %.*s   %.*s   %.*s   %s\n",
+	       GCOL_GUID, "--------",
+	       GCOL_COUNT, "--------",
+	       GCOL_DEF, "----------",
+	       GCOL_PLAT, "--------",
+	       "--------");
+
+	if (!n_sys) {
+		printf("  (none)\n");
+		return 0;
+	}
+
+	for (int i = 0; i < n_sys; i++) {
+		struct guid_sys *gs = &sys[i];
+		int idx = guid_defs_find(defs, num_defs, gs->guid);
+		const struct pmt_guid *pg = pmt_guid_lookup(gs->guid);
+		const char *pmt_name = (pg && pg->name && *pg->name) ? pg->name : "---";
+		struct guid_defs *gd;
+
+		printf("  %08x   %-*d   ", gs->guid, GCOL_COUNT, gs->endpoints);
+
+		if (idx < 0) {
+			printf("%-*s   %-*s   %s\n",
+			       GCOL_DEF, "---",
+			       GCOL_PLAT, "---",
+			       pmt_name);
+			continue;
+		}
+
+		gd = &defs[idx];
+
+		if (gd->from_builtin || gd->from_json) {
+			bool need_comma = false;
+
+			if (gd->from_builtin) {
+				printf("%-*s", GCOL_DEF, "builtin");
+				need_comma = true;
+			}
+
+			if (gd->from_json) {
+				if (need_comma)
+					fputc(',', stdout);
+				printf("%-*s", GCOL_DEF, "json");
+			}
+		} else {
+			printf("%-*s", GCOL_DEF, "unknown");
+		}
+
+		if (gd->platform_group && !is_blank_string(gd->platform_group))
+			printf("   %-*s", GCOL_PLAT, gd->platform_group);
+		else
+			printf("   %-*s", GCOL_PLAT, "---");
+
+		printf("   %s\n", pmt_name);
+	}
+
+	return 0;
+}
+
+#define PLAT_W 12
+static void print_guid_group_line(uint32_t guid, const struct guid_defs *gd)
+{
+	const char *plat = (gd && gd->platform_group) ? gd->platform_group : "---";
+	const struct pmt_guid *pg = gd ? gd->pg : pmt_guid_lookup(guid);
+	const char *pmt_name = (pg && pg->name && *pg->name) ? pg->name : "---";
+
+	printf("  GUID %08x  platform: %-*s  name: %s\n", guid, PLAT_W, plat, pmt_name);
+
+	if (pg && pg->description && *pg->description)
+		printf("    %s\n", pg->description);
+}
+
+#define NAME_W 24
+static void print_dev_entry(const struct pmt_device *dev)
+{
+	if (dev->pkg_id >= 0 && dev->die_id >= 0)
+		printf("    %-*s  pkg=%d die=%d\n", NAME_W, dev->name, dev->pkg_id, dev->die_id);
+	else
+		printf("    %s\n", dev->name);
+}
+
+static int do_list_devices(const int *dev_indices, int num_sel_devices)
+{
+	const struct pmtctl_context *ctx = pmtctl_get_ctx();
+	auto_free struct guid_defs *defs = NULL;
+	auto_free uint32_t *guid_order = NULL;
+	int num_defs = 0;
+	int num_guids = 0;
+
+	if (ctx->num_devices == 0 || num_sel_devices == 0) {
+		printf("No PMT devices found.\n");
+		return 0;
+	}
+
+	/* Build GUID -> platform_group/name map from metric defs */
+	if (ctx->metrics.total > 0) {
+		defs = calloc(ctx->metrics.total, sizeof(*defs));
+		if (!defs)
+			return log_ret(-ENOMEM, "could not allocate guid info");
+
+		for (int i = 0; i < ctx->metrics.total; i++) {
+			const struct pmt_metric_def *md = pmt_metrics_at(&ctx->metrics, i);
+			uint32_t guid;
+			int idx;
+
+			if (!md || !md->guid)
+				continue;
+
+			guid = md->guid->guid;
+			idx = guid_defs_find(defs, num_defs, guid);
+			if (idx == -1) {
+				idx = num_defs++;
+				defs[idx].guid = guid;
+				defs[idx].pg = md->guid;
+			}
+
+			defs[idx].platform_group = md->platform_group;
+			defs[idx].name = md->group;
+		}
+	}
+
+	/* Collect unique GUIDs in selected-device order */
+	guid_order = calloc(num_sel_devices, sizeof(*guid_order));
+	if (!guid_order)
+		return log_ret(-ENOMEM, "could not allocate guid order");
+
+	for (int k = 0; k < num_sel_devices; k++) {
+		const struct pmt_device *dev = &ctx->devices[dev_indices[k]];
+		uint32_t g;
+		bool found = false;
+
+		if (!dev->guid) {
+			log_warn("device %s has no GUID; skipping", dev->name ? dev->name : "?");
+			continue;
+		}
+
+		g = dev->guid->guid;
+
+		for (int j = 0; j < num_guids; j++) {
+			if (guid_order[j] == g) {
+				found = true;
+				break;
+			}
+		}
+
+		if (!found)
+			guid_order[num_guids++] = g;
+	}
+
+	printf("PMT devices on this system:\n\n");
+
+	for (int g = 0; g < num_guids; g++) {
+		uint32_t guid = guid_order[g];
+		int idx = defs ? guid_defs_find(defs, num_defs, guid) : -1;
+
+		print_guid_group_line(guid, idx >= 0 ? &defs[idx] : NULL);
+
+		for (int k = 0; k < num_sel_devices; k++) {
+			const struct pmt_device *dev = &ctx->devices[dev_indices[k]];
+
+			if (!dev->guid || dev->guid->guid != guid)
+				continue;
+
+			print_dev_entry(dev);
+		}
+		printf("\n");
+	}
+
+	return 0;
+}
+
+static int get_term_columns(void)
+{
+	const char *col_env = getenv("COLUMNS");
+	struct winsize ws;
+
+	if (col_env) {
+		int n = atoi(col_env);
+
+		if (n > 0)
+			return n;
+	}
+#ifdef TIOCGWINSZ
+	if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col)
+		return ws.ws_col;
+#endif
+	return LIST_DEFAULT_COLS;
+}
+
+/*
+ * Print @s word-wrapped at @term_cols, with every line (including the first)
+ * indented by @indent spaces.  Mirrors the wordwrap() behaviour in
+ * tools/perf/builtin-list.c: words are broken at spaces/tabs; explicit
+ * newlines in the source text force a break and restart the indent.
+ */
+static void print_wrapped_desc(FILE *out, const char *s, int indent, int term_cols)
+{
+	int column = indent;
+	bool at_line_start = true;
+
+	fprintf(out, "%*s", indent, "");
+
+	while (*s) {
+		int wlen = strcspn(s, " \t\n");
+
+		/* Wrap if the next word would overflow the right margin. */
+		if (!at_line_start && column + 1 + wlen > term_cols) {
+			fprintf(out, "\n%*s", indent, "");
+			column = indent;
+			at_line_start = true;
+		}
+
+		if (!at_line_start) {
+			fputc(' ', out);
+			column++;
+		}
+
+		fprintf(out, "%.*s", wlen, s);
+		column += wlen;
+		at_line_start = false;
+		s += wlen;
+
+		/* Skip spaces/tabs; honour explicit newlines. */
+		while (*s == ' ' || *s == '\t')
+			s++;
+		if (*s == '\n') {
+			fprintf(out, "\n%*s", indent, "");
+			column = indent;
+			at_line_start = true;
+			s++;
+		}
+	}
+	fputc('\n', out);
+}
+
+static void print_group_header(FILE *out, const char *device_name, const char *platform_group,
+			       const char *name, const char *description, int term_cols)
+{
+	const char *pg = (platform_group && *platform_group) ? platform_group : "---";
+	const char *nm = (name && *name)                     ? name           : "---";
+
+	fprintf(out, "TELEM %s   %s  %s\n", device_name, pg, nm);
+	if (description && *description)
+		print_wrapped_desc(out, description, LIST_BASE_INDENT, term_cols);
+}
+
+static void print_group_separator(FILE *out, int width)
+{
+	if (width < 8)
+		width = 8;
+	if (width > 100)
+		width = 100;
+
+	fprintf(out, "%*s", LIST_BASE_INDENT, "");
+
+	for (int i = 0; i < width; i++)
+		fputc('-', out);
+	fputc('\n', out);
+}
+
+/*
+ * The caller will free(*out) with free().
+ */
+static int build_list_view(struct list_metric_view **out, int *out_nr, const int *dev_indices,
+			   int num_sel_devices)
+{
+	const struct pmtctl_context *ctx = pmtctl_get_ctx();
+	const struct pmt_binding *b  = ctx->bindings;
+	struct list_metric_view *arr;
+	int nb = ctx->num_bindings;
+	int arr_count = 0;
+
+	if (ctx->metrics.total == 0)
+		return log_ret(PMTCTL_ERR_CMD_LIST, "no metrics loaded");
+
+	if (!nb)
+		return 0;
+
+	*out = NULL;
+	*out_nr = 0;
+
+	arr = calloc(nb, sizeof(*arr));
+	if (!arr)
+		return -ENOMEM;
+
+	for (int i = 0; i < nb; i++) {
+		const struct pmt_metric_def *md  = pmt_metrics_at(&ctx->metrics, b[i].metric_idx);
+		const struct pmt_device *dev = &ctx->devices[b[i].device_idx];
+		struct list_metric_view *m;
+		bool device_selected = false;
+		char buf[16];
+
+		if (!md || !dev)
+			continue;
+
+		if (!dev->guid) {
+			log_warn("device %s has no GUID; skipping", dev->name ? dev->name : "?");
+			continue;
+		}
+
+		/* Check if this device is in the selected device list */
+		for (int j = 0; j < num_sel_devices; j++) {
+			if (dev_indices[j] == b[i].device_idx) {
+				device_selected = true;
+				break;
+			}
+		}
+
+		if (!device_selected)
+			continue;
+
+		m = &arr[arr_count++];
+
+		snprintf(buf, sizeof(buf), "%08x", dev->guid->guid);
+		m->device_name = strdup(buf);
+
+		m->name = md->event_name;
+		m->desc = md->description;
+		m->platform_group = md->platform_group;
+		m->group_name = (md->guid && md->guid->name && *md->guid->name)
+				? md->guid->name : NULL;
+		m->group_description = (md->guid && md->guid->description && *md->guid->description)
+				? md->guid->description : NULL;
+	}
+
+	*out = arr;
+	*out_nr = arr_count;
+
+	return 0;
+}
+
+/* sort by device_name then metric name */
+static int list_metric_cmp(const void *a, const void *b)
+{
+	const struct list_metric_view *ma = a;
+	const struct list_metric_view *mb = b;
+	int d;
+
+	d = strcmp(ma->device_name, mb->device_name);
+	if (d)
+		return d;
+
+	return strcmp(ma->name, mb->name);
+}
+
+static int do_list_metrics(const struct pmt_global_opts *gopts, const int *dev_indices,
+			   int num_sel_devices)
+{
+	FILE *out = pmtctl_start_pager(gopts);
+	auto_free struct list_metric_view *mview = NULL;
+	int gstart = 0;
+	bool first_group = true;
+	int nr = 0;
+	int ret;
+
+	ret = build_list_view(&mview, &nr, dev_indices, num_sel_devices);
+	if (ret)
+		return ret;
+
+	if (!mview) {
+		log_warn("no metrics to view");
+		return 0;
+	}
+
+	if (!nr) {
+		log_warn("no metrics to view after filtering");
+		return 0;
+	}
+
+	qsort(mview, nr, sizeof(mview[0]), list_metric_cmp);
+
+	int term_cols = get_term_columns();
+
+	/* Iterate group-by-group (group = device_name) */
+
+	while (gstart < nr) {
+		int gend = gstart + 1;
+		int max_name = (int)strlen(mview[gstart].name);
+
+		while (gend < nr && strcmp(mview[gend].device_name,
+					   mview[gstart].device_name) == 0) {
+			int n = (int)strlen(mview[gend].name);
+
+			if (n > max_name)
+				max_name = n;
+			gend++;
+		}
+
+		if (!first_group)
+			fputc('\n', out);
+		first_group = false;
+
+		print_group_header(out,
+				   mview[gstart].device_name,
+				   mview[gstart].platform_group,
+				   mview[gstart].group_name,
+				   mview[gstart].group_description,
+				   term_cols);
+
+		print_group_separator(out, max_name + LIST_CONT_INDENT);
+
+		for (int i = gstart; i < gend; i++) {
+			struct list_metric_view *m = &mview[i];
+
+			fprintf(out, "%*s%s\n", LIST_BASE_INDENT, "", m->name);
+			if (m->desc && *m->desc) {
+				print_wrapped_desc(out, m->desc,
+						   LIST_BASE_INDENT + LIST_CONT_INDENT,
+						   term_cols);
+			}
+		}
+
+		gstart = gend;
+	}
+
+	for (int i = 0; i < nr; i++)
+		free((char *)mview[i].device_name);
+
+	pmtctl_finish_pager(out);
+
+	return 0;
+}
+
+static void list_usage(FILE *out)
+{
+	fprintf(out,
+		"Usage: pmtctl list [options]\n"
+		"\n"
+		"List PMT metrics and optionally devices.\n"
+		"\n"
+		"Options:\n"
+		"  -h, --help              Show this help.\n"
+		"  -d, --device <selector> Restrict to a single endpoint.\n"
+		"                          Can be supplied globally before the command or\n"
+		"                          locally after the command as a fallback.\n"
+		"                          Global value takes precedence when both are given.\n"
+		"                          Selectors: guid=<hex>, ep=<name>\n"
+		"      --devices           List devices only (no metrics).\n"
+		"      --guids             List GUIDs from defs and system.\n"
+		"  -X, --keep              Keep the last pager screen on exit.\n");
+}
+
+#define OPT_LIST_DEVICES 1000
+#define OPT_LIST_GUIDS   1001
+
+static int parse_list_opts(struct list_opts *opts, int argc, char **argv)
+{
+	int c;
+
+	memset(opts, 0, sizeof(*opts));
+	optind = 1;
+
+	static const struct option long_opts[] = {
+		{ "help",    no_argument,       NULL, 'h' },
+		{ "device",  required_argument, NULL, 'd' },
+		{ "devices", no_argument,       NULL, OPT_LIST_DEVICES },
+		{ "guids",   no_argument,       NULL, OPT_LIST_GUIDS },
+		{ "keep",    no_argument,       NULL, 'X' },
+		{ 0, 0, 0, 0 }
+	};
+
+	while ((c = getopt_long(argc, argv, "hd:X", long_opts, NULL)) != -1) {
+		switch (c) {
+		case 'h':
+			opts->show_help = true;
+			break;
+		case 'd':
+			opts->device_selector = optarg;
+			break;
+		case OPT_LIST_DEVICES: /* --devices */
+			opts->devices_only = true;
+			break;
+		case OPT_LIST_GUIDS:  /* --guids */
+			opts->guids_only = true;
+			break;
+		case 'X':
+			opts->keep_screen = true;
+			break;
+		case '?':
+		default:
+			return -EINVAL;
+		}
+	}
+
+	if (optind != argc)
+		return log_ret(-EINVAL, "unexpected extra arguments to 'list'");
+
+	return 0;
+}
+
+int cmd_list(int argc, char **argv, const struct pmt_global_opts *gopts)
+{
+	const struct pmtctl_context *ctx;
+	struct pmt_ep_selector sel;
+	struct list_opts opts;
+	auto_free int *dev_indices = NULL;
+	int num_sel_devices;
+	int ret;
+
+	ret = parse_list_opts(&opts, argc, argv);
+	if (ret) {
+		list_usage(stderr);
+		return PMTCTL_ERR_CMD_PARSE;
+	}
+
+	/*
+	 * Compatibility fallback: prefer global device selector if provided;
+	 * otherwise use command-local --device if supplied after the command.
+	 */
+	if (gopts && gopts->device_selector)
+		opts.device_selector = gopts->device_selector;
+
+	if (opts.show_help) {
+		list_usage(stdout);
+		return 0;
+	}
+
+	if (opts.keep_screen) {
+		const char *less = getenv("LESS");
+		char buf[512];
+
+		if (less && *less)
+			snprintf(buf, sizeof(buf), "-X %s", less);
+		else
+			snprintf(buf, sizeof(buf), "-X");
+		setenv("LESS", buf, 1);
+	}
+
+	/* Initialize pmtctl to get device context */
+	ret = pmtctl_init(gopts);
+	if (ret)
+		return ret;
+
+	/* Apply device selector filter (must precede --devices, --guids,
+	 * and metric listing so all three honour the global -d selector). */
+	ctx = pmtctl_get_ctx();
+
+	dev_indices = calloc(ctx->num_devices, sizeof(*dev_indices));
+	if (!dev_indices)
+		return log_ret(-ENOMEM, "failed to allocate device indices");
+
+	if (opts.device_selector) {
+		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_LIST, "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;
+	}
+
+	if (opts.devices_only)
+		return do_list_devices(dev_indices, num_sel_devices);
+
+	if (opts.guids_only)
+		return do_list_guids(dev_indices, num_sel_devices);
+
+	return do_list_metrics(gopts, dev_indices, num_sel_devices);
+}
diff --git a/tools/arch/x86/pmtctl/src/main.c b/tools/arch/x86/pmtctl/src/main.c
index e93b544d9343..d9666956c27b 100644
--- a/tools/arch/x86/pmtctl/src/main.c
+++ b/tools/arch/x86/pmtctl/src/main.c
@@ -42,6 +42,11 @@ static void print_usage(FILE *out)
 		"\n"
 		"  -q, --quiet             Suppress non-essential messages\n"
 		"      --debug             Enable debug logging\n"
+		"\n"
+		"Commands:\n"
+		"  list      List available PMT devices and metrics\n"
+		"\n"
+		"Run 'pmtctl <command> --help' for command-specific options.\n"
 	);
 }
 
@@ -64,6 +69,8 @@ static int cmd_dispatch(int argc, char **argv)
 	const char *cmd;
 	int option_index = 0;
 	int opt;
+	int cmd_argc;
+	char **cmd_argv;
 
 	while ((opt = getopt_long(argc, argv, "+hVJ:qd:", long_options, &option_index)) != -1) {
 		switch (opt) {
@@ -103,6 +110,12 @@ static int cmd_dispatch(int argc, char **argv)
 	}
 
 	cmd = argv[optind];
+	cmd_argc = argc - optind;
+	cmd_argv = &argv[optind];
+
+	if (!strcmp(cmd, "list"))
+		return cmd_list(cmd_argc, cmd_argv, &gopts);
+
 	if (!strcmp(cmd, "--help") || !strcmp(cmd, "help")) {
 		print_usage(stdout);
 		return 0;
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [PATCH 14/17] tools/arch/x86/pmtctl: Add pmtctl 'stat' command
  2026-05-26  1:46 [PATCH 00/17] tools/arch/x86/pmtctl: Add Intel PMT command-line tool David E. Box
                   ` (12 preceding siblings ...)
  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
  2026-05-26  1:47 ` [PATCH 15/17] tools/arch/x86/pmtctl: Add pmtxml2json conversion tool David E. Box
                   ` (2 subsequent siblings)
  16 siblings, 0 replies; 25+ messages in thread
From: David E. Box @ 2026-05-26  1:47 UTC (permalink / raw)
  To: linux-kernel, david.e.box, ilpo.jarvinen, andriy.shevchenko,
	platform-driver-x86

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


^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [PATCH 15/17] tools/arch/x86/pmtctl: Add pmtxml2json conversion tool
  2026-05-26  1:46 [PATCH 00/17] tools/arch/x86/pmtctl: Add Intel PMT command-line tool David E. Box
                   ` (13 preceding siblings ...)
  2026-05-26  1:47 ` [PATCH 14/17] tools/arch/x86/pmtctl: Add pmtctl 'stat' command David E. Box
@ 2026-05-26  1:47 ` 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
  16 siblings, 0 replies; 25+ messages in thread
From: David E. Box @ 2026-05-26  1:47 UTC (permalink / raw)
  To: linux-kernel, david.e.box, ilpo.jarvinen, andriy.shevchenko,
	platform-driver-x86

Add a Python converter that turns Intel PMT XML metric definitions into the
pmtctl/perf-style JSON consumed by pmtctl and by the built-in metric
definition generator.

The converter supports two input modes:

  Local path: point it at an existing Intel-PMT xml tree (--by-path
              /path/to/Intel-PMT/xml) and convert in place.

  Fetch:      --fetch-pmt-repo clones the upstream Intel-PMT repository
              into a cache (default ~/.cache/pmtctl).

              --refresh-pmt-repo updates the cache.

Output JSON files are written under --output-dir, one file per metric
group, suitable for direct use with pmtctl -J or as input to
gen_builtin_defs.py for compiled-in definitions.

The document pmtxml2json.md provages usage examples covering the
different workflows.

Assisted-by: GitHub-Copilot:claude-sonnet-4.6
Signed-off-by: David E. Box <david.e.box@linux.intel.com>
---
 tools/arch/x86/pmtctl/Makefile               |  96 +-
 tools/arch/x86/pmtctl/scripts/pmtxml2json.md | 158 ++++
 tools/arch/x86/pmtctl/scripts/pmtxml2json.py | 883 +++++++++++++++++++
 3 files changed, 1129 insertions(+), 8 deletions(-)
 create mode 100644 tools/arch/x86/pmtctl/scripts/pmtxml2json.md
 create mode 100755 tools/arch/x86/pmtctl/scripts/pmtxml2json.py

diff --git a/tools/arch/x86/pmtctl/Makefile b/tools/arch/x86/pmtctl/Makefile
index 52e50597b5c1..d55819372f79 100644
--- a/tools/arch/x86/pmtctl/Makefile
+++ b/tools/arch/x86/pmtctl/Makefile
@@ -1,6 +1,27 @@
 # SPDX-License-Identifier: GPL-2.0-only
 
+# Remove targets whose recipe exited non-zero so a failed codegen step
+# does not leave a truncated $@ behind that fools the next build.
+.DELETE_ON_ERROR:
+
 CC      ?= gcc
+PYTHON  ?= python3
+
+# Directories for the XML -> JSON -> C codegen pipeline.
+DEFS_DIR        ?= defs
+GENERATED_DIR   ?= generated
+PMT_CACHE_DIR   ?= $(HOME)/.cache/pmtctl
+
+XML2JSON_SCRIPT := scripts/pmtxml2json.py
+GEN_DEFS_SCRIPT := scripts/gen_builtin_defs.py
+
+# JSON sources that define built-in metrics. pmtxml2json.py writes
+# one subdirectory per platform under $(DEFS_DIR)/, so recurse.
+DEFS_JSON       ?= $(shell find $(DEFS_DIR) -name '*.json' 2>/dev/null)
+
+# Stamp marks "the XML->JSON conversion has run". The exact set of
+# generated files is not known up front, so we depend on a single stamp.
+DEFS_JSON_STAMP := $(DEFS_DIR)/.stamp
 
 BUILD	?= release
 
@@ -35,7 +56,6 @@ TARGET  := pmtctl
 LIBDIR  := lib
 LIBPMTCTL_CORE      := $(BUILDDIR)/lib/libpmtctl_core.a
 LIBPMTCTL_ARTIFACTS := $(LIBPMTCTL_CORE)
-LIBPMTCTL_STAMP := $(BUILDDIR)/lib/.built
 SAMPLE_SRC := samples/libpmtctl_sample.c
 SAMPLE_TARGET := $(BUILDDIR)/samples/libpmtctl_sample
 
@@ -50,13 +70,21 @@ SRC := \
 OBJ := $(patsubst $(SRCDIR)/%.c,$(BUILDDIR)/%.o,$(SRC))
 CLEAN_BUILDS := release debug
 
-.PHONY: all clean libpmtctl_core sample FORCE
+.PHONY: all clean defs defs-json-fetch defs-json-pull defs-clean \
+        libpmtctl_core sample FORCE
 
 all: $(TARGET)
 
 $(TARGET): $(OBJ) $(LIBPMTCTL_ARTIFACTS)
 	$(CC) $(CFLAGS) -o $@ $(OBJ) $(LIBPMTCTL_ARTIFACTS) $(LDLIBS)
 
+# If JSON definitions exist, ensure the generated built-in defs are up to
+# date before the lib sub-make runs. Without this, edits under defs/ would
+# not propagate into pmtctl until the user explicitly ran 'make defs'.
+ifneq ($(DEFS_JSON),)
+$(LIBPMTCTL_CORE): $(GENERATED_DIR)/builtin_defs.c
+endif
+
 libpmtctl_core: $(LIBPMTCTL_CORE)
 
 sample: $(SAMPLE_TARGET)
@@ -69,15 +97,58 @@ $(SAMPLE_TARGET): $(SAMPLE_SRC) $(LIBPMTCTL_ARTIFACTS)
 	@mkdir -p $(dir $@)
 	$(CC) $(CPPFLAGS) $(CFLAGS) -o $@ $< $(LIBPMTCTL_ARTIFACTS) $(LDLIBS)
 
-$(LIBPMTCTL_ARTIFACTS): $(LIBPMTCTL_STAMP)
-
-$(LIBPMTCTL_STAMP): FORCE
+# Recurse into lib/ on every invocation. The sub-make is incremental and
+# does nothing when up to date. Because $(LIBPMTCTL_CORE) has its own
+# recipe here, GNU make re-stats it afterwards, so any mtime advance from
+# sub-make correctly propagates to $(TARGET) and triggers a relink.
+$(LIBPMTCTL_CORE): FORCE
 	$(MAKE) -C $(LIBDIR) BUILD=$(BUILD)
-	@mkdir -p $(dir $@)
-	@touch $@
 
 FORCE:
 
+# --- XML -> JSON step (network-bound; opt-in) ---
+#
+# Fetches the Intel-PMT git repo (cached under $(PMT_CACHE_DIR)) and
+# converts every aggregator XML into perf-style JSON under $(DEFS_DIR)/.
+# Not wired into 'all' on purpose: avoid surprise git clones.
+defs-json-fetch: $(DEFS_JSON_STAMP)
+
+$(DEFS_JSON_STAMP): $(XML2JSON_SCRIPT)
+	@echo "defs-json-fetch: git cloning Intel-PMT into $(PMT_CACHE_DIR)"
+	@command -v $(PYTHON) >/dev/null 2>&1 || { \
+		echo "$(PYTHON) is required for $(XML2JSON_SCRIPT)" >&2; exit 1; }
+	@mkdir -p $(DEFS_DIR)
+	$(PYTHON) $(XML2JSON_SCRIPT) \
+		--fetch-pmt-repo \
+		--pmt-cache-dir $(PMT_CACHE_DIR) \
+		--output-dir $(DEFS_DIR)
+	@touch $@
+
+# Run 'git pull' on the cached Intel-PMT repo, then regenerate JSON.
+defs-json-pull: $(XML2JSON_SCRIPT)
+	@echo "defs-json-pull: running 'git pull' on $(PMT_CACHE_DIR)"
+	@mkdir -p $(DEFS_DIR)
+	$(PYTHON) $(XML2JSON_SCRIPT) \
+		--fetch-pmt-repo --refresh-pmt-repo \
+		--pmt-cache-dir $(PMT_CACHE_DIR) \
+		--output-dir $(DEFS_DIR)
+	@touch $(DEFS_JSON_STAMP)
+
+# --- JSON -> C step (does NOT build pmtctl) ---
+#
+# DEFS_JSON is expanded at parse time, so 'make defs-json-fetch' must be run
+# in a separate invocation before 'make defs' the first time.
+$(GENERATED_DIR)/builtin_defs.c: $(GEN_DEFS_SCRIPT) $(DEFS_JSON)
+	@mkdir -p $(GENERATED_DIR)
+	@if [ -z "$(DEFS_JSON)" ]; then \
+		echo "No JSON files under $(DEFS_DIR)/. Run 'make defs-json-fetch' first," >&2; \
+		echo "then re-run 'make defs'." >&2; \
+		exit 1; \
+	fi
+	@command -v $(PYTHON) >/dev/null 2>&1 || { \
+		echo "$(PYTHON) is required for $(GEN_DEFS_SCRIPT)" >&2; exit 1; }
+	$(PYTHON) $(GEN_DEFS_SCRIPT) $(DEFS_JSON) > $@
+
 # Install settings
 PREFIX ?= /usr/local
 DESTDIR ?=
@@ -105,6 +176,15 @@ uninstall:
 	$(MAKE) -C $(LIBDIR) BUILD=$(BUILD) PREFIX=$(PREFIX) DESTDIR=$(DESTDIR) uninstall-headers
 	$(MAKE) -C $(LIBDIR) BUILD=$(BUILD) PREFIX=$(PREFIX) DESTDIR=$(DESTDIR) uninstall-pkgconfig
 	@echo "Removed $(DESTDIR)$(PREFIX)/bin/$(TARGET) (if present)"
+defs: $(GENERATED_DIR)/builtin_defs.c
+	@if [ -f $(GENERATED_DIR)/builtin_defs.c ]; then \
+		echo "Generated defs in $(GENERATED_DIR)/builtin_defs.c"; \
+	fi
+
+# Separate from 'clean' so a routine clean does not throw away the
+# (potentially slow) fetched/converted JSON tree.
+defs-clean:
+	rm -rf $(DEFS_DIR) $(GENERATED_DIR)/builtin_defs.c
 
 $(BUILDDIR)/%.o: $(SRCDIR)/%.c
 	@mkdir -p $(BUILDDIR)
@@ -115,4 +195,4 @@ clean:
 		$(MAKE) -C $(LIBDIR) BUILD=$$build_type clean; \
 		rm -rf build/$$build_type; \
 	done
-	rm -rf $(BUILDDIR) $(TARGET)
+	rm -rf $(BUILDDIR) $(TARGET) $(GENERATED_DIR)/builtin_defs.c
diff --git a/tools/arch/x86/pmtctl/scripts/pmtxml2json.md b/tools/arch/x86/pmtctl/scripts/pmtxml2json.md
new file mode 100644
index 000000000000..67eb08a83c86
--- /dev/null
+++ b/tools/arch/x86/pmtctl/scripts/pmtxml2json.md
@@ -0,0 +1,158 @@
+# pmtxml2json: XML → perf JSON conversion
+
+[`pmtxml2json.py`](pmtxml2json.py) converts Intel PMT (Platform Monitoring
+Technology) Aggregator XML files into perf-style JSON event definitions
+consumed by `pmtctl` (via `gen_builtin_defs.py` → `generated/builtin_defs.c`).
+
+This document focuses on the **EventName naming convention** — the rule used
+to derive a perf-style event name from each `<TELC:sample>` element.
+
+## Inputs
+
+For each sample, only two XML inputs participate in naming:
+
+| Input             | XML source                                | Example value             |
+| ----------------- | ----------------------------------------- | ------------------------- |
+| `name`            | `name=` attribute on `<TELC:sample>`      | `IA_SCALABILITY`          |
+| `sampleSubGroup`  | `<TELC:sampleSubGroup>` child text        | `IA_SCALABILITY_CORE7`    |
+
+The aggregator's `<TELEM:uniqueid>` (GUID) is used for the output filename
+(`pmt_ep_<guid>.json`), **not** for naming.
+
+`sampleID`, `sampleGroupID`, `lsb`, `msb`, and `productid` are not used to
+build `EventName`. They describe bit layout, packaging, or platform
+identity rather than the metric's identity, and using them would either
+produce names that change when the XML is regenerated or names that
+duplicate information already conveyed by `PMU` / `ConfigCode`.
+
+## Pre-filter: reserved samples
+
+Before naming, samples are dropped if any of the following match the
+case-insensitive pattern `reserved|rsvd` (optionally with trailing digits,
+not embedded in larger tokens):
+
+- the `name` attribute,
+- the `<TELC:sampleSubGroup>` text, or
+- the sample/group `<TELC:description>` text.
+
+Reserved samples never receive an `EventName`.
+
+## Naming rule (lazy prefix)
+
+Within a single aggregator XML, let `N(name)` be the number of non-reserved
+samples sharing the same `name`. For each surviving sample:
+
+1. **Unique name** — `N(name) == 1`:
+   `EventName = sanitize(name)`
+2. **Name collides** and `sampleSubGroup` is non-empty and `sampleSubGroup != name`:
+   `EventName = sanitize(sampleSubGroup) + "." + sanitize(name)`
+3. **Name collides** but `sampleSubGroup` is empty or equals `name`:
+   `EventName = sanitize(name)` (no disambiguation available)
+
+### Why lazy?
+
+`sampleSubGroup` plays two different roles in practice:
+
+- A **metric-instance index** — e.g. `IA_SCALABILITY_CORE7` qualifies a
+  per-core copy of `IA_SCALABILITY`. Prefixing is meaningful and useful.
+- A **container alias** — e.g. `INTEL_VERSION_2` is just an enclosing
+  container around an already-unique `RTL_VERSION`. Prefixing here would
+  produce a confusing label like `intel_version_2.rtl_version`.
+
+The lazy rule borrows `sampleSubGroup` **only when `name` actually collides**,
+yielding clean labels in the common case and disambiguated ones when needed.
+
+### `sanitize()`
+
+`_sanitize_token()` normalizes free-form text into a perf-friendly token:
+
+1. Strip leading/trailing whitespace.
+2. Replace any run of non-alphanumeric characters with a single `_`.
+3. Collapse repeated `_` and trim leading/trailing `_`.
+4. Lowercase.
+
+When concatenating subgroup and name, **each part is sanitized
+separately** and joined with a literal `.`, so the dot is preserved in the
+final `EventName` (e.g. `ia_scalability_core7.ia_scalability`).
+
+## Worked example
+
+Consider an aggregator XML containing three samples:
+
+```xml
+<TELC:sampleGroup sampleID="0x0">
+  <TELC:sample name="RTL_VERSION" ...>
+    <TELC:sampleSubGroup>INTEL_VERSION_2</TELC:sampleSubGroup>
+    <TELC:lsb>0</TELC:lsb><TELC:msb>15</TELC:msb>
+  </TELC:sample>
+</TELC:sampleGroup>
+
+<TELC:sampleGroup sampleID="0x10">
+  <TELC:sample name="IA_SCALABILITY" ...>
+    <TELC:sampleSubGroup>IA_SCALABILITY_CORE0</TELC:sampleSubGroup>
+    <TELC:lsb>0</TELC:lsb><TELC:msb>7</TELC:msb>
+  </TELC:sample>
+</TELC:sampleGroup>
+
+<TELC:sampleGroup sampleID="0x11">
+  <TELC:sample name="IA_SCALABILITY" ...>
+    <TELC:sampleSubGroup>IA_SCALABILITY_CORE7</TELC:sampleSubGroup>
+    <TELC:lsb>0</TELC:lsb><TELC:msb>7</TELC:msb>
+  </TELC:sample>
+</TELC:sampleGroup>
+```
+
+Per-aggregator name counts:
+
+| `name`           | count |
+| ---------------- | ----- |
+| `RTL_VERSION`    | 1     |
+| `IA_SCALABILITY` | 2     |
+
+Resulting `EventName`s:
+
+| Sample            | Rule branch                      | EventName                              |
+| ----------------- | -------------------------------- | -------------------------------------- |
+| `RTL_VERSION`     | (1) unique                       | `rtl_version`                          |
+| `IA_SCALABILITY` (CORE0) | (2) collision + distinct subgroup | `ia_scalability_core0.ia_scalability`  |
+| `IA_SCALABILITY` (CORE7) | (2) collision + distinct subgroup | `ia_scalability_core7.ia_scalability`  |
+
+Note that `RTL_VERSION` is **not** prefixed with its `INTEL_VERSION_2`
+container, even though `sampleSubGroup` is set — because the name is
+already unique within the aggregator.
+
+## Output shape
+
+For each emitted sample, the JSON object is:
+
+```json
+{
+  "PMU": "pmt_ep_<guid>",
+  "EventName": "<name per rule above>",
+  "BriefDescription": "<sample or group description>",
+  "MetricGroup": "pmt",
+  "ConfigCode": "0x<msb><lsb><sampleID>",
+  "PlatformGroup": "<optional, from --by-path>"
+}
+```
+
+`ConfigCode` packs the perf config bits as:
+
+```
+bits  0..15  sampleID
+bits 16..23  lsb
+bits 24..31  msb
+```
+
+## EventName uniqueness
+
+Within a single aggregator XML the three-rule scheme above resolves most
+collisions.  If two samples still share the same `EventName` after
+subgroup-prefix disambiguation (rule 2) — for example because neither has a
+usable `sampleSubGroup` — the converter applies a last-resort ordinal suffix
+`__0`, `__1`, … and emits a `WARN` line to stderr.  The double-underscore is
+chosen to be visually distinct from any XML field so it is not mistaken for a
+meaningful part of the metric name.
+
+Across **different** GUIDs, names may repeat — they live in distinct PMUs
+and are disambiguated by the `PMU` field.
diff --git a/tools/arch/x86/pmtctl/scripts/pmtxml2json.py b/tools/arch/x86/pmtctl/scripts/pmtxml2json.py
new file mode 100755
index 000000000000..31995f0fc72e
--- /dev/null
+++ b/tools/arch/x86/pmtctl/scripts/pmtxml2json.py
@@ -0,0 +1,883 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-only
+"""Convert Intel PMT aggregator XML files into perf JSON events.
+
+Provides core XML-to-event conversion plus optional Intel-PMT repository
+fetch/cache support.
+"""
+
+import argparse
+import glob
+import json
+import os
+import re
+import shutil
+import subprocess
+import sys
+import traceback
+
+from dataclasses import dataclass
+from typing import Dict, List, Optional, Tuple
+
+from lxml import etree  # pylint: disable=c-extension-no-member
+
+METRIC_GROUP = "pmt"
+INTEL_PMT_REPO_URL = "https://github.com/intel/Intel-PMT"
+
+
+def _expand_path(path: str) -> str:
+    """Return an absolute path with user home expansion applied."""
+    return os.path.abspath(os.path.expanduser(path))
+
+
+def _repo_dir_name_from_url(repo_url: str) -> str:
+    """Return a deterministic cache directory name for a git repository URL."""
+    cleaned = (repo_url or "").rstrip("/")
+    if cleaned.endswith(".git"):
+        cleaned = cleaned[: -len(".git")]
+
+    base = os.path.basename(cleaned) or "repo"
+    base = re.sub(r"[^0-9A-Za-z._-]+", "-", base).strip("-")
+    return base or "repo"
+
+
+def _fetch_intel_pmt_xml_root(
+    cache_dir: str,
+    refresh: bool = False,
+    debug: bool = False,
+    repo_url: str = INTEL_PMT_REPO_URL,
+) -> Optional[str]:
+    """Ensure Intel-PMT exists in cache and return its xml root path."""
+    cache_root = _expand_path(cache_dir)
+    repo_dir = os.path.join(cache_root, _repo_dir_name_from_url(repo_url))
+
+    os.makedirs(cache_root, exist_ok=True)
+
+    try:
+        if not os.path.isdir(repo_dir):
+            if debug:
+                print(
+                    f"# fetch: cloning {repo_url} into {repo_dir}",
+                    file=sys.stderr,
+                )
+            subprocess.run(
+                ["git", "clone", "--depth", "1", repo_url, repo_dir],
+                check=True,
+                stdout=subprocess.PIPE,
+                stderr=subprocess.PIPE,
+                text=True,
+                timeout=300,
+            )
+        elif refresh:
+            if debug:
+                print(f"# fetch: refreshing cached repo at {repo_dir}", file=sys.stderr)
+            subprocess.run(
+                ["git", "-C", repo_dir, "pull", "--ff-only"],
+                check=True,
+                stdout=subprocess.PIPE,
+                stderr=subprocess.PIPE,
+                text=True,
+                timeout=300,
+            )
+        elif debug:
+            print(f"# fetch: using cached repo at {repo_dir}", file=sys.stderr)
+    except FileNotFoundError:
+        print("ERROR: git is not installed or not found in PATH.", file=sys.stderr)
+        return None
+    except subprocess.TimeoutExpired:
+        print("ERROR: fetching Intel-PMT timed out.", file=sys.stderr)
+        return None
+    except subprocess.CalledProcessError as ex:
+        err = (ex.stderr or "").strip()
+        print("ERROR: failed to fetch Intel-PMT repository.", file=sys.stderr)
+        if err:
+            print(f"       git stderr: {err}", file=sys.stderr)
+        return None
+
+    xml_root = os.path.join(repo_dir, "xml")
+    if not os.path.isdir(xml_root):
+        print(
+            (
+                "ERROR: fetched repository does not contain expected xml "
+                f"directory: {xml_root}"
+            ),
+            file=sys.stderr,
+        )
+        return None
+
+    return xml_root
+
+
+def _find_pmt_xml(
+    fetched_xml_root: Optional[str], by_path: Optional[str]
+) -> Optional[str]:
+    """Locate pmt.xml in the Intel-PMT xml/ folder.
+
+    Prefer the fetched repo's xml root. Otherwise, walk upward from --by-path
+    looking for a pmt.xml sibling (xml/ folder root).
+    """
+    candidates: List[str] = []
+    if fetched_xml_root:
+        candidates.append(os.path.join(fetched_xml_root, "pmt.xml"))
+
+    if by_path:
+        start = by_path
+        if os.path.isfile(start):
+            start = os.path.dirname(start) or "."
+        start = os.path.abspath(start)
+        cur = start
+        while True:
+            candidates.append(os.path.join(cur, "pmt.xml"))
+            parent = os.path.dirname(cur)
+            if parent == cur:
+                break
+            cur = parent
+
+    for c in candidates:
+        if os.path.isfile(c):
+            return c
+    return None
+
+
+# ---------- Reserved/RSVD skipping ----------
+# Name: match 'reserved' or 'rsvd' with optional digits, not embedded in larger tokens
+RESERVED_RX = re.compile(
+    r"(?<![a-z0-9])(reserved|rsvd)(?:\d+)?(?![a-z0-9])", re.IGNORECASE
+)
+# Description: exact 'reserved' or 'rsvd' with optional trailing digits/whitespace
+DESC_RESERVED_RX = re.compile(r"\s*(?:reserved|rsvd)(?:\s*\d+)?\s*$", re.IGNORECASE)
+
+
+@dataclass(frozen=True)
+class SampleDef:  # pylint: disable=too-many-instance-attributes
+    """Normalized representation of one PMT sample field definition."""
+
+    guid: int
+    group_name: str
+    sample_id: int
+    sample_name: str
+    lsb: int
+    msb: int
+    datatype_idref: Optional[str]
+    description: Optional[str]
+    sample_type: Optional[str]
+    sample_subgroup: Optional[str]
+
+
+@dataclass
+class Counters:
+    """Per-file conversion counters for summary diagnostics."""
+
+    total: int = 0
+    emitted: int = 0
+    skipped: int = 0
+
+
+def norm(tag: str) -> str:
+    """Return XML tag name without namespace prefix."""
+    return tag[tag.rfind("}") + 1 :] if "}" in tag else tag
+
+
+def parse_xml(xml_path: str):
+    """Parse and return the XML root element for the given file path."""
+    # pylint: disable-next=c-extension-no-member
+    parser = etree.XMLParser(load_dtd=True, resolve_entities=True, no_network=False)
+    # pylint: disable-next=c-extension-no-member
+    root = etree.parse(xml_path, parser).getroot()
+
+    return root
+
+
+def _basedir_to_name(basedir: str) -> str:
+    """Normalize pmt.xml <basedir> into the per-GUID short name.
+
+    Lowercases the string and replaces '/' with '_'. Other characters
+    (including existing hyphens like 'RMID-EE') are preserved.
+    """
+    if not basedir:
+        return ""
+    return basedir.strip().lower().replace("/", "_")
+
+
+def _parse_mapping_entry(
+    m,
+) -> Optional[Tuple[int, str, str]]:
+    """Extract (guid, name, description) from a <mapping> element.
+
+    Returns None if the mapping has no usable GUID.
+    """
+    guid_txt = m.attrib.get("guid")
+    if not guid_txt:
+        return None
+
+    try:
+        guid = int(guid_txt, 0)
+    except ValueError:
+        guid = int(guid_txt, 16)
+
+    description = ""
+    basedir = ""
+    for ch in m:
+        t = norm(ch.tag).lower()
+        if t == "description":
+            description = (ch.text or "").strip()
+        elif t == "xmlset":
+            for sub in ch:
+                if norm(sub.tag).lower() == "basedir":
+                    basedir = (sub.text or "").strip()
+
+    return guid, _basedir_to_name(basedir), description
+
+
+def _merge_duplicate_mapping(existing: Dict[str, object], name: str) -> None:
+    """Merge a duplicate-GUID mapping's alternate name into existing record."""
+    if not name or name == existing["name"]:
+        return
+    extra = f"(also: {name})"
+    if existing["description"]:
+        if extra not in existing["description"]:
+            existing["description"] = f"{existing['description']} {extra}"
+    else:
+        existing["description"] = extra
+
+
+def _parse_pmt_xml_guids(pmt_xml_path: str) -> List[Dict[str, object]]:
+    """Parse pmt.xml and return one record per unique <mapping> GUID.
+
+    Each record has: {"guid": int, "name": str, "description": str}.
+    When the same GUID appears in multiple <mapping> entries (unrelated
+    platforms occasionally reuse early GUIDs), the first occurrence wins
+    and subsequent ones are merged into the description (best-effort, for
+    diagnostics).
+    """
+    root = parse_xml(pmt_xml_path)
+    entries: List[Dict[str, object]] = []
+    by_guid: Dict[int, Dict[str, object]] = {}
+
+    for m in root.iter():
+        if norm(m.tag).lower() != "mapping":
+            continue
+
+        parsed = _parse_mapping_entry(m)
+        if parsed is None:
+            continue
+        guid, name, description = parsed
+
+        existing = by_guid.get(guid)
+        if existing is None:
+            rec = {"guid": guid, "name": name, "description": description}
+            by_guid[guid] = rec
+            entries.append(rec)
+        else:
+            _merge_duplicate_mapping(existing, name)
+
+    entries.sort(key=lambda e: e["guid"])
+    return entries
+
+
+def _write_pmt_guids_json(
+    pmt_xml_src: str, output_dir: str, debug: bool = False
+) -> None:
+    """Parse pmt.xml and write a sidecar pmt_guids.json into output_dir."""
+    entries = _parse_pmt_xml_guids(pmt_xml_src)
+    out_path = os.path.join(output_dir, "pmt_guids.json")
+    serial = [
+        {
+            "guid": f"0x{e['guid']:08x}",
+            "name": e["name"],
+            "description": e["description"],
+        }
+        for e in entries
+    ]
+    with open(out_path, "w", encoding="utf-8") as f:
+        json.dump(serial, f, indent=2)
+        f.write("\n")
+    if debug:
+        print(f"# wrote {out_path} ({len(serial)} entries)", file=sys.stderr)
+
+
+def get_guid(root) -> int:
+    """Extract the telemetry GUID from <uniqueid>."""
+    for e in root.iter():
+        if norm(e.tag).lower() == "uniqueid":
+            v = (e.text or "").strip()
+            if not v:
+                break
+
+            try:
+                # works for "0x1234" or "1234" if decimal was intended
+                return int(v, 0)
+            except ValueError:
+                # force hex for values without prefix
+                return int(v, 16)
+
+    raise ValueError("Missing <TELEM:uniqueid>")
+
+
+# pylint: disable=too-many-locals,too-many-branches,too-many-statements
+def parse_samples(
+    root,
+) -> List[SampleDef]:
+    """Parse SampleGroup/Sample entries into filtered SampleDef records."""
+    guid = get_guid(root)
+    out: List[SampleDef] = []
+
+    for sg in root.iter():
+        if norm(sg.tag).lower() != "samplegroup":
+            continue
+
+        sid_txt = sg.attrib.get("sampleID") or sg.attrib.get("sampleid")
+        if sid_txt is None:
+            raise ValueError("SampleGroup missing sampleID")
+
+        sample_id = int(sid_txt, 0)
+        group_name = (sg.attrib.get("name") or "").strip() or f"group_{sample_id}"
+        group_len = None
+        group_desc = None
+
+        for child in sg:
+            t = norm(child.tag).lower()
+            if t == "length":
+                try:
+                    group_len = int((child.text or "").strip(), 0)
+                except (TypeError, ValueError):
+                    pass
+            elif t == "description":
+                group_desc = (child.text or "").strip()
+
+        if group_len is not None and group_len != 64:
+            raise ValueError(
+                f"{group_name} sampleID={sample_id} length={group_len} (expected 64)"
+            )
+
+        samples = [c for c in sg if norm(c.tag).lower() == "sample"]
+        if not samples:
+            continue
+
+        for s in samples:
+            sname = (s.attrib.get("name") or f"sample_{sample_id}").strip()
+
+            lsb = None
+            msb = None
+            stype = None
+            sdesc = None
+            ssubgroup = None
+            dtype_ref = s.attrib.get("dataTypeIDREF") or s.attrib.get("datatypeIDREF")
+
+            for ch in s:
+                t = norm(ch.tag).lower()
+                if t == "lsb":
+                    lsb = int((ch.text or "").strip(), 0)
+                elif t == "msb":
+                    msb = int((ch.text or "").strip(), 0)
+                elif t == "sampletype":
+                    stype = (ch.text or "").strip()
+                elif t == "description":
+                    sdesc = (ch.text or "").strip()
+                elif t == "samplesubgroup":
+                    ssubgroup = (ch.text or "").strip()
+                elif t == "datatypeidref" and not dtype_ref:
+                    dtype_ref = (ch.text or "").strip()
+
+            if lsb is None or msb is None:
+                raise ValueError(f"{sname} (sampleID={sample_id}): missing lsb/msb")
+
+            if not 0 <= lsb <= msb < 64:
+                raise ValueError(
+                    f"{sname} (sampleID={sample_id}): invalid bit range {lsb}-{msb}"
+                )
+
+            desc_text = sdesc if sdesc else group_desc
+
+            # Skip reserved/rsvd samples by name, sampleSubGroup, or description
+            is_reserved_name = RESERVED_RX.search(sname)
+            is_reserved_sub = ssubgroup and RESERVED_RX.search(ssubgroup)
+            is_reserved_desc = desc_text and DESC_RESERVED_RX.fullmatch(desc_text)
+            if is_reserved_name or is_reserved_sub or is_reserved_desc:
+                continue
+
+            out.append(
+                SampleDef(
+                    guid=guid,
+                    group_name=group_name,
+                    sample_id=sample_id,
+                    sample_name=sname,
+                    lsb=lsb,
+                    msb=msb,
+                    datatype_idref=dtype_ref,
+                    description=desc_text,
+                    sample_type=stype,
+                    sample_subgroup=ssubgroup,
+                )
+            )
+
+    return out
+
+
+def pack_config(sample_id: int, lsb: int, msb: int) -> int:
+    """Pack sample_id/lsb/msb into perf ConfigCode bit layout."""
+    return (sample_id & 0xFFFF) | ((lsb & 0xFF) << 16) | ((msb & 0xFF) << 24)
+
+
+def _sanitize_token(s: str) -> str:
+    """Normalize free-form text into a lowercase underscore token."""
+    t = re.sub(r"[^0-9a-zA-Z]+", "_", s.strip()).lower()
+    t = re.sub(r"_+", "_", t).strip("_")
+
+    return t
+
+
+def brief_desc(s: SampleDef) -> str:
+    """Build a short description for perf JSON output."""
+    if s.description:
+        return re.sub(r"\s+", " ", s.description)[:240]
+
+    width = s.msb - s.lsb + 1
+
+    return f"{s.sample_name.replace('_', ' ').title()} ({width}b)"
+
+
+def make_event(
+    s: SampleDef,
+    pmu_name: str,
+    name_counts: Dict[str, int],
+    platform_group: Optional[str] = None,
+) -> Dict[str, str]:
+    """Create one perf event dictionary for a sample."""
+    cfg = pack_config(s.sample_id, s.lsb, s.msb)
+    # Lazy-prefix disambiguation: only borrow sampleSubGroup when the bare
+    # sample name collides with another non-reserved sample in this
+    # aggregator. sampleSubGroup is sometimes a metric-instance index
+    # (e.g. IA_SCALABILITY_CORE7) and sometimes a container alias
+    # (e.g. INTEL_VERSION_2); unconditional prefixing produces confusing
+    # labels in the latter case.
+    if name_counts.get(s.sample_name, 0) <= 1:
+        evname = _sanitize_token(s.sample_name)
+    elif s.sample_subgroup and s.sample_subgroup != s.sample_name:
+        evname = (
+            f"{_sanitize_token(s.sample_subgroup)}.{_sanitize_token(s.sample_name)}"
+        )
+    else:
+        # No subgroup available for disambiguation; return the bare name.
+        # The caller is responsible for detecting and resolving any resulting
+        # duplicate EventName via _resolve_duplicate_event_names().
+        evname = _sanitize_token(s.sample_name)
+
+    e = {
+        "PMU": pmu_name,
+        "EventName": evname,
+        "BriefDescription": brief_desc(s),
+        "MetricGroup": METRIC_GROUP,
+        "ConfigCode": f"0x{cfg:08x}",
+    }
+
+    if platform_group:
+        e["PlatformGroup"] = platform_group
+
+    return e
+
+
+def _resolve_duplicate_event_names(
+    out: List[Dict[str, str]], pmu_name: str, agg_xml: str
+) -> None:
+    """Detect duplicate EventNames and rename collisions as name__0, name__1, ...
+
+    The subgroup-prefix disambiguation in make_event covers the common case.
+    This function is a last-resort safety net for names that could not be
+    disambiguated there (e.g. no usable sampleSubGroup).  The double-underscore
+    suffix is intentionally distinct from any XML field so it is not mistaken
+    for a meaningful part of the metric name.
+    """
+    seen: Dict[str, List[int]] = {}
+    for i, e in enumerate(out):
+        name = e["EventName"]
+        seen.setdefault(name, []).append(i)
+
+    for name, indices in seen.items():
+        if len(indices) <= 1:
+            continue
+        print(
+            f"WARN: {agg_xml}: PMU={pmu_name}: "
+            f"EventName '{name}' collision ({len(indices)} entries); "
+            f"renaming as {name}__0 .. {name}__{len(indices) - 1}",
+            file=sys.stderr,
+        )
+        for ordinal, idx in enumerate(indices):
+            out[idx]["EventName"] = f"{name}__{ordinal}"
+
+
+# ------------------------------
+# main()
+# ------------------------------
+# pylint: disable=too-many-locals,too-many-branches,too-many-statements
+def main(
+    argv: List[str],
+) -> int:
+    """CLI entry point: discover XML files, convert, and write JSON outputs."""
+    ap = argparse.ArgumentParser(
+        description="Convert Intel PMT Aggregator XML to perf JSON (intel_pmt only)"
+    )
+    ap.add_argument(
+        "xml",
+        nargs="?",
+        help="Input PMT Aggregator XML file (optional when using --by-path)",
+    )
+    ap.add_argument(
+        "--by-path",
+        default=None,
+        help=(
+            "Directory to auto-discover PMT XMLs. When used without a "
+            "positional XML, processes all *_aggregator.xml recursively "
+            "and emits one output per directory."
+        ),
+    )
+    ap.add_argument(
+        "--output-dir",
+        default=None,
+        help=(
+            "Directory where JSON output files will be placed. Used "
+            "verbatim (files are written flat by GUID). If omitted, "
+            "the deepest folder name from --by-path is used (lowercased)."
+        ),
+    )
+    ap.add_argument(
+        "--fetch-pmt-repo",
+        action="store_true",
+        help=(
+            "Fetch Intel-PMT repository and use its xml folder when "
+            "local xml/by-path inputs are not provided."
+        ),
+    )
+    ap.add_argument(
+        "--pmt-cache-dir",
+        default="~/.cache/pmtctl",
+        help=(
+            "Cache directory for Intel-PMT repository clone "
+            "(default: ~/.cache/pmtctl)."
+        ),
+    )
+    ap.add_argument(
+        "--refresh-pmt-repo",
+        action="store_true",
+        help=(
+            "Refresh cached Intel-PMT repository before conversion "
+            "(used with --fetch-pmt-repo)."
+        ),
+    )
+    ap.add_argument(
+        "--pmt-repo-url",
+        default=INTEL_PMT_REPO_URL,
+        help=argparse.SUPPRESS,
+    )
+    ap.add_argument("--debug", action="store_true")
+    args = ap.parse_args(argv)
+
+    if args.refresh_pmt_repo and not args.fetch_pmt_repo:
+        print(
+            "ERROR: --refresh-pmt-repo requires --fetch-pmt-repo.",
+            file=sys.stderr,
+        )
+        return 2
+
+    fetched_xml_root: Optional[str] = None
+    if args.fetch_pmt_repo:
+        fetched_xml_root = _fetch_intel_pmt_xml_root(
+            cache_dir=args.pmt_cache_dir,
+            refresh=args.refresh_pmt_repo,
+            debug=args.debug,
+            repo_url=args.pmt_repo_url,
+        )
+        if fetched_xml_root is None:
+            return 2
+        if args.debug:
+            print(f"# fetch: xml root={fetched_xml_root}", file=sys.stderr)
+        if args.by_path is None and args.xml is None:
+            args.by_path = fetched_xml_root
+
+    # ------------------------------
+    # Auto-discovery helpers
+    # ------------------------------
+    def _pick_one(cands, label):
+        """Pick deterministically: shortest path first, then alphabetical."""
+        if not cands:
+            return None
+
+        cands = sorted(cands, key=lambda p: (len(p), p))
+        if args.debug and len(cands) > 1:
+            print(
+                (
+                    f"# by-path: multiple {label} matches, choosing: {cands[0]} ; "
+                    f"others: {cands[1:]}"
+                ),
+                file=sys.stderr,
+            )
+
+        return cands[0]
+
+    def _discover_xmls_by_path(p):
+        """Return (aggregator, common) or (None, None) if not found."""
+        if not p:
+            return (None, None)
+
+        base = p
+        if os.path.isfile(base):
+            base = os.path.dirname(base) or "."
+
+        # First, non-recursive search
+        agg = glob.glob(os.path.join(base, "*_aggregator.xml"))
+        com = glob.glob(os.path.join(base, "*_common.xml"))
+
+        # If any missing, try recursive
+        if not agg or not com:
+            agg = agg or glob.glob(
+                os.path.join(base, "**", "*_aggregator.xml"), recursive=True
+            )
+            com = com or glob.glob(
+                os.path.join(base, "**", "*_common.xml"), recursive=True
+            )
+
+        return (
+            _pick_one(agg, "aggregator"),
+            _pick_one(com, "common"),
+        )
+
+    def _rel_parts_from_root(d: str, root: str) -> List[str]:
+        """Return sanitized relative path segments from root to d."""
+        try:
+            rel = os.path.relpath(os.path.normpath(d), os.path.normpath(root))
+        except ValueError:
+            return []
+        if rel in (".", "") or rel.startswith(".."):
+            return []
+
+        def _seg(s: str) -> str:
+            s = (s or "").strip().lower()
+            s = re.sub(r"[^0-9a-z]+", "-", s)
+            return re.sub(r"-+", "-", s).strip("-")
+
+        out: List[str] = []
+        for seg in rel.split(os.sep):
+            if seg in (".", ".."):
+                continue
+            s = _seg(seg)
+            if s:
+                out.append(s)
+        return out
+
+    def _discover_all_xml_sets_by_path(
+        p: str,
+    ) -> List[Tuple[str, List[str]]]:
+        """Return a list of (aggregator, rel_parts) work items."""
+        if not p:
+            return []
+
+        base = p
+        if os.path.isfile(base):
+            base = os.path.dirname(base) or "."
+
+        # Find every directory that contains an *_aggregator.xml
+        agg_all = glob.glob(
+            os.path.join(base, "**", "*_aggregator.xml"), recursive=True
+        )
+        if not agg_all:
+            # allow the base directory itself
+            agg_all = glob.glob(os.path.join(base, "*_aggregator.xml"))
+
+        dir_to_aggs: Dict[str, List[str]] = {}
+        for a in agg_all:
+            d = os.path.dirname(a) or "."
+            dir_to_aggs.setdefault(d, []).append(a)
+
+        work: List[Tuple[str, List[str]]] = []
+        for d in sorted(dir_to_aggs.keys()):
+            agg = _pick_one(dir_to_aggs[d], "aggregator")
+            if not agg:
+                continue
+
+            rel_parts = _rel_parts_from_root(d, base)
+            work.append((agg, rel_parts))
+
+        return work
+
+    # Determine work items
+    work_items: List[Tuple[str, List[str]]] = []
+
+    if args.by_path and args.xml is None:
+        # Recursive multi-mode
+        work_items = _discover_all_xml_sets_by_path(args.by_path)
+        if args.debug:
+            print(
+                f"# by-path discovered {len(work_items)} aggregator directory(ies)",
+                file=sys.stderr,
+            )
+    else:
+        # Single-mode (backwards compatible): by-path can auto-fill missing files
+        if args.by_path:
+            a_auto, _ = _discover_xmls_by_path(args.by_path)
+            if args.xml is None:
+                args.xml = a_auto
+            if args.debug:
+                print(
+                    f"# by-path resolved: xml={args.xml}",
+                    file=sys.stderr,
+                )
+
+        if args.xml:
+            rel_parts: List[str] = []
+            if args.by_path:
+                rel_parts = _rel_parts_from_root(
+                    os.path.dirname(args.xml) or ".", args.by_path
+                )
+            work_items = [(args.xml, rel_parts)]
+
+    # Sanity check: we must have at least one aggregator XML to process
+    if not work_items:
+        print(
+            (
+                "ERROR: No aggregator XML specified or discovered. "
+                "Provide a file or use --by-path."
+            ),
+            file=sys.stderr,
+        )
+        return 2
+
+    # Determine output directory.
+    #
+    # If --output-dir is given, use it verbatim; outputs are written flat
+    # by GUID (pmt_ep_<guid>.json). If omitted, fall back to a folder
+    # named after the deepest --by-path directory (lowercased).
+    output_dir: Optional[str] = None
+    if args.output_dir:
+        output_dir = args.output_dir
+    elif args.by_path:
+        by_path = args.by_path
+        if os.path.isfile(by_path):
+            by_path = os.path.dirname(by_path) or "."
+        deepest_folder = os.path.basename(os.path.normpath(by_path))
+        output_dir = (
+            "jsons" if deepest_folder.lower() == "xml" else deepest_folder.lower()
+        )
+
+    # Create output directory if specified
+    if output_dir:
+        os.makedirs(output_dir, exist_ok=True)
+        if args.debug:
+            print(f"# output directory: {output_dir}", file=sys.stderr)
+
+    # Copy pmt.xml from the Intel-PMT xml/ folder into the output directory
+    if output_dir:
+        pmt_xml_src = _find_pmt_xml(fetched_xml_root, args.by_path)
+        if pmt_xml_src:
+            pmt_xml_dst = os.path.join(output_dir, "pmt.xml")
+            shutil.copyfile(pmt_xml_src, pmt_xml_dst)
+            if args.debug:
+                print(
+                    f"# copied {pmt_xml_src} -> {pmt_xml_dst}",
+                    file=sys.stderr,
+                )
+            _write_pmt_guids_json(pmt_xml_src, output_dir, debug=args.debug)
+        elif args.debug:
+            print("# pmt.xml not found; skipping copy", file=sys.stderr)
+
+    # Process each discovered set
+    any_failed = False
+    written_by_guid: Dict[int, str] = {}
+
+    for agg_xml, rel_parts in work_items:
+        try:
+            # Load the main aggregator XML
+            root = parse_xml(agg_xml)
+            guid = get_guid(root)
+
+            pmu_name = f"pmt_ep_{guid:08x}"
+            base_filename = f"{pmu_name}.json"
+            out_filename = (
+                os.path.join(output_dir, base_filename) if output_dir else base_filename
+            )
+
+            # GUIDs are globally unique to a telemetry layout; a duplicate
+            # across aggregators indicates a source-data bug, not something
+            # to silently paper over by namespacing the output.
+            prior = written_by_guid.get(guid)
+            if prior is not None:
+                raise ValueError(
+                    f"duplicate GUID 0x{guid:08x} from {agg_xml}; "
+                    f"previously emitted by {prior}"
+                )
+
+            samples = parse_samples(root)
+            ctr = Counters(total=len(samples))
+
+            # Per-aggregator platform group derived from its location under
+            # the discovery root (e.g. "alderlake-s"). Falls back to the
+            # by-path basename for the single-mode case.
+            platform_group: Optional[str] = None
+            if rel_parts:
+                platform_group = rel_parts[0].upper()
+            elif args.by_path:
+                by_path = args.by_path
+                if os.path.isfile(by_path):
+                    by_path = os.path.dirname(by_path) or "."
+                deepest_folder = os.path.basename(os.path.normpath(by_path))
+                if deepest_folder and deepest_folder.lower() != "xml":
+                    platform_group = deepest_folder.upper()
+
+            out = []
+
+            # Pre-pass: count bare sample-name occurrences within this
+            # aggregator so make_event can apply lazy-prefix disambiguation.
+            name_counts: Dict[str, int] = {}
+            for s in samples:
+                name_counts[s.sample_name] = name_counts.get(s.sample_name, 0) + 1
+
+            for s in samples:
+                try:
+                    # Build event
+                    e = make_event(
+                        s,
+                        pmu_name,
+                        name_counts,
+                        platform_group=platform_group,
+                    )
+
+                    out.append(e)
+                    ctr.emitted += 1
+                except Exception as ex:  # pylint: disable=broad-exception-caught
+                    ctr.skipped += 1
+                    print(
+                        (
+                            f"WARN: skipping {s.sample_name} "
+                            f"(sampleID={s.sample_id}): {ex}"
+                        ),
+                        file=sys.stderr,
+                    )
+                    traceback.print_exc()
+
+            # Last-resort: detect and rename any duplicate EventNames that
+            # subgroup-prefix disambiguation could not resolve.
+            _resolve_duplicate_event_names(out, pmu_name, agg_xml)
+
+            # Write events JSON
+            with open(out_filename, "w", encoding="utf-8") as f:
+                json.dump(out, f, indent=2)
+                f.write("\n")
+
+            written_by_guid[guid] = agg_xml
+            print(f"# wrote {out_filename}", file=sys.stderr)
+            print(
+                (
+                    f"# PMU={pmu_name} total={ctr.total} "
+                    f"emitted={ctr.emitted} skipped={ctr.skipped}"
+                ),
+                file=sys.stderr,
+            )
+
+        except Exception:  # pylint: disable=broad-exception-caught
+            any_failed = True
+            print(f"ERROR: failed processing aggregator={agg_xml}", file=sys.stderr)
+
+    return 1 if any_failed else 0
+
+
+if __name__ == "__main__":
+    sys.exit(main(sys.argv[1:]))
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [PATCH 16/17] tools/arch/x86/pmtctl: Add README.md
  2026-05-26  1:46 [PATCH 00/17] tools/arch/x86/pmtctl: Add Intel PMT command-line tool David E. Box
                   ` (14 preceding siblings ...)
  2026-05-26  1:47 ` [PATCH 15/17] tools/arch/x86/pmtctl: Add pmtxml2json conversion tool David E. Box
@ 2026-05-26  1:47 ` David E. Box
  2026-05-26  1:47 ` [PATCH 17/17] tools/arch/x86/pmtctl: Add man page David E. Box
  16 siblings, 0 replies; 25+ messages in thread
From: David E. Box @ 2026-05-26  1:47 UTC (permalink / raw)
  To: linux-kernel, david.e.box, ilpo.jarvinen, andriy.shevchenko,
	platform-driver-x86

Add the user-facing README documenting pmtctl usage, supported
metric sources, and command-line options.

Signed-off-by: David E. Box <david.e.box@linux.intel.com>
---
 tools/arch/x86/pmtctl/README.md | 276 ++++++++++++++++++++++++++++++++
 1 file changed, 276 insertions(+)
 create mode 100644 tools/arch/x86/pmtctl/README.md

diff --git a/tools/arch/x86/pmtctl/README.md b/tools/arch/x86/pmtctl/README.md
new file mode 100644
index 000000000000..0827ff20fc86
--- /dev/null
+++ b/tools/arch/x86/pmtctl/README.md
@@ -0,0 +1,276 @@
+# pmtctl
+Intel Platform Monitoring Technology (PMT) command-line tool for Linux
+
+## Overview
+
+pmtctl is a small command-line utility to query Intel Platform Monitoring
+Technology (PMT) metrics via the telem telemetry interface. It can load
+metric definitions from JSON files or use built-in definitions.
+
+The project is split into a CLI frontend and a reusable library:
+
+- `src/`: CLI frontend (`pmtctl`, commands, formatting, pager)
+- `lib/`: reusable PMT library implementation (`libpmtctl_core.a`)
+   - transport/enumeration/binding core
+   - metric DB container operations
+   - built-in and runtime-JSON metric-definition providers
+
+Core ownership model:
+
+- Core library owns PMT source selection, device enumeration, telem reads,
+   metric DB container operations, and both metric-definition providers
+   (built-in compiled defs and runtime JSON parsing/loading).
+
+## Prerequisites
+
+- A C toolchain (gcc/clang)
+- make
+- python3 (for JSON metric generation)
+- libjansson (JSON parsing library)
+- Linux (dev/test on systems with `/sys/bus/auxiliary/drivers/pmt_telemetry` for telem)
+
+## Build Instructions
+
+The built-in metrics pipeline has two opt-in stages, then the build:
+
+```text
+Intel-PMT repo (XML)  --[pmtxml2json.py]-->  defs/*.json
+                                                |
+                                   [gen_builtin_defs.py]
+                                                v
+                                generated/builtin_defs.c  -->  make  -->  pmtctl
+```
+
+Each stage is a separate Make target so a routine `make` never hits the
+network and never re-runs codegen unnecessarily.
+
+### Option 1: Build with built-in metrics (recommended)
+
+First-time setup (the `defs-json-fetch` step git clones the Intel-PMT repo into
+`~/.cache/pmtctl` and converts every aggregator XML to perf-style JSON):
+
+```bash
+make defs-json-fetch  # git clone Intel-PMT (cached) and produce defs/*.json
+make defs             # JSON -> generated/builtin_defs.c
+make                  # build pmtctl with built-in metric definitions
+```
+
+If you already have JSON files of your own, drop them under `defs/`
+and skip straight to `make defs && make`.
+
+To update from upstream Intel-PMT later:
+
+```bash
+make defs-json-pull   # git pull the cache and regenerate JSON
+make defs             # regenerate generated/builtin_defs.c
+make                  # rebuild
+```
+
+### Option 2: Build without built-in metrics
+
+If you don't have metric JSONs or prefer to load them at runtime:
+
+```bash
+make
+```
+
+This uses the empty stub (`lib/builtin_defs_empty.c`); load metrics at
+runtime with `-J/--json-file`.
+
+### Make targets (summary)
+
+| Target              | Purpose                                            |
+|---------------------|----------------------------------------------------|
+| `all` (default)     | Build `pmtctl` and `libpmtctl_core.a`              |
+| `defs-json-fetch`   | git clone Intel-PMT (cached) and emit `defs/*.json` |
+| `defs-json-pull`    | Same as above, but git pull the cache first        |
+| `defs`              | Generate `generated/builtin_defs.c` from JSON      |
+| `defs-clean`        | Remove `defs/` and `generated/builtin_defs.c`      |
+| `clean`             | Remove build outputs (keeps `defs/` intact)        |
+| `install` /         | Install or remove `pmtctl`, lib, headers, .pc      |
+| `uninstall`         |                                                    |
+
+Useful variables (override on the make command line):
+
+- `PYTHON` — Python interpreter (default `python3`)
+- `DEFS_DIR` — JSON output / input directory (default `defs`)
+- `GENERATED_DIR` — codegen output directory (default `generated`)
+- `PMT_CACHE_DIR` — Intel-PMT clone cache (default `~/.cache/pmtctl`)
+
+### Library artifacts produced
+
+Building `make` also builds the static library artifact under
+`build/<build-type>/lib/`:
+
+- `libpmtctl_core.a`
+
+## Usage (examples)
+
+General form:
+
+```text
+pmtctl [global options] <command> [command options]
+```
+
+Global options:
+
+- `-h, --help`              Show help and exit
+- `-V, --version`           Show version and exit
+- `-b, --source <mode>`     Data source: `telem` (pmu support is pending upstream driver availability)
+- `-J, --json-file <path>`  Path to metrics JSON (file or directory) — loads metrics at runtime
+- `-d, --device <selector>` Restrict to single endpoint (guid or ep name)
+
+Note: Metrics can be loaded in two ways:
+1. **Built-in** (compiled during build) — see "Build with JSON metrics" above
+2. **Runtime** (loaded via `-J` option) — useful for testing or using different metric sets
+
+Commands:
+
+- `list`  — list known metrics and/or discovered devices
+- `stat`  — sample/stream metrics over time (similar to `perf stat`)
+
+Examples (basic):
+
+```bash
+# list all metrics with built-in or runtime-loaded definitions
+./pmtctl list
+
+# list with GUIDs (useful for debugging metric definitions)
+./pmtctl list --guids
+
+# load metrics from a directory at runtime
+./pmtctl -J /path/to/metrics/ list --guids
+
+# load metrics from a single JSON file at runtime
+./pmtctl -J /path/to/metrics.json list --guids
+
+# list devices only
+./pmtctl list --devices
+
+# keep the last pager screen on exit (useful before running stat)
+./pmtctl list -X
+
+# sample a named metric at the default 1 s interval
+sudo ./pmtctl stat -e temp_socket
+
+# sample at 500 ms intervals
+sudo ./pmtctl stat -e socket_power --interval 500
+
+# take a single snapshot and exit
+sudo ./pmtctl stat -e fw_version_0 --once
+
+# sample multiple metrics (one line per metric per sample)
+sudo ./pmtctl stat -e temp_socket -e socket_power --vertical
+
+# restrict to a specific device by GUID
+sudo ./pmtctl -d guid=22806802 stat -e temp_core0
+
+# read raw sample data by sample id (no metric definitions required)
+sudo ./pmtctl stat --raw id=0,lsb=0,msb=31 --count 2
+
+# read multiple raw samples with bit slicing
+sudo ./pmtctl stat --raw id=2,msb=7 --raw id=3,lsb=8,msb=15
+
+# output values in hex
+sudo ./pmtctl stat -e fw_version_0 --once --hex
+```
+
+See `pmtctl_cli_spec.txt` in the repository root for the full CLI
+specification and additional example usage.
+
+## Developer Notes
+
+### Adding new C source files
+
+If you add new `.c` files:
+
+- CLI/frontend code goes in `src/` and is added to top-level `Makefile` `SRC`.
+- Reusable library core code goes in `lib/` and is added to `lib/Makefile`
+   `CORE_SRC`.
+- Built-in provider code goes in `lib/` and is added to `lib/Makefile`
+   `BUILTIN_OBJ` inputs.
+- JSON provider code goes in `lib/` and is added to `lib/Makefile` `JSON_OBJ`
+   inputs.
+
+This keeps transport and provider policy boundaries explicit.
+
+### Regenerating metrics from JSON
+
+The Makefile recursively tracks `*.json` under `defs/` (so per-platform
+subdirectories produced by `pmtxml2json.py` are picked up automatically):
+
+```bash
+# Re-generate after editing or adding JSON under defs/
+make defs
+
+# Throw away converted JSON + codegen output (does NOT touch the
+# Intel-PMT cache under ~/.cache/pmtctl)
+make defs-clean
+```
+
+`make clean` intentionally does *not* remove `defs/` — the XML->JSON step
+can be slow, so use `make defs-clean` when you really want to start over.
+
+The `scripts/gen_builtin_defs.py` script also runs standalone and accepts
+both files and directories:
+
+```bash
+python3 scripts/gen_builtin_defs.py defs/ > generated/builtin_defs.c
+```
+
+## PMTXML2JSON Conversion
+
+`scripts/pmtxml2json.py` turns Intel PMT XML definitions into pmtctl /
+perf-style JSON. The `make defs-json-fetch` target wraps the common case;
+invoke the script directly for one-off workflows:
+
+```bash
+# convert all XML groups under a local Intel-PMT xml tree
+python3 scripts/pmtxml2json.py --by-path /path/to/Intel-PMT/xml --output-dir defs
+
+# auto-fetch Intel-PMT into cache, then convert from the fetched xml directory
+python3 scripts/pmtxml2json.py --fetch-pmt-repo --output-dir defs
+
+# refresh the cached Intel-PMT repository before converting
+python3 scripts/pmtxml2json.py --fetch-pmt-repo --refresh-pmt-repo --output-dir defs
+```
+
+Fetch/cache behavior:
+
+- `--fetch-pmt-repo` is opt-in and preserves existing local-path workflows.
+- Default cache location is `~/.cache/pmtctl` (override with `--pmt-cache-dir`).
+- `--refresh-pmt-repo` only works with `--fetch-pmt-repo` and updates the cache explicitly.
+
+If `--fetch-pmt-repo` is used without a positional XML file and without
+`--by-path`, the converter automatically uses the fetched Intel-PMT `xml/`
+directory as the discovery root.
+
+## Install and Export
+
+Minimal install/export support is available for the CLI and static libraries:
+
+```bash
+# Stage install output
+make DESTDIR=$PWD/stage install
+```
+
+Installs:
+
+- `bin/pmtctl`
+- `lib/libpmtctl_core.a`
+- `include/pmtctl/{pmtctl.h,metrics_db.h,device.h}`
+- `lib/pkgconfig/libpmtctl-core.pc`
+
+Uninstall:
+
+```bash
+make DESTDIR=$PWD/stage uninstall
+```
+
+## Testing
+
+Stat commands require elevated privileges to read telemetry files:
+
+```bash
+sudo ./pmtctl stat -e <metric_name>
+```
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [PATCH 17/17] tools/arch/x86/pmtctl: Add man page
  2026-05-26  1:46 [PATCH 00/17] tools/arch/x86/pmtctl: Add Intel PMT command-line tool David E. Box
                   ` (15 preceding siblings ...)
  2026-05-26  1:47 ` [PATCH 16/17] tools/arch/x86/pmtctl: Add README.md David E. Box
@ 2026-05-26  1:47 ` David E. Box
  16 siblings, 0 replies; 25+ messages in thread
From: David E. Box @ 2026-05-26  1:47 UTC (permalink / raw)
  To: linux-kernel, david.e.box, ilpo.jarvinen, andriy.shevchenko,
	platform-driver-x86

Add a man page for pmtctl describing the command-line interface, the
list and stat subcommands, common options, and output formats.

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  |   6 +
 tools/arch/x86/pmtctl/README.md |   3 +
 tools/arch/x86/pmtctl/pmtctl.8  | 317 ++++++++++++++++++++++++++++++++
 3 files changed, 326 insertions(+)
 create mode 100644 tools/arch/x86/pmtctl/pmtctl.8

diff --git a/tools/arch/x86/pmtctl/Makefile b/tools/arch/x86/pmtctl/Makefile
index d55819372f79..a8e76e84d7c5 100644
--- a/tools/arch/x86/pmtctl/Makefile
+++ b/tools/arch/x86/pmtctl/Makefile
@@ -152,6 +152,7 @@ $(GENERATED_DIR)/builtin_defs.c: $(GEN_DEFS_SCRIPT) $(DEFS_JSON)
 # Install settings
 PREFIX ?= /usr/local
 DESTDIR ?=
+MANDIR ?= $(PREFIX)/share/man
 
 
 .PHONY: install uninstall install-lib install-headers install-pkgconfig uninstall-lib uninstall-headers uninstall-pkgconfig
@@ -159,7 +160,10 @@ DESTDIR ?=
 install: $(TARGET) install-lib install-headers install-pkgconfig
 	install -d $(DESTDIR)$(PREFIX)/bin
 	install -m 0755 $(TARGET) $(DESTDIR)$(PREFIX)/bin
+	install -d $(DESTDIR)$(MANDIR)/man8
+	install -m 0644 pmtctl.8 $(DESTDIR)$(MANDIR)/man8/pmtctl.8
 	@echo "Installed $(TARGET) to $(DESTDIR)$(PREFIX)/bin/"
+	@echo "Installed pmtctl.8 to $(DESTDIR)$(MANDIR)/man8/"
 
 install-lib:
 	$(MAKE) -C $(LIBDIR) BUILD=$(BUILD) PREFIX=$(PREFIX) DESTDIR=$(DESTDIR) install-lib
@@ -172,10 +176,12 @@ install-pkgconfig:
 
 uninstall:
 	rm -f $(DESTDIR)$(PREFIX)/bin/$(TARGET)
+	rm -f $(DESTDIR)$(MANDIR)/man8/pmtctl.8
 	$(MAKE) -C $(LIBDIR) BUILD=$(BUILD) PREFIX=$(PREFIX) DESTDIR=$(DESTDIR) uninstall-lib
 	$(MAKE) -C $(LIBDIR) BUILD=$(BUILD) PREFIX=$(PREFIX) DESTDIR=$(DESTDIR) uninstall-headers
 	$(MAKE) -C $(LIBDIR) BUILD=$(BUILD) PREFIX=$(PREFIX) DESTDIR=$(DESTDIR) uninstall-pkgconfig
 	@echo "Removed $(DESTDIR)$(PREFIX)/bin/$(TARGET) (if present)"
+	@echo "Removed $(DESTDIR)$(MANDIR)/man8/pmtctl.8 (if present)"
 defs: $(GENERATED_DIR)/builtin_defs.c
 	@if [ -f $(GENERATED_DIR)/builtin_defs.c ]; then \
 		echo "Generated defs in $(GENERATED_DIR)/builtin_defs.c"; \
diff --git a/tools/arch/x86/pmtctl/README.md b/tools/arch/x86/pmtctl/README.md
index 0827ff20fc86..865c7af2c266 100644
--- a/tools/arch/x86/pmtctl/README.md
+++ b/tools/arch/x86/pmtctl/README.md
@@ -260,6 +260,9 @@ Installs:
 - `lib/libpmtctl_core.a`
 - `include/pmtctl/{pmtctl.h,metrics_db.h,device.h}`
 - `lib/pkgconfig/libpmtctl-core.pc`
+- `share/man/man8/pmtctl.8`
+
+Override `MANDIR` (default `$(PREFIX)/share/man`) to relocate the man page.
 
 Uninstall:
 
diff --git a/tools/arch/x86/pmtctl/pmtctl.8 b/tools/arch/x86/pmtctl/pmtctl.8
new file mode 100644
index 000000000000..f79480f9febf
--- /dev/null
+++ b/tools/arch/x86/pmtctl/pmtctl.8
@@ -0,0 +1,317 @@
+.\" SPDX-License-Identifier: GPL-2.0-only
+.TH PMTCTL 8 "May 2026" "Linux" "System Administration"
+.SH NAME
+pmtctl \- query Intel Platform Monitoring Technology (PMT) telemetry metrics
+.SH SYNOPSIS
+.B pmtctl
+.RI [ "global options" ]
+.B list
+.RI [ options ]
+.br
+.B pmtctl
+.RI [ "global options" ]
+.B stat
+.RI [ options ]
+.BR \-e\ \fImetric\fR[,\fImetric\fR...]\ ...
+.br
+.B pmtctl
+.RI [ "global options" ]
+.B stat
+.RI [ options ]
+.BR \-\-raw\ id= \fIN\fR [,lsb= \fIN\fR ][,msb= \fIN\fR ]\ ...
+.SH DESCRIPTION
+.B pmtctl
+is a command-line tool for inspecting and sampling Intel Platform
+Monitoring Technology (PMT) telemetry exposed through the kernel
+.B pmt_telemetry
+auxiliary driver
+.RB ( /sys/bus/auxiliary/drivers/pmt_telemetry ).
+.PP
+Metric definitions can come from one of two sources:
+.IP \(bu 2
+Built-in definitions compiled into the binary at build time from
+the Intel PMT XML descriptions (used when
+.B \-J
+is not given).
+.IP \(bu 2
+A runtime JSON file or directory supplied with
+.BR \-J / \-\-json\-file .
+.PP
+Most operations require access to PMT telemetry files and therefore
+typically need to be run as
+.BR root .
+.PP
+.B pmtctl
+also installs a reusable C library
+.RB ( libpmtctl_core )
+and public headers under
+.IR <prefix>/include/pmtctl/ ;
+see
+.B SEE ALSO
+below.
+.SH GLOBAL OPTIONS
+.TP
+.BR \-h ", " \-\-help
+Show help and exit.
+.TP
+.BR \-V ", " \-\-version
+Show version and exit.
+.TP
+.BR \-J ", " "\-\-json\-file " \fIpath\fR
+Load metric definitions from the given JSON file or directory at
+runtime. When omitted, the built-in definitions (if any) compiled
+into the binary are used.
+.TP
+.BR \-d ", " "\-\-device " \fIselector\fR
+Restrict operation to a single PMT endpoint. The selector takes one
+of the following forms:
+.RS
+.IP \fBguid=\fIhex\fR
+Match by 32-bit PMT GUID, e.g. \fBguid=27971628\fR.
+.IP \fBep=\fIname\fR
+Match by endpoint name as reported by \fBpmtctl list \-\-devices\fR.
+.RE
+.IP
+The selector may also be supplied after the command name as a
+fallback; if both are given, the global value takes precedence.
+.TP
+.BR \-q ", " \-\-quiet
+Suppress non-essential messages.
+.TP
+.B \-\-debug
+Enable debug logging on stderr.
+.SH COMMANDS
+.SS list
+.B pmtctl list
+.RI [ options ]
+.PP
+List PMT metrics known to
+.B pmtctl
+and, optionally, the PMT devices/endpoints discovered on the running
+system. Without options, metrics are grouped by GUID.
+.TP
+.BR \-h ", " \-\-help
+Show help for
+.B list
+and exit.
+.TP
+.BR \-d ", " "\-\-device " \fIselector\fR
+Restrict the listing to a single endpoint. See
+.B GLOBAL OPTIONS
+above for selector syntax.
+.TP
+.B \-\-devices
+List discovered PMT devices/endpoints only; do not list metric
+definitions.
+.TP
+.B \-\-guids
+Emit a report that shows the GUIDs present in the loaded metric
+definitions, the GUIDs discovered on the system, and the
+intersection of the two.
+.TP
+.BR \-X ", " \-\-keep
+Keep the last pager screen visible after quitting. Useful when
+the output will be referenced immediately (for example, to copy a
+metric name for use with
+.BR "pmtctl stat" ).
+.SS stat
+.B pmtctl stat
+.RI [ options ]
+.B \-e
+.IR metric [,\fImetric\fR...]
+\&...
+.br
+.B pmtctl stat
+.RI [ options ]
+.B \-\-raw
+.BI id= N
+.RI [,\fBlsb=\fP N ][,\fBmsb=\fP N ]
+\&...
+.PP
+Sample one or more PMT metrics (or raw sample values) over time, in
+a manner similar to
+.BR "perf stat" .
+On a metric read failure the value is printed as
+.BR NaN .
+.TP
+.BR \-h ", " \-\-help
+Show help for
+.B stat
+and exit.
+.TP
+.BR \-e ", " "\-\-event " \fIspec\fR
+Metric name, or a comma-separated list of metric names. May be
+repeated. When more than one metric is requested, the
+.B \-\-vertical
+output mode is required. Mutually exclusive with
+.BR \-\-raw .
+.TP
+.BR \-d ", " "\-\-device " \fIselector\fR
+Restrict to a single device/endpoint. See
+.B GLOBAL OPTIONS
+above for selector syntax.
+.TP
+.BR \-i ", " "\-\-interval " \fIms\fR
+Sampling interval in milliseconds. Must be greater than 0. Default
+is 1000 ms.
+.TP
+.BR \-c ", " "\-\-count " \fIN\fR
+Number of samples to take. Must be greater than 0. Default is to
+sample indefinitely.
+.TP
+.B \-\-once
+Take a single snapshot and exit (equivalent to
+.B "\-\-count 1"
+with no inter-sample delay).
+.TP
+.B \-\-raw \fBid=\fIN\fR[\fB,lsb=\fIN\fR][\fB,msb=\fIN\fR]
+Read raw sample values by sample
+.B id
+with optional bit-field slicing. Constraints:
+.B id
+must be \(>= 0;
+.B lsb
+defaults to 0 and
+.B msb
+defaults to 63; both must satisfy
+0 \(<=
+.B lsb
+\(<=
+.B msb
+\(<= 63. May be specified multiple times. Mutually exclusive with
+.BR \-e .
+.TP
+.BR \-\-header ", " \-\-no\-header
+Force header printing on or off.
+.TP
+.B \-\-hex
+Print sample values in hexadecimal.
+.TP
+.B \-\-vertical
+One line per metric per sample (sample index, time in ms, device,
+metric, value). Required when more than one metric is selected.
+.SH EXAMPLES
+List all known metrics (built-in or loaded via \fB\-J\fR):
+.PP
+.RS 4
+.nf
+pmtctl list
+.fi
+.RE
+.PP
+Show the GUID overlap between definitions and the running system:
+.PP
+.RS 4
+.nf
+pmtctl list \-\-guids
+.fi
+.RE
+.PP
+Load definitions from a directory at runtime and list devices only:
+.PP
+.RS 4
+.nf
+pmtctl \-J /path/to/metrics/ list \-\-devices
+.fi
+.RE
+.PP
+Keep the last screen visible on exit (handy before running stat):
+.PP
+.RS 4
+.nf
+pmtctl list \-X
+.fi
+.RE
+.PP
+Sample a single metric every 500 ms until interrupted:
+.PP
+.RS 4
+.nf
+sudo pmtctl stat \-e socket_power \-\-interval 500
+.fi
+.RE
+.PP
+Take a single snapshot and exit:
+.PP
+.RS 4
+.nf
+sudo pmtctl stat \-e fw_version_0 \-\-once
+.fi
+.RE
+.PP
+Take a single snapshot of a metric on a specific GUID:
+.PP
+.RS 4
+.nf
+sudo pmtctl stat \-e temp_core0 \-d guid=27971628 \-\-once
+.fi
+.RE
+.PP
+Sample multiple metrics (requires \fB\-\-vertical\fR):
+.PP
+.RS 4
+.nf
+sudo pmtctl stat \-e temp_socket,socket_power \-\-vertical \-c 5
+.fi
+.RE
+.PP
+Print a value in hexadecimal:
+.PP
+.RS 4
+.nf
+sudo pmtctl stat \-e fw_version_0 \-\-once \-\-hex
+.fi
+.RE
+.PP
+Read raw sample values, slicing bit fields out of two samples:
+.PP
+.RS 4
+.nf
+sudo pmtctl stat \-\-raw id=2,msb=7 \-\-raw id=3,lsb=8,msb=15
+.fi
+.RE
+.SH FILES
+.TP
+.B /sys/bus/auxiliary/drivers/pmt_telemetry
+Sysfs entry point for the kernel
+.B pmt_telemetry
+auxiliary driver that
+.B pmtctl
+reads from.
+.TP
+.IR <prefix>/include/pmtctl/
+Public C headers for
+.BR libpmtctl_core .
+.TP
+.IR <prefix>/lib/pkgconfig/libpmtctl\-core.pc
+pkg-config file for
+.BR libpmtctl_core .
+.SH EXIT STATUS
+.TP
+.B 0
+Success.
+.TP
+.B 1
+Usage or user error (for example, invalid command-line options).
+.TP
+.B 2
+Device, system, or runtime error (for example, the PMT telemetry
+driver is not loaded or a read failed).
+.SH SEE ALSO
+.BR perf (1),
+.BR turbostat (8)
+.PP
+Intel Platform Monitoring Technology XML metric definitions:
+.UR https://github.com/intel/Intel-PMT
+.UE .
+.SH AUTHORS
+.B pmtctl
+was written by David E. Box <david.e.box@linux.intel.com>.
+.SH REPORTING BUGS
+Report bugs via the Linux kernel mailing lists; see the
+.B MAINTAINERS
+file in the kernel source tree for current maintainers of the
+Intel PMT subsystem.
+.SH COPYRIGHT
+.B pmtctl
+is licensed under GPL-2.0-only.
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 25+ messages in thread

* Re: [PATCH 02/17] tools/arch/x86/pmtctl: Add libpmtctl shared type enumerations
  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
  0 siblings, 0 replies; 25+ messages in thread
From: Ilpo Järvinen @ 2026-05-26  9:20 UTC (permalink / raw)
  To: David E. Box; +Cc: LKML, Andy Shevchenko, platform-driver-x86

On Mon, 25 May 2026, David E. Box wrote:

> Add pmtctl_types.h to centralize shared libpmtctl type definitions used
> across both the public and internal library interfaces.
> 
> This provides a common definition point for core library-facing types,
> including device backend classification and logging verbosity levels,
> avoiding duplication across API boundaries.
> 
> Assisted-by: GitHub-Copilot:claude-sonnet-4.6
> Signed-off-by: David E. Box <david.e.box@linux.intel.com>
> ---
>  tools/arch/x86/pmtctl/include/lib/pmtctl_types.h | 16 ++++++++++++++++
>  1 file changed, 16 insertions(+)
>  create mode 100644 tools/arch/x86/pmtctl/include/lib/pmtctl_types.h
> 
> diff --git a/tools/arch/x86/pmtctl/include/lib/pmtctl_types.h b/tools/arch/x86/pmtctl/include/lib/pmtctl_types.h
> new file mode 100644
> index 000000000000..8e3bf5a173c4
> --- /dev/null
> +++ b/tools/arch/x86/pmtctl/include/lib/pmtctl_types.h
> @@ -0,0 +1,16 @@
> +/* SPDX-License-Identifier: GPL-2.0-only */
> +#ifndef PMTCTL_TYPES_H
> +#define PMTCTL_TYPES_H
> +
> +enum pmt_device_type {
> +	PMT_DEVICE_TELEM

Missing comma.

> +};
> +
> +enum pmtctl_log_level {
> +	PMTCTL_LOG_ERROR = 0,
> +	PMTCTL_LOG_WARN,
> +	PMTCTL_LOG_INFO,
> +	PMTCTL_LOG_DEBUG,
> +};
> +
> +#endif
> 

-- 
 i.


^ permalink raw reply	[flat|nested] 25+ messages in thread

* Re: [PATCH 03/17] tools/arch/x86/pmtctl: Add libpmtctl internal logging and utility functions
  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
  0 siblings, 0 replies; 25+ messages in thread
From: Ilpo Järvinen @ 2026-05-26  9:59 UTC (permalink / raw)
  To: David E. Box; +Cc: LKML, Andy Shevchenko, platform-driver-x86

On Mon, 25 May 2026, David E. Box wrote:

> Add the internal logging infrastructure and shared utility helpers used by
> libpmtctl_core.
> 
> The logging layer provides consistent level-filtered diagnostics across the
> library, while the utility helpers centralize common sysfs access, string
> handling, and scoped resource cleanup functionality used across backend
> implementations.
> 
> Logging levels mirror the public pmtctl_log_level enum and can be used to
> control library verbosity.
> 
> Assisted-by: GitHub-Copilot:claude-sonnet-4.6
> Signed-off-by: David E. Box <david.e.box@linux.intel.com>
> ---
>  tools/arch/x86/pmtctl/include/lib/common.h   |  34 ++++
>  tools/arch/x86/pmtctl/include/lib/log.h      |  80 ++++++++
>  tools/arch/x86/pmtctl/include/lib/pmt_guid.h |  63 ++++++
>  tools/arch/x86/pmtctl/lib/common.c           | 178 +++++++++++++++++
>  tools/arch/x86/pmtctl/lib/log.c              |  80 ++++++++
>  tools/arch/x86/pmtctl/lib/pmt_guid.c         | 200 +++++++++++++++++++
>  6 files changed, 635 insertions(+)
>  create mode 100644 tools/arch/x86/pmtctl/include/lib/common.h
>  create mode 100644 tools/arch/x86/pmtctl/include/lib/log.h
>  create mode 100644 tools/arch/x86/pmtctl/include/lib/pmt_guid.h
>  create mode 100644 tools/arch/x86/pmtctl/lib/common.c
>  create mode 100644 tools/arch/x86/pmtctl/lib/log.c
>  create mode 100644 tools/arch/x86/pmtctl/lib/pmt_guid.c
> 
> diff --git a/tools/arch/x86/pmtctl/include/lib/common.h b/tools/arch/x86/pmtctl/include/lib/common.h
> new file mode 100644
> index 000000000000..cf1540326ec6
> --- /dev/null
> +++ b/tools/arch/x86/pmtctl/include/lib/common.h
> @@ -0,0 +1,34 @@
> +/* SPDX-License-Identifier: GPL-2.0-only */
> +#ifndef PMTCTL_COMMON_H
> +#define PMTCTL_COMMON_H
> +
> +#include <stdlib.h>
> +#include <stdio.h>
> +#include <stdint.h>
> +
> +#ifndef __GNUC__
> +#error "pmtctl: cleanup helpers require GCC/Clang (__GNUC__)."
> +#endif
> +
> +static inline void freep(void *p)
> +{
> +	void **ptr = (void **)p;
> +
> +	if (*ptr) {
> +		free(*ptr);
> +		*ptr = NULL;
> +	}
> +}
> +
> +#define auto_free __attribute__((cleanup(freep)))
> +
> +#ifndef ARRAY_SIZE
> +#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
> +#endif
> +
> +char *xstrdup(const char *s);
> +int read_attr_text(const char *dir, const char *name, char *buf, size_t buflen);
> +int read_optional_int_attr(const char *devpath, const char *attr, int *out_id);
> +int read_int_attr(const char *devpath, const char *attr, int *out_id);
> +int read_u32_hex_attr(const char *devpath, const char *attr, uint32_t *out, int err_invalid);
> +#endif
> diff --git a/tools/arch/x86/pmtctl/include/lib/log.h b/tools/arch/x86/pmtctl/include/lib/log.h
> new file mode 100644
> index 000000000000..32f4de05f9de
> --- /dev/null
> +++ b/tools/arch/x86/pmtctl/include/lib/log.h
> @@ -0,0 +1,80 @@
> +/* SPDX-License-Identifier: GPL-2.0-only */
> +#ifndef PMTCTL_LOG_H
> +#define PMTCTL_LOG_H
> +
> +#include <linux/compiler.h>
> +#include "pmtctl_types.h"
> +
> +#define PMTCTL_EXIT_USER    1
> +#define PMTCTL_EXIT_SYSTEM  2
> +#define PMTCTL_EXIT_BUG     3
> +
> +enum {
> +	PMTCTL_ERR_PARSE = 1,   /* malformed JSON, invalid numbers, missing fields */
> +	PMTCTL_ERR_NOTFOUND,   /* metric/device/group not found */
> +	PMTCTL_ERR_BAD_ARG,
> +	PMTCTL_ERR_CMD_PARSE,
> +	PMTCTL_ERR_CMD_LIST,
> +	PMTCTL_ERR_CMD_STAT,
> +	PMTCTL_ERR_NOMETRICS,
> +	PMTCTL_ERR_METRICS,
> +	PMTCTL_ERR_INVALID,   /* bad selector, bad arguments, bad config */
> +	PMTCTL_ERR_DEVICE,   /* device mismatch or device internal failure */
> +	PMTCTL_ERR_BINDING,   /* binding table construction failed */
> +	PMTCTL_ERR_UNSUPPORTED,   /* metric/feature not supported on this system */
> +};
> +
> +#ifdef LOG_PREFIX
> +#define _LOG_PREFIXED(fmt) LOG_PREFIX ": " fmt
> +#else
> +#define _LOG_PREFIXED(fmt) fmt
> +#endif
> +
> +#define LOG_ERROR PMTCTL_LOG_ERROR
> +#define LOG_WARN PMTCTL_LOG_WARN
> +#define LOG_INFO PMTCTL_LOG_INFO
> +#define LOG_DEBUG PMTCTL_LOG_DEBUG
> +
> +void log_impl(enum pmtctl_log_level lvl, int err, const char *fmt, ...)
> +	__printf(3, 4);
> +void log_set_level(enum pmtctl_log_level lvl);
> +
> +#define log_bug_and_exit(fmt, ...) \
> +	log_bug_impl(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
> +
> +#define log_err(err, fmt, ...) \
> +	log_impl(LOG_ERROR, (err), _LOG_PREFIXED(fmt), ##__VA_ARGS__)
> +
> +#define log_warn(fmt, ...) \
> +	log_impl(LOG_WARN, 0, _LOG_PREFIXED(fmt), ##__VA_ARGS__)
> +
> +#define log_info(fmt, ...) \
> +	log_impl(LOG_INFO, 0, _LOG_PREFIXED(fmt), ##__VA_ARGS__)
> +
> +#ifdef DEBUG
> +#define log_debug(fmt, ...) \
> +	log_impl(LOG_DEBUG, 0, _LOG_PREFIXED(fmt), ##__VA_ARGS__)
> +#else
> +#define log_debug(fmt, ...) \
> +	((void)(0 ? log_info(fmt, ##__VA_ARGS__) : 0))
> +#endif
> +
> +int log_ret(int ret, const char *fmt, ...);
> +
> +__noreturn
> +void log_bug_impl(const char *file, int line, const char *fmt, ...);
> +
> +#define LOG_ONCE(level, fmt, ...)                             \
> +	do {                                                  \
> +		static int __done_##__LINE__;                 \
> +		if (!__done_##__LINE__) {                     \
> +			level(fmt, ##__VA_ARGS__);            \
> +			__done_##__LINE__ = 1;                \
> +		}                                             \
> +	} while (0)
> +
> +#define log_err_once(fmt, ...)  LOG_ONCE(log_err,  fmt, ##__VA_ARGS__)
> +#define log_warn_once(fmt, ...) LOG_ONCE(log_warn, fmt, ##__VA_ARGS__)
> +#define log_info_once(fmt, ...) LOG_ONCE(log_info, fmt, ##__VA_ARGS__)
> +
> +#endif
> diff --git a/tools/arch/x86/pmtctl/include/lib/pmt_guid.h b/tools/arch/x86/pmtctl/include/lib/pmt_guid.h
> new file mode 100644
> index 000000000000..d21bda5bc71b
> --- /dev/null
> +++ b/tools/arch/x86/pmtctl/include/lib/pmt_guid.h
> @@ -0,0 +1,63 @@
> +/* SPDX-License-Identifier: GPL-2.0-only */
> +#ifndef PMTCTL_PMT_GUID_H
> +#define PMTCTL_PMT_GUID_H
> +
> +#include <stdbool.h>
> +#include <stdint.h>
> +
> +/*
> + * Per-GUID metadata derived from the Intel-PMT pmt.xml top-level mapping
> + * table. The compiled-in built-in table (builtin_guids[]) is generated by
> + * scripts/gen_builtin_defs.py. Runtime-loaded providers may register
> + * additional entries via pmt_guid_register().
> + *
> + * A global intern registry ensures pointer-equality across pmt_device and
> + * pmt_metric_def: two references to the same GUID always yield the same
> + * struct pmt_guid pointer.
> + */
> +struct pmt_guid {
> +	uint32_t    guid;
> +	const char *name;        /* basedir lowercased with '/' -> '_'; may be NULL */
> +	const char *description; /* may be NULL */
> +};
> +
> +/*
> + * Register an array of pmt_guid entries with the global registry. The
> + * pointer must remain valid for the registry's lifetime. Entries with a
> + * GUID already present in the registry are skipped (first wins).
> + *
> + * Returns 0 on success, negative errno on failure.
> + */
> +int pmt_guid_register(const struct pmt_guid *entries, int count);
> +
> +/*
> + * Same as pmt_guid_register(), but the registry takes ownership of `block`
> + * (typically the heap allocation that backs `entries` and any pooled
> + * strings the entries point into). The block is freed by pmt_guid_cleanup().
> + *
> + * `block` may equal `entries` when the array sits at the head of the
> + * allocation. Passing a NULL block degrades to pmt_guid_register().
> + *
> + * Returns 0 on success, negative errno on failure. On failure the caller
> + * retains ownership of `block` and must free it.
> + */
> +int pmt_guid_register_owned(void *block, const struct pmt_guid *entries, int count);
> +
> +/*
> + * Lookup the registered pmt_guid for the given numeric GUID. Returns NULL
> + * if no entry has been registered or interned for that GUID.
> + */
> +const struct pmt_guid *pmt_guid_lookup(uint32_t guid);
> +
> +/*
> + * Resolve numeric GUID to a stable struct pmt_guid pointer. If no entry is
> + * registered, a synthetic entry (name = NULL, description = NULL) is
> + * allocated and returned; subsequent calls with the same GUID return the
> + * same pointer. Returns NULL only on allocation failure.
> + */
> +const struct pmt_guid *pmt_guid_intern(uint32_t guid);
> +
> +/* Free all synthetic (interned) entries and clear registrations. */
> +void pmt_guid_cleanup(void);
> +
> +#endif
> diff --git a/tools/arch/x86/pmtctl/lib/common.c b/tools/arch/x86/pmtctl/lib/common.c
> new file mode 100644
> index 000000000000..42931bf79480
> --- /dev/null
> +++ b/tools/arch/x86/pmtctl/lib/common.c
> @@ -0,0 +1,178 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +#include <errno.h>
> +#include <fcntl.h>
> +#include <limits.h>
> +#include <stdlib.h>
> +#include <stdint.h>
> +#include <stdio.h>
> +#include <string.h>
> +#include <sys/types.h>
> +#include <unistd.h>
> +
> +#include "lib/common.h"
> +#include "lib/log.h"
> +
> +char *xstrdup(const char *s)
> +{
> +	char *p;
> +
> +	if (!s)
> +		return NULL;
> +
> +	p = strdup(s);
> +	if (!p) {
> +		log_err(errno, "out of memory");
> +		exit(EXIT_FAILURE);
> +	}
> +
> +	return p;
> +}
> +
> +static void trim_newline(char *s)
> +{
> +	size_t len;
> +	char *end;
> +
> +	if (!s)
> +		return;
> +
> +	len = strlen(s);
> +	if (!len)
> +		return;
> +
> +	end = s + len - 1;
> +
> +	while (end >= s && (*end == '\n' || *end == '\r')) {

Is it beneficial to trim just newlines or would it be better to 
trim anything for which isspace() is true?

> +		*end = '\0';
> +		end--;
> +	}
> +}
> +
> +int read_attr_text(const char *dir, const char *name, char *buf, size_t buflen)
> +{
> +	char path[PATH_MAX];
> +	int fd;
> +	ssize_t n;
> +
> +	if (!dir || !name || !buf || buflen == 0)
> +		return -EINVAL;
> +
> +	if (snprintf(path, sizeof(path), "%s/%s", dir, name) >= (int)sizeof(path))
> +		return -ENAMETOOLONG;
> +
> +	fd = open(path, O_RDONLY | O_CLOEXEC);

Why is O_CLOEXEC required? (The is closed just a few lines down.)

> +	if (fd == -1)
> +		return -errno;
> +
> +	n = read(fd, buf, buflen - 1);
> +	close(fd);
> +
> +	if (n == -1)
> +		return -errno;
> +
> +	buf[n] = '\0';
> +	trim_newline(buf);
> +
> +	return 0;
> +}
> +
> +int read_optional_int_attr(const char *devpath, const char *attr, int *out_id)
> +{
> +	char buf[64];
> +	char *end;
> +	long v;
> +	int ret;
> +
> +	if (!out_id || !devpath || !attr)
> +		return -EINVAL;
> +
> +	ret = read_attr_text(devpath, attr, buf, sizeof(buf));
> +	if (ret != 0) {
> +		if (ret == -ENOENT) {
> +			/* file not found */
> +			*out_id = -1;
> +			return 0;
> +		}
> +
> +		return ret;
> +	}
> +
> +	errno = 0;
> +

This belongs tightly together with strtol() so I don't know why there's 
empty in between.

> +	/* accept decimal or 0x-prefixed hex */
> +	v = strtol(buf, &end, 0);
> +	if (errno)
> +		return -errno;
> +
> +	if (end == buf || *end != '\0')
> +		return -EINVAL;
> +
> +	if (v < INT_MIN || v > INT_MAX)
> +		return -ERANGE;
> +
> +	*out_id = (int)v;
> +
> +	return 0;
> +}
> +
> +int read_int_attr(const char *devpath, const char *attr, int *out_id)
> +{
> +	char buf[64];
> +	char *end;
> +	long v;
> +	int ret;
> +
> +	if (!out_id || !devpath || !attr)
> +		return -EINVAL;
> +
> +	ret = read_attr_text(devpath, attr, buf, sizeof(buf));
> +	if (ret != 0)
> +		return ret;
> +
> +	errno = 0;
> +
> +	/* accept decimal or 0x-prefixed hex */
> +	v = strtol(buf, &end, 0);
> +	if (errno)
> +		return -errno;
> +
> +	if (end == buf || *end != '\0')
> +		return -EINVAL;
> +
> +	if (v < INT_MIN || v > INT_MAX)
> +		return -ERANGE;
> +
> +	*out_id = (int)v;
> +
> +	return 0;
> +}

Lots of code duplication.

> +
> +int read_u32_hex_attr(const char *devpath, const char *attr, uint32_t *out, int err_invalid)
> +{
> +	char buf[64];
> +	char *end;
> +	unsigned long v;
> +	int ret;
> +
> +	if (!out || !devpath || !attr)
> +		return -EINVAL;
> +
> +	ret = read_attr_text(devpath, attr, buf, sizeof(buf));
> +	if (ret != 0)
> +		return ret;
> +
> +	errno = 0;
> +	v = strtoul(buf, &end, 16);
> +	if (errno)
> +		return -errno;
> +
> +	if (end == buf || *end != '\0')
> +		return err_invalid;
> +
> +	if (v > UINT32_MAX)
> +		return err_invalid;
> +
> +	*out = (uint32_t)v;
> +
> +	return 0;
> +}
> diff --git a/tools/arch/x86/pmtctl/lib/log.c b/tools/arch/x86/pmtctl/lib/log.c
> new file mode 100644
> index 000000000000..2ce9edacc97a
> --- /dev/null
> +++ b/tools/arch/x86/pmtctl/lib/log.c
> @@ -0,0 +1,80 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +#include <stdarg.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +#include <errno.h>
> +
> +#include "lib/log.h"
> +
> +static enum pmtctl_log_level g_log_level = LOG_INFO;
> +
> +void log_set_level(enum pmtctl_log_level lvl)
> +{
> +	if (lvl < LOG_ERROR || lvl > LOG_DEBUG)
> +		g_log_level = LOG_INFO;
> +	else
> +		g_log_level = lvl;
> +}
> +
> +static void log_vmsg(enum pmtctl_log_level lvl, int err, const char *fmt,
> +		     va_list ap)
> +{
> +	if (lvl > g_log_level)
> +		return;
> +
> +	const char *tag =
> +		(lvl == LOG_ERROR) ? "error"  :
> +		(lvl == LOG_WARN)  ? "warning" :
> +		(lvl == LOG_INFO)  ? "info"   :

Aligning is off.

> +				     "debug";
> +
> +	fprintf(stderr, "%s: ", tag);
> +	vfprintf(stderr, fmt, ap);
> +
> +	if (err != 0) {
> +		int e = (err < 0) ? -err : err;

???

> +
> +		if (err < 0)

???

> +			fprintf(stderr, ": %s", strerror(e));
> +	}
> +
> +	fputc('\n', stderr);
> +}
> +
> +void log_impl(enum pmtctl_log_level lvl, int err, const char *fmt, ...)
> +{
> +	va_list ap;
> +
> +	va_start(ap, fmt);
> +	log_vmsg(lvl, err, fmt, ap);
> +	va_end(ap);
> +}
> +
> +int log_ret(int ret, const char *fmt, ...)
> +{
> +	if (ret == 0)
> +		return 0;
> +
> +	va_list ap;
> +
> +	va_start(ap, fmt);
> +	log_vmsg(LOG_ERROR, ret, fmt, ap);
> +	va_end(ap);
> +
> +	return ret;
> +}
> +
> +__noreturn
> +void log_bug_impl(const char *file, int line, const char *fmt, ...)
> +{
> +	va_list ap;
> +
> +	fprintf(stderr, "pmtctl: BUG at %s:%d: ", file, line);
> +	va_start(ap, fmt);
> +	vfprintf(stderr, fmt, ap);
> +	va_end(ap);
> +	fputc('\n', stderr);
> +
> +	exit(PMTCTL_EXIT_BUG);
> +}
> diff --git a/tools/arch/x86/pmtctl/lib/pmt_guid.c b/tools/arch/x86/pmtctl/lib/pmt_guid.c
> new file mode 100644
> index 000000000000..f4d74fc7a977
> --- /dev/null
> +++ b/tools/arch/x86/pmtctl/lib/pmt_guid.c
> @@ -0,0 +1,200 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +#include <errno.h>
> +#include <stdbool.h>
> +#include <stdint.h>
> +#include <stdlib.h>
> +#include <string.h>
> +
> +#include "lib/pmt_guid.h"
> +
> +/*
> + * Global intern registry for struct pmt_guid.
> + *
> + * Two sources feed this table:
> + *   1. Pre-registered blocks (e.g. builtin_guids[] from generated code,
> + *      or JSON-loaded pmt_guids.json from the runtime provider). Pointers
> + *      to those entries are stored verbatim and assumed to outlive the
> + *      registry's use.
> + *   2. Synthetic entries allocated on demand by pmt_guid_intern() for
> + *      GUIDs that no provider knows about. These have name = NULL and
> + *      description = NULL.
> + *
> + * A simple linear-scan array is used; the registry holds at most a few
> + * hundred entries on any realistic system.
> + */

This seems largely irrelevant to what it comments about (struct 
guid_slot is definitely not a "global intern registry" nor a "table").

> +struct guid_slot {
> +	const struct pmt_guid *entry; /* points into registered block or synth_storage */
> +	struct pmt_guid       *synth; /* non-NULL iff this slot owns the entry */
> +};
> +
> +static struct guid_slot *g_slots;
> +static int               g_nslots;
> +static int               g_cap;
> +
> +/*
> + * Heap blocks handed to pmt_guid_register_owned(). The registry frees
> + * them in pmt_guid_cleanup() after disposing of the slot array, so any
> + * pointers the slots held into these blocks are gone by then.
> + */
> +static void **g_owned_blocks;
> +static int    g_nowned;
> +static int    g_owned_cap;
> +
> +static int reserve_one(void)
> +{
> +	struct guid_slot *tmp;
> +	int new_cap;
> +
> +	if (g_nslots < g_cap)
> +		return 0;
> +
> +	new_cap = g_cap ? g_cap * 2 : 32;
> +	tmp = realloc(g_slots, (size_t)new_cap * sizeof(*tmp));

Why is new_cap int if it is needed as size_t ??

And a follow up from that, why are g_cap and g_nslots signed if only 
positive values are acceptable?

> +	if (!tmp)
> +		return -ENOMEM;
> +
> +	g_slots = tmp;
> +	g_cap = new_cap;
> +	return 0;
> +}
> +
> +const struct pmt_guid *pmt_guid_lookup(uint32_t guid)
> +{
> +	for (int i = 0; i < g_nslots; i++) {
> +		if (g_slots[i].entry && g_slots[i].entry->guid == guid)
> +			return g_slots[i].entry;
> +	}
> +	return NULL;
> +}
> +
> +int pmt_guid_register(const struct pmt_guid *entries, int count)
> +{
> +	if (!entries || count <= 0)
> +		return -EINVAL;
> +
> +	for (int i = 0; i < count; i++) {
> +		const struct pmt_guid *e = &entries[i];
> +		bool already_known = false;
> +		int ret;
> +
> +		/*
> +		 * If a slot already exists for this GUID, upgrade it when the
> +		 * existing entry is a synthetic placeholder (created earlier
> +		 * by pmt_guid_intern() before any provider was loaded) and the
> +		 * new entry carries real metadata. The synthetic struct must
> +		 * be kept alive because earlier callers (e.g. devices) hold
> +		 * pointers to it; patch its fields in place instead of
> +		 * swapping the slot entry.
> +		 */
> +		for (int s = 0; s < g_nslots; s++) {
> +			struct guid_slot *slot = &g_slots[s];
> +
> +			if (!slot->entry || slot->entry->guid != e->guid)
> +				continue;
> +
> +			if (slot->synth && e->name && *e->name) {
> +				slot->synth->name = e->name;
> +				slot->synth->description = e->description;
> +			}
> +			already_known = true;
> +			break;
> +		}
> +
> +		if (already_known)
> +			continue;
> +
> +		ret = reserve_one();
> +		if (ret < 0)
> +			return ret;
> +
> +		g_slots[g_nslots].entry = e;
> +		g_slots[g_nslots].synth = NULL;
> +		g_nslots++;

So does reserve_one() and this function, instead of returning the index, 
communicate using g_nslots variable???

> +	}
> +
> +	return 0;
> +}
> +
> +int pmt_guid_register_owned(void *block, const struct pmt_guid *entries, int count)
> +{
> +	void **tmp;
> +	int ret;
> +
> +	/*
> +	 * Grow g_owned_blocks first, before registering anything. If this
> +	 * fails, no entries have been added to g_slots yet, so the caller
> +	 * can safely free(block) on error without leaving dangling slot
> +	 * pointers behind.
> +	 */
> +	if (block && g_nowned == g_owned_cap) {
> +		int new_cap = g_owned_cap ? g_owned_cap * 2 : 8;
> +
> +		tmp = realloc(g_owned_blocks, (size_t)new_cap * sizeof(*tmp));

Same typing problem as above but it also implies this duplicating code.

> +		if (!tmp)
> +			return -ENOMEM;
> +
> +		g_owned_blocks = tmp;
> +		g_owned_cap = new_cap;
> +	}
> +
> +	ret = pmt_guid_register(entries, count);
> +	if (ret < 0)
> +		return ret;
> +
> +	if (block)
> +		g_owned_blocks[g_nowned++] = block;
> +
> +	return 0;
> +}
> +
> +const struct pmt_guid *pmt_guid_intern(uint32_t guid)
> +{
> +	const struct pmt_guid *existing;
> +	struct pmt_guid *synth;
> +	int ret;
> +
> +	existing = pmt_guid_lookup(guid);
> +	if (existing)
> +		return existing;
> +
> +	ret = reserve_one();
> +	if (ret < 0)
> +		return NULL;
> +
> +	synth = calloc(1, sizeof(*synth));
> +	if (!synth)
> +		return NULL;
> +
> +	synth->guid = guid;
> +	synth->name = NULL;
> +	synth->description = NULL;
> +
> +	g_slots[g_nslots].entry = synth;
> +	g_slots[g_nslots].synth = synth;
> +	g_nslots++;
> +
> +	return synth;
> +}
> +
> +void pmt_guid_cleanup(void)
> +{
> +	for (int i = 0; i < g_nslots; i++)
> +		free(g_slots[i].synth);
> +
> +	free(g_slots);
> +	g_slots = NULL;
> +	g_nslots = 0;
> +	g_cap = 0;
> +
> +	/*
> +	 * Owned blocks are freed last: slot entries may have pointed into
> +	 * them, but those pointers are gone now that g_slots is released.
> +	 */
> +	for (int i = 0; i < g_nowned; i++)
> +		free(g_owned_blocks[i]);
> +
> +	free(g_owned_blocks);
> +	g_owned_blocks = NULL;
> +	g_nowned = 0;
> +	g_owned_cap = 0;
> +}
> 

-- 
 i.


^ permalink raw reply	[flat|nested] 25+ messages in thread

* Re: [PATCH 04/17] tools/arch/x86/pmtctl: Add libpmtctl metric definition database
  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
  0 siblings, 0 replies; 25+ messages in thread
From: Ilpo Järvinen @ 2026-05-26 10:06 UTC (permalink / raw)
  To: David E. Box; +Cc: LKML, Andy Shevchenko, platform-driver-x86

On Mon, 25 May 2026, David E. Box wrote:

> Add a growable container for PMT metric definitions to support loading and
> querying hardware metric descriptors at runtime.
> 
> PMT metrics vary by platform and are discovered dynamically from sysfs. A
> block-based database allows metric definitions to be appended as they are
> parsed, without requiring the final size to be known in advance, while
> bounding individual memory allocations.
> 
> Assisted-by: GitHub-Copilot:claude-sonnet-4.6
> Signed-off-by: David E. Box <david.e.box@linux.intel.com>
> ---
>  .../arch/x86/pmtctl/include/lib/metrics_db.h  | 69 +++++++++++++++++++
>  tools/arch/x86/pmtctl/lib/metrics_db.c        | 62 +++++++++++++++++
>  2 files changed, 131 insertions(+)
>  create mode 100644 tools/arch/x86/pmtctl/include/lib/metrics_db.h
>  create mode 100644 tools/arch/x86/pmtctl/lib/metrics_db.c
> 
> diff --git a/tools/arch/x86/pmtctl/include/lib/metrics_db.h b/tools/arch/x86/pmtctl/include/lib/metrics_db.h
> new file mode 100644
> index 000000000000..9dff27d8785a
> --- /dev/null
> +++ b/tools/arch/x86/pmtctl/include/lib/metrics_db.h
> @@ -0,0 +1,69 @@
> +/* SPDX-License-Identifier: GPL-2.0-only */
> +#ifndef PMTCTL_METRICS_DB_H
> +#define PMTCTL_METRICS_DB_H
> +
> +#include <stdbool.h>
> +#include <stddef.h>
> +#include <stdint.h>
> +
> +#include "lib/pmt_guid.h"
> +
> +/* Metric definition from builtin or JSON */
> +struct pmt_metric_def {
> +	const char *event_name;
> +	const char *description;
> +	const char *group;
> +	const char *platform_group;
> +
> +	const struct pmt_guid *guid;
> +	uint32_t sample_id;
> +	uint8_t  lsb;
> +	uint8_t  msb;
> +};
> +
> +struct pmt_metrics_block {
> +	const struct pmt_metric_def *defs;
> +	int count;
> +	bool is_builtin;
> +};
> +
> +struct pmt_metrics_db {
> +	struct pmt_metrics_block *blocks;
> +	int nblocks;
> +	int total;   /* sum of all block->count */
> +};
> +
> +/* Treat DB as a flat array [0..total-1] */
> +static inline const struct pmt_metric_def *pmt_metrics_at(const struct pmt_metrics_db *db, int idx)

inline???

> +{
> +	if (!db || idx < 0 || idx >= db->total)
> +		return NULL;
> +
> +	for (int i = 0; i < db->nblocks; i++) {
> +		if (idx < db->blocks[i].count)
> +			return &db->blocks[i].defs[idx];
> +		idx -= db->blocks[i].count;
> +	}
> +	return NULL;
> +}
> +
> +static inline const struct pmt_metrics_block *
> +pmt_metrics_block_for(const struct pmt_metrics_db *db, int idx)
> +{
> +	if (!db || idx < 0 || idx >= db->total)
> +		return NULL;
> +
> +	for (int i = 0; i < db->nblocks; i++) {
> +		if (idx < db->blocks[i].count)
> +			return &db->blocks[i];
> +		idx -= db->blocks[i].count;
> +	}
> +
> +	return NULL;
> +}

Code duplication.

> +
> +int pmt_metrics_add_block(struct pmt_metrics_db *db, const struct pmt_metric_def *defs,
> +			  int count, bool is_builtin);
> +void pmt_metrics_free(struct pmt_metrics_db *db);
> +
> +#endif
> diff --git a/tools/arch/x86/pmtctl/lib/metrics_db.c b/tools/arch/x86/pmtctl/lib/metrics_db.c
> new file mode 100644
> index 000000000000..82e8121a1b98
> --- /dev/null
> +++ b/tools/arch/x86/pmtctl/lib/metrics_db.c
> @@ -0,0 +1,62 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +#include <errno.h>
> +#include <limits.h>
> +#include <stdlib.h>
> +#include <string.h>
> +
> +#include "lib/metrics_db.h"
> +
> +int pmt_metrics_add_block(struct pmt_metrics_db *db, const struct pmt_metric_def *defs,
> +			  int count, bool is_builtin)
> +{
> +	struct pmt_metrics_block *blocks;
> +	size_t alloc_size;
> +	int new_cap;
> +
> +	if (!db || !defs || count <= 0)
> +		return -EINVAL;
> +
> +	if (db->nblocks > INT_MAX - 1)
> +		return -EOVERFLOW;
> +
> +	if (db->total > INT_MAX - count)
> +		return -EOVERFLOW;
> +
> +	new_cap = db->nblocks + 1;
> +
> +	if ((size_t)new_cap > SIZE_MAX / sizeof(*blocks))

Casts like this likely indicates some typing issue in your code.

> +		return -EOVERFLOW;
> +
> +	alloc_size = (size_t)new_cap * sizeof(*blocks);
> +
> +	blocks = realloc(db->blocks, alloc_size);

So this reallocs + 1 every time??

> +	if (!blocks)
> +		return -ENOMEM;
> +
> +	db->blocks = blocks;
> +
> +	db->blocks[db->nblocks].defs       = defs;
> +	db->blocks[db->nblocks].count      = count;
> +	db->blocks[db->nblocks].is_builtin = is_builtin;
> +
> +	db->nblocks++;
> +	db->total += count;
> +
> +	return 0;
> +}
> +
> +void pmt_metrics_free(struct pmt_metrics_db *db)
> +{
> +	if (!db)
> +		return;
> +
> +	if (db->blocks) {
> +		for (int i = 0; i < db->nblocks; i++) {
> +			if (!db->blocks[i].is_builtin && db->blocks[i].defs)
> +				free((void *)db->blocks[i].defs);
> +		}
> +		free(db->blocks);
> +	}
> +
> +	memset(db, 0, sizeof(*db));

-- 
 i.


^ permalink raw reply	[flat|nested] 25+ messages in thread

* Re: [PATCH 05/17] tools/arch/x86/pmtctl: Add libpmtctl device enumeration backend
  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
  0 siblings, 0 replies; 25+ messages in thread
From: Ilpo Järvinen @ 2026-05-26 10:35 UTC (permalink / raw)
  To: David E. Box; +Cc: LKML, Andy Shevchenko, platform-driver-x86

On Mon, 25 May 2026, David E. Box wrote:

> Add a sysfs-based backend for enumerating Intel PMT telemetry devices,
> enabling libpmtctl to discover and access hardware telemetry data at
> runtime without hardcoded device paths.
> 
> Intel PMT telemetry devices are exposed under
> /sys/bus/auxiliary/drivers/pmt_telemetry. This backend walks those sysfs
> entries to discover device metadata and data paths, populating per-device
> descriptors used by the metric access layer.
> 
> Introduce a pmt_device_ops vtable to allow additional enumeration backends
> or PMT transport types to be added without changing the calling code.
> 
> Assisted-by: GitHub-Copilot:claude-sonnet-4.6
> Signed-off-by: David E. Box <david.e.box@linux.intel.com>
> ---
>  tools/arch/x86/pmtctl/include/lib/device.h |  53 +++
>  tools/arch/x86/pmtctl/lib/device_telem.c   | 371 +++++++++++++++++++++
>  2 files changed, 424 insertions(+)
>  create mode 100644 tools/arch/x86/pmtctl/include/lib/device.h
>  create mode 100644 tools/arch/x86/pmtctl/lib/device_telem.c
> 
> diff --git a/tools/arch/x86/pmtctl/include/lib/device.h b/tools/arch/x86/pmtctl/include/lib/device.h
> new file mode 100644
> index 000000000000..5bee487e9b99
> --- /dev/null
> +++ b/tools/arch/x86/pmtctl/include/lib/device.h
> @@ -0,0 +1,53 @@
> +/* SPDX-License-Identifier: GPL-2.0-only */
> +#ifndef PMTCTL_DEVICES_H
> +#define PMTCTL_DEVICES_H
> +
> +#include <stdint.h>
> +
> +#include "metrics_db.h"
> +#include "pmtctl_types.h"
> +
> +#define PMT_SAMPLE_SIZE 8
> +
> +/* --- Device representation returned by any device --- */
> +struct pmt_device {
> +	const struct pmt_guid *guid;   /* per-GUID metadata (interned) */
> +	int        guid_inst;    /* global guid instance */
> +	int        dev_inst;     /* local device instance */
> +
> +	char      *name;         /* e.g. "pmt_ep_27971628_0" or "telem0" */
> +	char      *path;         /* e.g. "/sys/class/intel_pmt/telem0" */
> +	char      *data_path;    /* readable metric data path for this device */
> +	int        instance;     /* 0, 1, etc. */

Inconsistent alignment.

These are using spaces to align.

> +	int        pkg_id;       /* Package/Socket ID if known, else -1 */
> +	int        die_id;       /* Die ID if known, else -1 */
> +
> +	int        fd;           /* telem file fd */
> +};
> +
> +struct pmt_metric_desc {
> +	const struct pmt_metric_def *def;
> +	struct pmt_device *dev;
> +
> +	const char *name; /* JSON name */
> +	int guid_inst; /* global GUID instance (matches pmt_device.guid_inst) */
> +
> +	/* raw mode fields */
> +	uint32_t raw_sample_id;
> +	uint8_t  raw_lsb;
> +	uint8_t  raw_msb;

Extra spaces.

> +
> +};
> +
> +struct pmt_device_ops {
> +	enum pmt_device_type dev_type;
> +	int  (*init)(void);
> +	void (*cleanup)(void);
> +	int  (*read)(struct pmt_metric_desc *m, uint64_t *val);

It seems pretty random when things are aligned with extra spaces and when 
not.

> +	struct pmt_device *(*device_list)(int *count);
> +};
> +
> +extern struct pmt_device_ops device_telem_ops;
> +
> +#endif
> diff --git a/tools/arch/x86/pmtctl/lib/device_telem.c b/tools/arch/x86/pmtctl/lib/device_telem.c
> new file mode 100644
> index 000000000000..4c90dc95890f
> --- /dev/null
> +++ b/tools/arch/x86/pmtctl/lib/device_telem.c
> @@ -0,0 +1,371 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +#define LOG_PREFIX "telem"
> +#include <dirent.h>
> +#include <errno.h>
> +#include <fcntl.h>
> +#include <limits.h>
> +#include <stdio.h>
> +#include <stdint.h>
> +#include <stdlib.h>
> +#include <string.h>
> +#include <sys/stat.h>
> +#include <sys/types.h>
> +#include <unistd.h>
> +
> +#include "lib/common.h"
> +#include "lib/device.h"
> +#include "lib/log.h"
> +
> +#define PMT_TELEM_AUX_DIR    "/sys/bus/auxiliary/drivers/pmt_telemetry"
> +#define PMT_TELEM_PMT_DIR    "intel_pmt"
> +#define PMT_TELEM_DATA_FILE  "telem"
> +
> +static struct pmt_device *devices;
> +static int device_count;
> +static int device_initialized;
> +
> +static int telem_read(struct pmt_metric_desc *m, uint64_t *val)
> +{
> +	uint64_t raw;
> +	uint64_t mask;
> +	uint8_t width;
> +	ssize_t n;
> +	off_t off;
> +	uint32_t sample_id;
> +	uint8_t  lsb, msb;

Extra space.

> +
> +	if (!m || !val || !m->dev)
> +		log_bug_and_exit("unexpected NULL parameters");
> +
> +	/* Open the telem file on first read */
> +	if (m->dev->fd < 0) {
> +		int fd;
> +
> +		if (!m->dev->data_path)
> +			return log_ret(-EINVAL, "missing data path for %s", m->dev->name);
> +
> +		fd = open(m->dev->data_path, O_RDONLY | O_CLOEXEC);

Is CLOEXEC required? (I don't know the general structure of this program.)

> +		if (fd == -1)
> +			return log_ret(-errno, "could not open telem file %s", m->dev->data_path);
> +
> +		m->dev->fd = fd;
> +	}
> +
> +	if (m->def) {
> +		sample_id = m->def->sample_id;
> +		lsb       = m->def->lsb;
> +		msb       = m->def->msb;
> +	} else {
> +		sample_id = m->raw_sample_id;
> +		lsb       = m->raw_lsb;
> +		msb       = m->raw_msb;
> +	}
> +
> +	if (msb > 63 || lsb > msb)

Isn't that 63 a bit index? It should be written without literals.

> +		return PMTCTL_ERR_DEVICE;
> +
> +	off = (off_t)sample_id * PMT_SAMPLE_SIZE;
> +
> +	n = pread(m->dev->fd, &raw, sizeof(raw), off);
> +	if (n == -1) {
> +		log_err_once(-errno, "unable to read telem %s", m->dev->name);
> +		return -errno;
> +	}
> +
> +	if (n != (ssize_t)sizeof(raw)) {

If casting after error n checking, shouldn't the case be other way around?

> +		log_err_once(-EIO, "read expected %zu bytes, got %zd (dev=%s)",
> +			     sizeof(raw), n, m->dev->name);
> +		return -EIO;
> +	}
> +
> +	if (lsb == 0 && msb == 63) {
> +		*val = raw;
> +		return 0;
> +	}

Is this optimization useful?

> +	width = msb - lsb + 1;
> +	if (width >= 64)

How can this happen?

> +		mask = ~0ULL;
> +	else
> +		mask = (1ULL << width) - 1ULL;

Is shifting by 64 undefined behavior? If it's not, then it looks to me 
that this is all you need?

> +
> +	*val = (raw >> lsb) & mask;
> +
> +	return 0;
> +}
> +
> +static struct pmt_device *telem_device_list(int *count)
> +{
> +	if (!count || !device_initialized)
> +		return NULL;
> +
> +	*count = device_count;
> +
> +	return devices;
> +}
> +
> +/* ----------------- per-GUID instance tracking ----------------- */
> +struct guid_counter {
> +	uint32_t guid;
> +	int      next_guid_inst;
> +};
> +
> +struct dev_guid_counter {
> +	int      dev_index;
> +	uint32_t guid;
> +	int      next_dev_inst;
> +};
> +
> +static struct guid_counter     *guid_counters;
> +static int                      guid_counter_cnt;
> +
> +static struct dev_guid_counter *dev_guid_counters;
> +static int                      dev_guid_counter_cnt;
> +/* -------------------------------------------------------------- */
> +
> +static int next_guid_instance_global(uint32_t guid)
> +{
> +	struct guid_counter *tmp;
> +	int inst = 0;
> +	int i;
> +
> +	for (i = 0; i < guid_counter_cnt; i++) {
> +		if (guid_counters[i].guid == guid) {
> +			inst = guid_counters[i].next_guid_inst;
> +			guid_counters[i].next_guid_inst++;
> +			return inst;
> +		}
> +	}
> +
> +	/* new guid, start at 0 */
> +	tmp = realloc(guid_counters, (guid_counter_cnt + 1) * sizeof(*guid_counters));

This realloc by +1 seems really endemic and I'm not convinced its good 
way to handle things.

> +	if (!tmp)
> +		return -ENOMEM;
> +
> +	guid_counters = tmp;
> +	guid_counters[guid_counter_cnt].guid = guid;
> +	guid_counters[guid_counter_cnt].next_guid_inst = 1;
> +	guid_counter_cnt++;
> +
> +	return inst;
> +}
> +
> +static int next_guid_instance_device(int dev_index, uint32_t guid)
> +{
> +	struct dev_guid_counter *tmp;
> +	int inst = 0;
> +	int i;
> +
> +	for (i = 0; i < dev_guid_counter_cnt; i++) {
> +		if (dev_guid_counters[i].dev_index == dev_index &&
> +		    dev_guid_counters[i].guid == guid) {
> +			inst = dev_guid_counters[i].next_dev_inst;
> +			dev_guid_counters[i].next_dev_inst++;
> +			return inst;
> +		}
> +	}
> +
> +	/* new (dev_index, guid) pair */
> +	tmp = realloc(dev_guid_counters, (dev_guid_counter_cnt + 1) * sizeof(*dev_guid_counters));
> +	if (!tmp)
> +		return -ENOMEM;
> +
> +	dev_guid_counters = tmp;
> +	dev_guid_counters[dev_guid_counter_cnt].dev_index = dev_index;
> +	dev_guid_counters[dev_guid_counter_cnt].guid = guid;
> +	dev_guid_counters[dev_guid_counter_cnt].next_dev_inst = 1;
> +	dev_guid_counter_cnt++;
> +
> +	return inst;
> +}
> +
> +static int telem_add_device(const char *devpath, const char *devname, int dev_index)
> +{
> +	struct pmt_device dev = { 0 };

= {} is enough to initialize.

> +
> +	auto_free char *temp_name = NULL;
> +	auto_free char *temp_path = NULL;
> +	auto_free char *temp_data_path = NULL;
> +	char data_path[PATH_MAX];
> +	uint32_t raw_guid;
> +	int len;
> +	int ret;
> +
> +	if (!devpath || !devname)
> +		return -EINVAL;
> +
> +	temp_name = xstrdup(devname);
> +	temp_path = xstrdup(devpath);
> +
> +	len = snprintf(data_path, sizeof(data_path), "%s/" PMT_TELEM_DATA_FILE, devpath);
> +	if (len < 0 || (size_t)len >= sizeof(data_path))
> +		return log_ret(-EINVAL, "path too long for %s", devname);
> +
> +	temp_data_path = xstrdup(data_path);

Why you first write to stack and then strdup() instead of doing it 
directly?

> +
> +	/* Defaults */
> +	dev.pkg_id = -1; // not availeble in /sys/class/intel_pmt
> +	dev.die_id = -1; // not available in /sys/class/intel_pmt
> +
> +	/* Get guid */
> +	ret = read_u32_hex_attr(devpath, "guid", &raw_guid, PMTCTL_ERR_DEVICE);
> +	if (ret)
> +		return log_ret(ret, "unable to read guid for %s", devpath);
> +
> +	dev.guid = pmt_guid_intern(raw_guid);
> +	if (!dev.guid)
> +		return log_ret(-ENOMEM, "could not intern guid 0x%08x", raw_guid);
> +
> +	/* Compute instances */
> +	ret = next_guid_instance_global(raw_guid);
> +	if (ret < 0)
> +		return ret;
> +
> +	dev.guid_inst = ret;
> +
> +	ret = next_guid_instance_device(dev_index, raw_guid);
> +	if (ret < 0)
> +		return ret;
> +
> +	dev.dev_inst = ret;
> +
> +	/* Don't open telem file yet - defer until first read */
> +	dev.fd = -1;
> +
> +	/* Append to global array */
> +	struct pmt_device *tmp;
> +
> +	tmp = realloc(devices, (device_count + 1) * sizeof(*devices));
> +	if (!tmp)
> +		return log_ret(-ENOMEM, "could not add telem device %s", devpath);
> +
> +	dev.name = temp_name;
> +	dev.path = temp_path;
> +	dev.data_path = temp_data_path;
> +
> +	temp_name = NULL;
> +	temp_path = NULL;
> +	temp_data_path = NULL;
> +
> +	devices = tmp;
> +	devices[device_count++] = dev;
> +
> +	return 0;
> +}
> +
> +static int telem_scan_intel_pmt(void)
> +{
> +	DIR *aux_dir = opendir(PMT_TELEM_AUX_DIR);
> +	struct dirent *aux_de;
> +	int dev_index = 0;
> +
> +	if (!aux_dir) {
> +		log_debug("error opening %s: %s", PMT_TELEM_AUX_DIR, strerror(errno));
> +		return -errno;
> +	}
> +
> +	while ((aux_de = readdir(aux_dir))) {
> +		char intel_pmt_path[PATH_MAX];
> +		struct dirent *pmt_de;
> +		DIR *pmt_dir;
> +		ssize_t len;
> +
> +		/* skip . and .. */
> +		if (aux_de->d_name[0] == '.')
> +			continue;
> +
> +		len = snprintf(intel_pmt_path, sizeof(intel_pmt_path),
> +			       "%s/%s/" PMT_TELEM_PMT_DIR,
> +			       PMT_TELEM_AUX_DIR, aux_de->d_name);
> +
> +		if (len < 0 || (size_t)len >= sizeof(intel_pmt_path))

No emptylines between call and its error handling.

> +			continue;
> +
> +		pmt_dir = opendir(intel_pmt_path);
> +		if (!pmt_dir)
> +			continue;
> +
> +		while ((pmt_de = readdir(pmt_dir)) != NULL) {
> +			char telem_path[PATH_MAX];
> +			int ret;
> +
> +			/* only telem* directories */
> +			if (strncmp(pmt_de->d_name, PMT_TELEM_DATA_FILE,
> +				    strlen(PMT_TELEM_DATA_FILE)) != 0)

I tried to figure out if strncmp() can be replaced with strcmp() but I was 
left unsure. It seems telem_add_device() adds '/' to it which would 
indicate n variant is not required (and the comment is misleadingly 
lacking that /).

> +				continue;
> +
> +			len = snprintf(telem_path, sizeof(telem_path),
> +				       "%s/%s", intel_pmt_path, pmt_de->d_name);
> +			if (len < 0 || (size_t)len >= sizeof(telem_path))
> +				continue;
> +
> +			ret = telem_add_device(telem_path, pmt_de->d_name, dev_index);
> +			if (ret)
> +				log_warn("unable to add telem device %s (ret=%d)",
> +					 telem_path, ret);
> +		}
> +
> +		closedir(pmt_dir);
> +		dev_index++;
> +	}
> +	closedir(aux_dir);
> +
> +	if (device_count == 0)
> +		return PMTCTL_ERR_NOTFOUND;
> +
> +	return 0;
> +}
> +
> +static int telem_init(void)
> +{
> +	int ret;
> +
> +	if (device_initialized)
> +		return 0;
> +
> +	ret = telem_scan_intel_pmt();
> +	if (ret)
> +		return ret;
> +
> +	device_initialized = 1;
> +
> +	return 0;
> +}
> +
> +static void telem_cleanup(void)
> +{
> +	int i;
> +
> +	if (!device_initialized)
> +		return;
> +
> +	for (i = 0; i < device_count; i++) {
> +		if (devices[i].fd >= 0)
> +			close(devices[i].fd);
> +
> +		free(devices[i].name);
> +		free(devices[i].path);
> +		free(devices[i].data_path);
> +	}
> +
> +	free(guid_counters);
> +	guid_counters = NULL;
> +	guid_counter_cnt = 0;
> +
> +	free(dev_guid_counters);
> +	dev_guid_counters = NULL;
> +	dev_guid_counter_cnt = 0;
> +
> +	free(devices);
> +	devices = NULL;
> +	device_count = 0;
> +	device_initialized = 0;
> +}
> +
> +struct pmt_device_ops device_telem_ops = {
> +	.dev_type    = PMT_DEVICE_TELEM,
> +	.init        = telem_init,
> +	.cleanup     = telem_cleanup,
> +	.read        = telem_read,
> +	.device_list = telem_device_list,
> +};
> 

-- 
 i.


^ permalink raw reply	[flat|nested] 25+ messages in thread

* Re: [PATCH 07/17] tools/arch/x86/pmtctl: Add libpmtctl JSON metric provider
  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
  0 siblings, 0 replies; 25+ messages in thread
From: Ilpo Järvinen @ 2026-05-26 11:04 UTC (permalink / raw)
  To: David E. Box; +Cc: LKML, Andy Shevchenko, platform-driver-x86

On Mon, 25 May 2026, David E. Box wrote:

> Add an optional JSON-based metric definition provider to libpmtctl_core,
> allowing PMT metric definitions to be supplied at runtime from external
> files rather than compiled into the library.
> 
> This allows platform-specific metric definitions to be maintained
> separately from the kernel tree and updated without rebuilding the library.
> The provider accepts either a single JSON file or a directory of JSON
> files, loading discovered definitions into the metric database.
> 
> When built with HAVE_JANSSON, pmt_metrics_load() dispatches non-NULL path
> arguments to the JSON provider. Otherwise, PMTCTL_ERR_UNSUPPORTED is
> returned so callers can detect that JSON-backed loading is unavailable.
> 
> Build-system wiring is added in a later patch of the series.
> 
> Assisted-by: GitHub-Copilot:claude-sonnet-4.6
> Signed-off-by: David E. Box <david.e.box@linux.intel.com>
> ---
>  .../x86/pmtctl/lib/metrics_provider_json.c    | 459 ++++++++++++++++++
>  1 file changed, 459 insertions(+)
>  create mode 100644 tools/arch/x86/pmtctl/lib/metrics_provider_json.c
> 
> diff --git a/tools/arch/x86/pmtctl/lib/metrics_provider_json.c b/tools/arch/x86/pmtctl/lib/metrics_provider_json.c
> new file mode 100644
> index 000000000000..9c0a7e55407d
> --- /dev/null
> +++ b/tools/arch/x86/pmtctl/lib/metrics_provider_json.c
> @@ -0,0 +1,459 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +#define LOG_PREFIX "metrics_provider_json"

Empty lines missing, but shouldn't this be after includes?

> +#include <dirent.h>
> +#include <errno.h>
> +#include <jansson.h>
> +#include <limits.h>
> +#include <stdbool.h>
> +#include <stdlib.h>
> +#include <string.h>
> +#include <sys/stat.h>
> +
> +#include "lib/log.h"
> +#include "lib/metrics_db.h"
> +#include "lib/metrics_provider.h"
> +
> +static bool is_regular(const char *path)
> +{
> +	struct stat st;
> +
> +	if (stat(path, &st) < 0)
> +		return false;
> +
> +	return !!S_ISREG(st.st_mode);

Unnecessary !!

> +}
> +
> +static bool is_dir(const char *path)
> +{
> +	struct stat st;
> +
> +	if (stat(path, &st) < 0)
> +		return false;
> +
> +	return !!S_ISDIR(st.st_mode);
> +}
> +
> +static const char *pool_copy(char **pool, const char *s)
> +{
> +	char *dst;
> +	size_t len;
> +
> +	if (!pool || !s)
> +		return NULL;
> +
> +	len = strlen(s) + 1;
> +	dst = *pool;
> +
> +	memcpy(dst, s, len);

This is generally a dangerous pattern: length is taken from that of 
source, whereas we'd want to check that dst space does not overflow.

> +	*pool += len;

This looks some adhoc string heap management to me, and it honestly 
doesn't impress me.

> +	return dst;
> +}
> +
> +/*
> + * Example PMU name: "pmt_ep_3d4bb41a"
> + * Extract GUID = 0x3d4bb41a
> + */
> +static uint32_t parse_guid_from_pmu(const char *pmu)
> +{
> +	unsigned long val;
> +	const char *last;
> +	const char *p;
> +	char *end;
> +
> +	if (!pmu)
> +		return 0;
> +
> +	last = strrchr(pmu, '_');
> +	p = last ? last + 1 : pmu;
> +
> +	end = NULL;
> +	val = strtoul(p, &end, 16);
> +
> +	if (end == p || val > 0xffffffffUL)

Some _MAX?

> +		return 0;
> +
> +	return (uint32_t)val;
> +}
> +
> +/*
> + * Decode a perf-style ConfigCode string into (sample_id, lsb, msb).
> + *
> + * Bit layout (matches scripts/gen_builtin_defs.py::decode_config_code()
> + * and scripts/pmtxml2json.py::pack_config()):
> + *
> + *   bits[15:0]   sample_id
> + *   bits[23:16]  lsb
> + *   bits[31:24]  msb
> + *
> + * Returns 0 on success, or -EINVAL if the string is malformed or the
> + * decoded range is out of bounds. A missing ConfigCode (NULL) decodes
> + * to (0, 0, 0) successfully so callers don't need to special-case it.
> + */
> +static int parse_config_code(const char *s, uint32_t *sample_id,
> +			     uint8_t *lsb, uint8_t *msb)
> +{
> +	unsigned long val;
> +	char *end;
> +	uint8_t l, m;
> +
> +	*sample_id = 0;
> +	*lsb = 0;
> +	*msb = 0;
> +
> +	if (!s)
> +		return 0;
> +
> +	end = NULL;
> +	val = strtoul(s, &end, 0);
> +
> +	if (end == s || *end != '\0' || val > 0xffffffffUL)

*_MAX?

> +		return -EINVAL;
> +
> +	l = (val >> 16) & 0xff;
> +	m = (val >> 24) & 0xff;
> +
> +	if (m > 63 || l > m)

63 relates to bit index so don't write it as a literal)

> +		return -EINVAL;
> +
> +	*sample_id = val & 0xffff;
> +	*lsb = l;
> +	*msb = m;
> +	return 0;
> +}
> +
> +static int json_count_root(json_t *root, size_t *metric_count, size_t *string_bytes)
> +{
> +	size_t n;
> +
> +	if (!json_is_array(root))
> +		return -EINVAL;
> +
> +	n = json_array_size(root);
> +
> +	for (size_t i = 0; i < n; i++) {
> +		const char *name, *brief, *group, *platform_group;
> +		json_t *ev;
> +
> +		ev = json_array_get(root, i);
> +
> +		if (!json_is_object(ev))

No empty lines between call and its error handling.

> +			continue;
> +
> +		name = json_string_value(json_object_get(ev, "EventName"));
> +		brief = json_string_value(json_object_get(ev, "BriefDescription"));
> +		group = json_string_value(json_object_get(ev, "MetricGroup"));
> +		platform_group = json_string_value(json_object_get(ev, "PlatformGroup"));
> +
> +		if (name)
> +			*string_bytes += strlen(name) + 1;
> +		if (brief)
> +			*string_bytes += strlen(brief) + 1;
> +		if (group)
> +			*string_bytes += strlen(group) + 1;
> +		if (platform_group)
> +			*string_bytes += strlen(platform_group) + 1;

Another part of the adhoc heap management? Please don't do this.

> +
> +		(*metric_count)++;
> +	}
> +
> +	if (*metric_count == 0)
> +		return PMTCTL_ERR_NOMETRICS;
> +
> +	return 0;
> +}
> +
> +static int json_fill_root(json_t *root, struct pmt_metric_def *defs, char **pool)
> +{
> +	int idx = 0;
> +	size_t n;
> +
> +	if (!json_is_array(root))
> +		return -EINVAL;
> +
> +	n = json_array_size(root);
> +
> +	for (size_t i = 0; i < n; i++) {
> +		const char *pmu, *name, *brief, *group, *platform_group, *cfg;
> +		json_t *ev;
> +		struct pmt_metric_def *m;
> +		int ret;
> +
> +		ev = json_array_get(root, i);
> +
> +		if (!json_is_object(ev))
> +			continue;
> +
> +		m = &defs[idx++];
> +
> +		pmu = json_string_value(json_object_get(ev, "PMU"));
> +		name = json_string_value(json_object_get(ev, "EventName"));
> +		brief = json_string_value(json_object_get(ev, "BriefDescription"));
> +		group = json_string_value(json_object_get(ev, "MetricGroup"));
> +		platform_group = json_string_value(json_object_get(ev, "PlatformGroup"));
> +		cfg = json_string_value(json_object_get(ev, "ConfigCode"));
> +
> +		m->event_name = pool_copy(pool, name);
> +		m->description = pool_copy(pool, brief);
> +		m->group = pool_copy(pool, group);
> +		m->platform_group = pool_copy(pool, platform_group);
> +		m->guid = pmt_guid_intern(parse_guid_from_pmu(pmu));
> +		if (!m->guid)
> +			return -ENOMEM;
> +
> +		ret = parse_config_code(cfg, &m->sample_id, &m->lsb, &m->msb);
> +		if (ret)
> +			return log_ret(ret, "metric \"%s\" (PMU %s): invalid ConfigCode \"%s\"",
> +				       name ? name : "(null)",
> +				       pmu ? pmu : "(null)",
> +				       cfg ? cfg : "(null)");
> +	}
> +
> +	return 0;
> +}
> +
> +static struct pmt_metric_def *load_metrics_from_json_single(const char *path, int *count)
> +{
> +	json_error_t error;
> +	json_t *root;
> +	struct pmt_metric_def *defs;
> +	void *block;
> +	char *pool;
> +	size_t metric_count = 0;
> +	size_t string_bytes = 0;
> +	size_t def_bytes;
> +	size_t total_bytes;
> +	int ret;
> +
> +	if (!count)
> +		return NULL;
> +
> +	*count = 0;
> +
> +	root = json_load_file(path, 0, &error);
> +	if (!root) {
> +		log_err(PMTCTL_ERR_METRICS, "JSON parse error in %s: %s\n", path, error.text);
> +		return NULL;
> +	}
> +
> +	ret = json_count_root(root, &metric_count, &string_bytes);
> +	if (ret < 0) {
> +		json_decref(root);
> +		return NULL;
> +	}
> +
> +	def_bytes = metric_count * sizeof(struct pmt_metric_def);
> +	total_bytes = def_bytes + string_bytes;
> +
> +	block = malloc(total_bytes);
> +	if (!block) {
> +		json_decref(root);
> +		return NULL;
> +	}
> +
> +	defs = block;
> +	memset(defs, 0, def_bytes);
> +	pool = (char *)block + def_bytes;
> +
> +	ret = json_fill_root(root, defs, &pool);
> +	json_decref(root);
> +	if (ret < 0) {
> +		free(block);
> +		return NULL;
> +	}
> +
> +	*count = (int)metric_count;
> +
> +	return defs;
> +}
> +
> +static int load_pmt_guids_sidecar(const char *path)
> +{
> +	json_error_t error;
> +	json_t *root;
> +	struct pmt_guid *table;
> +	char *pool;
> +	size_t string_bytes = 0;
> +	size_t n;
> +	int ret = -EINVAL;
> +
> +	root = json_load_file(path, 0, &error);
> +	if (!root) {
> +		log_warn("pmt_guids JSON parse error in %s: %s\n", path, error.text);
> +		return -EINVAL;
> +	}
> +
> +	if (!json_is_array(root)) {
> +		json_decref(root);
> +		return -EINVAL;
> +	}
> +
> +	n = json_array_size(root);
> +	if (n == 0) {
> +		json_decref(root);
> +		return 0;
> +	}
> +
> +	for (size_t i = 0; i < n; i++) {
> +		json_t *e = json_array_get(root, i);
> +		const char *s;
> +
> +		if (!json_is_object(e))
> +			continue;
> +		s = json_string_value(json_object_get(e, "name"));
> +		if (s)
> +			string_bytes += strlen(s) + 1;
> +		s = json_string_value(json_object_get(e, "description"));
> +		if (s)
> +			string_bytes += strlen(s) + 1;
> +	}
> +
> +	table = calloc(1, n * sizeof(*table) + string_bytes);
> +	if (!table) {
> +		json_decref(root);
> +		return -ENOMEM;

Instead of duplication root decref, add a label and goto to the end.

> +	}
> +
> +	pool = (char *)table + n * sizeof(*table);

Can this be written without casting to an alien type???

> +
> +	for (size_t i = 0; i < n; i++) {
> +		json_t *e = json_array_get(root, i);
> +		const char *guid_s, *name, *desc;
> +		unsigned long guid_val;
> +		char *end;
> +
> +		if (!json_is_object(e))
> +			continue;
> +
> +		guid_s = json_string_value(json_object_get(e, "guid"));
> +		if (!guid_s)
> +			continue;
> +
> +		errno = 0;
> +		guid_val = strtoul(guid_s, &end, 0);
> +		if (errno || end == guid_s || *end != '\0' || guid_val > UINT32_MAX)
> +			continue;
> +
> +		name = json_string_value(json_object_get(e, "name"));
> +		desc = json_string_value(json_object_get(e, "description"));
> +
> +		table[i].guid = (uint32_t)guid_val;
> +		table[i].name = name ? pool_copy(&pool, name) : NULL;
> +		table[i].description = desc ? pool_copy(&pool, desc) : NULL;

pool_copy() handles NULL inputs so why is the check duplicated here?

> +	}
> +
> +	ret = pmt_guid_register_owned(table, table, (int)n);

A typing issue?

> +
> +	json_decref(root);
> +	if (ret < 0)
> +		free(table);

Error handling should be first.

> +
> +	return ret;
> +}
> +
> +static int load_metrics_from_json_dir(const char *dir_path, struct pmt_metrics_db *db)
> +{
> +	DIR *d = opendir(dir_path);
> +	struct dirent *de;
> +	char guids_path[PATH_MAX];
> +	int len;
> +	int ret = 0;
> +
> +	if (!d)
> +		return log_ret(-errno, "could not open %s", dir_path);
> +
> +	/*
> +	 * Load the optional pmt_guids.json sidecar first so any subsequent
> +	 * intern calls in json_fill_root() resolve to the registered
> +	 * struct pmt_guid entries (with name/description populated).
> +	 */
> +	len = snprintf(guids_path, sizeof(guids_path), "%s/%s", dir_path, "pmt_guids.json");
> +	if (len > 0 && (size_t)len < sizeof(guids_path) && is_regular(guids_path))
> +		(void)load_pmt_guids_sidecar(guids_path);
> +
> +	while ((de = readdir(d)) != NULL) {
> +		struct pmt_metric_def *defs;
> +		const char *name = de->d_name;
> +		const char *dot = strrchr(name, '.');
> +		char pathbuf[PATH_MAX];
> +		int count = 0;
> +
> +		if (name[0] == '.')

dot == name ?

> +			continue;
> +
> +		if (!dot || strcmp(dot, ".json"))
> +			continue;
> +
> +		/* Sidecar already handled above. */
> +		if (!strcmp(name, "pmt_guids.json"))
> +			continue;
> +
> +		if (snprintf(pathbuf, sizeof(pathbuf), "%s/%s", dir_path, name) >=
> +		    (int)sizeof(pathbuf))

No! Do the error check separately.

> +			continue;
> +		if (!is_regular(pathbuf))
> +			continue;
> +
> +		defs = load_metrics_from_json_single(pathbuf, &count);
> +		if (!defs) {
> +			log_warn("unable to load JSON %s\n", pathbuf);
> +			ret = -EINVAL;
> +			continue;
> +		}
> +
> +		ret = pmt_metrics_add_block(db, defs, count, false);
> +		if (ret < 0) {
> +			free(defs);
> +			log_ret(ret, "unable to add json %s", pathbuf);
> +			break;
> +		}
> +	}
> +	closedir(d);
> +
> +	if (ret < 0) {
> +		pmt_metrics_free(db);
> +		return ret;
> +	}
> +
> +	/*
> +	 * Successfully scanned the directory.  A zero count is reported via
> +	 * db->total == 0 to the caller; the dir itself was not broken, so
> +	 * do not invent an error rc here.  pmtctl_init() distinguishes
> +	 * "broken source" (negative rc) from "empty source" (rc == 0 with
> +	 * db->total == 0) and only fails the init in the former case.
> +	 */
> +	return 0;
> +}
> +
> +int pmt_metrics_load_json(const char *json_path, struct pmt_metrics_db *db)
> +{
> +	int ret;
> +
> +	if (!db)
> +		log_bug_and_exit("invalid metric db pointer");
> +
> +	if (!json_path)
> +		return PMTCTL_ERR_METRICS;
> +
> +	if (is_regular(json_path)) {
> +		struct pmt_metric_def *defs;
> +		int count = 0;
> +
> +		defs = load_metrics_from_json_single(json_path, &count);
> +		if (!defs)
> +			return log_ret(PMTCTL_ERR_METRICS, "unable to load json %s", json_path);
> +
> +		ret = pmt_metrics_add_block(db, defs, count, false);
> +		if (ret < 0) {
> +			free(defs);
> +			return log_ret(ret, "unable to add json %s", json_path);
> +		}
> +
> +		return 0;
> +	}
> +
> +	if (is_dir(json_path))
> +		return load_metrics_from_json_dir(json_path, db);
> +
> +	return PMTCTL_ERR_METRICS;
> +}
> 

-- 
 i.


^ permalink raw reply	[flat|nested] 25+ messages in thread

* Re: [PATCH 08/17] tools/arch/x86/pmtctl: Add libpmtctl public API and context
  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
  0 siblings, 1 reply; 25+ messages in thread
From: Ilpo Järvinen @ 2026-05-26 11:25 UTC (permalink / raw)
  To: David E. Box; +Cc: LKML, Andy Shevchenko, platform-driver-x86

[-- Attachment #1: Type: text/plain, Size: 15486 bytes --]

On Mon, 25 May 2026, David E. Box wrote:

> Add the public API for libpmtctl_core and the context object that backs it,
> providing callers with a stable, opaque interface for discovering Intel PMT
> telemetry devices and querying their metrics without depending on internal
> library structures.
> 
> This patch adds the primary lifecycle and query interfaces used by library
> consumers. pmtctl_init() performs device enumeration and metric database
> loading once during initialization so subsequent queries operate on cached
> state rather than re-scanning sysfs. pmtctl_cleanup() releases associated
> resources.
> 
> The library context (struct pmtctl_context) remains opaque to callers.
> pmtctl_get_ctx() provides controlled access for consumers that need to pass
> library context across translation units.
> 
> Add PMTCTL_SCOPE_GUARD() as a convenience macro for deterministic cleanup
> of library-managed resources in single-exit C code paths.
> 
> Build-system wiring is added a later patch of the series.
> 
> Assisted-by: GitHub-Copilot:claude-sonnet-4.6
> Signed-off-by: David E. Box <david.e.box@linux.intel.com>
> ---
>  tools/arch/x86/pmtctl/include/lib/pmtctl.h    |  90 +++++
>  .../x86/pmtctl/include/lib/pmtctl_context.h   |  21 ++
>  tools/arch/x86/pmtctl/lib/pmtctl.c            | 327 ++++++++++++++++++
>  3 files changed, 438 insertions(+)
>  create mode 100644 tools/arch/x86/pmtctl/include/lib/pmtctl.h
>  create mode 100644 tools/arch/x86/pmtctl/include/lib/pmtctl_context.h
>  create mode 100644 tools/arch/x86/pmtctl/lib/pmtctl.c
> 
> diff --git a/tools/arch/x86/pmtctl/include/lib/pmtctl.h b/tools/arch/x86/pmtctl/include/lib/pmtctl.h
> new file mode 100644
> index 000000000000..b243a48e8d72
> --- /dev/null
> +++ b/tools/arch/x86/pmtctl/include/lib/pmtctl.h
> @@ -0,0 +1,90 @@
> +/* SPDX-License-Identifier: GPL-2.0-only */
> +#ifndef PMTCTL_H
> +#define PMTCTL_H
> +
> +#include <stdbool.h>
> +#include <stdint.h>
> +
> +#include <linux/compiler.h>
> +#include "pmtctl_types.h"
> +
> +enum pmt_selector_kind {
> +	PMT_SEL_ANY = 0,   /* no selector provided */
> +	PMT_SEL_GUID,      /* guid=27971628 */
> +	PMT_SEL_EP_NAME,   /* ep=pmt_ep_27971628_0 OR ep=telem1 */
> +};
> +
> +struct pmt_ep_selector {
> +	enum pmt_selector_kind kind;
> +
> +	uint32_t guid;
> +
> +	/* For SEL_EP_NAME */
> +	const char *str;
> +};
> +
> +struct pmt_binding {
> +	int metric_idx;   /* index into defs[] */
> +	int device_idx;   /* index into devices[] */
> +};
> +
> +struct pmtctl_context;
> +
> +struct pmt_global_opts {
> +	const char            *json_path;        /* -J / --json-file */
> +	const char            *device_selector;  /* -d / --device (raw) */
> +	bool                   quiet;            /* -q / --quiet */
> +	bool                   debug;            /* --debug */
> +};
> +
> +/*
> + * Initialize library-global PMT state.
> + *
> + * This may return success even if no metric definitions are available.
> + * In that case, device enumeration is still usable and raw mode remains
> + * supported (raw reads do not require metric definitions).
> + */
> +int pmtctl_init(const struct pmt_global_opts *gopts);
> +const struct pmtctl_context *pmtctl_get_ctx(void);
> +enum pmt_device_type pmtctl_get_device_type(void);
> +int pmtctl_get_num_devices(void);
> +int pmtctl_get_num_metrics(void);
> +int pmtctl_get_num_bindings(void);
> +
> +/*
> + * Set process-global library logging verbosity.
> + *
> + * Invalid values are clamped to PMTCTL_LOG_INFO.
> + */
> +void pmtctl_set_log_level(enum pmtctl_log_level level);
> +
> +/*
> + * Thread-safety note:
> + *
> + * libpmtctl_core is generally not thread-safe. Callers should serialize
> + * pmtctl_init()/pmtctl_cleanup() and API usage around shared library state.
> + */
> +void pmtctl_cleanup(void);
> +
> +static inline __always_unused void pmtctl_scope_cleanup(int *unused)
> +{
> +	(void)unused;
> +	pmtctl_cleanup();
> +}
> +
> +#define PMTCTL_SCOPE_GUARD                              \
> +	__attribute__((cleanup(pmtctl_scope_cleanup)))  \
> +	int _pmtctl_scope_guard __always_unused \
> +
> +int pmt_select_devices(const struct pmtctl_context *ctx, const struct pmt_ep_selector *sel,
> +		       int *out_idx, int max_out);
> +
> +/*
> + * Parse an ep selector string like "guid=27971628" or "ep=pmt_ep_...".
> + * For selectors that set a string (ep/name) the implementation will
> + * allocate a copy into sel->str; the caller is responsible for freeing
> + * sel->str if non-NULL.
> + */
> +int pmtctl_parse_ep_selector(const char *s, struct pmt_ep_selector *out);
> +
> +#endif
> diff --git a/tools/arch/x86/pmtctl/include/lib/pmtctl_context.h b/tools/arch/x86/pmtctl/include/lib/pmtctl_context.h
> new file mode 100644
> index 000000000000..6f3e8563f7eb
> --- /dev/null
> +++ b/tools/arch/x86/pmtctl/include/lib/pmtctl_context.h
> @@ -0,0 +1,21 @@
> +/* SPDX-License-Identifier: GPL-2.0-only */
> +#ifndef PMTCTL_CONTEXT_H
> +#define PMTCTL_CONTEXT_H
> +
> +#include "lib/device.h"
> +#include "lib/metrics_db.h"
> +#include "lib/pmtctl.h"
> +
> +struct pmtctl_context {
> +	const struct pmt_device_ops *ops;
> +
> +	struct pmt_device *devices;
> +	int num_devices;
> +
> +	struct pmt_metrics_db metrics;
> +
> +	struct pmt_binding *bindings;
> +	int num_bindings;
> +};
> +
> +#endif
> diff --git a/tools/arch/x86/pmtctl/lib/pmtctl.c b/tools/arch/x86/pmtctl/lib/pmtctl.c
> new file mode 100644
> index 000000000000..5ee2f576316a
> --- /dev/null
> +++ b/tools/arch/x86/pmtctl/lib/pmtctl.c
> @@ -0,0 +1,327 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +#include <errno.h>
> +#include <string.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +
> +#include "lib/common.h"
> +#include "lib/device.h"
> +#include "lib/log.h"
> +#include "lib/metrics_db.h"
> +#include "lib/metrics_provider.h"
> +#include "lib/pmtctl.h"
> +#include "lib/pmtctl_context.h"
> +
> +static struct pmtctl_context g_pmtctl_ctx;
> +
> +const struct pmtctl_context *pmtctl_get_ctx(void)
> +{
> +	return &g_pmtctl_ctx;
> +}
> +
> +enum pmt_device_type pmtctl_get_device_type(void)
> +{
> +	const struct pmt_device_ops *ops = g_pmtctl_ctx.ops;
> +
> +	return ops ? ops->dev_type : PMT_DEVICE_TELEM;
> +}
> +
> +int pmtctl_get_num_devices(void)
> +{
> +	return g_pmtctl_ctx.num_devices;
> +}
> +
> +int pmtctl_get_num_metrics(void)
> +{
> +	return g_pmtctl_ctx.metrics.total;
> +}
> +
> +int pmtctl_get_num_bindings(void)
> +{
> +	return g_pmtctl_ctx.num_bindings;
> +}
> +
> +void pmtctl_set_log_level(enum pmtctl_log_level level)
> +{
> +	log_set_level(level);
> +}
> +
> +int pmt_select_devices(const struct pmtctl_context *ctx,
> +		       const struct pmt_ep_selector *sel,
> +		       int *out_idx, int max_out)
> +{
> +	int i, n = 0;
> +
> +	if (!ctx || !sel || !out_idx || max_out <= 0)
> +		return log_ret(PMTCTL_ERR_INVALID, "bad argument");
> +
> +	if (!ctx->devices || ctx->num_devices <= 0)
> +		return log_ret(PMTCTL_ERR_INVALID, "no devices");
> +
> +	switch (sel->kind) {
> +	case PMT_SEL_ANY:
> +		/* No filter: return all devices */
> +		for (i = 0; i < ctx->num_devices && n < max_out; i++)
> +			out_idx[n++] = i;
> +		break;
> +
> +	case PMT_SEL_GUID:
> +		for (i = 0; i < ctx->num_devices && n < max_out; i++) {
> +			const struct pmt_device *dev = &ctx->devices[i];
> +
> +			if (dev->guid && dev->guid->guid == sel->guid)
> +				out_idx[n++] = i;
> +		}
> +		break;
> +
> +	case PMT_SEL_EP_NAME:
> +		if (!sel->str || !sel->str[0])
> +			return log_ret(PMTCTL_ERR_CMD_PARSE, "empty ep selector");
> +
> +		for (i = 0; i < ctx->num_devices && n < max_out; i++) {
> +			const struct pmt_device *dev = &ctx->devices[i];
> +
> +			if (dev->name && strcmp(dev->name, sel->str) == 0)

!strcmp()

> +				out_idx[n++] = i;
> +		}


Move the code to helper with parameter(s) to filter variations.

> +		break;
> +
> +	default:
> +		return log_ret(PMTCTL_ERR_CMD_PARSE, "unknown selector kind %d", sel->kind);
> +	}
> +
> +	/* n == 0 is "no matches", not an API error. Caller decides what to do. */
> +	return n;
> +}
> +
> +int pmtctl_parse_ep_selector(const char *s, struct pmt_ep_selector *out)
> +{
> +	auto_free char *copy = NULL;
> +	char *end;
> +	char *eq;
> +	char *key = NULL;
> +	const char *val = NULL;
> +
> +	if (!out)
> +		return -EINVAL;
> +
> +	if (!s || !*s) {
> +		out->kind = PMT_SEL_ANY;
> +		out->str = NULL;
> +		out->guid = 0;
> +		return 0;
> +	}
> +
> +	/* Expect a single key=value selector: guid=..., ep=..., name=... */
> +	copy = strdup(s);
> +
> +	if (!copy)
> +		return -ENOMEM;

Hmm, so for some reason this can survive without invoking exit() right 
away. I wonder if exit() is really necessary with the other xstrdup() 
callers either...

> +
> +	eq = strchr(copy, '=');
> +	if (!eq)
> +		return -EINVAL;
> +
> +	*eq = '\0';
> +	key = copy;
> +	val = eq + 1;
> +
> +	if (!strcmp(key, "guid")) {
> +		errno = 0;
> +		unsigned long v = strtoul(val, &end, 16);
> +

Please fix this.

I think I've read enough of AI thrown curveballs. Please review _all_ the 
patches yourself before sending them again and don't let AI slipping from 
usual style or using unusual construct through but fix them all before 
submitting.

Given the size of this patch series, the next round, I'll just be passing 
the ball back to ypu on the spot where I see the usual style is blatantly
violated or some odd constructs are found.

I think I was quite generous to look this far.

> +		if (errno || end == val || *end != '\0' || v > UINT32_MAX)
> +			return errno ? -errno : -EINVAL;

Please split if you have to do elvis like this.

--
 i.

> +		out->kind = PMT_SEL_GUID;
> +		out->guid = (uint32_t)v;
> +		out->str = NULL;
> +	} else if (!strcmp(key, "ep") || !strcmp(key, "name")) {
> +		out->kind = PMT_SEL_EP_NAME;
> +		out->str = strdup(val);
> +		if (!out->str)
> +			return -ENOMEM;
> +		out->guid = 0;
> +	} else {
> +		log_err(PMTCTL_ERR_CMD_PARSE, "unknown device selector %s", key);
> +		return -EINVAL;
> +	}
> +	return 0;
> +}
> +
> +static bool
> +metric_matches_device(const struct pmt_metric_def *def, const struct pmt_device *dev)
> +{
> +	/*
> +	 * Compare the underlying numeric GUID rather than the pmt_guid pointer:
> +	 * builtin metric defs reference &builtin_guids[idx] directly, while
> +	 * devices intern through the registry. These may resolve to different
> +	 * pmt_guid entries for the same GUID depending on init order.
> +	 */
> +	return def->guid->guid == dev->guid->guid;
> +}
> +
> +static int pmt_bind_build(struct pmtctl_context *ctx)
> +{
> +	struct pmt_binding *bindings;
> +	size_t count = 0;
> +	int i, j, k;
> +
> +	/*
> +	 * -----------------------------
> +	 * Pass 1: count bindings
> +	 * -----------------------------
> +	 */
> +	for (i = 0; i < ctx->metrics.total; i++) {
> +		const struct pmt_metric_def *def = pmt_metrics_at(&ctx->metrics, i);
> +
> +		if (!def)
> +			continue;
> +
> +		for (j = 0; j < ctx->num_devices; j++) {
> +			const struct pmt_device *dev = &ctx->devices[j];
> +
> +			if (!metric_matches_device(def, dev))
> +				continue;
> +
> +			count++;
> +		}
> +	}
> +
> +	if (count == 0) {
> +		log_warn("no metric/device bindings found");
> +		return 0;
> +	}
> +
> +	bindings = calloc(count, sizeof(*bindings));
> +	if (!bindings)
> +		return log_ret(-ENOMEM, "count not allocate bindings");
> +
> +	/*
> +	 * -----------------------------
> +	 * Pass 2: fill bindings
> +	 * -----------------------------
> +	 */
> +	k = 0;
> +	for (i = 0; i < ctx->metrics.total; i++) {
> +		const struct pmt_metric_def *def = pmt_metrics_at(&ctx->metrics, i);
> +
> +		if (!def)
> +			continue;
> +
> +		for (j = 0; j < ctx->num_devices; j++) {
> +			const struct pmt_device *dev = &ctx->devices[j];
> +
> +			if (!metric_matches_device(def, dev))
> +				continue;
> +
> +			bindings[k].metric_idx = i;
> +			bindings[k].device_idx = j;
> +			k++;
> +		}
> +	}
> +
> +	ctx->bindings     = bindings;
> +	ctx->num_bindings = k;
> +
> +	return 0;
> +}
> +
> +int pmtctl_init(const struct pmt_global_opts *gopts)
> +{
> +	const struct pmt_device_ops *ops = NULL;
> +	int num_devices;
> +	int ret;
> +
> +	if (!gopts)
> +		return log_ret(PMTCTL_ERR_INVALID, "bad argument");
> +
> +	memset(&g_pmtctl_ctx, 0, sizeof(g_pmtctl_ctx));
> +
> +	/*
> +	 * 1) Initialize device backend.
> +	 *    Only the telem backend is currently supported; PMU support is
> +	 *    pending upstream driver availability.
> +	 */
> +	ret = device_telem_ops.init();
> +	if (ret != 0)
> +		return log_ret(ret, "failed to find PMT source");
> +	ops = &device_telem_ops;
> +	log_debug("Selecting from /sys/class/intel_pmt");
> +	g_pmtctl_ctx.ops = ops;
> +
> +	/*
> +	 * 2) Enumerate devices from the chosen device
> +	 */
> +	g_pmtctl_ctx.devices = ops->device_list(&num_devices);
> +	if (!g_pmtctl_ctx.devices) {
> +		pmtctl_cleanup();
> +		log_bug_and_exit("unexpected NULL device context");
> +	}
> +
> +	if (num_devices <= 0) {
> +		pmtctl_cleanup();
> +		log_bug_and_exit("unexpected zero device count");
> +	}
> +	g_pmtctl_ctx.num_devices = num_devices;
> +
> +	/*
> +	 * 3) Load metric definitions from JSON or built-in
> +	 *
> +	 * If metric load fails or returns zero metrics, we intentionally keep init as a
> +	 * degraded success (return 0) with a warning. This allows raw mode operation,
> +	 * which does not require metric definitions.
> +	 */
> +	ret = pmt_metrics_load(gopts->json_path, &g_pmtctl_ctx.metrics);
> +	/*
> +	 * Any nonzero rc here means the metric source itself was broken
> +	 * (e.g. -J pointed at a nonexistent path, or a JSON file failed to
> +	 * parse).  Treat that as a hard init failure so the CLI exits with
> +	 * PMTCTL_EXIT_SYSTEM instead of silently degrading to a metric-less
> +	 * session.  Empty source (ret == 0 but metrics.total == 0) stays a
> +	 * degraded success -- raw mode and `list --devices` are still usable.
> +	 *
> +	 * Some provider paths return positive PMTCTL_ERR_* codes; normalize
> +	 * those to -EIO so main()'s mapping selects EXIT_SYSTEM.
> +	 */
> +	if (ret != 0) {
> +		pmtctl_cleanup();
> +		if (ret > 0)
> +			ret = -EIO;
> +		return log_ret(ret, "failed to load metrics from %s",
> +			       gopts->json_path ? gopts->json_path : "<built-in>");
> +	}
> +	if (g_pmtctl_ctx.metrics.total == 0) {
> +		log_warn("no metrics from %s", gopts->json_path ? gopts->json_path : "<built-in>");
> +		return 0;
> +	}
> +
> +	/*
> +	 * 4) Build metric ↔ device bindings
> +	 */
> +	ret = pmt_bind_build(&g_pmtctl_ctx);
> +	if (ret != 0) {
> +		pmtctl_cleanup();
> +		return ret;
> +	}
> +
> +	return 0;
> +}
> +
> +void pmtctl_cleanup(void)
> +{
> +	if (g_pmtctl_ctx.bindings) {
> +		free(g_pmtctl_ctx.bindings);
> +		g_pmtctl_ctx.bindings = NULL;
> +		g_pmtctl_ctx.num_bindings = 0;
> +	}
> +
> +	if (g_pmtctl_ctx.ops && g_pmtctl_ctx.ops->cleanup)
> +		g_pmtctl_ctx.ops->cleanup();
> +
> +	pmt_metrics_free(&g_pmtctl_ctx.metrics);
> +	pmt_guid_cleanup();
> +
> +	g_pmtctl_ctx.ops         = NULL;
> +	g_pmtctl_ctx.devices     = NULL;
> +	g_pmtctl_ctx.num_devices = 0;
> +}
> 

^ permalink raw reply	[flat|nested] 25+ messages in thread

* Re: [PATCH 08/17] tools/arch/x86/pmtctl: Add libpmtctl public API and context
  2026-05-26 11:25   ` Ilpo Järvinen
@ 2026-05-26 17:44     ` David Box
  0 siblings, 0 replies; 25+ messages in thread
From: David Box @ 2026-05-26 17:44 UTC (permalink / raw)
  To: Ilpo Järvinen; +Cc: LKML, Andy Shevchenko, platform-driver-x86

On Tue, May 26, 2026 at 02:25:06PM +0300, Ilpo Järvinen wrote:
> On Mon, 25 May 2026, David E. Box wrote:
> 
> > Add the public API for libpmtctl_core and the context object that backs it,
> > providing callers with a stable, opaque interface for discovering Intel PMT
> > telemetry devices and querying their metrics without depending on internal
> > library structures.
> > 
> > This patch adds the primary lifecycle and query interfaces used by library
> > consumers. pmtctl_init() performs device enumeration and metric database
> > loading once during initialization so subsequent queries operate on cached
> > state rather than re-scanning sysfs. pmtctl_cleanup() releases associated
> > resources.
> > 
> > The library context (struct pmtctl_context) remains opaque to callers.
> > pmtctl_get_ctx() provides controlled access for consumers that need to pass
> > library context across translation units.
> > 
> > Add PMTCTL_SCOPE_GUARD() as a convenience macro for deterministic cleanup
> > of library-managed resources in single-exit C code paths.
> > 
> > Build-system wiring is added a later patch of the series.
> > 
> > Assisted-by: GitHub-Copilot:claude-sonnet-4.6
> > Signed-off-by: David E. Box <david.e.box@linux.intel.com>
> > ---
> >  tools/arch/x86/pmtctl/include/lib/pmtctl.h    |  90 +++++
> >  .../x86/pmtctl/include/lib/pmtctl_context.h   |  21 ++
> >  tools/arch/x86/pmtctl/lib/pmtctl.c            | 327 ++++++++++++++++++
> >  3 files changed, 438 insertions(+)
> >  create mode 100644 tools/arch/x86/pmtctl/include/lib/pmtctl.h
> >  create mode 100644 tools/arch/x86/pmtctl/include/lib/pmtctl_context.h
> >  create mode 100644 tools/arch/x86/pmtctl/lib/pmtctl.c
> > 
> > diff --git a/tools/arch/x86/pmtctl/include/lib/pmtctl.h b/tools/arch/x86/pmtctl/include/lib/pmtctl.h
> > new file mode 100644
> > index 000000000000..b243a48e8d72
> > --- /dev/null
> > +++ b/tools/arch/x86/pmtctl/include/lib/pmtctl.h
> > @@ -0,0 +1,90 @@
> > +/* SPDX-License-Identifier: GPL-2.0-only */
> > +#ifndef PMTCTL_H
> > +#define PMTCTL_H
> > +
> > +#include <stdbool.h>
> > +#include <stdint.h>
> > +
> > +#include <linux/compiler.h>
> > +#include "pmtctl_types.h"
> > +
> > +enum pmt_selector_kind {
> > +	PMT_SEL_ANY = 0,   /* no selector provided */
> > +	PMT_SEL_GUID,      /* guid=27971628 */
> > +	PMT_SEL_EP_NAME,   /* ep=pmt_ep_27971628_0 OR ep=telem1 */
> > +};
> > +
> > +struct pmt_ep_selector {
> > +	enum pmt_selector_kind kind;
> > +
> > +	uint32_t guid;
> > +
> > +	/* For SEL_EP_NAME */
> > +	const char *str;
> > +};
> > +
> > +struct pmt_binding {
> > +	int metric_idx;   /* index into defs[] */
> > +	int device_idx;   /* index into devices[] */
> > +};
> > +
> > +struct pmtctl_context;
> > +
> > +struct pmt_global_opts {
> > +	const char            *json_path;        /* -J / --json-file */
> > +	const char            *device_selector;  /* -d / --device (raw) */
> > +	bool                   quiet;            /* -q / --quiet */
> > +	bool                   debug;            /* --debug */
> > +};
> > +
> > +/*
> > + * Initialize library-global PMT state.
> > + *
> > + * This may return success even if no metric definitions are available.
> > + * In that case, device enumeration is still usable and raw mode remains
> > + * supported (raw reads do not require metric definitions).
> > + */
> > +int pmtctl_init(const struct pmt_global_opts *gopts);
> > +const struct pmtctl_context *pmtctl_get_ctx(void);
> > +enum pmt_device_type pmtctl_get_device_type(void);
> > +int pmtctl_get_num_devices(void);
> > +int pmtctl_get_num_metrics(void);
> > +int pmtctl_get_num_bindings(void);
> > +
> > +/*
> > + * Set process-global library logging verbosity.
> > + *
> > + * Invalid values are clamped to PMTCTL_LOG_INFO.
> > + */
> > +void pmtctl_set_log_level(enum pmtctl_log_level level);
> > +
> > +/*
> > + * Thread-safety note:
> > + *
> > + * libpmtctl_core is generally not thread-safe. Callers should serialize
> > + * pmtctl_init()/pmtctl_cleanup() and API usage around shared library state.
> > + */
> > +void pmtctl_cleanup(void);
> > +
> > +static inline __always_unused void pmtctl_scope_cleanup(int *unused)
> > +{
> > +	(void)unused;
> > +	pmtctl_cleanup();
> > +}
> > +
> > +#define PMTCTL_SCOPE_GUARD                              \
> > +	__attribute__((cleanup(pmtctl_scope_cleanup)))  \
> > +	int _pmtctl_scope_guard __always_unused \
> > +
> > +int pmt_select_devices(const struct pmtctl_context *ctx, const struct pmt_ep_selector *sel,
> > +		       int *out_idx, int max_out);
> > +
> > +/*
> > + * Parse an ep selector string like "guid=27971628" or "ep=pmt_ep_...".
> > + * For selectors that set a string (ep/name) the implementation will
> > + * allocate a copy into sel->str; the caller is responsible for freeing
> > + * sel->str if non-NULL.
> > + */
> > +int pmtctl_parse_ep_selector(const char *s, struct pmt_ep_selector *out);
> > +
> > +#endif
> > diff --git a/tools/arch/x86/pmtctl/include/lib/pmtctl_context.h b/tools/arch/x86/pmtctl/include/lib/pmtctl_context.h
> > new file mode 100644
> > index 000000000000..6f3e8563f7eb
> > --- /dev/null
> > +++ b/tools/arch/x86/pmtctl/include/lib/pmtctl_context.h
> > @@ -0,0 +1,21 @@
> > +/* SPDX-License-Identifier: GPL-2.0-only */
> > +#ifndef PMTCTL_CONTEXT_H
> > +#define PMTCTL_CONTEXT_H
> > +
> > +#include "lib/device.h"
> > +#include "lib/metrics_db.h"
> > +#include "lib/pmtctl.h"
> > +
> > +struct pmtctl_context {
> > +	const struct pmt_device_ops *ops;
> > +
> > +	struct pmt_device *devices;
> > +	int num_devices;
> > +
> > +	struct pmt_metrics_db metrics;
> > +
> > +	struct pmt_binding *bindings;
> > +	int num_bindings;
> > +};
> > +
> > +#endif
> > diff --git a/tools/arch/x86/pmtctl/lib/pmtctl.c b/tools/arch/x86/pmtctl/lib/pmtctl.c
> > new file mode 100644
> > index 000000000000..5ee2f576316a
> > --- /dev/null
> > +++ b/tools/arch/x86/pmtctl/lib/pmtctl.c
> > @@ -0,0 +1,327 @@
> > +// SPDX-License-Identifier: GPL-2.0-only
> > +#include <errno.h>
> > +#include <string.h>
> > +#include <stdio.h>
> > +#include <stdlib.h>
> > +
> > +#include "lib/common.h"
> > +#include "lib/device.h"
> > +#include "lib/log.h"
> > +#include "lib/metrics_db.h"
> > +#include "lib/metrics_provider.h"
> > +#include "lib/pmtctl.h"
> > +#include "lib/pmtctl_context.h"
> > +
> > +static struct pmtctl_context g_pmtctl_ctx;
> > +
> > +const struct pmtctl_context *pmtctl_get_ctx(void)
> > +{
> > +	return &g_pmtctl_ctx;
> > +}
> > +
> > +enum pmt_device_type pmtctl_get_device_type(void)
> > +{
> > +	const struct pmt_device_ops *ops = g_pmtctl_ctx.ops;
> > +
> > +	return ops ? ops->dev_type : PMT_DEVICE_TELEM;
> > +}
> > +
> > +int pmtctl_get_num_devices(void)
> > +{
> > +	return g_pmtctl_ctx.num_devices;
> > +}
> > +
> > +int pmtctl_get_num_metrics(void)
> > +{
> > +	return g_pmtctl_ctx.metrics.total;
> > +}
> > +
> > +int pmtctl_get_num_bindings(void)
> > +{
> > +	return g_pmtctl_ctx.num_bindings;
> > +}
> > +
> > +void pmtctl_set_log_level(enum pmtctl_log_level level)
> > +{
> > +	log_set_level(level);
> > +}
> > +
> > +int pmt_select_devices(const struct pmtctl_context *ctx,
> > +		       const struct pmt_ep_selector *sel,
> > +		       int *out_idx, int max_out)
> > +{
> > +	int i, n = 0;
> > +
> > +	if (!ctx || !sel || !out_idx || max_out <= 0)
> > +		return log_ret(PMTCTL_ERR_INVALID, "bad argument");
> > +
> > +	if (!ctx->devices || ctx->num_devices <= 0)
> > +		return log_ret(PMTCTL_ERR_INVALID, "no devices");
> > +
> > +	switch (sel->kind) {
> > +	case PMT_SEL_ANY:
> > +		/* No filter: return all devices */
> > +		for (i = 0; i < ctx->num_devices && n < max_out; i++)
> > +			out_idx[n++] = i;
> > +		break;
> > +
> > +	case PMT_SEL_GUID:
> > +		for (i = 0; i < ctx->num_devices && n < max_out; i++) {
> > +			const struct pmt_device *dev = &ctx->devices[i];
> > +
> > +			if (dev->guid && dev->guid->guid == sel->guid)
> > +				out_idx[n++] = i;
> > +		}
> > +		break;
> > +
> > +	case PMT_SEL_EP_NAME:
> > +		if (!sel->str || !sel->str[0])
> > +			return log_ret(PMTCTL_ERR_CMD_PARSE, "empty ep selector");
> > +
> > +		for (i = 0; i < ctx->num_devices && n < max_out; i++) {
> > +			const struct pmt_device *dev = &ctx->devices[i];
> > +
> > +			if (dev->name && strcmp(dev->name, sel->str) == 0)
> 
> !strcmp()
> 
> > +				out_idx[n++] = i;
> > +		}
> 
> 
> Move the code to helper with parameter(s) to filter variations.
> 
> > +		break;
> > +
> > +	default:
> > +		return log_ret(PMTCTL_ERR_CMD_PARSE, "unknown selector kind %d", sel->kind);
> > +	}
> > +
> > +	/* n == 0 is "no matches", not an API error. Caller decides what to do. */
> > +	return n;
> > +}
> > +
> > +int pmtctl_parse_ep_selector(const char *s, struct pmt_ep_selector *out)
> > +{
> > +	auto_free char *copy = NULL;
> > +	char *end;
> > +	char *eq;
> > +	char *key = NULL;
> > +	const char *val = NULL;
> > +
> > +	if (!out)
> > +		return -EINVAL;
> > +
> > +	if (!s || !*s) {
> > +		out->kind = PMT_SEL_ANY;
> > +		out->str = NULL;
> > +		out->guid = 0;
> > +		return 0;
> > +	}
> > +
> > +	/* Expect a single key=value selector: guid=..., ep=..., name=... */
> > +	copy = strdup(s);
> > +
> > +	if (!copy)
> > +		return -ENOMEM;
> 
> Hmm, so for some reason this can survive without invoking exit() right 
> away. I wonder if exit() is really necessary with the other xstrdup() 
> callers either...
> 
> > +
> > +	eq = strchr(copy, '=');
> > +	if (!eq)
> > +		return -EINVAL;
> > +
> > +	*eq = '\0';
> > +	key = copy;
> > +	val = eq + 1;
> > +
> > +	if (!strcmp(key, "guid")) {
> > +		errno = 0;
> > +		unsigned long v = strtoul(val, &end, 16);
> > +
> 
> Please fix this.
> 
> I think I've read enough of AI thrown curveballs. Please review _all_ the 
> patches yourself before sending them again and don't let AI slipping from 
> usual style or using unusual construct through but fix them all before 
> submitting.
> 
> Given the size of this patch series, the next round, I'll just be passing 
> the ball back to ypu on the spot where I see the usual style is blatantly
> violated or some odd constructs are found.
> 
> I think I was quite generous to look this far.

Yes you were.

I agree with the feedback. I started leaning too much on generated code toward
the end of development and did not give the final result the full line-by-line
review it needed before posting.

Rather than respond to each comment, I'll do a full pass over the series for v2,
addressing the issues you mentioned along with any other inconsistencies.

Thanks for taking the time to review it this far.

David

> 
> > +		if (errno || end == val || *end != '\0' || v > UINT32_MAX)
> > +			return errno ? -errno : -EINVAL;
> 
> Please split if you have to do elvis like this.
> 
> --
>  i.
> 
> > +		out->kind = PMT_SEL_GUID;
> > +		out->guid = (uint32_t)v;
> > +		out->str = NULL;
> > +	} else if (!strcmp(key, "ep") || !strcmp(key, "name")) {
> > +		out->kind = PMT_SEL_EP_NAME;
> > +		out->str = strdup(val);
> > +		if (!out->str)
> > +			return -ENOMEM;
> > +		out->guid = 0;
> > +	} else {
> > +		log_err(PMTCTL_ERR_CMD_PARSE, "unknown device selector %s", key);
> > +		return -EINVAL;
> > +	}
> > +	return 0;
> > +}
> > +
> > +static bool
> > +metric_matches_device(const struct pmt_metric_def *def, const struct pmt_device *dev)
> > +{
> > +	/*
> > +	 * Compare the underlying numeric GUID rather than the pmt_guid pointer:
> > +	 * builtin metric defs reference &builtin_guids[idx] directly, while
> > +	 * devices intern through the registry. These may resolve to different
> > +	 * pmt_guid entries for the same GUID depending on init order.
> > +	 */
> > +	return def->guid->guid == dev->guid->guid;
> > +}
> > +
> > +static int pmt_bind_build(struct pmtctl_context *ctx)
> > +{
> > +	struct pmt_binding *bindings;
> > +	size_t count = 0;
> > +	int i, j, k;
> > +
> > +	/*
> > +	 * -----------------------------
> > +	 * Pass 1: count bindings
> > +	 * -----------------------------
> > +	 */
> > +	for (i = 0; i < ctx->metrics.total; i++) {
> > +		const struct pmt_metric_def *def = pmt_metrics_at(&ctx->metrics, i);
> > +
> > +		if (!def)
> > +			continue;
> > +
> > +		for (j = 0; j < ctx->num_devices; j++) {
> > +			const struct pmt_device *dev = &ctx->devices[j];
> > +
> > +			if (!metric_matches_device(def, dev))
> > +				continue;
> > +
> > +			count++;
> > +		}
> > +	}
> > +
> > +	if (count == 0) {
> > +		log_warn("no metric/device bindings found");
> > +		return 0;
> > +	}
> > +
> > +	bindings = calloc(count, sizeof(*bindings));
> > +	if (!bindings)
> > +		return log_ret(-ENOMEM, "count not allocate bindings");
> > +
> > +	/*
> > +	 * -----------------------------
> > +	 * Pass 2: fill bindings
> > +	 * -----------------------------
> > +	 */
> > +	k = 0;
> > +	for (i = 0; i < ctx->metrics.total; i++) {
> > +		const struct pmt_metric_def *def = pmt_metrics_at(&ctx->metrics, i);
> > +
> > +		if (!def)
> > +			continue;
> > +
> > +		for (j = 0; j < ctx->num_devices; j++) {
> > +			const struct pmt_device *dev = &ctx->devices[j];
> > +
> > +			if (!metric_matches_device(def, dev))
> > +				continue;
> > +
> > +			bindings[k].metric_idx = i;
> > +			bindings[k].device_idx = j;
> > +			k++;
> > +		}
> > +	}
> > +
> > +	ctx->bindings     = bindings;
> > +	ctx->num_bindings = k;
> > +
> > +	return 0;
> > +}
> > +
> > +int pmtctl_init(const struct pmt_global_opts *gopts)
> > +{
> > +	const struct pmt_device_ops *ops = NULL;
> > +	int num_devices;
> > +	int ret;
> > +
> > +	if (!gopts)
> > +		return log_ret(PMTCTL_ERR_INVALID, "bad argument");
> > +
> > +	memset(&g_pmtctl_ctx, 0, sizeof(g_pmtctl_ctx));
> > +
> > +	/*
> > +	 * 1) Initialize device backend.
> > +	 *    Only the telem backend is currently supported; PMU support is
> > +	 *    pending upstream driver availability.
> > +	 */
> > +	ret = device_telem_ops.init();
> > +	if (ret != 0)
> > +		return log_ret(ret, "failed to find PMT source");
> > +	ops = &device_telem_ops;
> > +	log_debug("Selecting from /sys/class/intel_pmt");
> > +	g_pmtctl_ctx.ops = ops;
> > +
> > +	/*
> > +	 * 2) Enumerate devices from the chosen device
> > +	 */
> > +	g_pmtctl_ctx.devices = ops->device_list(&num_devices);
> > +	if (!g_pmtctl_ctx.devices) {
> > +		pmtctl_cleanup();
> > +		log_bug_and_exit("unexpected NULL device context");
> > +	}
> > +
> > +	if (num_devices <= 0) {
> > +		pmtctl_cleanup();
> > +		log_bug_and_exit("unexpected zero device count");
> > +	}
> > +	g_pmtctl_ctx.num_devices = num_devices;
> > +
> > +	/*
> > +	 * 3) Load metric definitions from JSON or built-in
> > +	 *
> > +	 * If metric load fails or returns zero metrics, we intentionally keep init as a
> > +	 * degraded success (return 0) with a warning. This allows raw mode operation,
> > +	 * which does not require metric definitions.
> > +	 */
> > +	ret = pmt_metrics_load(gopts->json_path, &g_pmtctl_ctx.metrics);
> > +	/*
> > +	 * Any nonzero rc here means the metric source itself was broken
> > +	 * (e.g. -J pointed at a nonexistent path, or a JSON file failed to
> > +	 * parse).  Treat that as a hard init failure so the CLI exits with
> > +	 * PMTCTL_EXIT_SYSTEM instead of silently degrading to a metric-less
> > +	 * session.  Empty source (ret == 0 but metrics.total == 0) stays a
> > +	 * degraded success -- raw mode and `list --devices` are still usable.
> > +	 *
> > +	 * Some provider paths return positive PMTCTL_ERR_* codes; normalize
> > +	 * those to -EIO so main()'s mapping selects EXIT_SYSTEM.
> > +	 */
> > +	if (ret != 0) {
> > +		pmtctl_cleanup();
> > +		if (ret > 0)
> > +			ret = -EIO;
> > +		return log_ret(ret, "failed to load metrics from %s",
> > +			       gopts->json_path ? gopts->json_path : "<built-in>");
> > +	}
> > +	if (g_pmtctl_ctx.metrics.total == 0) {
> > +		log_warn("no metrics from %s", gopts->json_path ? gopts->json_path : "<built-in>");
> > +		return 0;
> > +	}
> > +
> > +	/*
> > +	 * 4) Build metric ↔ device bindings
> > +	 */
> > +	ret = pmt_bind_build(&g_pmtctl_ctx);
> > +	if (ret != 0) {
> > +		pmtctl_cleanup();
> > +		return ret;
> > +	}
> > +
> > +	return 0;
> > +}
> > +
> > +void pmtctl_cleanup(void)
> > +{
> > +	if (g_pmtctl_ctx.bindings) {
> > +		free(g_pmtctl_ctx.bindings);
> > +		g_pmtctl_ctx.bindings = NULL;
> > +		g_pmtctl_ctx.num_bindings = 0;
> > +	}
> > +
> > +	if (g_pmtctl_ctx.ops && g_pmtctl_ctx.ops->cleanup)
> > +		g_pmtctl_ctx.ops->cleanup();
> > +
> > +	pmt_metrics_free(&g_pmtctl_ctx.metrics);
> > +	pmt_guid_cleanup();
> > +
> > +	g_pmtctl_ctx.ops         = NULL;
> > +	g_pmtctl_ctx.devices     = NULL;
> > +	g_pmtctl_ctx.num_devices = 0;
> > +}
> > 


^ permalink raw reply	[flat|nested] 25+ messages in thread

end of thread, other threads:[~2026-05-26 17:44 UTC | newest]

Thread overview: 25+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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 ` [PATCH 14/17] tools/arch/x86/pmtctl: Add pmtctl 'stat' command David E. Box
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

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.