* [PATCH v3 0/3] perf pmu: Add tool-provided NVMe PMU
@ 2026-11-13 19:12 Ian Rogers
2026-11-13 19:12 ` [PATCH v3 1/3] perf build: Add libnvme feature detection Ian Rogers
` (2 more replies)
0 siblings, 3 replies; 7+ messages in thread
From: Ian Rogers @ 2026-11-13 19:12 UTC (permalink / raw)
To: Peter Zijlstra, Ingo Molnar, Arnaldo Carvalho de Melo,
Namhyung Kim, Jiri Olsa, Ian Rogers
Cc: Christoph Hellwig, Daniel Wagner, Yuzhuo Jing, Adrian Hunter,
James Clark, Tomas Glozar, Michael Jeanson, Dmitrii Dolgov,
Alexandre Chartre, Leo Yan, German Gomez, Anubhav Shelat,
linux-perf-users, linux-nvme, linux-kernel
This series adds a tool-provided PMU for NVMe controllers using libnvme,
following the same pattern as the existing tool, hwmon, and drm PMUs.
Each discovered controller is exposed as an 'nvme_<ctrl>' PMU (with
alias '<ctrl>', e.g. 'nvme_nvme0' and 'nvme0') providing events from
standard NVMe log pages: SMART / Health Information, Endurance Group
Information, Flexible Data Placement (FDP) Statistics, Error
Information, and Zoned Namespaces (ZNS) Changed Zones.
Motivation / Use Cases
======================
While standalone tools such as nvme-cli can dump individual log pages on
demand, exposing NVMe controller log metrics as a perf tool PMU allows
storage metrics (host/media bytes written, write amplification and FDP
efficiency, read/write command counts, controller busy time, error
counters, and device temperature) to be sampled on a unified timeline
alongside CPU hardware counters, kernel/block events, uncore counters,
and hwmon/drm sensors in 'perf stat' (including interval mode '-I', CSV
'-x', JSON '-j', and perf metric formulas), without custom wrapper
scripts. Discovery and log-page probing are gated behind
PERF_TOOL_PMU_TYPE_NVME_MASK so commands that do not request NVMe events
incur zero overhead.
Example Output
==============
Listing available NVMe events (unsupported log pages on the controller
are marked deprecated and hidden from default 'perf list'):
$ perf list nvme
List of pre-defined events (to be used in -e or -M):
nvme:
smart_crit_comp_time
[Critical composite temperature time. Unit: nvme_nvme0]
smart_ctrl_busy_time
[Controller busy time. Unit: nvme_nvme0]
smart_data_units_read
[Data units read (in 1000s of 512-byte units). Unit: nvme_nvme0]
smart_data_units_written
[Data units written (in 1000s of 512-byte units). Unit: nvme_nvme0]
smart_host_read_commands
[Host read commands. Unit: nvme_nvme0]
smart_host_write_commands
[Host write commands. Unit: nvme_nvme0]
smart_media_errors
[Media errors. Unit: nvme_nvme0]
smart_num_err_log_entries
[Number of error log entries. Unit: nvme_nvme0]
smart_power_cycles
[Power cycles. Unit: nvme_nvme0]
smart_power_on_hours
[Power on hours. Unit: nvme_nvme0]
smart_temperature
[Temperature. Unit: nvme_nvme0]
smart_unsafe_shutdowns
[Unsafe shutdowns. Unit: nvme_nvme0]
smart_warning_temp_time
[Warning temperature time. Unit: nvme_nvme0]
$ perf list --details nvme_nvme0/smart_temperature/
List of pre-defined events (to be used in -e or -M):
nvme:
smart_temperature
[Temperature. Unit: nvme_nvme0]
nvme_nvme0/config=0x20001/
Interval 'perf stat' output (captured with mocked libnvme SMART log
responses on /dev/nvme0):
$ perf stat -a -I 500 -e smart_temperature,smart_data_units_read,\
smart_data_units_written,smart_host_read_commands,\
smart_host_write_commands -- sleep 1
# time counts unit events
0.500432960 311 K smart_temperature
0.500432960 36,864,000 B smart_data_units_read
0.500432960 12,288,000 B smart_data_units_written
0.500432960 900 smart_host_read_commands
0.500432960 270 smart_host_write_commands
1.001068028 311 K smart_temperature
1.001068028 36,864,000 B smart_data_units_read
1.001068028 12,288,000 B smart_data_units_written
1.001068028 900 smart_host_read_commands
1.001068028 270 smart_host_write_commands
Changes in v3:
- Remove accidental internal tracking tags from commit messages
(Christoph Hellwig).
- Document use cases and include actual 'perf list' and 'perf stat'
example output in the cover letter and Patch 2 commit message, and
document NVMe events in Documentation/perf-list.txt (Christoph
Hellwig, Namhyung Kim).
- Rely on $(FEATURE_CHECK_LDFLAGS-libnvme) from pkg-config when building
test-libnvme.bin instead of hardcoding -lnvme, and add test-libnvme.bin
to FILES in tools/build/feature/Makefile.
- Report smart_temperature in native Kelvins ('1K') to avoid unsigned
uint64_t underflow on sub-zero Celsius or 0 K readings; metrics can
convert Kelvin to Celsius.
- Support advertised 'config=0x...' raw encodings, 'event=<name>', and
standard event qualifiers ('name=', 'metric-id=', 'cpu='), and guard
NULL term->config when formatting parse errors.
- Check xyarray__new() allocation failure (-ENOMEM), propagate initial
log read errors (-EPERM / -EOPNOTSUPP) in evsel__nvme_pmu_open(), and
bound incremental open error unwind by start_cpu_map_idx.
- Use an aligned union of NVMe log structures in nvme_pmu__read_val(),
use valid Endurance Group ID 1 (0 is reserved in NVMe) for Endurance
Group and FDP logs, fix Endurance Group data/media unit scaling to
billions of bytes ('1000000000B') per NVMe Base Spec, wire up
perf_pmus__find_by_type() for NVMe PMU types, and propagate
nvme_pmu__check_alias() errors in perf_pmu__check_alias().
- Expand unit tests to cover alias PMUs, 'event=', 'config=0x...',
'name=' qualifiers, unit/scale checks, and invalid term rejection.
Changes in v2:
https://lore.kernel.org/linux-perf-users/20260609165726.786694-1-irogers@google.com/
- Include test-libnvme.c in test-all.c and add libnvme to
FEATURE_PKG_CONFIG.
- Use get_unaligned_le*() helpers when reading NVMe log fields.
- Move baseline initial_values xyarray from tool-owned evsel->priv into
struct evsel's tool union (evsel->nvme.initial_values) and free it in
evsel__exit().
v1:
https://lore.kernel.org/linux-perf-users/20260609070348.541964-1-irogers@google.com/
Ian Rogers (3):
perf build: Add libnvme feature detection
perf pmu: Implement tool-provided NVMe PMU
perf tests: Add NVMe PMU event parsing test
tools/build/Makefile.feature | 5 +-
tools/build/feature/Makefile | 4 +
tools/build/feature/test-all.c | 5 +
tools/build/feature/test-libnvme.c | 11 +
tools/perf/Documentation/perf-list.txt | 10 +-
tools/perf/Makefile.config | 12 +
tools/perf/tests/Build | 1 +
tools/perf/tests/builtin-test.c | 1 +
tools/perf/tests/nvme_pmu.c | 303 +++++++++++
tools/perf/tests/tests.h | 1 +
tools/perf/util/Build | 1 +
tools/perf/util/evsel.c | 13 +-
tools/perf/util/evsel.h | 4 +
tools/perf/util/nvme_pmu.c | 691 +++++++++++++++++++++++++
tools/perf/util/nvme_pmu.h | 144 ++++++
tools/perf/util/pmu.c | 17 +
tools/perf/util/pmu.h | 8 +-
tools/perf/util/pmus.c | 24 +
tools/perf/util/pmus.h | 1 +
19 files changed, 1250 insertions(+), 6 deletions(-)
create mode 100644 tools/build/feature/test-libnvme.c
create mode 100644 tools/perf/tests/nvme_pmu.c
create mode 100644 tools/perf/util/nvme_pmu.c
create mode 100644 tools/perf/util/nvme_pmu.h
--
2.56.1.703.gf848ed807c-goog
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH v3 1/3] perf build: Add libnvme feature detection
2026-11-13 19:12 [PATCH v3 0/3] perf pmu: Add tool-provided NVMe PMU Ian Rogers
@ 2026-11-13 19:12 ` Ian Rogers
2026-11-13 19:22 ` sashiko-bot
2026-11-13 19:12 ` [PATCH v3 2/3] perf pmu: Implement tool-provided NVMe PMU Ian Rogers
2026-11-13 19:12 ` [PATCH v3 3/3] perf tests: Add NVMe PMU event parsing test Ian Rogers
2 siblings, 1 reply; 7+ messages in thread
From: Ian Rogers @ 2026-11-13 19:12 UTC (permalink / raw)
To: Peter Zijlstra, Ingo Molnar, Arnaldo Carvalho de Melo,
Namhyung Kim, Jiri Olsa, Ian Rogers
Cc: Christoph Hellwig, Daniel Wagner, Yuzhuo Jing, Adrian Hunter,
James Clark, Tomas Glozar, Michael Jeanson, Dmitrii Dolgov,
Alexandre Chartre, Leo Yan, German Gomez, Anubhav Shelat,
linux-perf-users, linux-nvme, linux-kernel
Introduce a feature check for libnvme using pkg-config so that the
tool-provided NVMe PMU can be conditionally compiled when libnvme is
available.
Add libnvme to FEATURE_TESTS_BASIC, FEATURE_DISPLAY, FEATURE_PKG_CONFIG,
and test-all.c so fast-path discovery and individual feature checks stay
consistent with the pkg-config flags used during linking.
Signed-off-by: Ian Rogers <irogers@google.com>
---
tools/build/Makefile.feature | 5 ++++-
tools/build/feature/Makefile | 4 ++++
tools/build/feature/test-all.c | 5 +++++
tools/build/feature/test-libnvme.c | 11 +++++++++++
tools/perf/Makefile.config | 12 ++++++++++++
5 files changed, 36 insertions(+), 1 deletion(-)
create mode 100644 tools/build/feature/test-libnvme.c
diff --git a/tools/build/Makefile.feature b/tools/build/Makefile.feature
index 4d37d11f674c..54b51251c4c3 100644
--- a/tools/build/Makefile.feature
+++ b/tools/build/Makefile.feature
@@ -73,6 +73,7 @@ FEATURE_TESTS_BASIC := \
libbfd \
libbfd-threadsafe \
libelf \
+ libnvme \
libelf-getphdrnum \
libelf-gelf_getnote \
libelf-getshdrstrndx \
@@ -143,6 +144,7 @@ FEATURE_DISPLAY ?= \
libdw \
glibc \
libelf \
+ libnvme \
libnuma \
numa_num_possible_cpus \
python-module \
@@ -168,7 +170,8 @@ FEATURE_GROUP_MEMBERS-libbfd = libbfd-liberty libbfd-liberty-z
FEATURE_PKG_CONFIG ?= \
babeltrace2-ctf-writer \
libtraceevent \
- libtracefs
+ libtracefs \
+ libnvme
feature_pkg_config = $(eval $(feature_pkg_config_code))
define feature_pkg_config_code
diff --git a/tools/build/feature/Makefile b/tools/build/feature/Makefile
index 10340a537224..851f458163f6 100644
--- a/tools/build/feature/Makefile
+++ b/tools/build/feature/Makefile
@@ -24,6 +24,7 @@ FILES= \
test-cxa-demangle.bin \
test-libcap.bin \
test-libelf.bin \
+ test-libnvme.bin \
test-libelf-getphdrnum.bin \
test-libelf-gelf_getnote.bin \
test-libelf-getshdrstrndx.bin \
@@ -147,6 +148,9 @@ $(OUTPUT)test-libcap.bin:
$(OUTPUT)test-libelf.bin:
$(BUILD) -lelf
+$(OUTPUT)test-libnvme.bin:
+ $(BUILD) # -lnvme provided by $(FEATURE_CHECK_LDFLAGS-libnvme)
+
$(OUTPUT)test-eventfd.bin:
$(BUILD)
diff --git a/tools/build/feature/test-all.c b/tools/build/feature/test-all.c
index 4a6890290f74..155f92efc2c8 100644
--- a/tools/build/feature/test-all.c
+++ b/tools/build/feature/test-all.c
@@ -150,6 +150,10 @@
# include "test-libopenssl.c"
#undef main
+#define main main_test_libnvme
+# include "test-libnvme.c"
+#undef main
+
int main(int argc, char *argv[])
{
main_test_python_module();
@@ -183,6 +187,7 @@ int main(int argc, char *argv[])
main_test_libzstd();
main_test_libtraceevent();
main_test_libopenssl();
+ main_test_libnvme();
return 0;
}
diff --git a/tools/build/feature/test-libnvme.c b/tools/build/feature/test-libnvme.c
new file mode 100644
index 000000000000..3fefad70a680
--- /dev/null
+++ b/tools/build/feature/test-libnvme.c
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
+#include <libnvme.h>
+
+int main(void)
+{
+ nvme_root_t r = nvme_scan(NULL);
+
+ if (r)
+ nvme_free_tree(r);
+ return 0;
+}
diff --git a/tools/perf/Makefile.config b/tools/perf/Makefile.config
index b4764f69f47f..d82273dadf39 100644
--- a/tools/perf/Makefile.config
+++ b/tools/perf/Makefile.config
@@ -1065,6 +1065,18 @@ ifndef NO_CAPSTONE
endif
endif
+ifndef NO_LIBNVME
+ ifeq ($(feature-libnvme), 1)
+ CFLAGS += -DHAVE_LIBNVME_SUPPORT $(shell $(PKG_CONFIG) --cflags libnvme)
+ LDFLAGS += $(shell $(PKG_CONFIG) --libs-only-L libnvme)
+ EXTLIBS += $(shell $(PKG_CONFIG) --libs-only-l libnvme)
+ $(call detected,CONFIG_LIBNVME)
+ else
+ $(warning No libnvme found, disables NVMe PMU support, please install libnvme-dev/libnvme-devel)
+ NO_LIBNVME := 1
+ endif
+endif
+
ifdef EXTRA_TESTS
$(call detected,CONFIG_EXTRA_TESTS)
CFLAGS += -DHAVE_EXTRA_TESTS
--
2.56.1.703.gf848ed807c-goog
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH v3 2/3] perf pmu: Implement tool-provided NVMe PMU
2026-11-13 19:12 [PATCH v3 0/3] perf pmu: Add tool-provided NVMe PMU Ian Rogers
2026-11-13 19:12 ` [PATCH v3 1/3] perf build: Add libnvme feature detection Ian Rogers
@ 2026-11-13 19:12 ` Ian Rogers
2026-11-13 19:40 ` sashiko-bot
2026-11-13 19:12 ` [PATCH v3 3/3] perf tests: Add NVMe PMU event parsing test Ian Rogers
2 siblings, 1 reply; 7+ messages in thread
From: Ian Rogers @ 2026-11-13 19:12 UTC (permalink / raw)
To: Peter Zijlstra, Ingo Molnar, Arnaldo Carvalho de Melo,
Namhyung Kim, Jiri Olsa, Ian Rogers
Cc: Christoph Hellwig, Daniel Wagner, Yuzhuo Jing, Adrian Hunter,
James Clark, Tomas Glozar, Michael Jeanson, Dmitrii Dolgov,
Alexandre Chartre, Leo Yan, German Gomez, Anubhav Shelat,
linux-perf-users, linux-nvme, linux-kernel
Add a tool-provided PMU for NVMe controllers using libnvme, similar to
the existing hwmon and drm tool PMUs. Each discovered controller is
exposed as an 'nvme_<ctrl>' PMU (with alias '<ctrl>', e.g. 'nvme_nvme0'
and 'nvme0') providing events from standard NVMe log pages:
- SMART / Health Information (data units read/written, host read/write
commands, controller busy time, power cycles, power on hours, unsafe
shutdowns, media errors, error log entries, temperature, warning and
critical temperature times)
- Endurance Group Information
- Flexible Data Placement (FDP) Statistics (host/media bytes written,
media bytes erased)
- Error Information
- Zoned Namespaces (ZNS) Changed Zones
Exposing these controller log metrics as a perf tool PMU allows storage
activity, write amplification / FDP efficiency, error counters, and
device thermals to be measured on a unified timeline alongside CPU
hardware counters, block/kernel events, uncore counters, and hwmon
sensors in 'perf stat' (including interval mode '-I', CSV '-x', JSON
'-j', and perf metric formulas), without separate polling scripts. As
with hwmon_pmu and drm_pmu, NVMe controller discovery and log probing
are gated behind PERF_TOOL_PMU_TYPE_NVME_MASK so there is no overhead
unless NVMe events or wildcards are requested.
Features:
- Generic configuration encoding (log type, field size, and byte offset
packed into attr.config), supporting symbolic event names, 'event='
terms, and raw 'config=0x...' encodings.
- Dynamic probing of supported log pages on /dev/nvmeX. Unsupported log
events are marked deprecated and hidden from 'perf list' by default.
- Baseline offset snapshots stored in struct evsel's tool union
(evsel->nvme.initial_values) so cumulative device counters report
interval deltas while gauges (temperature, percent_used, zns_nrzid)
report instantaneous readings.
- Temperature is reported in native Kelvins ('K') matching the NVMe
SMART log specification, avoiding unsigned counter underflow on
sub-zero Celsius values while allowing metrics to convert to Celsius.
Example 'perf list' output (when only the SMART log is supported):
$ perf list nvme
List of pre-defined events (to be used in -e or -M):
nvme:
smart_crit_comp_time
[Critical composite temperature time. Unit: nvme_nvme0]
smart_ctrl_busy_time
[Controller busy time. Unit: nvme_nvme0]
smart_data_units_read
[Data units read (in 1000s of 512-byte units). Unit: nvme_nvme0]
smart_data_units_written
[Data units written (in 1000s of 512-byte units). Unit: nvme_nvme0]
smart_host_read_commands
[Host read commands. Unit: nvme_nvme0]
smart_host_write_commands
[Host write commands. Unit: nvme_nvme0]
smart_media_errors
[Media errors. Unit: nvme_nvme0]
smart_num_err_log_entries
[Number of error log entries. Unit: nvme_nvme0]
smart_power_cycles
[Power cycles. Unit: nvme_nvme0]
smart_power_on_hours
[Power on hours. Unit: nvme_nvme0]
smart_temperature
[Temperature. Unit: nvme_nvme0]
smart_unsafe_shutdowns
[Unsafe shutdowns. Unit: nvme_nvme0]
smart_warning_temp_time
[Warning temperature time. Unit: nvme_nvme0]
Example 'perf stat' interval output (captured with mocked libnvme SMART
log responses on /dev/nvme0):
$ perf stat -a -I 500 -e smart_temperature,smart_data_units_read,\
smart_data_units_written,smart_host_read_commands,\
smart_host_write_commands -- sleep 1
# time counts unit events
0.500432960 311 K smart_temperature
0.500432960 36,864,000 B smart_data_units_read
0.500432960 12,288,000 B smart_data_units_written
0.500432960 900 smart_host_read_commands
0.500432960 270 smart_host_write_commands
1.001068028 311 K smart_temperature
1.001068028 36,864,000 B smart_data_units_read
1.001068028 12,288,000 B smart_data_units_written
1.001068028 900 smart_host_read_commands
1.001068028 270 smart_host_write_commands
Signed-off-by: Ian Rogers <irogers@google.com>
---
tools/perf/Documentation/perf-list.txt | 10 +-
tools/perf/util/Build | 1 +
tools/perf/util/evsel.c | 13 +-
tools/perf/util/evsel.h | 4 +
tools/perf/util/nvme_pmu.c | 691 +++++++++++++++++++++++++
tools/perf/util/nvme_pmu.h | 144 ++++++
tools/perf/util/pmu.c | 17 +
tools/perf/util/pmu.h | 8 +-
tools/perf/util/pmus.c | 19 +
9 files changed, 902 insertions(+), 5 deletions(-)
create mode 100644 tools/perf/util/nvme_pmu.c
create mode 100644 tools/perf/util/nvme_pmu.h
diff --git a/tools/perf/Documentation/perf-list.txt b/tools/perf/Documentation/perf-list.txt
index 17e582998474..520e08a6efc9 100644
--- a/tools/perf/Documentation/perf-list.txt
+++ b/tools/perf/Documentation/perf-list.txt
@@ -254,8 +254,8 @@ For accessing trace point events perf needs to have read access to
/sys/kernel/tracing, even when perf_event_paranoid is in a relaxed
setting.
-TOOL/HWMON EVENTS
------------------
+TOOL/HWMON/NVME EVENTS
+----------------------
Some events don't have an associated PMU instead reading values
available to software without perf_event_open. As these events don't
@@ -268,6 +268,12 @@ Hwmon events provide easy access to hwmon sysfs data typically in
/sys/class/hwmon. This information includes temperatures, fan speeds
and energy usage.
+NVMe events (when built with libnvme) expose controller log page
+metrics for /dev/nvmeX devices under nvme_nvmeX PMUs. This includes
+SMART health counters and temperature (in Kelvin), endurance group
+counters, Flexible Data Placement (FDP) statistics, error counts and
+Zoned Namespace (ZNS) changed zones.
+
TRACING
-------
diff --git a/tools/perf/util/Build b/tools/perf/util/Build
index 2c1f880c4c47..d7740aa06249 100644
--- a/tools/perf/util/Build
+++ b/tools/perf/util/Build
@@ -23,6 +23,7 @@ perf-util-y += disasm.o
perf-util-y += env.o
perf-util-y += event.o
perf-util-y += evlist.o
+perf-util-y += nvme_pmu.o
perf-util-y += sideband_evlist.o
perf-util-y += evsel.o
perf-util-y += evsel_fprintf.o
diff --git a/tools/perf/util/evsel.c b/tools/perf/util/evsel.c
index 6d69f63b7fb1..9c98478692dc 100644
--- a/tools/perf/util/evsel.c
+++ b/tools/perf/util/evsel.c
@@ -63,6 +63,7 @@
#include "hashmap.h"
#include "hist.h"
#include "hwmon_pmu.h"
+#include "nvme_pmu.h"
#include "intel-tpebs.h"
#include "memswap.h"
#include "off_cpu.h"
@@ -2104,6 +2105,8 @@ static void evsel__exit(struct evsel *evsel)
xyarray__delete(evsel->process_time.start_times);
xyarray__delete(evsel->process_time.accumulated_times);
}
+ if (evsel__is_nvme(evsel))
+ xyarray__delete(evsel->nvme.initial_values);
}
void evsel__put(struct evsel *evsel)
@@ -2308,6 +2311,8 @@ int evsel__read_counter(struct evsel *evsel, int cpu_map_idx, int thread)
if (evsel__is_hwmon(evsel))
return evsel__hwmon_pmu_read(evsel, cpu_map_idx, thread);
+ if (evsel__is_nvme(evsel))
+ return evsel__nvme_pmu_read(evsel, cpu_map_idx, thread);
if (evsel__is_drm(evsel))
return evsel__drm_pmu_read(evsel, cpu_map_idx, thread);
@@ -3067,8 +3072,12 @@ static int evsel__open_cpu(struct evsel *evsel, struct perf_cpu_map *cpus,
}
if (evsel__is_hwmon(evsel)) {
err = evsel__hwmon_pmu_open(evsel, threads,
- start_cpu_map_idx,
- end_cpu_map_idx);
+ start_cpu_map_idx, end_cpu_map_idx);
+ goto out;
+ }
+ if (evsel__is_nvme(evsel)) {
+ err = evsel__nvme_pmu_open(evsel, threads,
+ start_cpu_map_idx, end_cpu_map_idx);
goto out;
}
if (evsel__is_drm(evsel)) {
diff --git a/tools/perf/util/evsel.h b/tools/perf/util/evsel.h
index 53f3641b66e1..b30b5b6251af 100644
--- a/tools/perf/util/evsel.h
+++ b/tools/perf/util/evsel.h
@@ -208,6 +208,10 @@ struct evsel {
struct xyarray *start_times;
struct xyarray *accumulated_times;
} process_time;
+ /* For NVMe events */
+ struct {
+ struct xyarray *initial_values;
+ } nvme;
};
/* Is the tool's fd for /proc/pid/stat or /proc/stat. */
bool pid_stat;
diff --git a/tools/perf/util/nvme_pmu.c b/tools/perf/util/nvme_pmu.c
new file mode 100644
index 000000000000..3ee967505dec
--- /dev/null
+++ b/tools/perf/util/nvme_pmu.c
@@ -0,0 +1,691 @@
+// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
+#include "counts.h"
+#include "debug.h"
+#include "evsel.h"
+#include "nvme_pmu.h"
+#include "pmu.h"
+#include <internal/xyarray.h>
+#include <internal/threadmap.h>
+#include <perf/threadmap.h>
+#include <sys/types.h>
+#include <assert.h>
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <unistd.h>
+#include <linux/kernel.h>
+#include <linux/string.h>
+#include <linux/unaligned.h>
+#include <linux/zalloc.h>
+
+#ifdef HAVE_LIBNVME_SUPPORT
+#include <libnvme.h>
+
+/* Valid NVMe Endurance Group Identifiers start at 1 (0 is reserved). */
+#define NVME_DEFAULT_ENDGID 1
+
+struct nvme_event {
+ const char *name;
+ const char *desc;
+ const char *scale_unit;
+ uint64_t config;
+};
+
+static const struct nvme_event nvme_events[] = {
+ { "smart_data_units_read",
+ "Data units read (in 1000s of 512-byte units)",
+ "512000B", NVME_SMART(16, data_units_read) },
+ { "smart_data_units_written",
+ "Data units written (in 1000s of 512-byte units)",
+ "512000B", NVME_SMART(16, data_units_written) },
+ { "smart_host_read_commands", "Host read commands", NULL, NVME_SMART(16, host_reads) },
+ { "smart_host_write_commands", "Host write commands", NULL, NVME_SMART(16, host_writes) },
+ { "smart_ctrl_busy_time", "Controller busy time", "60s", NVME_SMART(16, ctrl_busy_time) },
+ { "smart_power_cycles", "Power cycles", NULL, NVME_SMART(16, power_cycles) },
+ { "smart_power_on_hours", "Power on hours", "1h", NVME_SMART(16, power_on_hours) },
+ { "smart_unsafe_shutdowns", "Unsafe shutdowns", NULL, NVME_SMART(16, unsafe_shutdowns) },
+ { "smart_media_errors", "Media errors", NULL, NVME_SMART(16, media_errors) },
+ { "smart_num_err_log_entries",
+ "Number of error log entries",
+ NULL, NVME_SMART(16, num_err_log_entries) },
+ { "smart_warning_temp_time",
+ "Warning temperature time",
+ "60s", NVME_SMART(4, warning_temp_time) },
+ { "smart_crit_comp_time",
+ "Critical composite temperature time",
+ "60s", NVME_SMART(4, critical_comp_time) },
+ { "smart_temperature", "Temperature", "1K", NVME_SMART(2, temperature) },
+
+ { "endurance_percent_used",
+ "Endurance group percentage used",
+ NULL, NVME_ENDURANCE(1, percent_used) },
+ { "endurance_data_units_read",
+ "Endurance group data units read (in billions of bytes)",
+ "1000000000B", NVME_ENDURANCE(16, data_units_read) },
+ { "endurance_data_units_written",
+ "Endurance group data units written (in billions of bytes)",
+ "1000000000B", NVME_ENDURANCE(16, data_units_written) },
+ { "endurance_media_units_written",
+ "Endurance group media units written (in billions of bytes)",
+ "1000000000B", NVME_ENDURANCE(16, media_units_written) },
+ { "endurance_host_read_cmds",
+ "Endurance group host read commands",
+ NULL, NVME_ENDURANCE(16, host_read_cmds) },
+ { "endurance_host_write_cmds",
+ "Endurance group host write commands",
+ NULL, NVME_ENDURANCE(16, host_write_cmds) },
+ { "endurance_num_err_info_log_entries",
+ "Endurance group number of error information log entries",
+ NULL, NVME_ENDURANCE(16, num_err_info_log_entries) },
+
+ { "fdp_hbmw", "FDP host bytes with metadata written", "1B", NVME_FDP(16, hbmw) },
+ { "fdp_mbmw", "FDP media bytes with metadata written", "1B", NVME_FDP(16, mbmw) },
+ { "fdp_mbe", "FDP media bytes erased", "1B", NVME_FDP(16, mbe) },
+
+ { "error_count", "Error info log error count", NULL, NVME_ERROR(8, error_count) },
+
+ { "zns_nrzid", "ZNS changed zone nrzid", NULL, NVME_ZNS(2, nrzid) },
+};
+
+struct nvme_pmu {
+ struct perf_pmu pmu;
+ char *dev_name;
+ bool support_checked;
+ bool log_supported[256];
+};
+
+bool perf_pmu__is_nvme(const struct perf_pmu *pmu)
+{
+ return pmu && pmu->type >= PERF_PMU_TYPE_NVME_START &&
+ pmu->type <= PERF_PMU_TYPE_NVME_END;
+}
+
+bool evsel__is_nvme(const struct evsel *evsel)
+{
+ return perf_pmu__is_nvme(evsel->pmu);
+}
+
+struct perf_pmu *nvme_pmu__new(struct list_head *pmus, const char *sysfs_name, const char *name)
+{
+ struct perf_pmu *existing;
+ struct nvme_pmu *nvm;
+ char buf[64];
+ unsigned long idx = 0;
+ __u32 type;
+
+ if (!sysfs_name || !name)
+ return NULL;
+
+ /*
+ * Usually sysfs_name is something like "nvme0".
+ * Extract the controller index; if parsing fails, use 0.
+ */
+ if (!strncmp(sysfs_name, "nvme", 4) && isdigit((unsigned char)sysfs_name[4]))
+ idx = strtoul(sysfs_name + 4, NULL, 10);
+
+ if (idx > PERF_PMU_TYPE_NVME_END - PERF_PMU_TYPE_NVME_START) {
+ pr_err("Unable to encode NVMe type from %s in valid PMU type\n", sysfs_name);
+ return NULL;
+ }
+ type = PERF_PMU_TYPE_NVME_START + idx;
+
+ snprintf(buf, sizeof(buf), "nvme_%s", name);
+
+ list_for_each_entry(existing, pmus, list) {
+ if (!strcmp(existing->name, buf) || existing->type == type)
+ return NULL;
+ }
+
+ nvm = zalloc(sizeof(*nvm));
+ if (!nvm)
+ return NULL;
+
+ if (perf_pmu__init(&nvm->pmu, type, buf) != 0) {
+ perf_pmu__delete(&nvm->pmu);
+ return NULL;
+ }
+
+ nvm->dev_name = strdup(sysfs_name);
+ if (!nvm->dev_name) {
+ perf_pmu__delete(&nvm->pmu);
+ return NULL;
+ }
+ nvm->pmu.alias_name = strdup(sysfs_name);
+ if (!nvm->pmu.alias_name) {
+ perf_pmu__delete(&nvm->pmu);
+ return NULL;
+ }
+ nvm->pmu.cpus = perf_cpu_map__new_int(0);
+ if (!nvm->pmu.cpus) {
+ perf_pmu__delete(&nvm->pmu);
+ return NULL;
+ }
+
+ list_add_tail(&nvm->pmu.list, pmus);
+ return &nvm->pmu;
+}
+
+void nvme_pmu__exit(struct perf_pmu *pmu)
+{
+ struct nvme_pmu *nvm = container_of(pmu, struct nvme_pmu, pmu);
+
+ zfree(&nvm->dev_name);
+}
+
+static void nvme_pmu__check_support(struct nvme_pmu *nvm)
+{
+ int fd;
+ char path[PATH_MAX];
+ struct nvme_smart_log smart_log;
+ struct nvme_endurance_group_log endurance_log;
+ struct nvme_fdp_stats_log fdp_log;
+ struct nvme_error_log_page error_log;
+ struct nvme_zns_changed_zone_log zns_log;
+
+ if (nvm->support_checked)
+ return;
+
+ nvm->support_checked = true;
+
+ /* Assume all supported if we can't test. */
+ memset(nvm->log_supported, 1, sizeof(nvm->log_supported));
+
+ snprintf(path, sizeof(path), "/dev/%s", nvm->dev_name);
+ fd = open(path, O_RDONLY | O_CLOEXEC);
+ if (fd < 0)
+ return;
+
+ if (nvme_get_log_smart(fd, NVME_NSID_ALL, true, &smart_log) != 0)
+ nvm->log_supported[NVME_LOG_SMART] = false;
+
+ if (nvme_get_log_endurance_group(fd, NVME_DEFAULT_ENDGID, &endurance_log) != 0)
+ nvm->log_supported[NVME_LOG_ENDURANCE] = false;
+
+ if (nvme_get_log_fdp_stats(fd, NVME_DEFAULT_ENDGID, 0, sizeof(fdp_log), &fdp_log) != 0)
+ nvm->log_supported[NVME_LOG_FDP] = false;
+
+ if (nvme_get_log_error(fd, 1, true, &error_log) != 0)
+ nvm->log_supported[NVME_LOG_ERROR] = false;
+
+ if (nvme_get_log_zns_changed_zones(fd, NVME_NSID_ALL, true, &zns_log) != 0)
+ nvm->log_supported[NVME_LOG_ZNS] = false;
+
+ close(fd);
+}
+
+int nvme_pmu__for_each_event(struct perf_pmu *pmu, void *state, pmu_event_callback cb)
+{
+ struct nvme_pmu *nvm = container_of(pmu, struct nvme_pmu, pmu);
+ size_t i;
+
+ nvme_pmu__check_support(nvm);
+ for (i = 0; i < ARRAY_SIZE(nvme_events); i++) {
+ const struct nvme_event *e = &nvme_events[i];
+ char encoding_buf[128];
+ struct pmu_event_info info = {
+ .pmu = pmu,
+ .name = e->name,
+ .alias = NULL,
+ .scale_unit = e->scale_unit,
+ .desc = e->desc,
+ .long_desc = NULL,
+ .encoding_desc = encoding_buf,
+ .topic = "nvme",
+ .pmu_name = pmu->name,
+ .event_type_desc = "NVMe event",
+ .deprecated = !nvm->log_supported[(e->config >> 24) & 0xFF],
+ };
+ int ret;
+
+ snprintf(encoding_buf, sizeof(encoding_buf),
+ "%s/config=0x%" PRIx64 "/", pmu->name, e->config);
+
+ ret = cb(state, &info);
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+
+size_t nvme_pmu__num_events(struct perf_pmu *pmu __maybe_unused)
+{
+ return ARRAY_SIZE(nvme_events);
+}
+
+bool nvme_pmu__have_event(struct perf_pmu *pmu __maybe_unused, const char *name)
+{
+ size_t i;
+
+ for (i = 0; i < ARRAY_SIZE(nvme_events); i++) {
+ if (!strcasecmp(name, nvme_events[i].name))
+ return true;
+ }
+ return false;
+}
+
+static bool nvme_pmu__valid_config(uint64_t config)
+{
+ unsigned int log_type = (config >> 24) & 0xFF;
+ unsigned int size = (config >> 16) & 0xFF;
+ unsigned int offset = config & 0xFFFF;
+ size_t log_size;
+
+ if (config >> 32)
+ return false;
+
+ if (size != 1 && size != 2 && size != 4 && size != 8 && size != 16)
+ return false;
+
+ switch (log_type) {
+ case NVME_LOG_SMART:
+ log_size = sizeof(struct nvme_smart_log);
+ break;
+ case NVME_LOG_ENDURANCE:
+ log_size = sizeof(struct nvme_endurance_group_log);
+ break;
+ case NVME_LOG_FDP:
+ log_size = sizeof(struct nvme_fdp_stats_log);
+ break;
+ case NVME_LOG_ERROR:
+ log_size = sizeof(struct nvme_error_log_page);
+ break;
+ case NVME_LOG_ZNS:
+ log_size = sizeof(struct nvme_zns_changed_zone_log);
+ break;
+ default:
+ return false;
+ }
+
+ return offset + size <= log_size;
+}
+
+static const struct nvme_event *nvme_pmu__find_user_event(const struct parse_events_term *term)
+{
+ const char *event_name = NULL;
+
+ if (term->type_term != PARSE_EVENTS__TERM_TYPE_USER)
+ return NULL;
+
+ if (term->type_val == PARSE_EVENTS__TERM_TYPE_NUM && term->no_value)
+ event_name = term->config;
+ else if (term->type_val == PARSE_EVENTS__TERM_TYPE_STR &&
+ term->config && !strcasecmp(term->config, "event"))
+ event_name = term->val.str;
+
+ if (!event_name)
+ return NULL;
+
+ for (size_t i = 0; i < ARRAY_SIZE(nvme_events); i++) {
+ if (!strcasecmp(event_name, nvme_events[i].name))
+ return &nvme_events[i];
+ }
+ return NULL;
+}
+
+static const struct nvme_event *nvme_pmu__find_config_event(uint64_t config)
+{
+ for (size_t i = 0; i < ARRAY_SIZE(nvme_events); i++) {
+ if (nvme_events[i].config == config)
+ return &nvme_events[i];
+ }
+ return NULL;
+}
+
+static int nvme_pmu__err_term(struct parse_events_error *err,
+ const struct parse_events_term *term)
+{
+ if (err) {
+ char *err_str;
+ int idx = term->err_term ?: term->err_val;
+
+ if (term->config) {
+ if (asprintf(&err_str,
+ "unexpected nvme event term (%s) %s",
+ parse_events__term_type_str(term->type_term),
+ term->config) < 0)
+ err_str = strdup("unexpected nvme event term");
+ } else {
+ if (asprintf(&err_str,
+ "unexpected nvme event term (%s)",
+ parse_events__term_type_str(term->type_term)) < 0)
+ err_str = strdup("unexpected nvme event term");
+ }
+ parse_events_error__handle(err, idx, err_str, NULL);
+ }
+ return -EINVAL;
+}
+
+static bool nvme_pmu__is_passthrough_term(const struct parse_events_term *term)
+{
+ return term->type_term == PARSE_EVENTS__TERM_TYPE_NAME ||
+ term->type_term == PARSE_EVENTS__TERM_TYPE_METRIC_ID ||
+ term->type_term == PARSE_EVENTS__TERM_TYPE_CPU;
+}
+
+int nvme_pmu__config_terms(const struct perf_pmu *pmu __maybe_unused,
+ struct perf_event_attr *attr,
+ struct parse_events_terms *terms,
+ struct parse_events_error *err)
+{
+ struct parse_events_term *term;
+ bool have_event = false;
+
+ list_for_each_entry(term, &terms->terms, list) {
+ const struct nvme_event *e = nvme_pmu__find_user_event(term);
+
+ if (e) {
+ if (have_event)
+ return nvme_pmu__err_term(err, term);
+ attr->config = e->config;
+ have_event = true;
+ continue;
+ }
+ if (term->type_term == PARSE_EVENTS__TERM_TYPE_CONFIG &&
+ !term->no_value && nvme_pmu__valid_config(term->val.num)) {
+ if (have_event)
+ return nvme_pmu__err_term(err, term);
+ attr->config = term->val.num;
+ have_event = true;
+ continue;
+ }
+ if (nvme_pmu__is_passthrough_term(term))
+ continue;
+
+ return nvme_pmu__err_term(err, term);
+ }
+
+ if (!have_event) {
+ if (err)
+ parse_events_error__handle(err, 0,
+ strdup("missing nvme event or config term"),
+ NULL);
+ return -EINVAL;
+ }
+ return 0;
+}
+
+static int nvme_pmu__set_info_scale(const struct nvme_event *e, struct perf_pmu_info *info)
+{
+ char *unit = NULL;
+
+ if (!e || !e->scale_unit)
+ return 0;
+
+ if (perf_pmu__convert_scale(e->scale_unit, &unit, &info->scale) < 0)
+ return -ENOMEM;
+
+ info->unit = unit;
+ return 0;
+}
+
+int nvme_pmu__check_alias(struct parse_events_terms *terms, struct perf_pmu_info *info,
+ struct parse_events_error *err)
+{
+ struct parse_events_term *term;
+ bool have_event = false;
+
+ list_for_each_entry(term, &terms->terms, list) {
+ const struct nvme_event *e = nvme_pmu__find_user_event(term);
+ int ret;
+
+ if (!e && term->type_term == PARSE_EVENTS__TERM_TYPE_CONFIG &&
+ !term->no_value && nvme_pmu__valid_config(term->val.num)) {
+ if (have_event)
+ return nvme_pmu__err_term(err, term);
+ have_event = true;
+ e = nvme_pmu__find_config_event(term->val.num);
+ ret = nvme_pmu__set_info_scale(e, info);
+ if (ret)
+ return ret;
+ continue;
+ }
+ if (e) {
+ if (have_event)
+ return nvme_pmu__err_term(err, term);
+ have_event = true;
+ ret = nvme_pmu__set_info_scale(e, info);
+ if (ret)
+ return ret;
+ continue;
+ }
+ if (nvme_pmu__is_passthrough_term(term))
+ continue;
+
+ return nvme_pmu__err_term(err, term);
+ }
+
+ if (!have_event) {
+ if (err)
+ parse_events_error__handle(err, 0,
+ strdup("missing nvme event or config term"),
+ NULL);
+ return -EINVAL;
+ }
+ return 0;
+}
+
+int perf_pmus__read_nvme_pmus(struct list_head *pmus)
+{
+ nvme_root_t r = nvme_scan(NULL);
+ nvme_host_t h;
+ nvme_subsystem_t s;
+ nvme_ctrl_t c;
+
+ if (!r)
+ return 0;
+
+ nvme_for_each_host(r, h) {
+ nvme_for_each_subsystem(h, s) {
+ nvme_subsystem_for_each_ctrl(s, c) {
+ const char *ctrl_name = nvme_ctrl_get_name(c);
+
+ if (ctrl_name)
+ nvme_pmu__new(pmus, ctrl_name, ctrl_name);
+ }
+ }
+ }
+ nvme_free_tree(r);
+ return 0;
+}
+
+static int nvme_pmu__read_val(int fd, uint64_t config, uint64_t *val)
+{
+ unsigned int log_type = (config >> 24) & 0xFF;
+ unsigned int size = (config >> 16) & 0xFF;
+ unsigned int offset = config & 0xFFFF;
+ union {
+ struct nvme_smart_log smart;
+ struct nvme_endurance_group_log endurance;
+ struct nvme_fdp_stats_log fdp;
+ struct nvme_error_log_page error;
+ struct nvme_zns_changed_zone_log zns;
+ } log;
+ const uint8_t *p;
+ int ret;
+
+ if (!nvme_pmu__valid_config(config))
+ return -EINVAL;
+
+ memset(&log, 0, sizeof(log));
+
+ switch (log_type) {
+ case NVME_LOG_SMART:
+ ret = nvme_get_log_smart(fd, NVME_NSID_ALL, true, &log.smart);
+ break;
+ case NVME_LOG_ENDURANCE:
+ ret = nvme_get_log_endurance_group(fd, NVME_DEFAULT_ENDGID, &log.endurance);
+ break;
+ case NVME_LOG_FDP:
+ ret = nvme_get_log_fdp_stats(fd, NVME_DEFAULT_ENDGID, 0,
+ sizeof(log.fdp), &log.fdp);
+ break;
+ case NVME_LOG_ERROR:
+ ret = nvme_get_log_error(fd, 1, true, &log.error);
+ break;
+ case NVME_LOG_ZNS:
+ ret = nvme_get_log_zns_changed_zones(fd, NVME_NSID_ALL, true, &log.zns);
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ if (ret < 0)
+ return -errno ?: -EIO;
+ if (ret > 0)
+ return -EOPNOTSUPP;
+
+ /*
+ * 16-byte NVMe log counters are 128-bit little-endian integers;
+ * reading the first 8 bytes extracts the lower 64 bits.
+ */
+ p = (const uint8_t *)&log + offset;
+ if (size == 16 || size == 8)
+ *val = get_unaligned_le64(p);
+ else if (size == 4)
+ *val = get_unaligned_le32(p);
+ else if (size == 2)
+ *val = get_unaligned_le16(p);
+ else if (size == 1)
+ *val = *p;
+ else
+ return -EINVAL;
+
+ return 0;
+}
+
+static bool nvme_pmu__is_gauge(uint64_t config)
+{
+ if (config == NVME_SMART(2, temperature) ||
+ config == NVME_ENDURANCE(1, percent_used) ||
+ config == NVME_ZNS(2, nrzid))
+ return true;
+ return false;
+}
+
+#define FD(e, x, y) (*(int *)xyarray__entry(e->core.fd, x, y))
+
+int evsel__nvme_pmu_open(struct evsel *evsel,
+ struct perf_thread_map *threads,
+ int start_cpu_map_idx, int end_cpu_map_idx)
+{
+ struct nvme_pmu *nvm = container_of(evsel->pmu, struct nvme_pmu, pmu);
+ int idx = start_cpu_map_idx, thread = 0, nthreads, err = 0;
+ int max_cpus, max_threads;
+ char path[PATH_MAX];
+
+ if (evsel->core.attr.sample_period || !evsel->core.fd)
+ return -EINVAL;
+
+ snprintf(path, sizeof(path), "/dev/%s", nvm->dev_name);
+
+ nthreads = perf_thread_map__nr(threads);
+ max_cpus = max(xyarray__max_x(evsel->core.fd), end_cpu_map_idx);
+ max_threads = max(xyarray__max_y(evsel->core.fd), nthreads);
+
+ if (evsel->nvme.initial_values &&
+ (xyarray__max_x(evsel->nvme.initial_values) < max_cpus ||
+ xyarray__max_y(evsel->nvme.initial_values) < max_threads)) {
+ xyarray__delete(evsel->nvme.initial_values);
+ evsel->nvme.initial_values = NULL;
+ }
+
+ if (!evsel->nvme.initial_values) {
+ evsel->nvme.initial_values = xyarray__new(max_cpus, max_threads,
+ sizeof(uint64_t));
+ if (!evsel->nvme.initial_values)
+ return -ENOMEM;
+ }
+
+ for (idx = start_cpu_map_idx; idx < end_cpu_map_idx; idx++) {
+ for (thread = 0; thread < nthreads; thread++) {
+ uint64_t *initial_val;
+ int fd = open(path, O_RDONLY | O_CLOEXEC);
+
+ FD(evsel, idx, thread) = fd;
+ if (fd < 0) {
+ err = -errno;
+ goto out_close;
+ }
+ initial_val = xyarray__entry(evsel->nvme.initial_values, idx, thread);
+ err = nvme_pmu__read_val(fd, evsel->core.attr.config, initial_val);
+ if (err) {
+ close(fd);
+ FD(evsel, idx, thread) = -1;
+ goto out_close;
+ }
+ }
+ }
+ return 0;
+out_close:
+ if (err)
+ threads->err_thread = thread;
+
+ do {
+ while (--thread >= 0) {
+ if (FD(evsel, idx, thread) >= 0)
+ close(FD(evsel, idx, thread));
+ FD(evsel, idx, thread) = -1;
+ }
+ thread = nthreads;
+ } while (--idx >= start_cpu_map_idx);
+ return err;
+}
+
+int evsel__nvme_pmu_read(struct evsel *evsel, int cpu_map_idx, int thread)
+{
+ int fd, err;
+ struct perf_counts_values *count, *old_count = NULL;
+ uint64_t val = 0, base = 0, delta;
+ uint64_t *initial_val;
+
+ if (evsel->prev_raw_counts)
+ old_count = perf_counts(evsel->prev_raw_counts, cpu_map_idx, thread);
+
+ count = perf_counts(evsel->counts, cpu_map_idx, thread);
+ fd = FD(evsel, cpu_map_idx, thread);
+ if (fd < 0) {
+ count->lost = (old_count ? old_count->lost : count->lost) + 1;
+ return -EINVAL;
+ }
+
+ err = nvme_pmu__read_val(fd, evsel->core.attr.config, &val);
+ if (err) {
+ count->lost = (old_count ? old_count->lost : count->lost) + 1;
+ return err;
+ }
+
+ if (evsel->nvme.initial_values) {
+ initial_val = xyarray__entry(evsel->nvme.initial_values, cpu_map_idx, thread);
+ if (initial_val)
+ base = *initial_val;
+ }
+ delta = val >= base ? val - base : val;
+
+ if (old_count) {
+ if (nvme_pmu__is_gauge(evsel->core.attr.config))
+ count->val = old_count->val + val;
+ else
+ count->val = delta;
+ count->run = old_count->run + 1;
+ count->ena = old_count->ena + 1;
+ count->lost = old_count->lost;
+ } else {
+ if (nvme_pmu__is_gauge(evsel->core.attr.config))
+ count->val = val;
+ else
+ count->val = delta;
+ count->run++;
+ count->ena++;
+ count->lost = 0;
+ }
+ return 0;
+}
+
+#endif
diff --git a/tools/perf/util/nvme_pmu.h b/tools/perf/util/nvme_pmu.h
new file mode 100644
index 000000000000..8e16494a69f3
--- /dev/null
+++ b/tools/perf/util/nvme_pmu.h
@@ -0,0 +1,144 @@
+/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
+#ifndef __NVME_PMU_H
+#define __NVME_PMU_H
+
+#include "pmu.h"
+#include <errno.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#ifdef HAVE_LIBNVME_SUPPORT
+#include <libnvme.h>
+
+#define NVME_CONFIG(log, size, offset) \
+ (((uint64_t)(log) << 24) | ((uint64_t)(size) << 16) | (offset))
+
+enum nvme_log_type {
+ NVME_LOG_SMART = 0,
+ NVME_LOG_ENDURANCE = 1,
+ NVME_LOG_FDP = 2,
+ NVME_LOG_ERROR = 3,
+ NVME_LOG_ZNS = 4,
+};
+
+#define NVME_SMART(size, field) \
+ NVME_CONFIG(NVME_LOG_SMART, size, offsetof(struct nvme_smart_log, field))
+
+#define NVME_ENDURANCE(size, field) \
+ NVME_CONFIG(NVME_LOG_ENDURANCE, size, offsetof(struct nvme_endurance_group_log, field))
+
+#define NVME_FDP(size, field) \
+ NVME_CONFIG(NVME_LOG_FDP, size, offsetof(struct nvme_fdp_stats_log, field))
+
+#define NVME_ERROR(size, field) \
+ NVME_CONFIG(NVME_LOG_ERROR, size, offsetof(struct nvme_error_log_page, field))
+
+#define NVME_ZNS(size, field) \
+ NVME_CONFIG(NVME_LOG_ZNS, size, offsetof(struct nvme_zns_changed_zone_log, field))
+#endif
+
+struct list_head;
+struct perf_thread_map;
+struct evsel;
+
+#ifdef HAVE_LIBNVME_SUPPORT
+struct perf_pmu *nvme_pmu__new(struct list_head *pmus, const char *sysfs_name, const char *name);
+void nvme_pmu__exit(struct perf_pmu *pmu);
+
+int nvme_pmu__for_each_event(struct perf_pmu *pmu, void *state, pmu_event_callback cb);
+size_t nvme_pmu__num_events(struct perf_pmu *pmu);
+bool nvme_pmu__have_event(struct perf_pmu *pmu, const char *name);
+int nvme_pmu__config_terms(const struct perf_pmu *pmu,
+ struct perf_event_attr *attr,
+ struct parse_events_terms *terms,
+ struct parse_events_error *err);
+int nvme_pmu__check_alias(struct parse_events_terms *terms, struct perf_pmu_info *info,
+ struct parse_events_error *err);
+
+bool perf_pmu__is_nvme(const struct perf_pmu *pmu);
+bool evsel__is_nvme(const struct evsel *evsel);
+
+int perf_pmus__read_nvme_pmus(struct list_head *pmus);
+
+int evsel__nvme_pmu_open(struct evsel *evsel,
+ struct perf_thread_map *threads,
+ int start_cpu_map_idx, int end_cpu_map_idx);
+int evsel__nvme_pmu_read(struct evsel *evsel, int cpu_map_idx, int thread);
+#else
+static inline struct perf_pmu *nvme_pmu__new(struct list_head *pmus __maybe_unused,
+ const char *sysfs_name __maybe_unused,
+ const char *name __maybe_unused)
+{
+ return NULL;
+}
+
+static inline void nvme_pmu__exit(struct perf_pmu *pmu __maybe_unused)
+{
+}
+
+static inline int nvme_pmu__for_each_event(struct perf_pmu *pmu __maybe_unused,
+ void *state __maybe_unused,
+ pmu_event_callback cb __maybe_unused)
+{
+ return 0;
+}
+
+static inline size_t nvme_pmu__num_events(struct perf_pmu *pmu __maybe_unused)
+{
+ return 0;
+}
+
+static inline bool nvme_pmu__have_event(struct perf_pmu *pmu __maybe_unused,
+ const char *name __maybe_unused)
+{
+ return false;
+}
+
+static inline int nvme_pmu__config_terms(const struct perf_pmu *pmu __maybe_unused,
+ struct perf_event_attr *attr __maybe_unused,
+ struct parse_events_terms *terms __maybe_unused,
+ struct parse_events_error *err __maybe_unused)
+{
+ return -EINVAL;
+}
+
+static inline int nvme_pmu__check_alias(struct parse_events_terms *terms __maybe_unused,
+ struct perf_pmu_info *info __maybe_unused,
+ struct parse_events_error *err __maybe_unused)
+{
+ return -EINVAL;
+}
+
+static inline bool perf_pmu__is_nvme(const struct perf_pmu *pmu __maybe_unused)
+{
+ return false;
+}
+
+static inline bool evsel__is_nvme(const struct evsel *evsel __maybe_unused)
+{
+ return false;
+}
+
+static inline int perf_pmus__read_nvme_pmus(struct list_head *pmus __maybe_unused)
+{
+ return 0;
+}
+
+static inline int evsel__nvme_pmu_open(struct evsel *evsel __maybe_unused,
+ struct perf_thread_map *threads __maybe_unused,
+ int start_cpu_map_idx __maybe_unused,
+ int end_cpu_map_idx __maybe_unused)
+{
+ return -EOPNOTSUPP;
+}
+
+static inline int evsel__nvme_pmu_read(struct evsel *evsel __maybe_unused,
+ int cpu_map_idx __maybe_unused,
+ int thread __maybe_unused)
+{
+ return -EOPNOTSUPP;
+}
+#endif
+
+#endif /* __NVME_PMU_H */
diff --git a/tools/perf/util/pmu.c b/tools/perf/util/pmu.c
index 01753d5c69d1..d33a313f5604 100644
--- a/tools/perf/util/pmu.c
+++ b/tools/perf/util/pmu.c
@@ -25,6 +25,7 @@
#include "pmu.h"
#include "drm_pmu.h"
#include "hwmon_pmu.h"
+#include "nvme_pmu.h"
#include "pmus.h"
#include "tool_pmu.h"
#include "tp_pmu.h"
@@ -1696,6 +1697,8 @@ int perf_pmu__config_terms(const struct perf_pmu *pmu,
if (perf_pmu__is_hwmon(pmu))
return hwmon_pmu__config_terms(pmu, attr, terms, err);
+ if (perf_pmu__is_nvme(pmu))
+ return nvme_pmu__config_terms(pmu, attr, terms, err);
if (perf_pmu__is_drm(pmu))
return drm_pmu__config_terms(pmu, attr, terms, err);
@@ -1860,6 +1863,12 @@ int perf_pmu__check_alias(struct perf_pmu *pmu, struct parse_events_terms *head_
ret = hwmon_pmu__check_alias(head_terms, info, err);
goto out;
}
+ if (perf_pmu__is_nvme(pmu)) {
+ ret = nvme_pmu__check_alias(head_terms, info, err);
+ if (ret)
+ return ret;
+ goto out;
+ }
if (perf_pmu__is_drm(pmu)) {
ret = drm_pmu__check_alias(pmu, head_terms, info, err);
goto out;
@@ -2121,6 +2130,8 @@ bool perf_pmu__have_event(struct perf_pmu *pmu, const char *name)
return tp_pmu__have_event(pmu, name);
if (perf_pmu__is_hwmon(pmu))
return hwmon_pmu__have_event(pmu, name);
+ if (perf_pmu__is_nvme(pmu))
+ return nvme_pmu__have_event(pmu, name);
if (perf_pmu__is_drm(pmu))
return drm_pmu__have_event(pmu, name);
if (perf_pmu__find_alias(pmu, name, /*load=*/ true) != NULL)
@@ -2142,6 +2153,8 @@ size_t perf_pmu__num_events(struct perf_pmu *pmu)
return tp_pmu__num_events(pmu);
if (perf_pmu__is_hwmon(pmu))
return hwmon_pmu__num_events(pmu);
+ if (perf_pmu__is_nvme(pmu))
+ return nvme_pmu__num_events(pmu);
if (perf_pmu__is_drm(pmu))
return drm_pmu__num_events(pmu);
@@ -2278,6 +2291,8 @@ int perf_pmu__for_each_event(struct perf_pmu *pmu, bool skip_duplicate_pmus,
return tp_pmu__for_each_event(pmu, state, cb);
if (perf_pmu__is_hwmon(pmu))
return hwmon_pmu__for_each_event(pmu, state, cb);
+ if (perf_pmu__is_nvme(pmu))
+ return nvme_pmu__for_each_event(pmu, state, cb);
if (perf_pmu__is_drm(pmu))
return drm_pmu__for_each_event(pmu, state, cb);
@@ -2786,6 +2801,8 @@ void perf_pmu__delete(struct perf_pmu *pmu)
if (perf_pmu__is_hwmon(pmu))
hwmon_pmu__exit(pmu);
+ else if (perf_pmu__is_nvme(pmu))
+ nvme_pmu__exit(pmu);
else if (perf_pmu__is_drm(pmu))
drm_pmu__exit(pmu);
diff --git a/tools/perf/util/pmu.h b/tools/perf/util/pmu.h
index 2c8916ad073e..fd33835da4de 100644
--- a/tools/perf/util/pmu.h
+++ b/tools/perf/util/pmu.h
@@ -46,6 +46,8 @@ enum pmu_kind {
PERF_PMU_KIND_DRM,
/* A perf tool provided HWMON PMU. */
PERF_PMU_KIND_HWMON,
+ /* A perf tool provided NVME PMU. */
+ PERF_PMU_KIND_NVME,
/* Perf tool provided PMU for tool events like time. */
PERF_PMU_KIND_TOOL,
/* A testing PMU kind. */
@@ -54,7 +56,9 @@ enum pmu_kind {
enum {
PERF_PMU_TYPE_PE_START = 0,
- PERF_PMU_TYPE_PE_END = 0xFFFDFFFF,
+ PERF_PMU_TYPE_PE_END = 0xFFFCFFFF,
+ PERF_PMU_TYPE_NVME_START = 0xFFFD0000,
+ PERF_PMU_TYPE_NVME_END = 0xFFFDFFFF,
PERF_PMU_TYPE_DRM_START = 0xFFFE0000,
PERF_PMU_TYPE_DRM_END = 0xFFFEFFFF,
PERF_PMU_TYPE_HWMON_START = 0xFFFF0000,
@@ -366,6 +370,8 @@ static inline enum pmu_kind perf_pmu__kind(const struct perf_pmu *pmu)
type = pmu->type;
if (type <= PERF_PMU_TYPE_PE_END)
return PERF_PMU_KIND_PE;
+ if (type <= PERF_PMU_TYPE_NVME_END)
+ return PERF_PMU_KIND_NVME;
if (type <= PERF_PMU_TYPE_DRM_END)
return PERF_PMU_KIND_DRM;
if (type <= PERF_PMU_TYPE_HWMON_END)
diff --git a/tools/perf/util/pmus.c b/tools/perf/util/pmus.c
index 1355b317f3ed..980035b640dc 100644
--- a/tools/perf/util/pmus.c
+++ b/tools/perf/util/pmus.c
@@ -18,6 +18,7 @@
#include "pmus.h"
#include "pmu.h"
#include "hwmon_pmu.h"
+#include "nvme_pmu.h"
#include "tool_pmu.h"
#include "print-events.h"
#include "strbuf.h"
@@ -45,18 +46,21 @@ enum perf_tool_pmu_type {
PERF_TOOL_PMU_TYPE_PE_OTHER,
PERF_TOOL_PMU_TYPE_TOOL,
PERF_TOOL_PMU_TYPE_HWMON,
+ PERF_TOOL_PMU_TYPE_NVME,
PERF_TOOL_PMU_TYPE_DRM,
#define PERF_TOOL_PMU_TYPE_PE_CORE_MASK (1 << PERF_TOOL_PMU_TYPE_PE_CORE)
#define PERF_TOOL_PMU_TYPE_PE_OTHER_MASK (1 << PERF_TOOL_PMU_TYPE_PE_OTHER)
#define PERF_TOOL_PMU_TYPE_TOOL_MASK (1 << PERF_TOOL_PMU_TYPE_TOOL)
#define PERF_TOOL_PMU_TYPE_HWMON_MASK (1 << PERF_TOOL_PMU_TYPE_HWMON)
+#define PERF_TOOL_PMU_TYPE_NVME_MASK (1 << PERF_TOOL_PMU_TYPE_NVME)
#define PERF_TOOL_PMU_TYPE_DRM_MASK (1 << PERF_TOOL_PMU_TYPE_DRM)
#define PERF_TOOL_PMU_TYPE_ALL_MASK (PERF_TOOL_PMU_TYPE_PE_CORE_MASK | \
PERF_TOOL_PMU_TYPE_PE_OTHER_MASK | \
PERF_TOOL_PMU_TYPE_TOOL_MASK | \
PERF_TOOL_PMU_TYPE_HWMON_MASK | \
+ PERF_TOOL_PMU_TYPE_NVME_MASK | \
PERF_TOOL_PMU_TYPE_DRM_MASK)
};
static unsigned int read_pmu_types;
@@ -180,6 +184,8 @@ struct perf_pmu *perf_pmus__find(const char *name)
/* Looking up an individual perf event PMU failed, check if a tool PMU should be read. */
if (!strncmp(name, "hwmon_", 6))
to_read_pmus |= PERF_TOOL_PMU_TYPE_HWMON_MASK;
+ else if (!strncmp(name, "nvme", 4))
+ to_read_pmus |= PERF_TOOL_PMU_TYPE_NVME_MASK;
else if (!strncmp(name, "drm_", 4))
to_read_pmus |= PERF_TOOL_PMU_TYPE_DRM_MASK;
else if (!strcmp(name, "tool"))
@@ -282,6 +288,10 @@ static void pmu_read_sysfs(unsigned int to_read_types)
(read_pmu_types & PERF_TOOL_PMU_TYPE_HWMON_MASK) == 0)
perf_pmus__read_hwmon_pmus(&other_pmus);
+ if ((to_read_types & PERF_TOOL_PMU_TYPE_NVME_MASK) != 0 &&
+ (read_pmu_types & PERF_TOOL_PMU_TYPE_NVME_MASK) == 0)
+ perf_pmus__read_nvme_pmus(&other_pmus);
+
if ((to_read_types & PERF_TOOL_PMU_TYPE_DRM_MASK) != 0 &&
(read_pmu_types & PERF_TOOL_PMU_TYPE_DRM_MASK) == 0)
perf_pmus__read_drm_pmus(&other_pmus);
@@ -318,6 +328,8 @@ struct perf_pmu *perf_pmus__find_by_type(unsigned int type)
if (type >= PERF_PMU_TYPE_PE_START && type <= PERF_PMU_TYPE_PE_END) {
to_read_pmus = PERF_TOOL_PMU_TYPE_PE_CORE_MASK |
PERF_TOOL_PMU_TYPE_PE_OTHER_MASK;
+ } else if (type >= PERF_PMU_TYPE_NVME_START && type <= PERF_PMU_TYPE_NVME_END) {
+ to_read_pmus = PERF_TOOL_PMU_TYPE_NVME_MASK;
} else if (type >= PERF_PMU_TYPE_DRM_START && type <= PERF_PMU_TYPE_DRM_END) {
to_read_pmus = PERF_TOOL_PMU_TYPE_DRM_MASK;
} else if (type >= PERF_PMU_TYPE_HWMON_START && type <= PERF_PMU_TYPE_HWMON_END) {
@@ -390,6 +402,10 @@ struct perf_pmu *perf_pmus__scan_for_event(struct perf_pmu *pmu, const char *eve
if (strlen(event) > 4 && strncmp("drm-", event, 4) == 0)
to_read_pmus |= PERF_TOOL_PMU_TYPE_DRM_MASK;
+ /* Could the event be an nvme event? */
+ if (nvme_pmu__have_event(NULL, event))
+ to_read_pmus |= PERF_TOOL_PMU_TYPE_NVME_MASK;
+
pmu_read_sysfs(to_read_pmus);
pmu = list_prepare_entry(pmu, &core_pmus, list);
}
@@ -427,11 +443,14 @@ struct perf_pmu *perf_pmus__scan_matching_wildcard(struct perf_pmu *pmu, const c
*/
if (strisglob(wildcard)) {
to_read_pmus |= PERF_TOOL_PMU_TYPE_HWMON_MASK |
+ PERF_TOOL_PMU_TYPE_NVME_MASK |
PERF_TOOL_PMU_TYPE_DRM_MASK;
} else if (strlen(wildcard) >= 4 && strncmp("drm_", wildcard, 4) == 0) {
to_read_pmus |= PERF_TOOL_PMU_TYPE_DRM_MASK;
} else if (strlen(wildcard) >= 5 && strncmp("hwmon", wildcard, 5) == 0) {
to_read_pmus |= PERF_TOOL_PMU_TYPE_HWMON_MASK;
+ } else if (strlen(wildcard) >= 4 && strncmp("nvme", wildcard, 4) == 0) {
+ to_read_pmus |= PERF_TOOL_PMU_TYPE_NVME_MASK;
}
pmu_read_sysfs(to_read_pmus);
--
2.56.1.703.gf848ed807c-goog
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH v3 3/3] perf tests: Add NVMe PMU event parsing test
2026-11-13 19:12 [PATCH v3 0/3] perf pmu: Add tool-provided NVMe PMU Ian Rogers
2026-11-13 19:12 ` [PATCH v3 1/3] perf build: Add libnvme feature detection Ian Rogers
2026-11-13 19:12 ` [PATCH v3 2/3] perf pmu: Implement tool-provided NVMe PMU Ian Rogers
@ 2026-11-13 19:12 ` Ian Rogers
2026-11-13 19:28 ` sashiko-bot
2 siblings, 1 reply; 7+ messages in thread
From: Ian Rogers @ 2026-11-13 19:12 UTC (permalink / raw)
To: Peter Zijlstra, Ingo Molnar, Arnaldo Carvalho de Melo,
Namhyung Kim, Jiri Olsa, Ian Rogers
Cc: Christoph Hellwig, Daniel Wagner, Yuzhuo Jing, Adrian Hunter,
James Clark, Tomas Glozar, Michael Jeanson, Dmitrii Dolgov,
Alexandre Chartre, Leo Yan, German Gomez, Anubhav Shelat,
linux-perf-users, linux-nvme, linux-kernel
Introduce a unit test suite for the NVMe tool PMU event parser. The test
registers a mock test NVMe PMU ('nvme_a_test_nvme_pmu' with alias
'nvme1234') and verifies:
- Event parsing without a PMU prefix and with both PMU name and alias.
- Event config encoding, unit, and scale across SMART, Endurance, FDP,
Error, and ZNS log events (including Kelvin temperature scaling).
- Support for 'event=<name>', raw 'config=0x...', and 'name=' qualifiers.
- Clean rejection of unknown event names, invalid raw configs, duplicate
event terms, and unsupported string terms where term->config is NULL.
Signed-off-by: Ian Rogers <irogers@google.com>
---
tools/perf/tests/Build | 1 +
tools/perf/tests/builtin-test.c | 1 +
tools/perf/tests/nvme_pmu.c | 303 ++++++++++++++++++++++++++++++++
tools/perf/tests/tests.h | 1 +
tools/perf/util/pmus.c | 5 +
tools/perf/util/pmus.h | 1 +
6 files changed, 312 insertions(+)
create mode 100644 tools/perf/tests/nvme_pmu.c
diff --git a/tools/perf/tests/Build b/tools/perf/tests/Build
index 5eebb2ec5dde..336bad77c8f4 100644
--- a/tools/perf/tests/Build
+++ b/tools/perf/tests/Build
@@ -71,6 +71,7 @@ perf-test-y += hybrid-merge.o
perf-test-y += symbols.o
perf-test-y += util.o
perf-test-y += hwmon_pmu.o
+perf-test-y += nvme_pmu.o
perf-test-y += tool_pmu.o
perf-test-y += subcmd-help.o
perf-test-y += kallsyms-split.o
diff --git a/tools/perf/tests/builtin-test.c b/tools/perf/tests/builtin-test.c
index 327483f96401..0b88018c7036 100644
--- a/tools/perf/tests/builtin-test.c
+++ b/tools/perf/tests/builtin-test.c
@@ -124,6 +124,7 @@ static struct test_suite *generic_tests[] = {
&suite__pmu,
&suite__pmu_events,
&suite__hwmon_pmu,
+ &suite__nvme_pmu,
&suite__tool_pmu,
&suite__dso_data,
&suite__perf_evsel__roundtrip_name_test,
diff --git a/tools/perf/tests/nvme_pmu.c b/tools/perf/tests/nvme_pmu.c
new file mode 100644
index 000000000000..c3c17ccc9ea0
--- /dev/null
+++ b/tools/perf/tests/nvme_pmu.c
@@ -0,0 +1,303 @@
+// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
+#include "nvme_pmu.h"
+
+#include <errno.h>
+#include <inttypes.h>
+#include <stdio.h>
+#include <string.h>
+#include <linux/compiler.h>
+#include <linux/kernel.h>
+#include <linux/list.h>
+#include <linux/string.h>
+
+#include "debug.h"
+#include "evlist.h"
+#include "evsel.h"
+#include "parse-events.h"
+#include "pmus.h"
+#include "tests.h"
+
+#ifdef HAVE_LIBNVME_SUPPORT
+
+enum test_encoding_mode {
+ TEST_ENCODING_NAME,
+ TEST_ENCODING_ALIAS_PMU,
+ TEST_ENCODING_EVENT_TERM,
+ TEST_ENCODING_RAW_CONFIG,
+ TEST_ENCODING_WITH_NAME_QUALIFIER,
+};
+
+static const struct test_event {
+ const char *name;
+ const char *unit;
+ double scale;
+ uint64_t config;
+} test_events[] = {
+ {
+ "smart_temperature",
+ "K",
+ 1.0,
+ NVME_SMART(2, temperature),
+ },
+ {
+ "smart_data_units_read",
+ "B",
+ 512000.0,
+ NVME_SMART(16, data_units_read),
+ },
+ {
+ "endurance_percent_used",
+ "",
+ 1.0,
+ NVME_ENDURANCE(1, percent_used),
+ },
+ {
+ "endurance_data_units_read",
+ "B",
+ 1000000000.0,
+ NVME_ENDURANCE(16, data_units_read),
+ },
+ {
+ "fdp_hbmw",
+ "B",
+ 1.0,
+ NVME_FDP(16, hbmw),
+ },
+ {
+ "error_count",
+ "",
+ 1.0,
+ NVME_ERROR(8, error_count),
+ },
+ {
+ "zns_nrzid",
+ "",
+ 1.0,
+ NVME_ZNS(2, nrzid),
+ },
+};
+
+static int do_test(size_t i, bool with_pmu, enum test_encoding_mode mode)
+{
+ struct evlist *evlist = evlist__new();
+ struct evsel *evsel;
+ struct parse_events_error err;
+ int ret;
+ char str[128];
+ bool found = false;
+
+ if (!evlist) {
+ pr_err("evlist allocation failed\n");
+ return TEST_FAIL;
+ }
+
+ if (!with_pmu) {
+ strlcpy(str, test_events[i].name, sizeof(str));
+ } else {
+ switch (mode) {
+ case TEST_ENCODING_NAME:
+ snprintf(str, sizeof(str), "nvme_a_test_nvme_pmu/%s/",
+ test_events[i].name);
+ break;
+ case TEST_ENCODING_ALIAS_PMU:
+ snprintf(str, sizeof(str), "nvme1234/%s/", test_events[i].name);
+ break;
+ case TEST_ENCODING_EVENT_TERM:
+ snprintf(str, sizeof(str), "nvme_a_test_nvme_pmu/event=%s/",
+ test_events[i].name);
+ break;
+ case TEST_ENCODING_RAW_CONFIG:
+ snprintf(str, sizeof(str), "nvme_a_test_nvme_pmu/config=0x%" PRIx64 "/",
+ test_events[i].config);
+ break;
+ case TEST_ENCODING_WITH_NAME_QUALIFIER:
+ snprintf(str, sizeof(str),
+ "nvme_a_test_nvme_pmu/%s,name=custom_nvme_event/",
+ test_events[i].name);
+ break;
+ default:
+ ret = TEST_FAIL;
+ goto out_put;
+ }
+ }
+
+ pr_debug("Testing '%s'\n", str);
+ parse_events_error__init(&err);
+ ret = parse_events(evlist, str, &err);
+ if (ret) {
+ pr_debug("FAILED %s:%d failed to parse event '%s', err %d\n",
+ __FILE__, __LINE__, str, ret);
+ parse_events_error__print(&err, str);
+ ret = TEST_FAIL;
+ goto out;
+ }
+
+ ret = TEST_OK;
+ if (with_pmu ? (evlist__nr_entries(evlist) != 1)
+ : (evlist__nr_entries(evlist) < 1)) {
+ pr_debug("FAILED %s:%d Unexpected number of events for '%s' of %d\n",
+ __FILE__, __LINE__, str, evlist__nr_entries(evlist));
+ ret = TEST_FAIL;
+ goto out;
+ }
+
+ evlist__for_each_entry(evlist, evsel) {
+ if (!evsel->pmu || !evsel->pmu->name ||
+ strcmp(evsel->pmu->name, "nvme_a_test_nvme_pmu"))
+ continue;
+
+ if (evsel->core.attr.config != test_events[i].config) {
+ pr_debug("FAILED %s:%d Unexpected config for '%s', %"
+ PRIu64 " != %" PRIu64 "\n",
+ __FILE__, __LINE__, str,
+ (uint64_t)evsel->core.attr.config,
+ test_events[i].config);
+ ret = TEST_FAIL;
+ goto out;
+ }
+ if (!evsel->unit || strcmp(evsel->unit, test_events[i].unit) != 0 ||
+ evsel->scale != test_events[i].scale) {
+ pr_debug("FAILED %s:%d Unexpected unit/scale for '%s', '%s'/%g != '%s'/%g\n",
+ __FILE__, __LINE__, str,
+ evsel->unit ?: "(null)", evsel->scale,
+ test_events[i].unit, test_events[i].scale);
+ ret = TEST_FAIL;
+ goto out;
+ }
+ if (mode == TEST_ENCODING_WITH_NAME_QUALIFIER &&
+ strcmp(evsel__name(evsel), "custom_nvme_event") != 0) {
+ pr_debug("FAILED %s:%d Unexpected custom name '%s'\n",
+ __FILE__, __LINE__, evsel__name(evsel));
+ ret = TEST_FAIL;
+ goto out;
+ }
+ found = true;
+ }
+
+ if (!found) {
+ pr_debug("FAILED %s:%d Didn't find nvme event '%s' in parsed evsels\n",
+ __FILE__, __LINE__, str);
+ ret = TEST_FAIL;
+ }
+
+out:
+ parse_events_error__exit(&err);
+out_put:
+ evlist__put(evlist);
+ return ret;
+}
+
+static int test__nvme_pmu(bool with_pmu)
+{
+ struct perf_pmu *pmu = perf_pmus__add_test_nvme_pmu("nvme1234", "a_test_nvme_pmu");
+ int ret = TEST_OK;
+
+ if (!pmu)
+ return TEST_FAIL;
+
+ for (size_t i = 0; i < ARRAY_SIZE(test_events); i++) {
+ ret = do_test(i, with_pmu, TEST_ENCODING_NAME);
+ if (ret != TEST_OK)
+ break;
+
+ if (with_pmu) {
+ ret = do_test(i, with_pmu, TEST_ENCODING_ALIAS_PMU);
+ if (ret != TEST_OK)
+ break;
+
+ ret = do_test(i, with_pmu, TEST_ENCODING_EVENT_TERM);
+ if (ret != TEST_OK)
+ break;
+
+ ret = do_test(i, with_pmu, TEST_ENCODING_RAW_CONFIG);
+ if (ret != TEST_OK)
+ break;
+
+ ret = do_test(i, with_pmu, TEST_ENCODING_WITH_NAME_QUALIFIER);
+ if (ret != TEST_OK)
+ break;
+ }
+ }
+
+ list_del(&pmu->list);
+ perf_pmu__delete(pmu);
+ return ret;
+}
+
+static int test__nvme_pmu_invalid_terms(struct test_suite *test __maybe_unused,
+ int subtest __maybe_unused)
+{
+ static const char * const invalid_events[] = {
+ "nvme_a_test_nvme_pmu/nonexistent_event/",
+ "nvme_a_test_nvme_pmu/call-graph=fp/",
+ "nvme_a_test_nvme_pmu/config=0xffffffff/",
+ "nvme_a_test_nvme_pmu/name=only_name/",
+ "nvme_a_test_nvme_pmu/smart_temperature,error_count/",
+ "nvme_a_test_nvme_pmu/smart_temperature=2/",
+ };
+ struct perf_pmu *pmu = perf_pmus__add_test_nvme_pmu("nvme1234", "a_test_nvme_pmu");
+ int ret = TEST_OK;
+
+ if (!pmu)
+ return TEST_FAIL;
+
+ for (size_t i = 0; i < ARRAY_SIZE(invalid_events); i++) {
+ struct evlist *evlist = evlist__new();
+ struct parse_events_error err;
+ int parse_ret;
+
+ if (!evlist) {
+ ret = TEST_FAIL;
+ break;
+ }
+ parse_events_error__init(&err);
+ parse_ret = parse_events(evlist, invalid_events[i], &err);
+ if (parse_ret == 0 || list_empty(&err.list)) {
+ pr_debug("FAILED %s:%d Unexpected parse result %d for invalid event '%s'\n",
+ __FILE__, __LINE__, parse_ret, invalid_events[i]);
+ parse_events_error__exit(&err);
+ evlist__put(evlist);
+ ret = TEST_FAIL;
+ break;
+ }
+ parse_events_error__exit(&err);
+ evlist__put(evlist);
+ }
+
+ list_del(&pmu->list);
+ perf_pmu__delete(pmu);
+ return ret;
+}
+
+static int test__nvme_pmu_without_pmu(struct test_suite *test __maybe_unused,
+ int subtest __maybe_unused)
+{
+ return test__nvme_pmu(/*with_pmu=*/false);
+}
+
+static int test__nvme_pmu_with_pmu(struct test_suite *test __maybe_unused,
+ int subtest __maybe_unused)
+{
+ return test__nvme_pmu(/*with_pmu=*/true);
+}
+
+static struct test_case tests__nvme_pmu[] = {
+ TEST_CASE("Parsing without PMU name", nvme_pmu_without_pmu),
+ TEST_CASE("Parsing with PMU name", nvme_pmu_with_pmu),
+ TEST_CASE("Rejecting invalid event terms", nvme_pmu_invalid_terms),
+ { .name = NULL, }
+};
+
+struct test_suite suite__nvme_pmu = {
+ .desc = "NVMe PMU",
+ .test_cases = tests__nvme_pmu,
+};
+
+#else
+
+struct test_suite suite__nvme_pmu = {
+ .desc = "NVMe PMU",
+ .test_cases = NULL,
+};
+
+#endif
diff --git a/tools/perf/tests/tests.h b/tools/perf/tests/tests.h
index af822885890d..5a77bc5ecf7a 100644
--- a/tools/perf/tests/tests.h
+++ b/tools/perf/tests/tests.h
@@ -133,6 +133,7 @@ DECLARE_SUITE(syscall_openat_tp_fields);
DECLARE_SUITE(pmu);
DECLARE_SUITE(pmu_events);
DECLARE_SUITE(hwmon_pmu);
+DECLARE_SUITE(nvme_pmu);
DECLARE_SUITE(tool_pmu);
DECLARE_SUITE(attr);
DECLARE_SUITE(dso_data);
diff --git a/tools/perf/util/pmus.c b/tools/perf/util/pmus.c
index 980035b640dc..66626ce3494b 100644
--- a/tools/perf/util/pmus.c
+++ b/tools/perf/util/pmus.c
@@ -925,6 +925,11 @@ struct perf_pmu *perf_pmus__add_test_hwmon_pmu(const char *hwmon_dir,
return hwmon_pmu__new(&other_pmus, hwmon_dir, sysfs_name, name);
}
+struct perf_pmu *perf_pmus__add_test_nvme_pmu(const char *sysfs_name, const char *name)
+{
+ return nvme_pmu__new(&other_pmus, sysfs_name, name);
+}
+
struct perf_pmu *perf_pmus__fake_pmu(void)
{
static struct perf_pmu fake = {
diff --git a/tools/perf/util/pmus.h b/tools/perf/util/pmus.h
index 0d55edb3f2fc..2045d4cf44d1 100644
--- a/tools/perf/util/pmus.h
+++ b/tools/perf/util/pmus.h
@@ -37,6 +37,7 @@ struct perf_pmu *perf_pmus__add_test_pmu(int test_sysfs_dirfd, const char *name)
struct perf_pmu *perf_pmus__add_test_hwmon_pmu(const char *hwmon_dir,
const char *sysfs_name,
const char *name);
+struct perf_pmu *perf_pmus__add_test_nvme_pmu(const char *sysfs_name, const char *name);
struct perf_pmu *perf_pmus__fake_pmu(void);
struct perf_pmu *perf_pmus__find_core_pmu(void);
--
2.56.1.703.gf848ed807c-goog
^ permalink raw reply related [flat|nested] 7+ messages in thread
* Re: [PATCH v3 1/3] perf build: Add libnvme feature detection
2026-11-13 19:12 ` [PATCH v3 1/3] perf build: Add libnvme feature detection Ian Rogers
@ 2026-11-13 19:22 ` sashiko-bot
0 siblings, 0 replies; 7+ messages in thread
From: sashiko-bot @ 2026-11-13 19:22 UTC (permalink / raw)
To: Ian Rogers; +Cc: linux-perf-users
> Introduce a feature check for libnvme using pkg-config so that the
> tool-provided NVMe PMU can be conditionally compiled when libnvme is
> available.
>
> Add libnvme to FEATURE_TESTS_BASIC, FEATURE_DISPLAY, FEATURE_PKG_CONFIG,
> and test-all.c so fast-path discovery and individual feature checks stay
> consistent with the pkg-config flags used during linking.
>
> Signed-off-by: Ian Rogers <irogers@google.com>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261113191234.2509964-1-irogers@google.com?part=1
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH v3 3/3] perf tests: Add NVMe PMU event parsing test
2026-11-13 19:12 ` [PATCH v3 3/3] perf tests: Add NVMe PMU event parsing test Ian Rogers
@ 2026-11-13 19:28 ` sashiko-bot
0 siblings, 0 replies; 7+ messages in thread
From: sashiko-bot @ 2026-11-13 19:28 UTC (permalink / raw)
To: Ian Rogers; +Cc: linux-perf-users, linux-nvme
Thank you for your contribution! Sashiko AI review found 1 potential
issue to consider:
- [Low] perf tests: suite__nvme_pmu has NULL test_cases when libnvme is
disabled
--
commit 606f3f12b454f7dd2fdaeea08ae54a0e801abbdb
Author: Ian Rogers <irogers@google.com>
perf tests: Add NVMe PMU event parsing test
This commit adds a unit test suite for the NVMe tool PMU event parser and
registers a helper to create a test NVMe PMU.
> diff --git a/tools/perf/tests/builtin-test.c b/tools/perf/tests/builtin-test.c
> index 0b8897fd42cac..fbcdcbf92aded 100644
> --- a/tools/perf/tests/builtin-test.c
> +++ b/tools/perf/tests/builtin-test.c
> @@ -110,6 +110,7 @@ static struct test_suite *generic_tests[] = {
> &suite__pmu,
> &suite__pmu_events,
> &suite__hwmon_pmu,
> + &suite__nvme_pmu,
> &suite__tool_pmu,
> &suite__dso_data,
> &suite__perf_evsel__roundtrip_name_test,
> diff --git a/tools/perf/tests/nvme_pmu.c b/tools/perf/tests/nvme_pmu.c
> new file mode 100644
> index 0000000000000..c3c17ccc9ea0b
> --- /dev/null
> +++ b/tools/perf/tests/nvme_pmu.c
[ ... ]
> +#else
> +
> +struct test_suite suite__nvme_pmu = {
> + .desc = "NVMe PMU",
> + .test_cases = NULL,
> +};
> +
> +#endif
[Severity: Low]
When HAVE_LIBNVME_SUPPORT is not defined, does setting
suite__nvme_pmu.test_cases to NULL cause __cmd_test() to silently drop the
suite during execution?
generic_tests[] registers suite__nvme_pmu unconditionally, so perf test
list assigns it a suite number, but __cmd_test() only invokes start_test()
and print_test_result() inside test_suite__for_each_test_case(), which
runs zero iterations when test_cases is NULL. On builds without libnvme,
perf test skips over that suite number without printing a Skip line,
counting it in the skipped summary, or emitting it in JUnit output.
Providing a stub test case that returns TEST_SKIP with a TEST_CASE_REASON
of "not compiled in" (like tools/perf/tests/pfm.c), or guarding
&suite__nvme_pmu in generic_tests[] with HAVE_LIBNVME_SUPPORT, would keep
the suite list and test runner in sync.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261113191234.2509964-1-irogers@google.com?part=3
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH v3 2/3] perf pmu: Implement tool-provided NVMe PMU
2026-11-13 19:12 ` [PATCH v3 2/3] perf pmu: Implement tool-provided NVMe PMU Ian Rogers
@ 2026-11-13 19:40 ` sashiko-bot
0 siblings, 0 replies; 7+ messages in thread
From: sashiko-bot @ 2026-11-13 19:40 UTC (permalink / raw)
To: Ian Rogers; +Cc: linux-perf-users, linux-nvme
Thank you for your contribution! Sashiko AI review found 2 potential
issues to consider:
- [Medium] perf pmu: missing errno and err_thread handling in
evsel__nvme_pmu_open()
- [Medium] perf evsel: PERF_EVENT_IOC_ENABLE/DISABLE ioctls issued on
NVMe device fds
--
commit 1cca3c688611b8ef7867f412d383cb3ae7a315e1
Author: Ian Rogers <irogers@google.com>
perf pmu: Implement tool-provided NVMe PMU
This commit adds a tool-provided PMU for NVMe controllers using libnvme,
exposing metrics from standard NVMe log pages under nvme_<ctrl> PMUs.
> diff --git a/tools/perf/util/nvme_pmu.c b/tools/perf/util/nvme_pmu.c
> new file mode 100644
> index 0000000000000..3ee967505dec6
> --- /dev/null
> +++ b/tools/perf/util/nvme_pmu.c
[ ... ]
> +static int nvme_pmu__read_val(int fd, uint64_t config, uint64_t *val)
> +{
[ ... ]
> + if (ret < 0)
> + return -errno ?: -EIO;
> + if (ret > 0)
> + return -EOPNOTSUPP;
[ ... ]
> +int evsel__nvme_pmu_open(struct evsel *evsel,
> + struct perf_thread_map *threads,
> + int start_cpu_map_idx, int end_cpu_map_idx)
> +{
> + struct nvme_pmu *nvm = container_of(evsel->pmu, struct nvme_pmu, pmu);
> + int idx = start_cpu_map_idx, thread = 0, nthreads, err = 0;
> + int max_cpus, max_threads;
> + char path[PATH_MAX];
> +
> + if (evsel->core.attr.sample_period || !evsel->core.fd)
> + return -EINVAL;
> +
> + snprintf(path, sizeof(path), "/dev/%s", nvm->dev_name);
[ ... ]
> + for (idx = start_cpu_map_idx; idx < end_cpu_map_idx; idx++) {
> + for (thread = 0; thread < nthreads; thread++) {
> + uint64_t *initial_val;
> + int fd = open(path, O_RDONLY | O_CLOEXEC);
> +
> + FD(evsel, idx, thread) = fd;
[Severity: Medium]
Does storing a /dev/nvmeX file descriptor in evsel->core.fd cause
evsel__enable_cpu(), evsel__enable(), evsel__disable_cpu() and
evsel__disable() to fail with -ENOTTY?
Those functions only special-case evsel__is_tool(evsel) and otherwise call
into libperf:
tools/perf/util/evsel.c:evsel__enable_cpu() {
if (evsel__is_tool(evsel))
err = evsel__tool_pmu_enable_cpu(evsel, cpu_map_idx);
else
err = perf_evsel__enable_cpu(&evsel->core, cpu_map_idx);
if (!err && evsel__is_group_leader(evsel)) {
...
}
perf_evsel__enable_cpu() and its enable/disable counterparts issue
PERF_EVENT_IOC_ENABLE and PERF_EVENT_IOC_DISABLE on every fd in
evsel->core.fd, which the NVMe character device rejects with -ENOTTY.
That leaves evsel->disabled stale and aborts the group loop before
userspace members such as duration_time, user_time or system_time are
enabled or disabled in mixed groups.
> + if (fd < 0) {
> + err = -errno;
> + goto out_close;
> + }
> + initial_val = xyarray__entry(evsel->nvme.initial_values, idx, thread);
> + err = nvme_pmu__read_val(fd, evsel->core.attr.config, initial_val);
> + if (err) {
> + close(fd);
> + FD(evsel, idx, thread) = -1;
> + goto out_close;
> + }
> + }
> + }
> + return 0;
> +out_close:
> + if (err)
> + threads->err_thread = thread;
> +
> + do {
> + while (--thread >= 0) {
> + if (FD(evsel, idx, thread) >= 0)
> + close(FD(evsel, idx, thread));
> + FD(evsel, idx, thread) = -1;
> + }
> + thread = nthreads;
> + } while (--idx >= start_cpu_map_idx);
> + return err;
> +}
[Severity: Medium]
Does evsel__nvme_pmu_open() leave errno unset or clobbered on failure while
also setting threads->err_thread for a controller-wide device?
Callers of evsel__open() and evsel__open_per_cpu_and_thread() such as
__run_perf_stat(), record__open() and __cmd_top() read errno after a
negative return. When a controller does not support an optional log page
(such as FDP, ZNS or Endurance Group), libnvme returns a positive NVMe
status code without setting errno, and nvme_pmu__read_val() returns
-EOPNOTSUPP with errno still 0 (or clobbered by close()). The -EINVAL and
!HAVE_LIBNVME_SUPPORT error returns likewise do not set errno.
In addition, setting threads->err_thread = thread causes
stat_handle_error() in perf stat --per-thread mode to treat a controller
open or log error as a dead thread, removing every thread from the thread
map in a retry loop until nthreads reaches 0 and evsel__nvme_pmu_open()
returns 0 with no descriptors opened.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261113191234.2509964-1-irogers@google.com?part=2
^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-11-13 19:40 UTC | newest]
Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-13 19:12 [PATCH v3 0/3] perf pmu: Add tool-provided NVMe PMU Ian Rogers
2026-11-13 19:12 ` [PATCH v3 1/3] perf build: Add libnvme feature detection Ian Rogers
2026-11-13 19:22 ` sashiko-bot
2026-11-13 19:12 ` [PATCH v3 2/3] perf pmu: Implement tool-provided NVMe PMU Ian Rogers
2026-11-13 19:40 ` sashiko-bot
2026-11-13 19:12 ` [PATCH v3 3/3] perf tests: Add NVMe PMU event parsing test Ian Rogers
2026-11-13 19:28 ` sashiko-bot
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox