Skip to content

Commit a68e030

Browse files
Tejas Manhasroot
authored andcommitted
perf_report_options.py:: Added new testcase to run all present and unused options for perf report
Implemented a new Avocado testcase to validate perf report options. The testcase runs perf report with both documented and less frequently used/unused options, capturing output and errors for each. Signed-off-by: Tejas Manhas <[email protected]>
1 parent f18ce52 commit a68e030

File tree

2 files changed

+320
-0
lines changed

2 files changed

+320
-0
lines changed

perf/perf_report_options.py

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
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 PerfReportOptions(Test):
24+
"""
25+
Test perf report options: extract help and source options,
26+
run general perf record, and test all final options.
27+
Uses -o and -i to ensure perf.data is correctly located.
28+
"""
29+
30+
PERF_DATA_FILE = "./perf.data"
31+
32+
def setUp(self):
33+
"""
34+
Checks required packages and compiles Final unused
35+
options to test with perf report
36+
"""
37+
self.log.info("Setting up PerfReportOptions test...")
38+
39+
detected_distro = distro.detect()
40+
smm = SoftwareManager()
41+
packages = []
42+
if "rhel" in detected_distro.name.lower():
43+
packages = ["perf", "kernel-devel"]
44+
elif "suse" in detected_distro.name.lower():
45+
packages = ["perf", "kernel-source"]
46+
else:
47+
self.cancel("Unsupported Linux distribution")
48+
49+
for pkg in packages:
50+
if not smm.check_installed(pkg):
51+
if not smm.install(pkg):
52+
self.cancel(f"{pkg} is required for this test")
53+
54+
self.unknown_options = set()
55+
self.failed_options = {}
56+
57+
# Step 1: Get help options
58+
self.perf_help_options = self.get_help_options()
59+
self.log.info(f"Perf report --help options: {self.perf_help_options}")
60+
61+
# Step 2: Get source options
62+
self.perf_src_options = self.get_src_options()
63+
self.log.info(f"Perf report source options: {self.perf_src_options}")
64+
65+
# Step 3: Final options to test
66+
self.final_to_test = self.perf_help_options - self.perf_src_options
67+
self.log.info(f"Final options to test: {self.final_to_test}")
68+
69+
def get_help_options(self):
70+
# Get available executable options for perf report
71+
72+
result = process.run("perf report --help", ignore_status=True)
73+
out = result.stdout.decode()
74+
opts = set()
75+
for line in out.splitlines():
76+
line = line.strip()
77+
if not line or set(line) == {"-"}:
78+
continue
79+
tokens = line.split()
80+
for token in tokens:
81+
if re.match(r"^-{1,2}[a-zA-Z]", token):
82+
clean_opt = self.sanitize_option(token)
83+
if clean_opt:
84+
opts.add(clean_opt)
85+
return opts
86+
87+
def get_src_options(self):
88+
# Get latest source directory and collect perf report options
89+
# --- Find latest kernel source under /usr/src ---
90+
candidate_dirs = []
91+
92+
for root, dirs, files in os.walk("/usr/src"):
93+
depth = root[len("/usr/src"):].count(os.sep)
94+
if depth > 2:
95+
continue
96+
perf_tests_path = os.path.join(root, "tools", "perf", "tests")
97+
if os.path.exists(perf_tests_path):
98+
candidate_dirs.append(root)
99+
100+
if not candidate_dirs:
101+
self.log.warning(
102+
"Linux source tree not found, src options set empty")
103+
return set()
104+
105+
# or improve for version sorting
106+
candidate_dirs.sort(key=lambda d: d.lower())
107+
src_dir = candidate_dirs[-1] # pick the latest
108+
109+
self.log.info(f"Using Linux source directory: {src_dir}")
110+
111+
cmd = f"grep -r 'perf report' {src_dir}/tools/perf/tests {src_dir}/tools/perf/tests/shell || true"
112+
result = process.run(cmd, ignore_status=True, shell=True)
113+
out = result.stdout.decode()
114+
opts = set()
115+
for line in out.splitlines():
116+
matches = re.findall(r"-{1,2}[a-zA-Z0-9][\w-]*", line)
117+
for opt in matches:
118+
clean_opt = self.sanitize_option(opt)
119+
if clean_opt:
120+
opts.add(clean_opt)
121+
return opts
122+
123+
def sanitize_option(self, opt):
124+
# Sanitize options that are derived from source or available options
125+
126+
if not opt.startswith("-"):
127+
return None
128+
opt = re.sub(r"[)\'\",.:;/\[\]]+$", "", opt)
129+
opt = re.split(r"[=/]", opt, 1)[0]
130+
opt = re.sub(r"^(-[a-zA-Z])\d+$", r"\1", opt)
131+
return opt.strip()
132+
133+
def run_record(self):
134+
# runs perf record with options that may be required for report options
135+
136+
record_cmd = f"perf record -g -a -e cycles,instructions -o {self.PERF_DATA_FILE} -- sleep 1"
137+
self.log.info(f"Running general perf record: {record_cmd}")
138+
result = process.run(record_cmd, shell=True, ignore_status=True)
139+
if result.exit_status != 0:
140+
self.fail(f"General perf record failed: {result.stderr_text}")
141+
self.log.info(
142+
f"General perf record completed successfully: {self.PERF_DATA_FILE}")
143+
144+
def run_report(self, opt=None):
145+
"""
146+
Run a perf report command for the given option, using YAML for minimal
147+
values and skipping unsupported infra or options.
148+
"""
149+
150+
# --- Step 1: Sanitize option ---
151+
opt = self.sanitize_option(opt)
152+
if not opt:
153+
return
154+
155+
# --- Step 2: Skip unsupported infra or options ---
156+
unsupported_opts = [
157+
"gtk",
158+
"cgroup",
159+
"bpf",
160+
"smi-cost",
161+
"interval-clear",
162+
"vmlinux",
163+
"smyfs"]
164+
if any(x in opt for x in unsupported_opts):
165+
self.log.info(f"Skipping unsupported option: {opt}")
166+
self.unknown_options.add(opt)
167+
return
168+
169+
# --- Step 3: Determine minimal value from YAML ---
170+
minimal = self.params.get(opt, default="")
171+
if minimal:
172+
self.log.info(
173+
f"For option {opt}, using minimal value from YAML: {minimal}")
174+
else:
175+
self.log.info(
176+
f"No YAML value found for option {opt}, using default if needed")
177+
178+
# --- Step 5: Construct command ---
179+
cmd_parts = [f"perf report -i {self.PERF_DATA_FILE} "]
180+
181+
# If minimal value exists, append it to the option
182+
if "=" in opt:
183+
base_opt = opt.split("=", 1)[0]
184+
cmd_parts.append(f"{base_opt}={minimal}")
185+
elif minimal:
186+
cmd_parts.extend([opt, minimal])
187+
elif opt:
188+
cmd_parts.append(opt)
189+
190+
cmd_parts.append("> /tmp/perf_report_options.txt 2>&1")
191+
cmd = " ".join(cmd_parts)
192+
193+
# --- Step 6: Run command ---
194+
self.log.info(f"Running perf report: {cmd}")
195+
process.run("rm -rf /tmp/perf_report_options.txt")
196+
result = process.run(cmd, shell=True, ignore_status=True)
197+
ret = result.exit_status
198+
out = result.stdout_text
199+
err = result.stderr_text
200+
201+
# --- Step 7: Handle results ---
202+
if ret != 0:
203+
if ret == 129 or "unknown option" in err.lower():
204+
self.log.info(f"Skipping report option {opt}: unknown option")
205+
self.unknown_options.add(opt)
206+
else:
207+
self.failed_options[opt or "general"] = {
208+
"exit_code": ret, "stderr": err.strip()}
209+
self.log.warning(f"Perf report failed with exit code {ret}")
210+
else:
211+
self.log.info(
212+
f"Perf report ran successfully with option: {opt or 'none'}")
213+
214+
return ret, out, err
215+
216+
def test_perf_report_options(self):
217+
"""
218+
Checks for yaml file and runs final options for perf report
219+
"""
220+
221+
# Step 1: Record
222+
self.run_record()
223+
yaml_provided = False
224+
try:
225+
if self.params.get("--prefix", default=None) is not None:
226+
yaml_provided = True
227+
except Exception:
228+
yaml_provided = False
229+
230+
if not yaml_provided:
231+
if not getattr(self.params, "_params", {}):
232+
self.log.info(
233+
"No YAML file provided, running plain perf report and exiting")
234+
result = process.run(
235+
"perf report -i ./perf.data > /tmp/perf_report_options.txt 2>&1",
236+
shell=True,
237+
ignore_status=False)
238+
if result.exit_status != 0:
239+
self.fail(f"Plain perf report failed: {result.stderr_text}")
240+
return
241+
242+
# Step 2: Loop through final options
243+
for opt in sorted(self.final_to_test):
244+
self.run_report(opt)
245+
246+
if self.unknown_options:
247+
self.log.warning(
248+
f"Unknown options skipped: {', '.join(self.unknown_options)}")
249+
if self.failed_options:
250+
self.log.error("Failed report options and their exit codes:")
251+
for opt, code in self.failed_options.items():
252+
self.log.error(f" {opt} -> {code}")
253+
self.fail(
254+
f"{len(self.failed_options)} options failed, see logs above")
255+
256+
def tearDown(self):
257+
# removes temporary files and perf.data files
258+
if os.path.exists(self.PERF_DATA_FILE):
259+
try:
260+
os.remove(self.PERF_DATA_FILE)
261+
self.log.info(f"Removed {self.PERF_DATA_FILE}")
262+
except Exception as e:
263+
self.log.warning(
264+
f"Failed to remove {self.PERF_DATA_FILE}: {e}")
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
--prefix: "/home/user/src"
2+
--prefix - strip: "1"
3+
-C: "0-3"
4+
-U: "0-3"
5+
--switch - on: "cycles"
6+
--switch - off: "cycles"
7+
--time: "12:00,12:05"
8+
-W: "80,80,80"
9+
-I: "100"
10+
-m: "4"
11+
--samples: "100"
12+
--socket - filter: "0"
13+
-T: "1234"
14+
--percent - type: "total"
15+
--source: "/usr/src/linux"
16+
--full - source - path: "/usr/src/linux"
17+
--time - quantum: "100"
18+
--field - separator: ","
19+
--pid: "1"
20+
--column - widths: "20,30,40"
21+
-j: "any"
22+
--fields: "overhead,comm,dso"
23+
--parent: "main"
24+
--input: "perf.data"
25+
--percentage: "absolute"
26+
--addr2line: "/usr/bin/addr2line"
27+
--tid: "1"
28+
--branch - filter: "any"
29+
-d: ""
30+
--sort: "dso,symbol"
31+
--disassembler - style: "intel"
32+
--ignore - callees: "foo"
33+
-p: "1"
34+
--group - sort - idx: "0"
35+
--symbol - filter: "malloc"
36+
-S: ""
37+
-M: "intel"
38+
--objdump: "/usr/bin/objdump"
39+
-w: "120"
40+
--percent - type: "local"
41+
--cpu: "0"
42+
-c: "/tmp/callchain-cmp"
43+
--comms: "bash"
44+
--dsos: "libc.so.6"
45+
--kallsyms: "/proc/kallsyms"
46+
-W: ""
47+
--symbols: ""
48+
--ignore - vmlinux: ""
49+
--pretty: "normal"
50+
--max - stack: "5"
51+
--time: "1.0,5.0"
52+
--prefix - strip: "3 --prefix 4"
53+
-S: "2"
54+
--symbols: "2"
55+
--percent - type: "local-period"
56+
-d: "3"

0 commit comments

Comments
 (0)