* [PATCH 0/3] perf jevents: Report the JSON keys that jevents.py never reads
@ 2026-11-10 22:22 TAKANO Mitsuhiro
2026-11-10 22:22 ` [PATCH 1/3] perf jevents: Name the metric when a MetricExpr is missing TAKANO Mitsuhiro
` (3 more replies)
0 siblings, 4 replies; 12+ messages in thread
From: TAKANO Mitsuhiro @ 2026-11-10 22:22 UTC (permalink / raw)
To: linux-perf-users, Arnaldo Carvalho de Melo, Namhyung Kim
Cc: Ian Rogers, linux-kernel, TAKANO Mitsuhiro, James Clark,
John Garry, Peter Zijlstra, Ingo Molnar, Mark Rutland
jevents.py takes the fields it wants out of each JSON object by name and
ignores the rest, so a key spelled differently is dropped without a word
and the object is built without whatever that field held.
Patches 1 and 2 make the generator name what went wrong, and let the
model argument be omitted. Patch 3 adds a --check mode that reports the
keys jevents.py never asks for, along with a make target and a perf test
so that it runs without anyone having to remember it.
This is the second posting of the check. It went out alone in November
(https://lore.kernel.org/linux-perf-
users/20261110193239.1741014-1-takano32@gmail.com/) and the review found
two ways it could call a tree clean that no build can use: a MetricName
with a dash in it that loses its MetricExpr was invisible to both the
reader and the report, and a metric without an expression was skipped
rather than reported unless its key was spelled close enough to be a near
miss. Both are fixed here, and the failure-path changes the check needs
are no longer mixed into it.
The test runs the check over a fixture table rather than over the tree,
so it is green whatever the tables happen to look like, and it checks the
three exit statuses: 0 for a clean table, 1 for a reported key, 2 for a
table that does not parse.
The two misspellings the check reports in the tree today were posted
separately in November:
https://lore.kernel.org/linux-perf-users/20261108184600.3810059-1-takano32@gmail.com/
https://lore.kernel.org/linux-perf-users/20261108183846.3804875-1-takano32@gmail.com/
TAKANO Mitsuhiro (3):
perf jevents: Name the metric when a MetricExpr is missing
perf jevents: Make the model argument optional
perf jevents: Check the JSON keys that jevents.py never reads
tools/perf/Makefile | 14 +-
tools/perf/Makefile.perf | 1 +
tools/perf/pmu-events/jevents.py | 394 +++++++++++++++++++++---
tools/perf/tests/shell/jevents_check.sh | 68 ++++
4 files changed, 428 insertions(+), 49 deletions(-)
create mode 100755 tools/perf/tests/shell/jevents_check.sh
base-commit: 7475ea8b58616b33c04afb976cf0fbaad9dd63fb
--
2.53.0
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH 1/3] perf jevents: Name the metric when a MetricExpr is missing
2026-11-10 22:22 [PATCH 0/3] perf jevents: Report the JSON keys that jevents.py never reads TAKANO Mitsuhiro
@ 2026-11-10 22:22 ` TAKANO Mitsuhiro
2026-11-10 22:22 ` [PATCH 2/3] perf jevents: Make the model argument optional TAKANO Mitsuhiro
` (2 subsequent siblings)
3 siblings, 0 replies; 12+ messages in thread
From: TAKANO Mitsuhiro @ 2026-11-10 22:22 UTC (permalink / raw)
To: linux-perf-users, Arnaldo Carvalho de Melo, Namhyung Kim
Cc: Ian Rogers, linux-kernel, TAKANO Mitsuhiro, James Clark,
John Garry, Peter Zijlstra, Ingo Molnar, Mark Rutland
jevents.py reads each field it wants out of a JSON object by name, so a
MetricExpr key that is missing or spelled another way leaves the object
with a MetricName and no expression. Two different failures followed,
neither of which named the metric: the rewrite in _read_json_events_impl()
raised AttributeError on the None, and a metric that got that far reached
print_pending_metrics(), whose assert fired on it.
Raise in the reader instead, naming the metric, before either of them can
happen. The check is not inside the '-' test that decides whether a metric
takes part in the rewrite: a name with a dash in it can lose its
expression the same way.
Two smaller changes on the same path:
- An architecture standard table that does not parse now carries the
reason in its message ("Failure processing '<file>' in '<dir>':
<cause>"). The traceback used to end with the file name alone, and
only the chained exception said why.
- A table that does not parse is reported on stderr with a "jevents:
error:" prefix instead of on stdout, where it was mixed with the
generated output.
And the output file is opened after the tables have been read, and not by
argparse, so a build that fails while reading them leaves a file the
caller named as it was instead of truncating it to the header.
Signed-off-by: TAKANO Mitsuhiro <takano32@gmail.com>
---
tools/perf/pmu-events/jevents.py | 35 ++++++++++++++++++++++++++------
1 file changed, 29 insertions(+), 6 deletions(-)
diff --git a/tools/perf/pmu-events/jevents.py b/tools/perf/pmu-events/jevents.py
index 080e04b51b8c..86060a228b75 100755
--- a/tools/perf/pmu-events/jevents.py
+++ b/tools/perf/pmu-events/jevents.py
@@ -460,11 +460,17 @@ def _read_json_events_impl(path: str, topic: str) -> Sequence[JsonEvent]:
with open(path, encoding='utf-8') as f:
events = json.load(f, object_hook=JsonEvent)
except BaseException:
- print(f"Exception processing {path}")
+ print(f'jevents: error: Exception processing {path}', file=sys.stderr)
raise
metrics: list[Tuple[str, str, metric.Expression]] = []
for event in events:
event.topic = topic
+ if event.metric_name and event.metric_expr is None:
+ # A MetricName without a MetricExpr leaves nothing to put in the
+ # generated tables, whether or not the name has a dash, so the build
+ # must not go on. Name the metric instead of letting the table printer
+ # assert on the None.
+ raise RuntimeError(f'Metric {event.metric_name!r} has no MetricExpr')
if event.metric_name and '-' not in event.metric_name:
metrics.append((event.pmu, event.metric_name, event.metric_expr))
updates = metric.RewriteMetricsInTermsOfOthers(metrics)
@@ -493,7 +499,7 @@ def preprocess_arch_std_files(archpath: str) -> None:
if event.metric_name:
_arch_std_events[event.metric_name.lower()] = event
except Exception as e:
- raise RuntimeError(f'Failure processing \'{item.name}\' in \'{archpath}\'') from e
+ raise RuntimeError(f'Failure processing \'{item.name}\' in \'{archpath}\': {e}') from e
def add_events_table_entries(item: os.DirEntry, topic: str) -> None:
@@ -1490,12 +1496,29 @@ such as "arm/cortex-a34".''',
type=dir_path,
help='Root of tree containing architecture directories containing json files'
)
- ap.add_argument(
- 'output_file', type=argparse.FileType('w', encoding='utf-8'), nargs='?', default=sys.stdout)
- ap.add_argument(
- 'output_string_file', type=argparse.FileType('w', encoding='utf-8'), nargs='?', default=None)
+ ap.add_argument('output_file', nargs='?', default=None,
+ help='file to write the tables to, default standard output')
+ ap.add_argument('output_string_file', nargs='?', default=None,
+ help='file to write big_c_string to when it is to be kept '
+ 'apart from the tables')
_args = ap.parse_args()
+ # Opened here, and not by argparse, so that a failure before the tables are
+ # written cannot empty a file the caller named.
+ if _args.output_file in (None, '-'):
+ _args.output_file = sys.stdout
+ else:
+ try:
+ _args.output_file = open(_args.output_file, 'w', encoding='utf-8')
+ except OSError as e:
+ ap.error(f'argument output_file: {e}')
+ if _args.output_string_file == '-':
+ _args.output_string_file = sys.stdout
+ elif _args.output_string_file is not None:
+ try:
+ _args.output_string_file = open(_args.output_string_file, 'w', encoding='utf-8')
+ except OSError as e:
+ ap.error(f'argument output_string_file: {e}')
_args.output_file.write(f"""/* SPDX-License-Identifier: GPL-2.0 */
/* THIS FILE WAS AUTOGENERATED BY `jevents.py arch={_args.arch} model={_args.model}` ! */
""")
--
2.53.0
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH 2/3] perf jevents: Make the model argument optional
2026-11-10 22:22 [PATCH 0/3] perf jevents: Report the JSON keys that jevents.py never reads TAKANO Mitsuhiro
2026-11-10 22:22 ` [PATCH 1/3] perf jevents: Name the metric when a MetricExpr is missing TAKANO Mitsuhiro
@ 2026-11-10 22:22 ` TAKANO Mitsuhiro
2026-11-10 22:22 ` [PATCH 3/3] perf jevents: Check the JSON keys that jevents.py never reads TAKANO Mitsuhiro
2026-11-11 2:34 ` [PATCH v2 0/3] perf jevents: Report " TAKANO Mitsuhiro
3 siblings, 0 replies; 12+ messages in thread
From: TAKANO Mitsuhiro @ 2026-11-10 22:22 UTC (permalink / raw)
To: linux-perf-users, Arnaldo Carvalho de Melo, Namhyung Kim
Cc: Ian Rogers, linux-kernel, TAKANO Mitsuhiro, James Clark,
John Garry, Peter Zijlstra, Ingo Molnar, Mark Rutland
The model argument carries a default of "all", but a positional argument
with a default and no nargs is still required: "jevents.py x86 <dir>" exits
with "the following arguments are required: model", and so does any caller
that only wants the tables for an architecture.
Make it nargs='?' so the default is reachable and an omitted model means
"all". Builds pass it explicitly, so the generated tables do not change.
Signed-off-by: TAKANO Mitsuhiro <takano32@gmail.com>
---
tools/perf/pmu-events/jevents.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/tools/perf/pmu-events/jevents.py b/tools/perf/pmu-events/jevents.py
index 86060a228b75..844fa82887bb 100755
--- a/tools/perf/pmu-events/jevents.py
+++ b/tools/perf/pmu-events/jevents.py
@@ -1486,10 +1486,10 @@ def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument('arch', help='Architecture name like x86')
- ap.add_argument('model', help='''Select a model such as skylake to
-reduce the code size. Normally set to "all". For architectures like
-ARM64 with an implementor/model, the model must include the implementor
-such as "arm/cortex-a34".''',
+ ap.add_argument('model', nargs='?', help='''Select a model such as skylake to
+reduce the code size. Normally set to "all", and that is what an omitted
+model means. For architectures like ARM64 with an implementor/model, the
+model must include the implementor such as "arm/cortex-a34".''',
default='all')
ap.add_argument(
'starting_dir',
--
2.53.0
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH 3/3] perf jevents: Check the JSON keys that jevents.py never reads
2026-11-10 22:22 [PATCH 0/3] perf jevents: Report the JSON keys that jevents.py never reads TAKANO Mitsuhiro
2026-11-10 22:22 ` [PATCH 1/3] perf jevents: Name the metric when a MetricExpr is missing TAKANO Mitsuhiro
2026-11-10 22:22 ` [PATCH 2/3] perf jevents: Make the model argument optional TAKANO Mitsuhiro
@ 2026-11-10 22:22 ` TAKANO Mitsuhiro
2026-11-11 2:34 ` [PATCH v2 0/3] perf jevents: Report " TAKANO Mitsuhiro
3 siblings, 0 replies; 12+ messages in thread
From: TAKANO Mitsuhiro @ 2026-11-10 22:22 UTC (permalink / raw)
To: linux-perf-users, Arnaldo Carvalho de Melo, Namhyung Kim
Cc: Ian Rogers, linux-kernel, TAKANO Mitsuhiro, James Clark,
John Garry, Peter Zijlstra, Ingo Molnar, Mark Rutland
jevents.py takes the fields it wants out of each JSON object by name:
jd.get('BriefDescription'), 'EventCode' in jd, and the names in
event_fields. A key spelled another way is dropped without a word, and
the object is built without whatever that field held.
Two of those are in the tree now. "BriefDescript6ion" is in one entry
each of amdzen4/cache.json, amdzen5/load-store.json and amdzen6/load-
store.json, and since those objects carry no other description,
ls_inef_sw_pref.all is generated with an empty text field.
"PublicDescrition" was in 16 of the 28 entries of
arm64/ampere/ampereonex/mmu.json; nothing a user saw changed there,
because each of the 16 also had a BriefDescription with the same text,
which is why the typo could sit there unnoticed.
Add a --check mode that reads every architecture, not just the one being
built: a typo is cheapest to fix by whoever reads that table next, and
that reader may be building something else. It records the keys
JsonEvent was never asked for and reports the ones whose spelling is a
near miss for a name jevents.py does read (difflib ratio 0.8). In this
tree 52 distinct key names occur, jevents.py reads 46 of them, the two
misspellings score 0.97, and the highest score among the remaining
unread names is 0.78 (Counter against CounterMask), so any cutoff in
that gap reports the same two.
It also reports a metric that carries a name but no expression, by
metric name and however the lost MetricExpr key was spelled: the key
report needs the spelling to be close to a name jevents.py reads, and a
metric without its expression cannot be generated at all.
The tracking is off unless --check asked for it, so a build parses each
object exactly as before and the generated tables are byte-identical
(x86, arm64 and all architectures compared against the same tree without
this patch). The check itself reads the 973 json files under arch/ and
takes seconds of CPU.
Exit status is 0 when nothing was reported, 1 when something was, and 2
when a table could not be read or the command line was wrong; make folds
both failures into its own status, so a script that needs the difference
runs jevents.py directly. Status 0 means the tables under that directory
were read and no unread key looks like a name jevents.py reads. It is
not a promise that a build will succeed: mapfile.csv coverage, the model
that selects a subset of the tables, and the C generation are not part
of it.
It is not a schema either: it checks nothing about types, required
fields or value ranges, and looks at key names only, so a misspelled
description in a value is out of scope. It does not read mapfile.csv,
which has no keys, or metricgroups.json, whose keys are group names.
Names that only metric.py or the C code read are outside it too, so an
"Expermental" key is not reported, and file names and topics are not
keys.
The check runs from the new make target "make -C tools/perf jevents-
check" (JEVENTS_CHECK_ARCH=x86 narrows it to one architecture) and from
perf test: tests/shell/jevents_check.sh runs it over a fixture table and
checks the status for a clean table (0), a misspelled key (1) and a
table that does not parse (2), and skips with status 2 when pmu-events/
is not next to the binary.
Written with Hermes Agent, an LLM assistant, which found the
misspellings and wrote this check, its test and this changelog. I built
perf with it, ran the perf test above, ran the check over the whole
tree, compared the generated tables with those from an unpatched tree on
x86, arm64 and all architectures at once, and ran mypy on jevents.py.
Signed-off-by: TAKANO Mitsuhiro <takano32@gmail.com>
---
tools/perf/Makefile | 14 +-
tools/perf/Makefile.perf | 1 +
tools/perf/pmu-events/jevents.py | 383 ++++++++++++++++++++----
tools/perf/tests/shell/jevents_check.sh | 68 +++++
4 files changed, 411 insertions(+), 55 deletions(-)
create mode 100755 tools/perf/tests/shell/jevents_check.sh
diff --git a/tools/perf/Makefile b/tools/perf/Makefile
index 00baf90e5bda..0b13f1e12899 100644
--- a/tools/perf/Makefile
+++ b/tools/perf/Makefile
@@ -140,6 +140,18 @@ check-headers:
sync-headers:
@./scripts/sync-headers.sh $(SYNC_HEADERS_ARGS)
+# The pmu-events tables are data, so a key nothing reads is invisible until a
+# description goes missing. Named in the wrapper like check-headers: this is
+# a data check, not a build, so it needs neither config nor fixdep and reads
+# the source tree whatever O= says. JEVENTS_CHECK_ARCH=x86 reads x86 and the
+# common and test tables every build reads.
+# Makefile.perf picks the interpreter for the build; the wrapper does not
+# include it, so default here and let PYTHON= win when it is given.
+JEVENTS_CHECK_ARCH ?= all
+PYTHON ?= python3
+jevents-check:
+ @$(PYTHON) ./pmu-events/jevents.py --check '$(JEVENTS_CHECK_ARCH)' all pmu-events/arch
+
#
# All other targets get passed through:
#
@@ -147,4 +159,4 @@ sync-headers:
$(print_msg)
$(make)
-.PHONY: all clean tags TAGS FORCE Makefile build-test build-test-tarball check-headers sync-headers
+.PHONY: all clean tags TAGS FORCE Makefile build-test build-test-tarball check-headers sync-headers jevents-check
diff --git a/tools/perf/Makefile.perf b/tools/perf/Makefile.perf
index b74b534e1153..a330cd9de0fc 100644
--- a/tools/perf/Makefile.perf
+++ b/tools/perf/Makefile.perf
@@ -871,6 +871,7 @@ help:
@echo 'Perf maintainer targets:'
@echo ' clean - clean all binary objects and build output'
@echo ' check-headers - check which header copies are out of sync with the kernel'
+ @echo ' jevents-check - report the pmu-events json keys that jevents.py never reads'
@echo ' sync-headers - sync those header copies with the kernel, creating commits'
@echo ' (SYNC_HEADERS_ARGS="..." passes options to the script)'
diff --git a/tools/perf/pmu-events/jevents.py b/tools/perf/pmu-events/jevents.py
index 844fa82887bb..3063282599a0 100755
--- a/tools/perf/pmu-events/jevents.py
+++ b/tools/perf/pmu-events/jevents.py
@@ -3,15 +3,16 @@
"""Convert directories of JSON events to C code."""
import argparse
import csv
+import difflib
import json
import metric
import os
import sys
-from typing import (Callable, Dict, Optional, Sequence, Set, Tuple)
+from typing import (Any, Callable, Dict, List, Optional, Sequence, Set, Tuple)
import collections
# Global command line arguments.
-_args = None
+_args: Optional[argparse.Namespace] = None
# List of regular event tables.
_event_tables: list[str] = []
# List of event tables generated from "/sys" directories.
@@ -190,11 +191,78 @@ class BigCString:
_bcs = BigCString()
+# Names that some object in this process has been asked for. Used only to
+# find the spelling an unread name was meant to be. The set is per process
+# and the workers of the pool never share it, so a name is in it once some
+# object in the same worker has been built; every object asks for the same
+# names, so they are there before the first object is reported, but a read
+# that moved behind a condition would make a report depend on which worker
+# happened to build which file.
+_read_keys: Set[str] = set()
+
+# Unread name -> the names it could be a misspelling of, or () for none. A table
+# repeats the same few names tens of thousands of times, and the name that
+# misses is the common case (Counter, many thousands of objects), so the
+# misses are kept too; _mark empties the cache when a name is read for the
+# first time, so a hint computed while _read_keys was still growing cannot
+# stick.
+_hints: Dict[str, Tuple[str, ...]] = {}
+
+# Whether the objects are wrapped at all. Only the check reads what the
+# tracking records, so a build does not wrap anything. Set in
+# main() and, because the workers may be fresh interpreters, passed to them
+# through _init_worker() rather than read from _args there.
+_track: bool = False
+
+
+class _JsonObject(dict):
+ """A JSON object that remembers which of its keys were looked at.
+
+ jevents.py picks the fields it wants out of each object by name and
+ ignores the rest, so a key it never names - a misspelled
+ "BriefDescript6ion", say - is dropped without a word and the event
+ silently loses its description. Handing the parsed object to this
+ class and asking it afterwards which keys were never looked at makes
+ the misspelling visible. Only get(), [] and 'in' are tracked: a read
+ through items(), values() or pop() would not be, so a new read of a
+ field belongs in one of the three. The alternative, a list of the key names
+ jevents.py reads, would be a second copy of the code below that goes
+ stale the first time someone adds a field.
+ """
+
+ def __init__(self, jd: dict):
+ super().__init__(jd)
+ # dict.keys() rather than the object itself: iterating the object would
+ # go through __iter__, should that ever be hooked below.
+ self.unread: Set[str] = set(dict.keys(jd))
+
+ def _mark(self, key: str) -> None:
+ self.unread.discard(key)
+ if key not in _read_keys:
+ _read_keys.add(key)
+ _hints.clear()
+
+ def get(self, key: Any, default: Any = None) -> Any:
+ self._mark(key)
+ return dict.get(self, key, default)
+
+ def __getitem__(self, key: Any) -> Any:
+ self._mark(key)
+ return dict.__getitem__(self, key)
+
+ def __contains__(self, key: object) -> bool:
+ if isinstance(key, str):
+ self._mark(key)
+ return dict.__contains__(self, key)
+
+
class JsonEvent:
"""Representation of an event loaded from a json file dictionary."""
def __init__(self, jd: dict):
"""Constructor passed the dictionary of parsed json values."""
+ if _track:
+ jd = _JsonObject(jd)
def llx(x: int) -> str:
"""Convert an int to a string similar to a printf modifier of %#llx."""
@@ -416,6 +484,42 @@ class JsonEvent:
raise argparse.ArgumentTypeError('Cannot find arch std event:', arch_std)
self.event = real_event(self.name, event)
+ # Keys of the JSON object that no code above looked at, set last so that
+ # the copy of an architecture standard event above does not overwrite it.
+ # A name that is a near miss for one the code reads, and whose name the
+ # object does not carry with the same text, is almost certainly a
+ # misspelling of it: the object lost the field the reader was looking for.
+ # A name that is not a near miss is left alone, because a table is free to
+ # carry metadata jevents.py has no use for.
+ self.mistyped: Optional[Dict[str, Tuple[str, ...]]] = None
+ if not _track:
+ return
+ assert isinstance(jd, _JsonObject)
+ unread = jd.unread
+ if unread:
+ found = {}
+ for key in unread:
+ if key not in _hints:
+ lowered = {name.lower(): name for name in _read_keys}
+ # Two names can be equally close - RetirementLatencyMax and its Min -
+ # and naming one of them would be a coin toss presented as an answer.
+ match = difflib.get_close_matches(key.lower(), sorted(lowered), n=2, cutoff=0.8)
+ _hints[key] = tuple(lowered[m] for m in match)
+ hints = _hints[key]
+ if not hints:
+ continue
+ # Nothing was lost if the object carries one of those names with the
+ # same text: that is a table with the field twice, not a misspelling.
+ # dict.__contains__ so that the look is not recorded as a read.
+ lost = True
+ for name in hints:
+ if dict.__contains__(jd, name) and dict.get(jd, key) == dict.get(jd, name):
+ lost = False
+ break
+ if lost:
+ found[key] = hints
+ if found:
+ self.mistyped = found
def __repr__(self) -> str:
"""String representation primarily for debugging."""
@@ -453,6 +557,83 @@ class JsonEvent:
return f'{make_comment(s)}\t{{ { _bcs.offsets[s] } }},\n'
+def _report_mistyped_keys() -> bool:
+ """Report the JSON keys that jevents.py did not look for.
+
+ jevents.py takes the fields it wants out of each object by name. A key
+ spelled differently from the name it looks for is never read, the object is
+ built without whatever the field held, and the build says nothing; that is
+ how "BriefDescript6ion" in the AMD tables and "PublicDescrition" in the
+ AmpereOneX mmu table got in. A name that is a near miss for one jevents.py
+ reads is reported, with the spelling it was probably meant to be and the
+ files carrying it. A name that is not a near miss is not reported: a
+ table may carry metadata for another reader, and reporting it would mean
+ keeping a list of such names in this file that every vendor adding one
+ would have to edit.
+
+ A table that cannot be parsed is reported the same way and does not stop
+ the rest of the tree from being read, so one broken file does not hide the
+ misspellings in the other 1000.
+ """
+ assert _args is not None
+ found: Dict[str, collections.Counter] = {}
+ hints: Dict[str, Tuple[str, ...]] = {}
+ objects = 0
+ for (path, _), events in _json_cache.items():
+ for event in events:
+ if not event.mistyped:
+ continue
+ objects += 1
+ for key, hint in event.mistyped.items():
+ found.setdefault(key, collections.Counter())[path] += 1
+ hints.setdefault(key, hint)
+ if not found:
+ return False
+
+ paths = {path for files in found.values() for path in files}
+ start = _args.starting_dir
+ print(f'jevents: error: {objects} object(s) in '
+ f'{len(paths)} file(s) carry a key that looks like a misspelling:', file=sys.stderr)
+ for key in sorted(found):
+ files = found[key]
+ guess = ' or '.join(repr(name) for name in hints[key])
+ print(f' {key!r}, did you mean {guess}? ({sum(files.values())} object(s) in '
+ f'{len(files)} file(s)):', file=sys.stderr)
+ where = sorted(os.path.relpath(path, start) for path in files)
+ for path in where[:5]:
+ print(f' {path}', file=sys.stderr)
+ if len(where) > 5:
+ print(f' ... and {len(where) - 5} more file(s)', file=sys.stderr)
+
+ return True
+
+
+def _report_metrics_without_expr() -> bool:
+ """Report the metrics that carry a name but no expression.
+
+ A MetricExpr key spelled another way is not read, so the object keeps its
+ name and loses its expression. Unlike the key report above this does not
+ depend on how close a spelling is: a metric the rewrite cannot handle would
+ fail a build however the lost key was spelled.
+ """
+ assert _args is not None
+ start = _args.starting_dir
+ missing = sorted((os.path.relpath(path, start), event.metric_name)
+ for (path, _), events in _json_cache.items()
+ for event in events
+ if event.metric_name and event.metric_expr is None)
+ if not missing:
+ return False
+ print(f'jevents: error: {len(missing)} metric(s) have a name but no '
+ f'expression, so their MetricExpr key is missing or misspelled:',
+ file=sys.stderr)
+ for where, name in missing[:20]:
+ print(f' {name!r} in {where}', file=sys.stderr)
+ if len(missing) > 20:
+ print(f' ... and {len(missing) - 20} more metric(s)', file=sys.stderr)
+ return True
+
+
_json_cache: Dict[Tuple[str, str], Sequence[JsonEvent]] = {}
def _read_json_events_impl(path: str, topic: str) -> Sequence[JsonEvent]:
"""Read json events from the specified file."""
@@ -460,17 +641,20 @@ def _read_json_events_impl(path: str, topic: str) -> Sequence[JsonEvent]:
with open(path, encoding='utf-8') as f:
events = json.load(f, object_hook=JsonEvent)
except BaseException:
- print(f'jevents: error: Exception processing {path}', file=sys.stderr)
+ if not _track:
+ print(f'jevents: error: Exception processing {path}', file=sys.stderr)
raise
metrics: list[Tuple[str, str, metric.Expression]] = []
for event in events:
event.topic = topic
if event.metric_name and event.metric_expr is None:
# A MetricName without a MetricExpr leaves nothing to put in the
- # generated tables, whether or not the name has a dash, so the build
- # must not go on. Name the metric instead of letting the table printer
- # assert on the None.
- raise RuntimeError(f'Metric {event.metric_name!r} has no MetricExpr')
+ # generated tables, whether or not the name has a dash, so a build must
+ # not go on. Name the metric instead of letting the table printer
+ # assert on the None; --check reports it and keeps reading the rest.
+ if not _track:
+ raise RuntimeError(f'Metric {event.metric_name!r} has no MetricExpr')
+ continue
if event.metric_name and '-' not in event.metric_name:
metrics.append((event.pmu, event.metric_name, event.metric_expr))
updates = metric.RewriteMetricsInTermsOfOthers(metrics)
@@ -487,8 +671,12 @@ def read_json_events(path: str, topic: str) -> Sequence[JsonEvent]:
_json_cache[key] = _read_json_events_impl(path, topic)
return _json_cache[key]
-def preprocess_arch_std_files(archpath: str) -> None:
- """Read in all architecture standard events."""
+def preprocess_arch_std_files(archpath: str, failures: Optional[List[Tuple[str, Exception]]] = None) -> None:
+ """Read in all architecture standard events.
+
+ A failure is raised, or appended to failures when the caller can carry on
+ without the table; either way the tables after it are still read.
+ """
for item in os.scandir(archpath):
if not item.is_file() or not item.name.endswith('.json'):
continue
@@ -499,7 +687,10 @@ def preprocess_arch_std_files(archpath: str) -> None:
if event.metric_name:
_arch_std_events[event.metric_name.lower()] = event
except Exception as e:
- raise RuntimeError(f'Failure processing \'{item.name}\' in \'{archpath}\': {e}') from e
+ failure = RuntimeError(f'Failure processing \'{item.name}\' in \'{archpath}\': {e}')
+ if failures is None:
+ raise failure from e
+ failures.append((item.path, failure))
def add_events_table_entries(item: os.DirEntry, topic: str) -> None:
@@ -1448,16 +1639,27 @@ def _parallel_worker_count(num_tasks: int) -> int:
return max(1, min(getattr(os, 'process_cpu_count', os.cpu_count)() or 1,
_max_parallel_workers, num_tasks))
-def _parallel_read_json_events(task: Tuple[str, str]) -> Tuple[str, str, Sequence[JsonEvent]]:
+def _parallel_read_json_events(task: Tuple[str, str]) -> Tuple[str, str, Any]:
path, topic = task
- return path, topic, _read_json_events_impl(path, topic)
-
-def _init_worker(std_events: dict) -> None:
- global _arch_std_events
+ try:
+ return path, topic, _read_json_events_impl(path, topic)
+ except Exception as e:
+ if not _track:
+ # The build stops here: keep the file and the reason in one message,
+ # the pool adds the traceback.
+ raise RuntimeError(f'Failure processing \'{path}\': {e}') from None
+ # Carried back rather than raised: the parent knows the tree and can
+ # report the file and keep going, which is the point of reading the
+ # tables this build does not use.
+ return path, topic, e
+
+def _init_worker(std_events: dict, track: bool) -> None:
+ global _arch_std_events, _track
_arch_std_events = std_events
+ _track = track
def main() -> None:
- global _args
+ global _args, _track
def dir_path(path: str) -> str:
"""Validate path is a directory for argparse."""
@@ -1484,12 +1686,12 @@ def main() -> None:
if item.is_dir():
ftw(item.path, list(parents) + [item.name], action)
- ap = argparse.ArgumentParser()
+ ap = argparse.ArgumentParser(description='Generate pmu-events.c from the json tables')
ap.add_argument('arch', help='Architecture name like x86')
ap.add_argument('model', nargs='?', help='''Select a model such as skylake to
-reduce the code size. Normally set to "all", and that is what an omitted
-model means. For architectures like ARM64 with an implementor/model, the
-model must include the implementor such as "arm/cortex-a34".''',
+reduce the code size. Normally set to "all". For architectures like
+ARM64 with an implementor/model, the model must include the implementor
+such as "arm/cortex-a34".''',
default='all')
ap.add_argument(
'starting_dir',
@@ -1497,14 +1699,104 @@ model must include the implementor such as "arm/cortex-a34".''',
help='Root of tree containing architecture directories containing json files'
)
ap.add_argument('output_file', nargs='?', default=None,
- help='file to write the tables to, default standard output')
+ help='file to write the tables to, default standard output; '
+ 'not written, and not opened, with --check')
ap.add_argument('output_string_file', nargs='?', default=None,
help='file to write big_c_string to when it is to be kept '
- 'apart from the tables')
+ 'apart from the tables; ignored with --check')
+ ap.add_argument('--check', action='store_true',
+ help='read the tables, report the keys that look like a '
+ 'misspelling of a name this file reads on standard '
+ 'error, and exit without writing any table; pass "all" '
+ 'as both arch and model to cover the whole tree. '
+ 'Nothing is reported when every key is read and every '
+ 'table parses. The exit status is 0 when the tree is '
+ 'clean, 1 when something was reported and 2 when a '
+ 'table could not be read, so it can gate a script')
_args = ap.parse_args()
- # Opened here, and not by argparse, so that a failure before the tables are
- # written cannot empty a file the caller named.
+ if _args.check and (_args.output_file is not None or _args.output_string_file is not None):
+ ap.error('--check writes no table; drop the output arguments')
+ if _args.check:
+ if _args.arch == 'none':
+ ap.error('--check needs an architecture to read')
+ # The model only picks the tables a build needs; a check is about the
+ # tree, so it reads all of it and the model means nothing here.
+ _args.model = 'all'
+ _track = True
+
+ archs = []
+ for item in os.scandir(_args.starting_dir):
+ if not item.is_dir():
+ continue
+ if item.name == _args.arch or _args.arch == 'all' or item.name == 'test' or item.name == 'common':
+ archs.append(item.name)
+
+ if _args.check and _args.arch not in ('all', 'none') and _args.arch not in [
+ item.name for item in os.scandir(_args.starting_dir) if item.is_dir()]:
+ print(f'jevents: error: no architecture directory named \'{_args.arch}\' '
+ f'under \'{_args.starting_dir}\'', file=sys.stderr)
+ sys.exit(2)
+ if not archs and _args.arch != 'none':
+ raise IOError(f'Missing architecture directory \'{_args.arch}\'')
+
+ archs.sort()
+ import concurrent.futures
+ tasks = []
+ def collect_json(parents: Sequence[str], item: os.DirEntry) -> None:
+ if len(parents) == 0:
+ return
+ if item.is_file() and item.name.endswith('.json') and not item.name.endswith('metricgroups.json'):
+ tasks.append((item.path, get_topic(item.name)))
+
+ failed: List[Tuple[str, Exception]] = []
+ try:
+ for arch in archs:
+ arch_path = f'{_args.starting_dir}/{arch}'
+ preprocess_arch_std_files(arch_path, failed if _args.check else None)
+ ftw(arch_path, [], collect_json)
+
+ if not tasks:
+ print(f'jevents: error: no event table under \'{_args.starting_dir}\' '
+ f'matched arch \'{_args.arch}\'', file=sys.stderr)
+ sys.exit(2 if _args.check else 1)
+
+ with concurrent.futures.ProcessPoolExecutor(max_workers=_parallel_worker_count(len(tasks)),
+ initializer=_init_worker,
+ initargs=(_arch_std_events, _args.check)) as executor:
+ for path, topic, events in executor.map(_parallel_read_json_events, tasks):
+ if isinstance(events, Exception):
+ if not _args.check:
+ raise RuntimeError(f'Failure processing \'{path}\'') from events
+ failed.append((path, events))
+ else:
+ _json_cache[(path, topic)] = events
+ except (RuntimeError, ValueError, OSError, argparse.ArgumentTypeError) as e:
+ if not _args.check:
+ raise
+ failed.append((_args.starting_dir, e))
+
+ if _args.check:
+ # A table this check does not need is reported, not fatal: the check reads
+ # the whole tree and a caller only asked about one part of it.
+ for path, error in failed:
+ text = str(error)
+ # The architecture standard preprocess names the file and its directory
+ # already, so prefixing the path here only repeats it.
+ if text.startswith('Failure processing'):
+ print(f'jevents: error: {text}', file=sys.stderr)
+ else:
+ print(f'jevents: error: '
+ f'{os.path.relpath(path, _args.starting_dir)}: {text}',
+ file=sys.stderr)
+ reported = _report_mistyped_keys()
+ reported = _report_metrics_without_expr() or reported
+ if failed:
+ sys.exit(2)
+ sys.exit(1 if reported else 0)
+
+ # Opened here, and not by argparse, so that --check leaves the files it was
+ # given alone and a failure before the tables are written cannot empty one.
if _args.output_file in (None, '-'):
_args.output_file = sys.stdout
else:
@@ -1540,35 +1832,6 @@ struct pmu_table_entry {
};
""")
- archs = []
- for item in os.scandir(_args.starting_dir):
- if not item.is_dir():
- continue
- if item.name == _args.arch or _args.arch == 'all' or item.name == 'test' or item.name == 'common':
- archs.append(item.name)
-
- if len(archs) < 2 and _args.arch != 'none':
- raise IOError(f'Missing architecture directory \'{_args.arch}\'')
-
- archs.sort()
- import concurrent.futures
- tasks = []
- def collect_json(parents: Sequence[str], item: os.DirEntry) -> None:
- if len(parents) == 0:
- return
- if item.is_file() and item.name.endswith('.json') and not item.name.endswith('metricgroups.json'):
- tasks.append((item.path, get_topic(item.name)))
-
- for arch in archs:
- arch_path = f'{_args.starting_dir}/{arch}'
- preprocess_arch_std_files(arch_path)
- ftw(arch_path, [], collect_json)
-
- with concurrent.futures.ProcessPoolExecutor(max_workers=_parallel_worker_count(len(tasks)),
- initializer=_init_worker,
- initargs=(_arch_std_events,)) as executor:
- for path, topic, events in executor.map(_parallel_read_json_events, tasks):
- _json_cache[(path, topic)] = events
for arch in archs:
arch_path = f'{_args.starting_dir}/{arch}'
@@ -1607,4 +1870,16 @@ struct pmu_table_entry {
_args.output_string_file.close()
if __name__ == '__main__':
- main()
+ try:
+ main()
+ except SystemExit:
+ raise
+ except KeyboardInterrupt:
+ raise
+ except BaseException as e:
+ # A build should see the traceback; a check gives its caller a status,
+ # and 2 is the one that says the tool could not do its job.
+ if _args is not None and getattr(_args, 'check', False):
+ print(f'jevents: error: {e}', file=sys.stderr)
+ sys.exit(2)
+ raise
diff --git a/tools/perf/tests/shell/jevents_check.sh b/tools/perf/tests/shell/jevents_check.sh
new file mode 100755
index 000000000000..2254bc96352c
--- /dev/null
+++ b/tools/perf/tests/shell/jevents_check.sh
@@ -0,0 +1,68 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Check that jevents.py reports unread json keys and refuses a broken table
+
+shelldir=$(dirname "$0")
+. "${shelldir}"/lib/setup_python.sh
+
+# The check reads the tables and needs no build and no hardware, but an
+# installed perf has no pmu-events/ next to the binary.
+pmudir=$(readlink -f "${shelldir}/../../pmu-events")
+if [ ! -f "${pmudir}/jevents.py" ] || [ ! -f "${pmudir}/metric.py" ]; then
+ echo "Skipping: no pmu-events/jevents.py under ${pmudir}"
+ exit 2
+fi
+
+workdir=$(mktemp -d /tmp/jevents_check.XXXXXX)
+trap 'rm -rf "${workdir}"' EXIT
+
+# A table with one key jevents.py reads and one event, so that a clean run
+# says nothing and exits 0.
+mkdir -p "${workdir}/arch/x86/model"
+cat > "${workdir}/arch/x86/model/events.json" <<'JSON'
+[
+ {
+ "EventName": "CHECK_TEST_EVENT",
+ "EventCode": "0x1",
+ "BriefDescription": "a table the check should accept"
+ }
+]
+JSON
+
+run_check() {
+ "${PYTHON}" "${pmudir}/jevents.py" --check x86 all "${workdir}/arch"
+}
+
+run_check
+if [ $? -ne 0 ]; then
+ echo "pmu-events json keys [Failed: a clean table was reported]"
+ exit 1
+fi
+
+# A near miss for a name jevents.py reads is reported, and the status is 1.
+sed -i 's/"BriefDescription"/"BriefDescript6ion"/' \
+ "${workdir}/arch/x86/model/events.json"
+output=$(run_check 2>&1)
+ret=$?
+case "${ret}" in
+1) ;;
+*) echo "pmu-events json keys [Failed: a misspelled key exited ${ret}]"
+ exit 1 ;;
+esac
+if ! echo "${output}" | grep -q "BriefDescript6ion"; then
+ echo "pmu-events json keys [Failed: the misspelled key was not reported]"
+ exit 1
+fi
+
+# A table that does not parse is a failure of the check and not a finding:
+# the status is 2, which this script must not pass on as a skip.
+echo '{ "truncated": ' > "${workdir}/arch/x86/model/broken.json"
+run_check >/dev/null 2>&1
+ret=$?
+if [ "${ret}" -ne 2 ]; then
+ echo "pmu-events json keys [Failed: an unreadable table exited ${ret}]"
+ exit 1
+fi
+
+echo "pmu-events json keys [Success]"
+exit 0
--
2.53.0
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH v2 0/3] perf jevents: Report the JSON keys that jevents.py never reads
2026-11-10 22:22 [PATCH 0/3] perf jevents: Report the JSON keys that jevents.py never reads TAKANO Mitsuhiro
` (2 preceding siblings ...)
2026-11-10 22:22 ` [PATCH 3/3] perf jevents: Check the JSON keys that jevents.py never reads TAKANO Mitsuhiro
@ 2026-11-11 2:34 ` TAKANO Mitsuhiro
2026-11-11 2:34 ` [PATCH v2 1/3] perf jevents: Name the metric when a MetricExpr is missing TAKANO Mitsuhiro
` (3 more replies)
3 siblings, 4 replies; 12+ messages in thread
From: TAKANO Mitsuhiro @ 2026-11-11 2:34 UTC (permalink / raw)
To: linux-perf-users, Arnaldo Carvalho de Melo, Namhyung Kim
Cc: Ian Rogers, linux-kernel, TAKANO Mitsuhiro, James Clark,
John Garry, Peter Zijlstra, Ingo Molnar, Mark Rutland
jevents.py takes the fields it wants out of each JSON object by name and
ignores the rest, so a key spelled differently is dropped without a word
and the object is built without whatever that field held.
Patches 1 and 2 make the generator name what went wrong, and let the
model argument be omitted. Patch 3 adds a --check mode that reports the
keys jevents.py never asks for, along with a make target and a perf test
so that it runs without anyone having to remember it.
This is the second posting of the check. It went out alone in November
(https://lore.kernel.org/linux-perf-
users/20261110193239.1741014-1-takano32@gmail.com/) and the review found
two ways it could call a tree clean that no build can use: a MetricName
with a dash in it that loses its MetricExpr was invisible to both the
reader and the report, and a metric without an expression was skipped
rather than reported unless its key was spelled close enough to be a near
miss. Both are fixed here, and the failure-path changes the check needs
are no longer mixed into it.
The test runs the check over a fixture table rather than over the tree,
so it is green whatever the tables happen to look like, and it checks the
three exit statuses: 0 for a clean table, 1 for a reported key, 2 for a
table that does not parse.
The two misspellings the check reports in the tree today were posted
separately in November:
https://lore.kernel.org/linux-perf-users/20261108184600.3810059-1-takano32@gmail.com/
https://lore.kernel.org/linux-perf-users/20261108183846.3804875-1-takano32@gmail.com/
TAKANO Mitsuhiro (3):
perf jevents: Name the metric when a MetricExpr is missing
perf jevents: Make the model argument optional
perf jevents: Check the JSON keys that jevents.py never reads
tools/perf/Makefile | 14 +-
tools/perf/Makefile.perf | 1 +
tools/perf/pmu-events/jevents.py | 391 +++++++++++++++++++++---
tools/perf/tests/shell/jevents_check.sh | 68 +++++
4 files changed, 425 insertions(+), 49 deletions(-)
create mode 100755 tools/perf/tests/shell/jevents_check.sh
base-commit: 7475ea8b58616b33c04afb976cf0fbaad9dd63fb
--
2.53.0
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH v2 1/3] perf jevents: Name the metric when a MetricExpr is missing
2026-11-11 2:34 ` [PATCH v2 0/3] perf jevents: Report " TAKANO Mitsuhiro
@ 2026-11-11 2:34 ` TAKANO Mitsuhiro
2026-11-11 2:34 ` [PATCH v2 2/3] perf jevents: Make the model argument optional TAKANO Mitsuhiro
` (2 subsequent siblings)
3 siblings, 0 replies; 12+ messages in thread
From: TAKANO Mitsuhiro @ 2026-11-11 2:34 UTC (permalink / raw)
To: linux-perf-users, Arnaldo Carvalho de Melo, Namhyung Kim
Cc: Ian Rogers, linux-kernel, TAKANO Mitsuhiro, James Clark,
John Garry, Peter Zijlstra, Ingo Molnar, Mark Rutland
jevents.py reads each field it wants out of a JSON object by name, so a
MetricExpr key that is missing or spelled another way leaves the object
with a MetricName and no expression. Two different failures followed,
neither of which named the metric: the rewrite in _read_json_events_impl()
raised AttributeError on the None, and a metric that got that far reached
print_pending_metrics(), whose assert fired on it.
Raise in the reader instead, naming the metric, before either of them can
happen. The check is not inside the '-' test that decides whether a metric
takes part in the rewrite: a name with a dash in it can lose its
expression the same way.
Two smaller changes on the same path:
- An architecture standard table that does not parse now carries the
reason in its message ("Failure processing '<file>' in '<dir>':
<cause>"). The traceback used to end with the file name alone, and
only the chained exception said why.
- A table that does not parse is reported on stderr with a "jevents:
error:" prefix instead of on stdout, where it was mixed with the
generated output.
And the output file is opened after the tables have been read, and not by
argparse, so a build that fails while reading them leaves a file the
caller named as it was instead of truncating it to the header.
Signed-off-by: TAKANO Mitsuhiro <takano32@gmail.com>
---
tools/perf/pmu-events/jevents.py | 35 ++++++++++++++++++++++++++------
1 file changed, 29 insertions(+), 6 deletions(-)
diff --git a/tools/perf/pmu-events/jevents.py b/tools/perf/pmu-events/jevents.py
index 080e04b51b8c..86060a228b75 100755
--- a/tools/perf/pmu-events/jevents.py
+++ b/tools/perf/pmu-events/jevents.py
@@ -460,11 +460,17 @@ def _read_json_events_impl(path: str, topic: str) -> Sequence[JsonEvent]:
with open(path, encoding='utf-8') as f:
events = json.load(f, object_hook=JsonEvent)
except BaseException:
- print(f"Exception processing {path}")
+ print(f'jevents: error: Exception processing {path}', file=sys.stderr)
raise
metrics: list[Tuple[str, str, metric.Expression]] = []
for event in events:
event.topic = topic
+ if event.metric_name and event.metric_expr is None:
+ # A MetricName without a MetricExpr leaves nothing to put in the
+ # generated tables, whether or not the name has a dash, so the build
+ # must not go on. Name the metric instead of letting the table printer
+ # assert on the None.
+ raise RuntimeError(f'Metric {event.metric_name!r} has no MetricExpr')
if event.metric_name and '-' not in event.metric_name:
metrics.append((event.pmu, event.metric_name, event.metric_expr))
updates = metric.RewriteMetricsInTermsOfOthers(metrics)
@@ -493,7 +499,7 @@ def preprocess_arch_std_files(archpath: str) -> None:
if event.metric_name:
_arch_std_events[event.metric_name.lower()] = event
except Exception as e:
- raise RuntimeError(f'Failure processing \'{item.name}\' in \'{archpath}\'') from e
+ raise RuntimeError(f'Failure processing \'{item.name}\' in \'{archpath}\': {e}') from e
def add_events_table_entries(item: os.DirEntry, topic: str) -> None:
@@ -1490,12 +1496,29 @@ such as "arm/cortex-a34".''',
type=dir_path,
help='Root of tree containing architecture directories containing json files'
)
- ap.add_argument(
- 'output_file', type=argparse.FileType('w', encoding='utf-8'), nargs='?', default=sys.stdout)
- ap.add_argument(
- 'output_string_file', type=argparse.FileType('w', encoding='utf-8'), nargs='?', default=None)
+ ap.add_argument('output_file', nargs='?', default=None,
+ help='file to write the tables to, default standard output')
+ ap.add_argument('output_string_file', nargs='?', default=None,
+ help='file to write big_c_string to when it is to be kept '
+ 'apart from the tables')
_args = ap.parse_args()
+ # Opened here, and not by argparse, so that a failure before the tables are
+ # written cannot empty a file the caller named.
+ if _args.output_file in (None, '-'):
+ _args.output_file = sys.stdout
+ else:
+ try:
+ _args.output_file = open(_args.output_file, 'w', encoding='utf-8')
+ except OSError as e:
+ ap.error(f'argument output_file: {e}')
+ if _args.output_string_file == '-':
+ _args.output_string_file = sys.stdout
+ elif _args.output_string_file is not None:
+ try:
+ _args.output_string_file = open(_args.output_string_file, 'w', encoding='utf-8')
+ except OSError as e:
+ ap.error(f'argument output_string_file: {e}')
_args.output_file.write(f"""/* SPDX-License-Identifier: GPL-2.0 */
/* THIS FILE WAS AUTOGENERATED BY `jevents.py arch={_args.arch} model={_args.model}` ! */
""")
--
2.53.0
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH v2 2/3] perf jevents: Make the model argument optional
2026-11-11 2:34 ` [PATCH v2 0/3] perf jevents: Report " TAKANO Mitsuhiro
2026-11-11 2:34 ` [PATCH v2 1/3] perf jevents: Name the metric when a MetricExpr is missing TAKANO Mitsuhiro
@ 2026-11-11 2:34 ` TAKANO Mitsuhiro
2026-11-11 2:34 ` [PATCH v2 3/3] perf jevents: Check the JSON keys that jevents.py never reads TAKANO Mitsuhiro
2026-11-11 3:43 ` [PATCH v3 0/3] perf jevents: Report " TAKANO Mitsuhiro
3 siblings, 0 replies; 12+ messages in thread
From: TAKANO Mitsuhiro @ 2026-11-11 2:34 UTC (permalink / raw)
To: linux-perf-users, Arnaldo Carvalho de Melo, Namhyung Kim
Cc: Ian Rogers, linux-kernel, TAKANO Mitsuhiro, James Clark,
John Garry, Peter Zijlstra, Ingo Molnar, Mark Rutland
The model argument carries a default of "all", but a positional argument
with a default and no nargs is still required: "jevents.py x86 <dir>" exits
with "the following arguments are required: model", and so does any caller
that only wants the tables for an architecture.
Make it nargs='?' so the default is reachable and an omitted model means
"all". Builds pass it explicitly, so the generated tables do not change.
Fixes: 5a09b1fd1b1f ("perf jevents: Add model list option")
Signed-off-by: TAKANO Mitsuhiro <takano32@gmail.com>
---
tools/perf/pmu-events/jevents.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/tools/perf/pmu-events/jevents.py b/tools/perf/pmu-events/jevents.py
index 86060a228b75..844fa82887bb 100755
--- a/tools/perf/pmu-events/jevents.py
+++ b/tools/perf/pmu-events/jevents.py
@@ -1486,10 +1486,10 @@ def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument('arch', help='Architecture name like x86')
- ap.add_argument('model', help='''Select a model such as skylake to
-reduce the code size. Normally set to "all". For architectures like
-ARM64 with an implementor/model, the model must include the implementor
-such as "arm/cortex-a34".''',
+ ap.add_argument('model', nargs='?', help='''Select a model such as skylake to
+reduce the code size. Normally set to "all", and that is what an omitted
+model means. For architectures like ARM64 with an implementor/model, the
+model must include the implementor such as "arm/cortex-a34".''',
default='all')
ap.add_argument(
'starting_dir',
--
2.53.0
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH v2 3/3] perf jevents: Check the JSON keys that jevents.py never reads
2026-11-11 2:34 ` [PATCH v2 0/3] perf jevents: Report " TAKANO Mitsuhiro
2026-11-11 2:34 ` [PATCH v2 1/3] perf jevents: Name the metric when a MetricExpr is missing TAKANO Mitsuhiro
2026-11-11 2:34 ` [PATCH v2 2/3] perf jevents: Make the model argument optional TAKANO Mitsuhiro
@ 2026-11-11 2:34 ` TAKANO Mitsuhiro
2026-11-11 3:43 ` [PATCH v3 0/3] perf jevents: Report " TAKANO Mitsuhiro
3 siblings, 0 replies; 12+ messages in thread
From: TAKANO Mitsuhiro @ 2026-11-11 2:34 UTC (permalink / raw)
To: linux-perf-users, Arnaldo Carvalho de Melo, Namhyung Kim
Cc: Ian Rogers, linux-kernel, TAKANO Mitsuhiro, James Clark,
John Garry, Peter Zijlstra, Ingo Molnar, Mark Rutland
jevents.py takes the fields it wants out of each JSON object by name:
jd.get('BriefDescription'), 'EventCode' in jd, and the names in
event_fields. A key spelled another way is dropped without a word, and
the object is built without whatever that field held.
Two of those are in the tree now. "BriefDescript6ion" is in one entry
each of amdzen4/cache.json, amdzen5/load-store.json and amdzen6/load-
store.json, and since those objects carry no other description,
ls_inef_sw_pref.all is generated with an empty text field.
"PublicDescrition" was in 16 of the 28 entries of
arm64/ampere/ampereonex/mmu.json; nothing a user saw changed there,
because each of the 16 also had a BriefDescription with the same text,
which is why the typo could sit there unnoticed.
Add a --check mode that reads every architecture, not just the one being
built: a typo is cheapest to fix by whoever reads that table next, and
that reader may be building something else. It records the keys
JsonEvent was never asked for and reports the ones whose spelling is a
near miss for a name jevents.py does read (difflib ratio 0.8). In this
tree 52 distinct key names occur, jevents.py reads 46 of them, the two
misspellings score 0.97, and the highest score among the remaining
unread names is 0.78 (Counter against CounterMask), so any cutoff in
that gap reports the same two.
It also reports a metric that carries a name but no expression, by
metric name and however the lost MetricExpr key was spelled: the key
report needs the spelling to be close to a name jevents.py reads, and a
metric without its expression cannot be generated at all.
A key that is a near miss for two names at once is reported as well: the
object may carry one of them with the same value, but that does not say
which one the key was meant to be, and the key is one nothing reads
either way.
The tracking is off unless --check asked for it, so a build parses each
object exactly as before and the generated tables are byte-identical
(x86, arm64 and all architectures compared against the same tree without
this patch). The check itself reads the 973 json files under arch/ and
takes seconds of CPU.
Exit status is 0 when nothing was reported, 1 when something was, and 2
when a table could not be read or the command line was wrong; make folds
both failures into its own status, so a script that needs the difference
runs jevents.py directly. Status 0 means the tables under that directory
were read and no unread key looks like a name jevents.py reads. It is
not a promise that a build will succeed: mapfile.csv coverage, the model
that selects a subset of the tables, and the C generation are not part
of it.
It is not a schema either: it checks nothing about types, required
fields or value ranges, and looks at key names only, so a misspelled
description in a value is out of scope. It does not read mapfile.csv,
which has no keys, or metricgroups.json, whose keys are group names.
Names that only metric.py or the C code read are outside it too, so an
"Expermental" key is not reported, and file names and topics are not
keys.
The check runs from the new make target "make -C tools/perf jevents-
check" (JEVENTS_CHECK_ARCH=x86 narrows it to one architecture) and from
perf test, where tests/shell/jevents_check.sh skips with status 2 when
pmu-events/arch is not next to the binary.
Written with Hermes Agent, an LLM assistant, which found the
misspellings and wrote this check, its test and this changelog. I built
perf with it, ran the perf test above, ran the check over the whole
tree, compared the generated tables with those from an unpatched tree on
x86, arm64 and all architectures at once, and ran mypy on jevents.py.
Signed-off-by: TAKANO Mitsuhiro <takano32@gmail.com>
---
tools/perf/Makefile | 14 +-
tools/perf/Makefile.perf | 1 +
tools/perf/pmu-events/jevents.py | 380 ++++++++++++++++++++----
tools/perf/tests/shell/jevents_check.sh | 68 +++++
4 files changed, 408 insertions(+), 55 deletions(-)
create mode 100755 tools/perf/tests/shell/jevents_check.sh
diff --git a/tools/perf/Makefile b/tools/perf/Makefile
index 00baf90e5bda..0b13f1e12899 100644
--- a/tools/perf/Makefile
+++ b/tools/perf/Makefile
@@ -140,6 +140,18 @@ check-headers:
sync-headers:
@./scripts/sync-headers.sh $(SYNC_HEADERS_ARGS)
+# The pmu-events tables are data, so a key nothing reads is invisible until a
+# description goes missing. Named in the wrapper like check-headers: this is
+# a data check, not a build, so it needs neither config nor fixdep and reads
+# the source tree whatever O= says. JEVENTS_CHECK_ARCH=x86 reads x86 and the
+# common and test tables every build reads.
+# Makefile.perf picks the interpreter for the build; the wrapper does not
+# include it, so default here and let PYTHON= win when it is given.
+JEVENTS_CHECK_ARCH ?= all
+PYTHON ?= python3
+jevents-check:
+ @$(PYTHON) ./pmu-events/jevents.py --check '$(JEVENTS_CHECK_ARCH)' all pmu-events/arch
+
#
# All other targets get passed through:
#
@@ -147,4 +159,4 @@ sync-headers:
$(print_msg)
$(make)
-.PHONY: all clean tags TAGS FORCE Makefile build-test build-test-tarball check-headers sync-headers
+.PHONY: all clean tags TAGS FORCE Makefile build-test build-test-tarball check-headers sync-headers jevents-check
diff --git a/tools/perf/Makefile.perf b/tools/perf/Makefile.perf
index b74b534e1153..a330cd9de0fc 100644
--- a/tools/perf/Makefile.perf
+++ b/tools/perf/Makefile.perf
@@ -871,6 +871,7 @@ help:
@echo 'Perf maintainer targets:'
@echo ' clean - clean all binary objects and build output'
@echo ' check-headers - check which header copies are out of sync with the kernel'
+ @echo ' jevents-check - report the pmu-events json keys that jevents.py never reads'
@echo ' sync-headers - sync those header copies with the kernel, creating commits'
@echo ' (SYNC_HEADERS_ARGS="..." passes options to the script)'
diff --git a/tools/perf/pmu-events/jevents.py b/tools/perf/pmu-events/jevents.py
index 844fa82887bb..d31f8e2fd6d3 100755
--- a/tools/perf/pmu-events/jevents.py
+++ b/tools/perf/pmu-events/jevents.py
@@ -3,15 +3,16 @@
"""Convert directories of JSON events to C code."""
import argparse
import csv
+import difflib
import json
import metric
import os
import sys
-from typing import (Callable, Dict, Optional, Sequence, Set, Tuple)
+from typing import (Any, Callable, Dict, List, Optional, Sequence, Set, Tuple)
import collections
# Global command line arguments.
-_args = None
+_args: Optional[argparse.Namespace] = None
# List of regular event tables.
_event_tables: list[str] = []
# List of event tables generated from "/sys" directories.
@@ -190,11 +191,78 @@ class BigCString:
_bcs = BigCString()
+# Names that some object in this process has been asked for. Used only to
+# find the spelling an unread name was meant to be. The set is per process
+# and the workers of the pool never share it, so a name is in it once some
+# object in the same worker has been built; every object asks for the same
+# names, so they are there before the first object is reported, but a read
+# that moved behind a condition would make a report depend on which worker
+# happened to build which file.
+_read_keys: Set[str] = set()
+
+# Unread name -> the names it could be a misspelling of, or () for none. A table
+# repeats the same few names tens of thousands of times, and the name that
+# misses is the common case (Counter, many thousands of objects), so the
+# misses are kept too; _mark empties the cache when a name is read for the
+# first time, so a hint computed while _read_keys was still growing cannot
+# stick.
+_hints: Dict[str, Tuple[str, ...]] = {}
+
+# Whether the objects are wrapped at all. Only the check reads what the
+# tracking records, so a build does not wrap anything. Set in
+# main() and, because the workers may be fresh interpreters, passed to them
+# through _init_worker() rather than read from _args there.
+_track: bool = False
+
+
+class _JsonObject(dict):
+ """A JSON object that remembers which of its keys were looked at.
+
+ jevents.py picks the fields it wants out of each object by name and
+ ignores the rest, so a key it never names - a misspelled
+ "BriefDescript6ion", say - is dropped without a word and the event
+ silently loses its description. Handing the parsed object to this
+ class and asking it afterwards which keys were never looked at makes
+ the misspelling visible. Only get(), [] and 'in' are tracked: a read
+ through items(), values() or pop() would not be, so a new read of a
+ field belongs in one of the three. The alternative, a list of the key names
+ jevents.py reads, would be a second copy of the code below that goes
+ stale the first time someone adds a field.
+ """
+
+ def __init__(self, jd: dict):
+ super().__init__(jd)
+ # dict.keys() rather than the object itself: iterating the object would
+ # go through __iter__, should that ever be hooked below.
+ self.unread: Set[str] = set(dict.keys(jd))
+
+ def _mark(self, key: str) -> None:
+ self.unread.discard(key)
+ if key not in _read_keys:
+ _read_keys.add(key)
+ _hints.clear()
+
+ def get(self, key: Any, default: Any = None) -> Any:
+ self._mark(key)
+ return dict.get(self, key, default)
+
+ def __getitem__(self, key: Any) -> Any:
+ self._mark(key)
+ return dict.__getitem__(self, key)
+
+ def __contains__(self, key: object) -> bool:
+ if isinstance(key, str):
+ self._mark(key)
+ return dict.__contains__(self, key)
+
+
class JsonEvent:
"""Representation of an event loaded from a json file dictionary."""
def __init__(self, jd: dict):
"""Constructor passed the dictionary of parsed json values."""
+ if _track:
+ jd = _JsonObject(jd)
def llx(x: int) -> str:
"""Convert an int to a string similar to a printf modifier of %#llx."""
@@ -416,6 +484,39 @@ class JsonEvent:
raise argparse.ArgumentTypeError('Cannot find arch std event:', arch_std)
self.event = real_event(self.name, event)
+ # Keys of the JSON object that no code above looked at, set last so that
+ # the copy of an architecture standard event above does not overwrite it.
+ # A name that is a near miss for one the code reads, and whose name the
+ # object does not carry with the same text, is almost certainly a
+ # misspelling of it: the object lost the field the reader was looking for.
+ # A name that is not a near miss is left alone, because a table is free to
+ # carry metadata jevents.py has no use for.
+ self.mistyped: Optional[Dict[str, Tuple[str, ...]]] = None
+ if not _track:
+ return
+ assert isinstance(jd, _JsonObject)
+ unread = jd.unread
+ if unread:
+ found = {}
+ for key in unread:
+ if key not in _hints:
+ lowered = {name.lower(): name for name in _read_keys}
+ # Two names can be equally close - RetirementLatencyMax and its Min -
+ # and naming one of them would be a coin toss presented as an answer.
+ match = difflib.get_close_matches(key.lower(), sorted(lowered), n=2, cutoff=0.8)
+ _hints[key] = tuple(lowered[m] for m in match)
+ hints = _hints[key]
+ if not hints:
+ continue
+ # Reported even when the object also carries a name it could be a
+ # misspelling of: two names can be equally close (RetirementLatencyMax
+ # next to its Min, which can hold the same value), so a matching value
+ # does not say which one this key was meant to be, and suppressing on
+ # it hid a typo of the other. Either way this is a key that nothing
+ # reads, which is what the check reports.
+ found[key] = hints
+ if found:
+ self.mistyped = found
def __repr__(self) -> str:
"""String representation primarily for debugging."""
@@ -453,6 +554,83 @@ class JsonEvent:
return f'{make_comment(s)}\t{{ { _bcs.offsets[s] } }},\n'
+def _report_mistyped_keys() -> bool:
+ """Report the JSON keys that jevents.py did not look for.
+
+ jevents.py takes the fields it wants out of each object by name. A key
+ spelled differently from the name it looks for is never read, the object is
+ built without whatever the field held, and the build says nothing; that is
+ how "BriefDescript6ion" in the AMD tables and "PublicDescrition" in the
+ AmpereOneX mmu table got in. A name that is a near miss for one jevents.py
+ reads is reported, with the spelling it was probably meant to be and the
+ files carrying it. A name that is not a near miss is not reported: a
+ table may carry metadata for another reader, and reporting it would mean
+ keeping a list of such names in this file that every vendor adding one
+ would have to edit.
+
+ A table that cannot be parsed is reported the same way and does not stop
+ the rest of the tree from being read, so one broken file does not hide the
+ misspellings in the other 1000.
+ """
+ assert _args is not None
+ found: Dict[str, collections.Counter] = {}
+ hints: Dict[str, Tuple[str, ...]] = {}
+ objects = 0
+ for (path, _), events in _json_cache.items():
+ for event in events:
+ if not event.mistyped:
+ continue
+ objects += 1
+ for key, hint in event.mistyped.items():
+ found.setdefault(key, collections.Counter())[path] += 1
+ hints.setdefault(key, hint)
+ if not found:
+ return False
+
+ paths = {path for files in found.values() for path in files}
+ start = _args.starting_dir
+ print(f'jevents: error: {objects} object(s) in '
+ f'{len(paths)} file(s) carry a key that looks like a misspelling:', file=sys.stderr)
+ for key in sorted(found):
+ files = found[key]
+ guess = ' or '.join(repr(name) for name in hints[key])
+ print(f' {key!r}, did you mean {guess}? ({sum(files.values())} object(s) in '
+ f'{len(files)} file(s)):', file=sys.stderr)
+ where = sorted(os.path.relpath(path, start) for path in files)
+ for path in where[:5]:
+ print(f' {path}', file=sys.stderr)
+ if len(where) > 5:
+ print(f' ... and {len(where) - 5} more file(s)', file=sys.stderr)
+
+ return True
+
+
+def _report_metrics_without_expr() -> bool:
+ """Report the metrics that carry a name but no expression.
+
+ A MetricExpr key spelled another way is not read, so the object keeps its
+ name and loses its expression. Unlike the key report above this does not
+ depend on how close a spelling is: a metric the rewrite cannot handle would
+ fail a build however the lost key was spelled.
+ """
+ assert _args is not None
+ start = _args.starting_dir
+ missing = sorted((os.path.relpath(path, start), event.metric_name)
+ for (path, _), events in _json_cache.items()
+ for event in events
+ if event.metric_name and event.metric_expr is None)
+ if not missing:
+ return False
+ print(f'jevents: error: {len(missing)} metric(s) have a name but no '
+ f'expression, so their MetricExpr key is missing or misspelled:',
+ file=sys.stderr)
+ for where, name in missing[:20]:
+ print(f' {name!r} in {where}', file=sys.stderr)
+ if len(missing) > 20:
+ print(f' ... and {len(missing) - 20} more metric(s)', file=sys.stderr)
+ return True
+
+
_json_cache: Dict[Tuple[str, str], Sequence[JsonEvent]] = {}
def _read_json_events_impl(path: str, topic: str) -> Sequence[JsonEvent]:
"""Read json events from the specified file."""
@@ -460,17 +638,20 @@ def _read_json_events_impl(path: str, topic: str) -> Sequence[JsonEvent]:
with open(path, encoding='utf-8') as f:
events = json.load(f, object_hook=JsonEvent)
except BaseException:
- print(f'jevents: error: Exception processing {path}', file=sys.stderr)
+ if not _track:
+ print(f'jevents: error: Exception processing {path}', file=sys.stderr)
raise
metrics: list[Tuple[str, str, metric.Expression]] = []
for event in events:
event.topic = topic
if event.metric_name and event.metric_expr is None:
# A MetricName without a MetricExpr leaves nothing to put in the
- # generated tables, whether or not the name has a dash, so the build
- # must not go on. Name the metric instead of letting the table printer
- # assert on the None.
- raise RuntimeError(f'Metric {event.metric_name!r} has no MetricExpr')
+ # generated tables, whether or not the name has a dash, so a build must
+ # not go on. Name the metric instead of letting the table printer
+ # assert on the None; --check reports it and keeps reading the rest.
+ if not _track:
+ raise RuntimeError(f'Metric {event.metric_name!r} has no MetricExpr')
+ continue
if event.metric_name and '-' not in event.metric_name:
metrics.append((event.pmu, event.metric_name, event.metric_expr))
updates = metric.RewriteMetricsInTermsOfOthers(metrics)
@@ -487,8 +668,12 @@ def read_json_events(path: str, topic: str) -> Sequence[JsonEvent]:
_json_cache[key] = _read_json_events_impl(path, topic)
return _json_cache[key]
-def preprocess_arch_std_files(archpath: str) -> None:
- """Read in all architecture standard events."""
+def preprocess_arch_std_files(archpath: str, failures: Optional[List[Tuple[str, Exception]]] = None) -> None:
+ """Read in all architecture standard events.
+
+ A failure is raised, or appended to failures when the caller can carry on
+ without the table; either way the tables after it are still read.
+ """
for item in os.scandir(archpath):
if not item.is_file() or not item.name.endswith('.json'):
continue
@@ -499,7 +684,10 @@ def preprocess_arch_std_files(archpath: str) -> None:
if event.metric_name:
_arch_std_events[event.metric_name.lower()] = event
except Exception as e:
- raise RuntimeError(f'Failure processing \'{item.name}\' in \'{archpath}\': {e}') from e
+ failure = RuntimeError(f'Failure processing \'{item.name}\' in \'{archpath}\': {e}')
+ if failures is None:
+ raise failure from e
+ failures.append((item.path, failure))
def add_events_table_entries(item: os.DirEntry, topic: str) -> None:
@@ -1448,16 +1636,27 @@ def _parallel_worker_count(num_tasks: int) -> int:
return max(1, min(getattr(os, 'process_cpu_count', os.cpu_count)() or 1,
_max_parallel_workers, num_tasks))
-def _parallel_read_json_events(task: Tuple[str, str]) -> Tuple[str, str, Sequence[JsonEvent]]:
+def _parallel_read_json_events(task: Tuple[str, str]) -> Tuple[str, str, Any]:
path, topic = task
- return path, topic, _read_json_events_impl(path, topic)
-
-def _init_worker(std_events: dict) -> None:
- global _arch_std_events
+ try:
+ return path, topic, _read_json_events_impl(path, topic)
+ except Exception as e:
+ if not _track:
+ # The build stops here: keep the file and the reason in one message,
+ # the pool adds the traceback.
+ raise RuntimeError(f'Failure processing \'{path}\': {e}') from None
+ # Carried back rather than raised: the parent knows the tree and can
+ # report the file and keep going, which is the point of reading the
+ # tables this build does not use.
+ return path, topic, e
+
+def _init_worker(std_events: dict, track: bool) -> None:
+ global _arch_std_events, _track
_arch_std_events = std_events
+ _track = track
def main() -> None:
- global _args
+ global _args, _track
def dir_path(path: str) -> str:
"""Validate path is a directory for argparse."""
@@ -1484,12 +1683,12 @@ def main() -> None:
if item.is_dir():
ftw(item.path, list(parents) + [item.name], action)
- ap = argparse.ArgumentParser()
+ ap = argparse.ArgumentParser(description='Generate pmu-events.c from the json tables')
ap.add_argument('arch', help='Architecture name like x86')
ap.add_argument('model', nargs='?', help='''Select a model such as skylake to
-reduce the code size. Normally set to "all", and that is what an omitted
-model means. For architectures like ARM64 with an implementor/model, the
-model must include the implementor such as "arm/cortex-a34".''',
+reduce the code size. Normally set to "all". For architectures like
+ARM64 with an implementor/model, the model must include the implementor
+such as "arm/cortex-a34".''',
default='all')
ap.add_argument(
'starting_dir',
@@ -1497,14 +1696,104 @@ model must include the implementor such as "arm/cortex-a34".''',
help='Root of tree containing architecture directories containing json files'
)
ap.add_argument('output_file', nargs='?', default=None,
- help='file to write the tables to, default standard output')
+ help='file to write the tables to, default standard output; '
+ 'not written, and not opened, with --check')
ap.add_argument('output_string_file', nargs='?', default=None,
help='file to write big_c_string to when it is to be kept '
- 'apart from the tables')
+ 'apart from the tables; ignored with --check')
+ ap.add_argument('--check', action='store_true',
+ help='read the tables, report the keys that look like a '
+ 'misspelling of a name this file reads on standard '
+ 'error, and exit without writing any table; pass "all" '
+ 'as both arch and model to cover the whole tree. '
+ 'Nothing is reported when every key is read and every '
+ 'table parses. The exit status is 0 when the tree is '
+ 'clean, 1 when something was reported and 2 when a '
+ 'table could not be read, so it can gate a script')
_args = ap.parse_args()
- # Opened here, and not by argparse, so that a failure before the tables are
- # written cannot empty a file the caller named.
+ if _args.check and (_args.output_file is not None or _args.output_string_file is not None):
+ ap.error('--check writes no table; drop the output arguments')
+ if _args.check:
+ if _args.arch == 'none':
+ ap.error('--check needs an architecture to read')
+ # The model only picks the tables a build needs; a check is about the
+ # tree, so it reads all of it and the model means nothing here.
+ _args.model = 'all'
+ _track = True
+
+ archs = []
+ for item in os.scandir(_args.starting_dir):
+ if not item.is_dir():
+ continue
+ if item.name == _args.arch or _args.arch == 'all' or item.name == 'test' or item.name == 'common':
+ archs.append(item.name)
+
+ if _args.check and _args.arch not in ('all', 'none') and _args.arch not in [
+ item.name for item in os.scandir(_args.starting_dir) if item.is_dir()]:
+ print(f'jevents: error: no architecture directory named \'{_args.arch}\' '
+ f'under \'{_args.starting_dir}\'', file=sys.stderr)
+ sys.exit(2)
+ if not archs and _args.arch != 'none':
+ raise IOError(f'Missing architecture directory \'{_args.arch}\'')
+
+ archs.sort()
+ import concurrent.futures
+ tasks = []
+ def collect_json(parents: Sequence[str], item: os.DirEntry) -> None:
+ if len(parents) == 0:
+ return
+ if item.is_file() and item.name.endswith('.json') and not item.name.endswith('metricgroups.json'):
+ tasks.append((item.path, get_topic(item.name)))
+
+ failed: List[Tuple[str, Exception]] = []
+ try:
+ for arch in archs:
+ arch_path = f'{_args.starting_dir}/{arch}'
+ preprocess_arch_std_files(arch_path, failed if _args.check else None)
+ ftw(arch_path, [], collect_json)
+
+ if not tasks:
+ print(f'jevents: error: no event table under \'{_args.starting_dir}\' '
+ f'matched arch \'{_args.arch}\'', file=sys.stderr)
+ sys.exit(2 if _args.check else 1)
+
+ with concurrent.futures.ProcessPoolExecutor(max_workers=_parallel_worker_count(len(tasks)),
+ initializer=_init_worker,
+ initargs=(_arch_std_events, _args.check)) as executor:
+ for path, topic, events in executor.map(_parallel_read_json_events, tasks):
+ if isinstance(events, Exception):
+ if not _args.check:
+ raise RuntimeError(f'Failure processing \'{path}\'') from events
+ failed.append((path, events))
+ else:
+ _json_cache[(path, topic)] = events
+ except (RuntimeError, ValueError, OSError, argparse.ArgumentTypeError) as e:
+ if not _args.check:
+ raise
+ failed.append((_args.starting_dir, e))
+
+ if _args.check:
+ # A table this check does not need is reported, not fatal: the check reads
+ # the whole tree and a caller only asked about one part of it.
+ for path, error in failed:
+ text = str(error)
+ # The architecture standard preprocess names the file and its directory
+ # already, so prefixing the path here only repeats it.
+ if text.startswith('Failure processing'):
+ print(f'jevents: error: {text}', file=sys.stderr)
+ else:
+ print(f'jevents: error: '
+ f'{os.path.relpath(path, _args.starting_dir)}: {text}',
+ file=sys.stderr)
+ reported = _report_mistyped_keys()
+ reported = _report_metrics_without_expr() or reported
+ if failed:
+ sys.exit(2)
+ sys.exit(1 if reported else 0)
+
+ # Opened here, and not by argparse, so that --check leaves the files it was
+ # given alone and a failure before the tables are written cannot empty one.
if _args.output_file in (None, '-'):
_args.output_file = sys.stdout
else:
@@ -1540,35 +1829,6 @@ struct pmu_table_entry {
};
""")
- archs = []
- for item in os.scandir(_args.starting_dir):
- if not item.is_dir():
- continue
- if item.name == _args.arch or _args.arch == 'all' or item.name == 'test' or item.name == 'common':
- archs.append(item.name)
-
- if len(archs) < 2 and _args.arch != 'none':
- raise IOError(f'Missing architecture directory \'{_args.arch}\'')
-
- archs.sort()
- import concurrent.futures
- tasks = []
- def collect_json(parents: Sequence[str], item: os.DirEntry) -> None:
- if len(parents) == 0:
- return
- if item.is_file() and item.name.endswith('.json') and not item.name.endswith('metricgroups.json'):
- tasks.append((item.path, get_topic(item.name)))
-
- for arch in archs:
- arch_path = f'{_args.starting_dir}/{arch}'
- preprocess_arch_std_files(arch_path)
- ftw(arch_path, [], collect_json)
-
- with concurrent.futures.ProcessPoolExecutor(max_workers=_parallel_worker_count(len(tasks)),
- initializer=_init_worker,
- initargs=(_arch_std_events,)) as executor:
- for path, topic, events in executor.map(_parallel_read_json_events, tasks):
- _json_cache[(path, topic)] = events
for arch in archs:
arch_path = f'{_args.starting_dir}/{arch}'
@@ -1607,4 +1867,16 @@ struct pmu_table_entry {
_args.output_string_file.close()
if __name__ == '__main__':
- main()
+ try:
+ main()
+ except SystemExit:
+ raise
+ except KeyboardInterrupt:
+ raise
+ except BaseException as e:
+ # A build should see the traceback; a check gives its caller a status,
+ # and 2 is the one that says the tool could not do its job.
+ if _args is not None and getattr(_args, 'check', False):
+ print(f'jevents: error: {e}', file=sys.stderr)
+ sys.exit(2)
+ raise
diff --git a/tools/perf/tests/shell/jevents_check.sh b/tools/perf/tests/shell/jevents_check.sh
new file mode 100755
index 000000000000..2254bc96352c
--- /dev/null
+++ b/tools/perf/tests/shell/jevents_check.sh
@@ -0,0 +1,68 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Check that jevents.py reports unread json keys and refuses a broken table
+
+shelldir=$(dirname "$0")
+. "${shelldir}"/lib/setup_python.sh
+
+# The check reads the tables and needs no build and no hardware, but an
+# installed perf has no pmu-events/ next to the binary.
+pmudir=$(readlink -f "${shelldir}/../../pmu-events")
+if [ ! -f "${pmudir}/jevents.py" ] || [ ! -f "${pmudir}/metric.py" ]; then
+ echo "Skipping: no pmu-events/jevents.py under ${pmudir}"
+ exit 2
+fi
+
+workdir=$(mktemp -d /tmp/jevents_check.XXXXXX)
+trap 'rm -rf "${workdir}"' EXIT
+
+# A table with one key jevents.py reads and one event, so that a clean run
+# says nothing and exits 0.
+mkdir -p "${workdir}/arch/x86/model"
+cat > "${workdir}/arch/x86/model/events.json" <<'JSON'
+[
+ {
+ "EventName": "CHECK_TEST_EVENT",
+ "EventCode": "0x1",
+ "BriefDescription": "a table the check should accept"
+ }
+]
+JSON
+
+run_check() {
+ "${PYTHON}" "${pmudir}/jevents.py" --check x86 all "${workdir}/arch"
+}
+
+run_check
+if [ $? -ne 0 ]; then
+ echo "pmu-events json keys [Failed: a clean table was reported]"
+ exit 1
+fi
+
+# A near miss for a name jevents.py reads is reported, and the status is 1.
+sed -i 's/"BriefDescription"/"BriefDescript6ion"/' \
+ "${workdir}/arch/x86/model/events.json"
+output=$(run_check 2>&1)
+ret=$?
+case "${ret}" in
+1) ;;
+*) echo "pmu-events json keys [Failed: a misspelled key exited ${ret}]"
+ exit 1 ;;
+esac
+if ! echo "${output}" | grep -q "BriefDescript6ion"; then
+ echo "pmu-events json keys [Failed: the misspelled key was not reported]"
+ exit 1
+fi
+
+# A table that does not parse is a failure of the check and not a finding:
+# the status is 2, which this script must not pass on as a skip.
+echo '{ "truncated": ' > "${workdir}/arch/x86/model/broken.json"
+run_check >/dev/null 2>&1
+ret=$?
+if [ "${ret}" -ne 2 ]; then
+ echo "pmu-events json keys [Failed: an unreadable table exited ${ret}]"
+ exit 1
+fi
+
+echo "pmu-events json keys [Success]"
+exit 0
--
2.53.0
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH v3 0/3] perf jevents: Report the JSON keys that jevents.py never reads
2026-11-11 2:34 ` [PATCH v2 0/3] perf jevents: Report " TAKANO Mitsuhiro
` (2 preceding siblings ...)
2026-11-11 2:34 ` [PATCH v2 3/3] perf jevents: Check the JSON keys that jevents.py never reads TAKANO Mitsuhiro
@ 2026-11-11 3:43 ` TAKANO Mitsuhiro
2026-11-11 3:43 ` [PATCH v3 1/3] perf jevents: Name the metric when a MetricExpr is missing TAKANO Mitsuhiro
` (2 more replies)
3 siblings, 3 replies; 12+ messages in thread
From: TAKANO Mitsuhiro @ 2026-11-11 3:43 UTC (permalink / raw)
To: linux-perf-users, Arnaldo Carvalho de Melo, Namhyung Kim
Cc: Ian Rogers, linux-kernel, TAKANO Mitsuhiro, James Clark,
John Garry, Peter Zijlstra, Ingo Molnar, Mark Rutland
jevents.py takes the fields it wants out of each JSON object by name and
ignores the rest, so a key spelled differently is dropped without a word
and the object is built without whatever that field held.
Patches 1 and 2 make the generator name what went wrong, and let the
model argument be omitted. Patch 3 adds a --check mode that reports the
keys jevents.py never asks for, along with a make target and a perf test
so that it runs without anyone having to remember it.
This is the second posting of the check. It went out alone in November
(https://lore.kernel.org/linux-perf-
users/20261110193239.1741014-1-takano32@gmail.com/) and the review found
two ways it could call a tree clean that no build can use: a MetricName
with a dash in it that loses its MetricExpr was invisible to both the
reader and the report, and a metric without an expression was skipped
rather than reported unless its key was spelled close enough to be a near
miss. Both are fixed here, and the failure-path changes the check needs
are no longer mixed into it.
The test runs the check over a fixture table rather than over the tree,
so it is green whatever the tables happen to look like, and it checks the
three exit statuses: 0 for a clean table, 1 for a reported key, 2 for a
table that does not parse.
The two misspellings the check reports in the tree today were posted
separately in November:
https://lore.kernel.org/linux-perf-users/20261108184600.3810059-1-takano32@gmail.com/
https://lore.kernel.org/linux-perf-users/20261108183846.3804875-1-takano32@gmail.com/
TAKANO Mitsuhiro (3):
perf jevents: Name the metric when a MetricExpr is missing
perf jevents: Make the model argument optional
perf jevents: Check the JSON keys that jevents.py never reads
tools/perf/Makefile | 14 +-
tools/perf/Makefile.perf | 1 +
tools/perf/pmu-events/jevents.py | 397 +++++++++++++++++++++---
tools/perf/tests/shell/jevents_check.sh | 68 ++++
4 files changed, 429 insertions(+), 51 deletions(-)
create mode 100755 tools/perf/tests/shell/jevents_check.sh
base-commit: 7475ea8b58616b33c04afb976cf0fbaad9dd63fb
--
2.53.0
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH v3 1/3] perf jevents: Name the metric when a MetricExpr is missing
2026-11-11 3:43 ` [PATCH v3 0/3] perf jevents: Report " TAKANO Mitsuhiro
@ 2026-11-11 3:43 ` TAKANO Mitsuhiro
2026-11-11 3:43 ` [PATCH v3 2/3] perf jevents: Make the model argument optional TAKANO Mitsuhiro
2026-11-11 3:43 ` [PATCH v3 3/3] perf jevents: Check the JSON keys that jevents.py never reads TAKANO Mitsuhiro
2 siblings, 0 replies; 12+ messages in thread
From: TAKANO Mitsuhiro @ 2026-11-11 3:43 UTC (permalink / raw)
To: linux-perf-users, Arnaldo Carvalho de Melo, Namhyung Kim
Cc: Ian Rogers, linux-kernel, TAKANO Mitsuhiro, James Clark,
John Garry, Peter Zijlstra, Ingo Molnar, Mark Rutland
jevents.py reads each field it wants out of a JSON object by name, so a
MetricExpr key that is missing or spelled another way leaves the object
with a MetricName and no expression. Two different failures followed,
neither of which named the metric: the rewrite in _read_json_events_impl()
raised AttributeError on the None, and a metric that got that far reached
print_pending_metrics(), whose assert fired on it.
Raise in the reader instead, naming the metric, before either of them can
happen. The check is not inside the '-' test that decides whether a metric
takes part in the rewrite: a name with a dash in it can lose its
expression the same way.
Two smaller changes on the same path:
- An architecture standard table that does not parse now carries the
reason in its message ("Failure processing '<file>' in '<dir>':
<cause>"). The traceback used to end with the file name alone, and
only the chained exception said why.
- A table that does not parse is reported on stderr with a "jevents:
error:" prefix instead of on stdout, where it was mixed with the
generated output.
And the output files are opened after every table has been read, and not
by argparse, so a build that fails while reading leaves a file the caller
named as it was instead of truncating it to the header. "Every table"
includes metricgroups.json, whose first pass runs before this point, and
the string table built from all of them: nothing that parses a file runs
after the output is opened.
Signed-off-by: TAKANO Mitsuhiro <takano32@gmail.com>
---
v3: the pass that reads metricgroups.json, and the string table built
from it, now run before the output files are opened. Sashiko asked
whether a failure there still truncates them: with this ordering
nothing that parses a table runs after the open, and a table that
does not parse leaves a named output file as it was.
tools/perf/pmu-events/jevents.py | 78 +++++++++++++++++++++-----------
1 file changed, 51 insertions(+), 27 deletions(-)
diff --git a/tools/perf/pmu-events/jevents.py b/tools/perf/pmu-events/jevents.py
index 080e04b51b8c..61f778dcc1e5 100755
--- a/tools/perf/pmu-events/jevents.py
+++ b/tools/perf/pmu-events/jevents.py
@@ -460,11 +460,17 @@ def _read_json_events_impl(path: str, topic: str) -> Sequence[JsonEvent]:
with open(path, encoding='utf-8') as f:
events = json.load(f, object_hook=JsonEvent)
except BaseException:
- print(f"Exception processing {path}")
+ print(f'jevents: error: Exception processing {path}', file=sys.stderr)
raise
metrics: list[Tuple[str, str, metric.Expression]] = []
for event in events:
event.topic = topic
+ if event.metric_name and event.metric_expr is None:
+ # A MetricName without a MetricExpr leaves nothing to put in the
+ # generated tables, whether or not the name has a dash, so the build
+ # must not go on. Name the metric instead of letting the table printer
+ # assert on the None.
+ raise RuntimeError(f'Metric {event.metric_name!r} has no MetricExpr')
if event.metric_name and '-' not in event.metric_name:
metrics.append((event.pmu, event.metric_name, event.metric_expr))
updates = metric.RewriteMetricsInTermsOfOthers(metrics)
@@ -493,7 +499,7 @@ def preprocess_arch_std_files(archpath: str) -> None:
if event.metric_name:
_arch_std_events[event.metric_name.lower()] = event
except Exception as e:
- raise RuntimeError(f'Failure processing \'{item.name}\' in \'{archpath}\'') from e
+ raise RuntimeError(f'Failure processing \'{item.name}\' in \'{archpath}\': {e}') from e
def add_events_table_entries(item: os.DirEntry, topic: str) -> None:
@@ -1490,33 +1496,13 @@ such as "arm/cortex-a34".''',
type=dir_path,
help='Root of tree containing architecture directories containing json files'
)
- ap.add_argument(
- 'output_file', type=argparse.FileType('w', encoding='utf-8'), nargs='?', default=sys.stdout)
- ap.add_argument(
- 'output_string_file', type=argparse.FileType('w', encoding='utf-8'), nargs='?', default=None)
+ ap.add_argument('output_file', nargs='?', default=None,
+ help='file to write the tables to, default standard output')
+ ap.add_argument('output_string_file', nargs='?', default=None,
+ help='file to write big_c_string to when it is to be kept '
+ 'apart from the tables')
_args = ap.parse_args()
- _args.output_file.write(f"""/* SPDX-License-Identifier: GPL-2.0 */
-/* THIS FILE WAS AUTOGENERATED BY `jevents.py arch={_args.arch} model={_args.model}` ! */
-""")
- _args.output_file.write("""
-#include <pmu-events/pmu-events.h>
-#include "util/header.h"
-#include "util/pmu.h"
-#include <string.h>
-#include <stddef.h>
-
-struct compact_pmu_event {
-\tint offset;
-};
-
-struct pmu_table_entry {
-\tconst struct compact_pmu_event *entries;
-\tuint32_t num_entries;
-\tstruct compact_pmu_event pmu_name;
-};
-
-""")
archs = []
for item in os.scandir(_args.starting_dir):
if not item.is_dir():
@@ -1553,6 +1539,44 @@ struct pmu_table_entry {
assert _bcs is not None
_bcs.compute()
+
+ # Every table has been read by now, so opening the output here cannot truncate
+ # a file the caller named when a table turns out not to parse.
+ if _args.output_file in (None, '-'):
+ _args.output_file = sys.stdout
+ else:
+ try:
+ _args.output_file = open(_args.output_file, 'w', encoding='utf-8')
+ except OSError as e:
+ ap.error(f'argument output_file: {e}')
+ if _args.output_string_file == '-':
+ _args.output_string_file = sys.stdout
+ elif _args.output_string_file is not None:
+ try:
+ _args.output_string_file = open(_args.output_string_file, 'w', encoding='utf-8')
+ except OSError as e:
+ ap.error(f'argument output_string_file: {e}')
+ _args.output_file.write(f"""/* SPDX-License-Identifier: GPL-2.0 */
+/* THIS FILE WAS AUTOGENERATED BY `jevents.py arch={_args.arch} model={_args.model}` ! */
+""")
+ _args.output_file.write("""
+#include <pmu-events/pmu-events.h>
+#include "util/header.h"
+#include "util/pmu.h"
+#include <string.h>
+#include <stddef.h>
+
+struct compact_pmu_event {
+\tint offset;
+};
+
+struct pmu_table_entry {
+\tconst struct compact_pmu_event *entries;
+\tuint32_t num_entries;
+\tstruct compact_pmu_event pmu_name;
+};
+
+""")
_args.output_file.write('/* clang-format off */\n')
if not _args.output_string_file:
_args.output_file.write('static const char *const big_c_string =\n')
--
2.53.0
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH v3 2/3] perf jevents: Make the model argument optional
2026-11-11 3:43 ` [PATCH v3 0/3] perf jevents: Report " TAKANO Mitsuhiro
2026-11-11 3:43 ` [PATCH v3 1/3] perf jevents: Name the metric when a MetricExpr is missing TAKANO Mitsuhiro
@ 2026-11-11 3:43 ` TAKANO Mitsuhiro
2026-11-11 3:43 ` [PATCH v3 3/3] perf jevents: Check the JSON keys that jevents.py never reads TAKANO Mitsuhiro
2 siblings, 0 replies; 12+ messages in thread
From: TAKANO Mitsuhiro @ 2026-11-11 3:43 UTC (permalink / raw)
To: linux-perf-users, Arnaldo Carvalho de Melo, Namhyung Kim
Cc: Ian Rogers, linux-kernel, TAKANO Mitsuhiro, James Clark,
John Garry, Peter Zijlstra, Ingo Molnar, Mark Rutland
The model argument carries a default of "all", but a positional argument
with a default and no nargs is still required: "jevents.py x86 <dir>" exits
with "the following arguments are required: model", and so does any caller
that only wants the tables for an architecture.
Make it nargs='?' so the default is reachable and an omitted model means
"all". Builds pass it explicitly, so the generated tables do not change.
Fixes: 5a09b1fd1b1f ("perf jevents: Add model list option")
Signed-off-by: TAKANO Mitsuhiro <takano32@gmail.com>
---
v3: the pass that reads metricgroups.json, and the string table built
from it, now run before the output files are opened. Sashiko asked
whether a failure there still truncates them: with this ordering
nothing that parses a table runs after the open, and a table that
does not parse leaves a named output file as it was.
tools/perf/pmu-events/jevents.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/tools/perf/pmu-events/jevents.py b/tools/perf/pmu-events/jevents.py
index 61f778dcc1e5..f4dca47eb49b 100755
--- a/tools/perf/pmu-events/jevents.py
+++ b/tools/perf/pmu-events/jevents.py
@@ -1486,10 +1486,10 @@ def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument('arch', help='Architecture name like x86')
- ap.add_argument('model', help='''Select a model such as skylake to
-reduce the code size. Normally set to "all". For architectures like
-ARM64 with an implementor/model, the model must include the implementor
-such as "arm/cortex-a34".''',
+ ap.add_argument('model', nargs='?', help='''Select a model such as skylake to
+reduce the code size. Normally set to "all", and that is what an omitted
+model means. For architectures like ARM64 with an implementor/model, the
+model must include the implementor such as "arm/cortex-a34".''',
default='all')
ap.add_argument(
'starting_dir',
--
2.53.0
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH v3 3/3] perf jevents: Check the JSON keys that jevents.py never reads
2026-11-11 3:43 ` [PATCH v3 0/3] perf jevents: Report " TAKANO Mitsuhiro
2026-11-11 3:43 ` [PATCH v3 1/3] perf jevents: Name the metric when a MetricExpr is missing TAKANO Mitsuhiro
2026-11-11 3:43 ` [PATCH v3 2/3] perf jevents: Make the model argument optional TAKANO Mitsuhiro
@ 2026-11-11 3:43 ` TAKANO Mitsuhiro
2 siblings, 0 replies; 12+ messages in thread
From: TAKANO Mitsuhiro @ 2026-11-11 3:43 UTC (permalink / raw)
To: linux-perf-users, Arnaldo Carvalho de Melo, Namhyung Kim
Cc: Ian Rogers, linux-kernel, TAKANO Mitsuhiro, James Clark,
John Garry, Peter Zijlstra, Ingo Molnar, Mark Rutland
jevents.py takes the fields it wants out of each JSON object by name:
jd.get('BriefDescription'), 'EventCode' in jd, and the names in
event_fields. A key spelled another way is dropped without a word, and
the object is built without whatever that field held.
Two of those are in the tree now. "BriefDescript6ion" is in one entry
each of amdzen4/cache.json, amdzen5/load-store.json and amdzen6/load-
store.json, and since those objects carry no other description,
ls_inef_sw_pref.all is generated with an empty text field.
"PublicDescrition" was in 16 of the 28 entries of
arm64/ampere/ampereonex/mmu.json; nothing a user saw changed there,
because each of the 16 also had a BriefDescription with the same text,
which is why the typo could sit there unnoticed.
Add a --check mode that reads every architecture, not just the one being
built: a typo is cheapest to fix by whoever reads that table next, and
that reader may be building something else. It records the keys
JsonEvent was never asked for and reports the ones whose spelling is a
near miss for a name jevents.py does read (difflib ratio 0.8). In this
tree 52 distinct key names occur, jevents.py reads 46 of them, the two
misspellings score 0.97, and the highest score among the remaining
unread names is 0.78 (Counter against CounterMask), so any cutoff in
that gap reports the same two.
It also reports a metric that carries a name but no expression, by
metric name and however the lost MetricExpr key was spelled: the key
report needs the spelling to be close to a name jevents.py reads, and a
metric without its expression cannot be generated at all.
A key that is a near miss for two names at once is reported as well: the
object may carry one of them with the same value, but that does not say
which one the key was meant to be, and the key is one nothing reads
either way.
The tracking is off unless --check asked for it, so a build parses each
object exactly as before and the generated tables are byte-identical
(x86, arm64 and all architectures compared against the same tree without
this patch). The check itself reads the 973 json files under arch/ and
takes seconds of CPU.
Exit status is 0 when nothing was reported, 1 when something was, and 2
when a table could not be read or the command line was wrong; make folds
both failures into its own status, so a script that needs the difference
runs jevents.py directly. Status 0 means the tables under that directory
were read and no unread key looks like a name jevents.py reads. It is
not a promise that a build will succeed: mapfile.csv coverage, the model
that selects a subset of the tables, and the C generation are not part
of it.
It is not a schema either: it checks nothing about types, required
fields or value ranges, and looks at key names only, so a misspelled
description in a value is out of scope. It does not read mapfile.csv,
which has no keys, or metricgroups.json, whose keys are group names.
Names that only metric.py or the C code read are outside it too, so an
"Expermental" key is not reported, and file names and topics are not
keys.
The check runs from the new make target "make -C tools/perf jevents-
check" (JEVENTS_CHECK_ARCH=x86 narrows it to one architecture) and from
perf test, where tests/shell/jevents_check.sh skips with status 2 when
pmu-events/arch is not next to the binary.
Written with Hermes Agent, an LLM assistant, which found the
misspellings and wrote this check, its test and this changelog. I built
perf with it, ran the perf test above, ran the check over the whole
tree, compared the generated tables with those from an unpatched tree on
x86, arm64 and all architectures at once, and ran mypy on jevents.py.
Signed-off-by: TAKANO Mitsuhiro <takano32@gmail.com>
---
v3: the pass that reads metricgroups.json, and the string table built
from it, now run before the output files are opened. Sashiko asked
whether a failure there still truncates them: with this ordering
nothing that parses a table runs after the open, and a table that
does not parse leaves a named output file as it was.
tools/perf/Makefile | 14 +-
tools/perf/Makefile.perf | 1 +
tools/perf/pmu-events/jevents.py | 345 +++++++++++++++++++++---
tools/perf/tests/shell/jevents_check.sh | 68 +++++
4 files changed, 391 insertions(+), 37 deletions(-)
create mode 100755 tools/perf/tests/shell/jevents_check.sh
diff --git a/tools/perf/Makefile b/tools/perf/Makefile
index 00baf90e5bda..0b13f1e12899 100644
--- a/tools/perf/Makefile
+++ b/tools/perf/Makefile
@@ -140,6 +140,18 @@ check-headers:
sync-headers:
@./scripts/sync-headers.sh $(SYNC_HEADERS_ARGS)
+# The pmu-events tables are data, so a key nothing reads is invisible until a
+# description goes missing. Named in the wrapper like check-headers: this is
+# a data check, not a build, so it needs neither config nor fixdep and reads
+# the source tree whatever O= says. JEVENTS_CHECK_ARCH=x86 reads x86 and the
+# common and test tables every build reads.
+# Makefile.perf picks the interpreter for the build; the wrapper does not
+# include it, so default here and let PYTHON= win when it is given.
+JEVENTS_CHECK_ARCH ?= all
+PYTHON ?= python3
+jevents-check:
+ @$(PYTHON) ./pmu-events/jevents.py --check '$(JEVENTS_CHECK_ARCH)' all pmu-events/arch
+
#
# All other targets get passed through:
#
@@ -147,4 +159,4 @@ sync-headers:
$(print_msg)
$(make)
-.PHONY: all clean tags TAGS FORCE Makefile build-test build-test-tarball check-headers sync-headers
+.PHONY: all clean tags TAGS FORCE Makefile build-test build-test-tarball check-headers sync-headers jevents-check
diff --git a/tools/perf/Makefile.perf b/tools/perf/Makefile.perf
index b74b534e1153..a330cd9de0fc 100644
--- a/tools/perf/Makefile.perf
+++ b/tools/perf/Makefile.perf
@@ -871,6 +871,7 @@ help:
@echo 'Perf maintainer targets:'
@echo ' clean - clean all binary objects and build output'
@echo ' check-headers - check which header copies are out of sync with the kernel'
+ @echo ' jevents-check - report the pmu-events json keys that jevents.py never reads'
@echo ' sync-headers - sync those header copies with the kernel, creating commits'
@echo ' (SYNC_HEADERS_ARGS="..." passes options to the script)'
diff --git a/tools/perf/pmu-events/jevents.py b/tools/perf/pmu-events/jevents.py
index f4dca47eb49b..3381ade201fd 100755
--- a/tools/perf/pmu-events/jevents.py
+++ b/tools/perf/pmu-events/jevents.py
@@ -3,15 +3,16 @@
"""Convert directories of JSON events to C code."""
import argparse
import csv
+import difflib
import json
import metric
import os
import sys
-from typing import (Callable, Dict, Optional, Sequence, Set, Tuple)
+from typing import (Any, Callable, Dict, List, Optional, Sequence, Set, Tuple)
import collections
# Global command line arguments.
-_args = None
+_args: Optional[argparse.Namespace] = None
# List of regular event tables.
_event_tables: list[str] = []
# List of event tables generated from "/sys" directories.
@@ -190,11 +191,78 @@ class BigCString:
_bcs = BigCString()
+# Names that some object in this process has been asked for. Used only to
+# find the spelling an unread name was meant to be. The set is per process
+# and the workers of the pool never share it, so a name is in it once some
+# object in the same worker has been built; every object asks for the same
+# names, so they are there before the first object is reported, but a read
+# that moved behind a condition would make a report depend on which worker
+# happened to build which file.
+_read_keys: Set[str] = set()
+
+# Unread name -> the names it could be a misspelling of, or () for none. A table
+# repeats the same few names tens of thousands of times, and the name that
+# misses is the common case (Counter, many thousands of objects), so the
+# misses are kept too; _mark empties the cache when a name is read for the
+# first time, so a hint computed while _read_keys was still growing cannot
+# stick.
+_hints: Dict[str, Tuple[str, ...]] = {}
+
+# Whether the objects are wrapped at all. Only the check reads what the
+# tracking records, so a build does not wrap anything. Set in
+# main() and, because the workers may be fresh interpreters, passed to them
+# through _init_worker() rather than read from _args there.
+_track: bool = False
+
+
+class _JsonObject(dict):
+ """A JSON object that remembers which of its keys were looked at.
+
+ jevents.py picks the fields it wants out of each object by name and
+ ignores the rest, so a key it never names - a misspelled
+ "BriefDescript6ion", say - is dropped without a word and the event
+ silently loses its description. Handing the parsed object to this
+ class and asking it afterwards which keys were never looked at makes
+ the misspelling visible. Only get(), [] and 'in' are tracked: a read
+ through items(), values() or pop() would not be, so a new read of a
+ field belongs in one of the three. The alternative, a list of the key names
+ jevents.py reads, would be a second copy of the code below that goes
+ stale the first time someone adds a field.
+ """
+
+ def __init__(self, jd: dict):
+ super().__init__(jd)
+ # dict.keys() rather than the object itself: iterating the object would
+ # go through __iter__, should that ever be hooked below.
+ self.unread: Set[str] = set(dict.keys(jd))
+
+ def _mark(self, key: str) -> None:
+ self.unread.discard(key)
+ if key not in _read_keys:
+ _read_keys.add(key)
+ _hints.clear()
+
+ def get(self, key: Any, default: Any = None) -> Any:
+ self._mark(key)
+ return dict.get(self, key, default)
+
+ def __getitem__(self, key: Any) -> Any:
+ self._mark(key)
+ return dict.__getitem__(self, key)
+
+ def __contains__(self, key: object) -> bool:
+ if isinstance(key, str):
+ self._mark(key)
+ return dict.__contains__(self, key)
+
+
class JsonEvent:
"""Representation of an event loaded from a json file dictionary."""
def __init__(self, jd: dict):
"""Constructor passed the dictionary of parsed json values."""
+ if _track:
+ jd = _JsonObject(jd)
def llx(x: int) -> str:
"""Convert an int to a string similar to a printf modifier of %#llx."""
@@ -416,6 +484,39 @@ class JsonEvent:
raise argparse.ArgumentTypeError('Cannot find arch std event:', arch_std)
self.event = real_event(self.name, event)
+ # Keys of the JSON object that no code above looked at, set last so that
+ # the copy of an architecture standard event above does not overwrite it.
+ # A name that is a near miss for one the code reads, and whose name the
+ # object does not carry with the same text, is almost certainly a
+ # misspelling of it: the object lost the field the reader was looking for.
+ # A name that is not a near miss is left alone, because a table is free to
+ # carry metadata jevents.py has no use for.
+ self.mistyped: Optional[Dict[str, Tuple[str, ...]]] = None
+ if not _track:
+ return
+ assert isinstance(jd, _JsonObject)
+ unread = jd.unread
+ if unread:
+ found = {}
+ for key in unread:
+ if key not in _hints:
+ lowered = {name.lower(): name for name in _read_keys}
+ # Two names can be equally close - RetirementLatencyMax and its Min -
+ # and naming one of them would be a coin toss presented as an answer.
+ match = difflib.get_close_matches(key.lower(), sorted(lowered), n=2, cutoff=0.8)
+ _hints[key] = tuple(lowered[m] for m in match)
+ hints = _hints[key]
+ if not hints:
+ continue
+ # Reported even when the object also carries a name it could be a
+ # misspelling of: two names can be equally close (RetirementLatencyMax
+ # next to its Min, which can hold the same value), so a matching value
+ # does not say which one this key was meant to be, and suppressing on
+ # it hid a typo of the other. Either way this is a key that nothing
+ # reads, which is what the check reports.
+ found[key] = hints
+ if found:
+ self.mistyped = found
def __repr__(self) -> str:
"""String representation primarily for debugging."""
@@ -453,6 +554,83 @@ class JsonEvent:
return f'{make_comment(s)}\t{{ { _bcs.offsets[s] } }},\n'
+def _report_mistyped_keys() -> bool:
+ """Report the JSON keys that jevents.py did not look for.
+
+ jevents.py takes the fields it wants out of each object by name. A key
+ spelled differently from the name it looks for is never read, the object is
+ built without whatever the field held, and the build says nothing; that is
+ how "BriefDescript6ion" in the AMD tables and "PublicDescrition" in the
+ AmpereOneX mmu table got in. A name that is a near miss for one jevents.py
+ reads is reported, with the spelling it was probably meant to be and the
+ files carrying it. A name that is not a near miss is not reported: a
+ table may carry metadata for another reader, and reporting it would mean
+ keeping a list of such names in this file that every vendor adding one
+ would have to edit.
+
+ A table that cannot be parsed is reported the same way and does not stop
+ the rest of the tree from being read, so one broken file does not hide the
+ misspellings in the other 1000.
+ """
+ assert _args is not None
+ found: Dict[str, collections.Counter] = {}
+ hints: Dict[str, Tuple[str, ...]] = {}
+ objects = 0
+ for (path, _), events in _json_cache.items():
+ for event in events:
+ if not event.mistyped:
+ continue
+ objects += 1
+ for key, hint in event.mistyped.items():
+ found.setdefault(key, collections.Counter())[path] += 1
+ hints.setdefault(key, hint)
+ if not found:
+ return False
+
+ paths = {path for files in found.values() for path in files}
+ start = _args.starting_dir
+ print(f'jevents: error: {objects} object(s) in '
+ f'{len(paths)} file(s) carry a key that looks like a misspelling:', file=sys.stderr)
+ for key in sorted(found):
+ files = found[key]
+ guess = ' or '.join(repr(name) for name in hints[key])
+ print(f' {key!r}, did you mean {guess}? ({sum(files.values())} object(s) in '
+ f'{len(files)} file(s)):', file=sys.stderr)
+ where = sorted(os.path.relpath(path, start) for path in files)
+ for path in where[:5]:
+ print(f' {path}', file=sys.stderr)
+ if len(where) > 5:
+ print(f' ... and {len(where) - 5} more file(s)', file=sys.stderr)
+
+ return True
+
+
+def _report_metrics_without_expr() -> bool:
+ """Report the metrics that carry a name but no expression.
+
+ A MetricExpr key spelled another way is not read, so the object keeps its
+ name and loses its expression. Unlike the key report above this does not
+ depend on how close a spelling is: a metric the rewrite cannot handle would
+ fail a build however the lost key was spelled.
+ """
+ assert _args is not None
+ start = _args.starting_dir
+ missing = sorted((os.path.relpath(path, start), event.metric_name)
+ for (path, _), events in _json_cache.items()
+ for event in events
+ if event.metric_name and event.metric_expr is None)
+ if not missing:
+ return False
+ print(f'jevents: error: {len(missing)} metric(s) have a name but no '
+ f'expression, so their MetricExpr key is missing or misspelled:',
+ file=sys.stderr)
+ for where, name in missing[:20]:
+ print(f' {name!r} in {where}', file=sys.stderr)
+ if len(missing) > 20:
+ print(f' ... and {len(missing) - 20} more metric(s)', file=sys.stderr)
+ return True
+
+
_json_cache: Dict[Tuple[str, str], Sequence[JsonEvent]] = {}
def _read_json_events_impl(path: str, topic: str) -> Sequence[JsonEvent]:
"""Read json events from the specified file."""
@@ -460,17 +638,20 @@ def _read_json_events_impl(path: str, topic: str) -> Sequence[JsonEvent]:
with open(path, encoding='utf-8') as f:
events = json.load(f, object_hook=JsonEvent)
except BaseException:
- print(f'jevents: error: Exception processing {path}', file=sys.stderr)
+ if not _track:
+ print(f'jevents: error: Exception processing {path}', file=sys.stderr)
raise
metrics: list[Tuple[str, str, metric.Expression]] = []
for event in events:
event.topic = topic
if event.metric_name and event.metric_expr is None:
# A MetricName without a MetricExpr leaves nothing to put in the
- # generated tables, whether or not the name has a dash, so the build
- # must not go on. Name the metric instead of letting the table printer
- # assert on the None.
- raise RuntimeError(f'Metric {event.metric_name!r} has no MetricExpr')
+ # generated tables, whether or not the name has a dash, so a build must
+ # not go on. Name the metric instead of letting the table printer
+ # assert on the None; --check reports it and keeps reading the rest.
+ if not _track:
+ raise RuntimeError(f'Metric {event.metric_name!r} has no MetricExpr')
+ continue
if event.metric_name and '-' not in event.metric_name:
metrics.append((event.pmu, event.metric_name, event.metric_expr))
updates = metric.RewriteMetricsInTermsOfOthers(metrics)
@@ -487,8 +668,12 @@ def read_json_events(path: str, topic: str) -> Sequence[JsonEvent]:
_json_cache[key] = _read_json_events_impl(path, topic)
return _json_cache[key]
-def preprocess_arch_std_files(archpath: str) -> None:
- """Read in all architecture standard events."""
+def preprocess_arch_std_files(archpath: str, failures: Optional[List[Tuple[str, Exception]]] = None) -> None:
+ """Read in all architecture standard events.
+
+ A failure is raised, or appended to failures when the caller can carry on
+ without the table; either way the tables after it are still read.
+ """
for item in os.scandir(archpath):
if not item.is_file() or not item.name.endswith('.json'):
continue
@@ -499,7 +684,10 @@ def preprocess_arch_std_files(archpath: str) -> None:
if event.metric_name:
_arch_std_events[event.metric_name.lower()] = event
except Exception as e:
- raise RuntimeError(f'Failure processing \'{item.name}\' in \'{archpath}\': {e}') from e
+ failure = RuntimeError(f'Failure processing \'{item.name}\' in \'{archpath}\': {e}')
+ if failures is None:
+ raise failure from e
+ failures.append((item.path, failure))
def add_events_table_entries(item: os.DirEntry, topic: str) -> None:
@@ -1448,16 +1636,27 @@ def _parallel_worker_count(num_tasks: int) -> int:
return max(1, min(getattr(os, 'process_cpu_count', os.cpu_count)() or 1,
_max_parallel_workers, num_tasks))
-def _parallel_read_json_events(task: Tuple[str, str]) -> Tuple[str, str, Sequence[JsonEvent]]:
+def _parallel_read_json_events(task: Tuple[str, str]) -> Tuple[str, str, Any]:
path, topic = task
- return path, topic, _read_json_events_impl(path, topic)
-
-def _init_worker(std_events: dict) -> None:
- global _arch_std_events
+ try:
+ return path, topic, _read_json_events_impl(path, topic)
+ except Exception as e:
+ if not _track:
+ # The build stops here: keep the file and the reason in one message,
+ # the pool adds the traceback.
+ raise RuntimeError(f'Failure processing \'{path}\': {e}') from None
+ # Carried back rather than raised: the parent knows the tree and can
+ # report the file and keep going, which is the point of reading the
+ # tables this build does not use.
+ return path, topic, e
+
+def _init_worker(std_events: dict, track: bool) -> None:
+ global _arch_std_events, _track
_arch_std_events = std_events
+ _track = track
def main() -> None:
- global _args
+ global _args, _track
def dir_path(path: str) -> str:
"""Validate path is a directory for argparse."""
@@ -1484,12 +1683,12 @@ def main() -> None:
if item.is_dir():
ftw(item.path, list(parents) + [item.name], action)
- ap = argparse.ArgumentParser()
+ ap = argparse.ArgumentParser(description='Generate pmu-events.c from the json tables')
ap.add_argument('arch', help='Architecture name like x86')
ap.add_argument('model', nargs='?', help='''Select a model such as skylake to
-reduce the code size. Normally set to "all", and that is what an omitted
-model means. For architectures like ARM64 with an implementor/model, the
-model must include the implementor such as "arm/cortex-a34".''',
+reduce the code size. Normally set to "all". For architectures like
+ARM64 with an implementor/model, the model must include the implementor
+such as "arm/cortex-a34".''',
default='all')
ap.add_argument(
'starting_dir',
@@ -1497,12 +1696,32 @@ model must include the implementor such as "arm/cortex-a34".''',
help='Root of tree containing architecture directories containing json files'
)
ap.add_argument('output_file', nargs='?', default=None,
- help='file to write the tables to, default standard output')
+ help='file to write the tables to, default standard output; '
+ 'not written, and not opened, with --check')
ap.add_argument('output_string_file', nargs='?', default=None,
help='file to write big_c_string to when it is to be kept '
- 'apart from the tables')
+ 'apart from the tables; ignored with --check')
+ ap.add_argument('--check', action='store_true',
+ help='read the tables, report the keys that look like a '
+ 'misspelling of a name this file reads on standard '
+ 'error, and exit without writing any table; pass "all" '
+ 'as both arch and model to cover the whole tree. '
+ 'Nothing is reported when every key is read and every '
+ 'table parses. The exit status is 0 when the tree is '
+ 'clean, 1 when something was reported and 2 when a '
+ 'table could not be read, so it can gate a script')
_args = ap.parse_args()
+ if _args.check and (_args.output_file is not None or _args.output_string_file is not None):
+ ap.error('--check writes no table; drop the output arguments')
+ if _args.check:
+ if _args.arch == 'none':
+ ap.error('--check needs an architecture to read')
+ # The model only picks the tables a build needs; a check is about the
+ # tree, so it reads all of it and the model means nothing here.
+ _args.model = 'all'
+ _track = True
+
archs = []
for item in os.scandir(_args.starting_dir):
if not item.is_dir():
@@ -1510,7 +1729,12 @@ model must include the implementor such as "arm/cortex-a34".''',
if item.name == _args.arch or _args.arch == 'all' or item.name == 'test' or item.name == 'common':
archs.append(item.name)
- if len(archs) < 2 and _args.arch != 'none':
+ if _args.check and _args.arch not in ('all', 'none') and _args.arch not in [
+ item.name for item in os.scandir(_args.starting_dir) if item.is_dir()]:
+ print(f'jevents: error: no architecture directory named \'{_args.arch}\' '
+ f'under \'{_args.starting_dir}\'', file=sys.stderr)
+ sys.exit(2)
+ if not archs and _args.arch != 'none':
raise IOError(f'Missing architecture directory \'{_args.arch}\'')
archs.sort()
@@ -1522,16 +1746,51 @@ model must include the implementor such as "arm/cortex-a34".''',
if item.is_file() and item.name.endswith('.json') and not item.name.endswith('metricgroups.json'):
tasks.append((item.path, get_topic(item.name)))
- for arch in archs:
- arch_path = f'{_args.starting_dir}/{arch}'
- preprocess_arch_std_files(arch_path)
- ftw(arch_path, [], collect_json)
-
- with concurrent.futures.ProcessPoolExecutor(max_workers=_parallel_worker_count(len(tasks)),
- initializer=_init_worker,
- initargs=(_arch_std_events,)) as executor:
- for path, topic, events in executor.map(_parallel_read_json_events, tasks):
- _json_cache[(path, topic)] = events
+ failed: List[Tuple[str, Exception]] = []
+ try:
+ for arch in archs:
+ arch_path = f'{_args.starting_dir}/{arch}'
+ preprocess_arch_std_files(arch_path, failed if _args.check else None)
+ ftw(arch_path, [], collect_json)
+
+ if not tasks:
+ print(f'jevents: error: no event table under \'{_args.starting_dir}\' '
+ f'matched arch \'{_args.arch}\'', file=sys.stderr)
+ sys.exit(2 if _args.check else 1)
+
+ with concurrent.futures.ProcessPoolExecutor(max_workers=_parallel_worker_count(len(tasks)),
+ initializer=_init_worker,
+ initargs=(_arch_std_events, _args.check)) as executor:
+ for path, topic, events in executor.map(_parallel_read_json_events, tasks):
+ if isinstance(events, Exception):
+ if not _args.check:
+ raise RuntimeError(f'Failure processing \'{path}\'') from events
+ failed.append((path, events))
+ else:
+ _json_cache[(path, topic)] = events
+ except (RuntimeError, ValueError, OSError, argparse.ArgumentTypeError) as e:
+ if not _args.check:
+ raise
+ failed.append((_args.starting_dir, e))
+
+ if _args.check:
+ # A table this check does not need is reported, not fatal: the check reads
+ # the whole tree and a caller only asked about one part of it.
+ for path, error in failed:
+ text = str(error)
+ # The architecture standard preprocess names the file and its directory
+ # already, so prefixing the path here only repeats it.
+ if text.startswith('Failure processing'):
+ print(f'jevents: error: {text}', file=sys.stderr)
+ else:
+ print(f'jevents: error: '
+ f'{os.path.relpath(path, _args.starting_dir)}: {text}',
+ file=sys.stderr)
+ reported = _report_mistyped_keys()
+ reported = _report_metrics_without_expr() or reported
+ if failed:
+ sys.exit(2)
+ sys.exit(1 if reported else 0)
for arch in archs:
arch_path = f'{_args.starting_dir}/{arch}'
@@ -1540,8 +1799,9 @@ model must include the implementor such as "arm/cortex-a34".''',
assert _bcs is not None
_bcs.compute()
- # Every table has been read by now, so opening the output here cannot truncate
- # a file the caller named when a table turns out not to parse.
+ # Everything that reads a table has run by now, so opening the output here
+ # cannot truncate one on a failure: --check leaves the files it was given
+ # alone, and a table that does not parse stops before this point.
if _args.output_file in (None, '-'):
_args.output_file = sys.stdout
else:
@@ -1577,6 +1837,7 @@ struct pmu_table_entry {
};
""")
+
_args.output_file.write('/* clang-format off */\n')
if not _args.output_string_file:
_args.output_file.write('static const char *const big_c_string =\n')
@@ -1608,4 +1869,16 @@ struct pmu_table_entry {
_args.output_string_file.close()
if __name__ == '__main__':
- main()
+ try:
+ main()
+ except SystemExit:
+ raise
+ except KeyboardInterrupt:
+ raise
+ except BaseException as e:
+ # A build should see the traceback; a check gives its caller a status,
+ # and 2 is the one that says the tool could not do its job.
+ if _args is not None and getattr(_args, 'check', False):
+ print(f'jevents: error: {e}', file=sys.stderr)
+ sys.exit(2)
+ raise
diff --git a/tools/perf/tests/shell/jevents_check.sh b/tools/perf/tests/shell/jevents_check.sh
new file mode 100755
index 000000000000..2254bc96352c
--- /dev/null
+++ b/tools/perf/tests/shell/jevents_check.sh
@@ -0,0 +1,68 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Check that jevents.py reports unread json keys and refuses a broken table
+
+shelldir=$(dirname "$0")
+. "${shelldir}"/lib/setup_python.sh
+
+# The check reads the tables and needs no build and no hardware, but an
+# installed perf has no pmu-events/ next to the binary.
+pmudir=$(readlink -f "${shelldir}/../../pmu-events")
+if [ ! -f "${pmudir}/jevents.py" ] || [ ! -f "${pmudir}/metric.py" ]; then
+ echo "Skipping: no pmu-events/jevents.py under ${pmudir}"
+ exit 2
+fi
+
+workdir=$(mktemp -d /tmp/jevents_check.XXXXXX)
+trap 'rm -rf "${workdir}"' EXIT
+
+# A table with one key jevents.py reads and one event, so that a clean run
+# says nothing and exits 0.
+mkdir -p "${workdir}/arch/x86/model"
+cat > "${workdir}/arch/x86/model/events.json" <<'JSON'
+[
+ {
+ "EventName": "CHECK_TEST_EVENT",
+ "EventCode": "0x1",
+ "BriefDescription": "a table the check should accept"
+ }
+]
+JSON
+
+run_check() {
+ "${PYTHON}" "${pmudir}/jevents.py" --check x86 all "${workdir}/arch"
+}
+
+run_check
+if [ $? -ne 0 ]; then
+ echo "pmu-events json keys [Failed: a clean table was reported]"
+ exit 1
+fi
+
+# A near miss for a name jevents.py reads is reported, and the status is 1.
+sed -i 's/"BriefDescription"/"BriefDescript6ion"/' \
+ "${workdir}/arch/x86/model/events.json"
+output=$(run_check 2>&1)
+ret=$?
+case "${ret}" in
+1) ;;
+*) echo "pmu-events json keys [Failed: a misspelled key exited ${ret}]"
+ exit 1 ;;
+esac
+if ! echo "${output}" | grep -q "BriefDescript6ion"; then
+ echo "pmu-events json keys [Failed: the misspelled key was not reported]"
+ exit 1
+fi
+
+# A table that does not parse is a failure of the check and not a finding:
+# the status is 2, which this script must not pass on as a skip.
+echo '{ "truncated": ' > "${workdir}/arch/x86/model/broken.json"
+run_check >/dev/null 2>&1
+ret=$?
+if [ "${ret}" -ne 2 ]; then
+ echo "pmu-events json keys [Failed: an unreadable table exited ${ret}]"
+ exit 1
+fi
+
+echo "pmu-events json keys [Success]"
+exit 0
--
2.53.0
^ permalink raw reply [flat|nested] 12+ messages in thread
end of thread, other threads:[~2026-11-11 3:43 UTC | newest]
Thread overview: 12+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-10 22:22 [PATCH 0/3] perf jevents: Report the JSON keys that jevents.py never reads TAKANO Mitsuhiro
2026-11-10 22:22 ` [PATCH 1/3] perf jevents: Name the metric when a MetricExpr is missing TAKANO Mitsuhiro
2026-11-10 22:22 ` [PATCH 2/3] perf jevents: Make the model argument optional TAKANO Mitsuhiro
2026-11-10 22:22 ` [PATCH 3/3] perf jevents: Check the JSON keys that jevents.py never reads TAKANO Mitsuhiro
2026-11-11 2:34 ` [PATCH v2 0/3] perf jevents: Report " TAKANO Mitsuhiro
2026-11-11 2:34 ` [PATCH v2 1/3] perf jevents: Name the metric when a MetricExpr is missing TAKANO Mitsuhiro
2026-11-11 2:34 ` [PATCH v2 2/3] perf jevents: Make the model argument optional TAKANO Mitsuhiro
2026-11-11 2:34 ` [PATCH v2 3/3] perf jevents: Check the JSON keys that jevents.py never reads TAKANO Mitsuhiro
2026-11-11 3:43 ` [PATCH v3 0/3] perf jevents: Report " TAKANO Mitsuhiro
2026-11-11 3:43 ` [PATCH v3 1/3] perf jevents: Name the metric when a MetricExpr is missing TAKANO Mitsuhiro
2026-11-11 3:43 ` [PATCH v3 2/3] perf jevents: Make the model argument optional TAKANO Mitsuhiro
2026-11-11 3:43 ` [PATCH v3 3/3] perf jevents: Check the JSON keys that jevents.py never reads TAKANO Mitsuhiro
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®