Skip to content

Commit 4496802

Browse files
Tejas ManhasTejas Manhas
authored andcommitted
perf_stat.py: Add testcase for present and unused options
This commit adds a new Avocado testcase to dynamically run all perf stat options that are currently present in the system help but missing from kernel source tests. Features include: - Automatic extraction of perf stat --help options. - Filtering out options already covered in kernel perf tests. - Minimal resource generation for options that require inputs. - Special handling for metric groups, topdown, transaction, PID, and TID. - Minimal workloads created dynamically to avoid errors. - Logging of unknown or failed options with exit codes. This testcase helps ensure coverage of all perf stat options and detects any discrepancies between help output and available kernel tests. Signed-off-by: Tejas Manhas <[email protected]>
1 parent f18ce52 commit 4496802

File tree

2 files changed

+328
-0
lines changed

2 files changed

+328
-0
lines changed

perf/perf_stat.py

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
#!/usr/bin/env python
2+
#
3+
# This program is free software; you can redistribute it and/or modify
4+
# it under the terms of the GNU General Public License as published by
5+
# the Free Software Foundation; either version 2 of the License, or
6+
# (at your option) any later version.
7+
#
8+
# This program is distributed in the hope that it will be useful,
9+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11+
#
12+
# See LICENSE for more details.
13+
#
14+
# Copyright: 2025 IBM
15+
# Author: Tejas Manhas <[email protected]>
16+
import re
17+
import os
18+
from avocado import Test
19+
from avocado.utils import process, distro
20+
from avocado.utils.software_manager.manager import SoftwareManager
21+
22+
23+
class PerfStatOptions(Test):
24+
"""
25+
Test perf stat options: compare --help options vs kernel source.
26+
Run only options present in help but missing from source.
27+
"""
28+
29+
def setUp(self):
30+
self.log.info("Setting up PerfStatOptions test...")
31+
32+
# Check dependencies for RHEL/SLES/upstream
33+
detected_distro = distro.detect()
34+
smm = SoftwareManager()
35+
packages = []
36+
if "rhel" in detected_distro.name.lower():
37+
packages = ["perf", "kernel-devel"]
38+
elif "suse" in detected_distro.name.lower():
39+
packages = ["perf", "kernel-source"]
40+
else:
41+
self.cancel("Unsupported Linux distribution")
42+
43+
for pkg in packages:
44+
if not smm.check_installed(pkg):
45+
if not smm.install(pkg):
46+
self.cancel(f"{pkg} is required for this test")
47+
48+
self.unknown_options = set()
49+
self.failed_options = {}
50+
51+
# Get help options
52+
self.perf_options = self.get_help_options()
53+
self.log.info(f"Perf --help options: {self.perf_options}")
54+
55+
# Get source options
56+
self.src_options = self.get_src_options()
57+
self.log.info(
58+
f"Source options from kernel perf tests: {self.src_options}")
59+
60+
# Final options to test
61+
self.final_to_test = self.perf_options - self.src_options
62+
self.log.info(f"Final options to test: {self.final_to_test}")
63+
64+
def get_help_options(self):
65+
"""
66+
Extract valid -/-- options from `perf stat --help`.
67+
Ignores separators and non-option lines.
68+
"""
69+
result = process.run("perf stat --help", ignore_status=True)
70+
out = result.stdout.decode()
71+
opts = set()
72+
for line in out.splitlines():
73+
line = line.strip()
74+
if not line or set(line) == {"-"}:
75+
continue
76+
tokens = line.split()
77+
for token in tokens:
78+
if re.match(r"^-{1,2}[a-zA-Z]", token):
79+
clean_opt = self.sanitize_option(token)
80+
if clean_opt:
81+
opts.add(clean_opt)
82+
return opts
83+
84+
def get_src_options(self):
85+
"""
86+
Grep perf kernel tests for 'perf stat' and extract options.
87+
"""
88+
src_dir = None
89+
# Find linux source tree
90+
for path in ["/usr/src/linux", "/usr/src/linux-*"]:
91+
if os.path.exists(path):
92+
src_dir = path
93+
break
94+
if not src_dir:
95+
self.log.warning(
96+
"Linux source tree not found, src options set will be empty")
97+
return set()
98+
99+
# Grep recursively for 'perf stat' in tools/perf/tests and tests/shell
100+
cmd = f"grep -r 'perf stat' {src_dir}/tools/perf/tests {src_dir}/tools/perf/tests/shell || true"
101+
result = process.run(cmd, ignore_status=True, shell=True)
102+
out = result.stdout.decode()
103+
opts = set()
104+
for line in out.splitlines():
105+
matches = re.findall(r"-{1,2}[a-zA-Z0-9][\w-]*", line)
106+
for opt in matches:
107+
clean_opt = self.sanitize_option(opt)
108+
if clean_opt:
109+
opts.add(clean_opt)
110+
return opts
111+
112+
def run_and_check(self, opt):
113+
"""
114+
Run a perf stat command for the given option, automatically generating
115+
required resources and minimal valid workloads to avoid errors.
116+
"""
117+
118+
# --- Step 1: Sanitize option ---
119+
opt = self.sanitize_option(opt)
120+
if not opt:
121+
return
122+
123+
# --- Step 2: Skip unsupported infra (cgroup / bpf) ---
124+
if any(
125+
x in opt for x in [
126+
"cgroup",
127+
"bpf",
128+
"smi-cost",
129+
"interval-clear"]):
130+
self.log.info(f"Skipping unsupported option: {opt}")
131+
self.unknown_options.add(opt)
132+
return
133+
134+
# --- Step 3: Determine the resource / value for this option ---
135+
minimal = self.params.get(opt, default="")
136+
if minimal:
137+
self.log.info(
138+
f"For option {opt}, using minimal value from YAML: {minimal}")
139+
else:
140+
self.log.info(
141+
f"No YAML value found for option {opt}, skipping or using default")
142+
143+
if opt in ["-M", "--metrics", "--metric-groups"]:
144+
cmd1 = "perf list metricgroup 2>/dev/null | grep -v '^$' | grep -v 'Metric Groups' | head -1"
145+
result1 = process.run(cmd1, ignore_status=True, shell=True)
146+
metric_group = result1.stdout.strip().decode()
147+
minimal = metric_group
148+
if not metric_group:
149+
self.cancel("No metric groups available on this system")
150+
self.log.info(f"Using metric group: {metric_group}")
151+
152+
if opt in ["--topdown", "-T", "--transaction", "-t"]:
153+
grep_pat = "^TopdownL1" if opt in [
154+
"--topdown", "-T"] else "^transaction"
155+
group = process.run(
156+
f"perf list metricgroups 2>/dev/null | grep '{grep_pat}' | head -1",
157+
shell=True, ignore_status=True
158+
).stdout.strip().decode()
159+
if not group:
160+
self.log.info(
161+
f"{opt} metric groups not present on this system")
162+
self.unknown_options.add(opt)
163+
return
164+
else:
165+
minimal = group
166+
167+
# Special handling for TID
168+
if opt in ["-t", "--tid"] or "--tid=" in opt:
169+
task_dir = "/proc/self/task"
170+
try:
171+
tids = os.listdir(task_dir)
172+
minimal = tids[0] if tids else str(os.getpid())
173+
except Exception:
174+
minimal = str(os.getpid())
175+
if opt in ["-p", "--pid"] or "--pid=" in opt:
176+
minimal = str(os.getpid())
177+
178+
# --- Step 4: Generate required files / workloads ---
179+
# Input data for perf
180+
if opt in ["--input"]:
181+
process.run(
182+
f"mkdir -p events_dir && echo -e 'cycles,instructions' > {minimal}",
183+
shell=True)
184+
185+
# Minimal post/pre scripts
186+
if opt in ["--post", "--pre"] and not os.path.exists(minimal):
187+
with open(minimal, "w") as f:
188+
f.write("#!/bin/bash\nsleep 0.1\n")
189+
os.chmod(minimal, 0o755)
190+
191+
# --- Step 5: Construct command ---
192+
cmd_parts = ["perf", "stat"]
193+
194+
# Flags that require a dependent event
195+
flags_with_deps = [
196+
"-b",
197+
"-u",
198+
"-s",
199+
"--metric-only",
200+
"--topdown",
201+
"--transaction",
202+
"-T"]
203+
if opt in flags_with_deps:
204+
cmd_parts.extend(["-e", self.params.get("-e")])
205+
206+
# Options with "="
207+
if "=" in opt:
208+
base_opt = opt.split("=", 1)[0]
209+
cmd_parts.append(f"{base_opt}={minimal}")
210+
elif minimal:
211+
cmd_parts.extend([opt, minimal])
212+
else:
213+
cmd_parts.append(opt)
214+
215+
# Default minimal workload
216+
workload = "sleep 5"
217+
cmd_parts.append(workload)
218+
219+
cmd = " ".join(cmd_parts)
220+
221+
# --- Step 6: Run command ---
222+
result = process.run(cmd, shell=True, ignore_status=True)
223+
ret = result.exit_status
224+
out = result.stdout_text
225+
err = result.stderr_text
226+
227+
# --- Step 7: Handle results ---
228+
if ret != 0:
229+
if ret == 129 or "unknown option" in err.lower():
230+
self.log.info(f"Skipping option {opt}: unknown option")
231+
self.unknown_options.add(opt)
232+
else:
233+
self.failed_options[opt] = {
234+
"exit_code": ret,
235+
"stderr": err.strip(),
236+
}
237+
self.log.warning(f"Option {opt} failed with exit code {ret}")
238+
else:
239+
self.log.info(f"Option {opt} ran successfully")
240+
241+
return ret, out, err
242+
243+
def sanitize_option(self, opt):
244+
"""
245+
Remove trailing non-alphanumeric chars commonly found in perf help/source.
246+
Keep leading '-' or '--'.
247+
"""
248+
# opt = opt.strip()
249+
if not opt.startswith("-"):
250+
return None
251+
# remove trailing junk characters
252+
opt = re.sub(r"[),.:;/\[\]]+$", "", opt)
253+
# handle attached arguments: -G/cgroup -> -G, --foo=bar -> --foo,
254+
# -j64 -> -j
255+
opt = re.split(r"[=/]", opt, 1)[0]
256+
opt = re.sub(r"^(-[a-zA-Z])\d+$", r"\1", opt)
257+
# remove leading/trailing whitespace
258+
opt = opt.strip()
259+
if not opt:
260+
return None
261+
return opt
262+
263+
def test_perf_stat_options(self):
264+
"""
265+
Run all final options with minimal values where required.
266+
"""
267+
for opt in sorted(self.final_to_test):
268+
self.log.info(f"Testing option: {opt}")
269+
self.run_and_check(opt)
270+
if self.unknown_options:
271+
self.log.warning(
272+
f"Unknown options skipped: {', '.join(self.unknown_options)}")
273+
if self.failed_options:
274+
self.log.error("Failed options and their exit codes:")
275+
for opt, code in self.failed_options.items():
276+
self.log.error(f" {opt} -> {code}")
277+
self.fail(
278+
f"{len(self.failed_options)} options failed, see logs above")
279+
280+
def tearDown(self):
281+
self.log.info("Tearing down PerfStatOptions test...")
282+
# Remove events directory if exists
283+
events_dir = "events_dir"
284+
if os.path.exists(events_dir):
285+
try:
286+
import shutil
287+
shutil.rmtree(events_dir)
288+
self.log.info(f"Removed temporary directory: {events_dir}")
289+
except Exception as e:
290+
self.log.warning(f"Failed to remove {events_dir}: {e}")
291+
292+
# Remove any post/pre scripts created dynamically
293+
for opt in ["--post", "--pre"]:
294+
minimal = self.params.get(opt, default="")
295+
if minimal and os.path.exists(minimal):
296+
try:
297+
os.remove(minimal)
298+
self.log.info(f"Removed temporary script: {minimal}")
299+
except Exception as e:
300+
self.log.warning(f"Failed to remove {minimal}: {e}")

perf/perf_stat.py.data/stat.yaml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
--td-level: "0"
2+
--field-separator: "."
3+
--log-fd: "1"
4+
--output: "/tmp/perf_out.txt"
5+
-o: "/tmp/perf_out.txt"
6+
-e: "cycles"
7+
--event: "cpu-cycles"
8+
-C: "0"
9+
--cpu: "0"
10+
-I: "1000"
11+
--interval-print: "1000"
12+
-r: "1"
13+
--repeat: "1"
14+
-M: "cycles"
15+
-p: "{{ pid }}"
16+
--pid: "{{ pid }}"
17+
--control: "fd:0"
18+
--control=fifo: "fifo:dummy"
19+
--input: "events_dir/events.txt"
20+
-j: "1"
21+
--delay: "1"
22+
--post: "/tmp/perf_post.sh"
23+
--pre: "/tmp/perf_pre.sh"
24+
--timeout: "100"
25+
--time: "100"
26+
--interval-clear: "-I 1000 -a"
27+
--table: "-r 2"
28+
--no-aggr: "-a"

0 commit comments

Comments
 (0)