summaryrefslogtreecommitdiff
path: root/tools/perf/tests/shell/lib/attr.py
diff options
context:
space:
mode:
Diffstat (limited to 'tools/perf/tests/shell/lib/attr.py')
-rw-r--r--tools/perf/tests/shell/lib/attr.py461
1 files changed, 423 insertions, 38 deletions
diff --git a/tools/perf/tests/shell/lib/attr.py b/tools/perf/tests/shell/lib/attr.py
index 68f06df9565a..df1993883199 100644
--- a/tools/perf/tests/shell/lib/attr.py
+++ b/tools/perf/tests/shell/lib/attr.py
@@ -1,6 +1,7 @@
# SPDX-License-Identifier: GPL-2.0
import configparser
+import errno
import os
import sys
import glob
@@ -28,6 +29,192 @@ def data_equal(a, b):
return False
+CORE_PMU_AVAILABLE = None
+CORE_PMU_COUNT = None
+
+def core_pmu_available():
+ """
+ Is there a core PMU on this machine? Without one hardware events aren't
+ supported and get replaced by the software fallback, so there's no way to
+ check the expectations that pin their legacy encodings. The answer can't
+ change while the tests run, so probe the binding only once.
+ """
+ core_pmu_probe()
+ return CORE_PMU_AVAILABLE
+
+def core_pmu_probe():
+ """
+ Ask perf itself, through its python binding, to parse and open 'cycles'
+ once: a parse failure or an ENOENT means no PMU can count it, any other
+ error means we can't tell whether there is one, and the number of events
+ it expanded to is the number of core PMUs, see
+ parse_events_multi_pmu_add(), which is what core_pmu_count() asks for.
+ """
+ global CORE_PMU_AVAILABLE, CORE_PMU_COUNT
+
+ if CORE_PMU_AVAILABLE is not None:
+ return
+
+ try:
+ import perf
+ parse_events = perf.parse_events
+ except (ImportError, AttributeError):
+ # Nothing to ask about it, let the expectations speak.
+ CORE_PMU_AVAILABLE = True
+ return
+
+ try:
+ evlist = parse_events('cycles')
+ except OSError:
+ # No PMU that can count it, the same as the ENOENT below.
+ CORE_PMU_AVAILABLE = False
+ return
+
+ CORE_PMU_COUNT = len(evlist)
+ try:
+ evlist.open()
+ except OSError as err:
+ # Anything else is a permission problem or alike, not our business here.
+ CORE_PMU_AVAILABLE = err.errno != errno.ENOENT
+ return
+ evlist.close()
+
+ CORE_PMU_AVAILABLE = True
+
+PERF_TYPE_HARDWARE = 0
+PERF_TYPE_HW_CACHE = 3
+PERF_SAMPLE_ID = 1 << 6
+PERF_SAMPLE_IDENTIFIER = 1 << 16
+PERF_TYPE_SOFTWARE = 1
+PERF_COUNT_SW_DUMMY = 9
+
+def is_dummy_tracking_event(event):
+ """
+ Is this the dummy event perf opens to carry the tracking events (mmap,
+ comm, task) when the requested ones can't, like when there is more than one
+ core PMU and each core PMU's events run on only part of the CPUs?
+ """
+ return (event.get('type') == str(PERF_TYPE_SOFTWARE) and
+ event.get('config') == str(PERF_COUNT_SW_DUMMY))
+
+# With more than one core PMU the tracking terms go to the dummy tracking event,
+# leaving the requested events without them, so they can't be matched there.
+TRACKING_TERMS = ['mmap', 'comm', 'task', 'mmap_data']
+
+# The legacy event types, where perf may qualify 'config' with the core PMU
+# type in its top 32 bits when there is more than one core PMU.
+LEGACY_TYPES = (str(PERF_TYPE_HARDWARE), str(PERF_TYPE_HW_CACHE))
+
+def mask_bits(value, bits):
+ """
+ Take the bits in 'bits' out of each of the '|'-separated alternatives in
+ 'value', keeping the rest of the expression, wildcards included.
+ """
+ masked = []
+
+ for alternative in value.split('|'):
+ try:
+ alternative = str(int(alternative, 0) & ~bits)
+ except ValueError:
+ pass
+ masked.append(alternative)
+
+ return '|'.join(masked)
+
+def hide_core_pmu_type(value):
+ """
+ The kernel can be told which core PMU a legacy type event is meant for by
+ putting its type in the top 32 bits of 'config', which perf only does when
+ there is more than one core PMU, so take those bits out to keep the
+ expectations written for the single core PMU case matching on
+ heterogeneous machines.
+ """
+ return mask_bits(value, 0xffffffff00000000)
+
+# The two sample id bits mean the same thing here: how the samples of the
+# events in the evlist get associated with the event that produced them.
+SAMPLE_ID_BITS = PERF_SAMPLE_ID | PERF_SAMPLE_IDENTIFIER
+
+# No real sample_type bit is this high, so it can stand for "either of the
+# sample id bits" when comparing the two sides.
+SAMPLE_ID_EITHER = 1 << 40
+
+PERF_FORMAT_GROUP = 1 << 3
+
+def core_pmu_count():
+ """
+ How many core PMUs does perf see? An event with no PMU named in it
+ becomes one event per core PMU, see parse_events_multi_pmu_add(), and
+ that is the expansion that leaves the clones for the other core PMUs
+ without any group members and that makes perf qualify 'config' with the
+ type of the PMU it is for, so this is the number of events perf makes of
+ 'cycles', from the probe core_pmu_available() shares.
+ """
+ core_pmu_probe()
+ return CORE_PMU_COUNT
+
+def single_core_pmu():
+ """
+ Does perf see only one core PMU? Asked for by the comparisons that need
+ it, not at import time, as a run may never get to one.
+ """
+ return core_pmu_count() == 1
+
+def hide_group_format(expected, recorded):
+ """
+ read_format's PERF_FORMAT_GROUP bit asks for the group members to be read
+ along with the leader, which aside from this bit is verified with the
+ fd/group_fd based group checking, and with more than one core PMU a group
+ whose members are pinned to one of them, say 'default_core', leaves the
+ clones for the other core PMUs without any members, so that bit isn't
+ there: whether it is there is a consequence of how the evlist got
+ expanded to more than one core PMU, not of what the expectations are
+ about, so take it out on both sides. On a machine with a single core PMU
+ there is nothing to expand, so the expectations keep verifying it.
+ """
+ if single_core_pmu():
+ return expected, recorded
+
+ return (mask_bits(expected, PERF_FORMAT_GROUP),
+ mask_bits(recorded, PERF_FORMAT_GROUP))
+
+def hide_sample_id(expected, recorded):
+ """
+ perf asks for a way to tell the samples apart, PERF_SAMPLE_IDENTIFIER where
+ available, PERF_SAMPLE_ID otherwise, see evlist__config(): whenever the
+ evlist has more than one event, and when asked to outright, with
+ --sample-identifier, by an auxtrace session, or by record__config() for a
+ pipe run, none of which depends on the sample types, as those only choose
+ between the two bits:
+ with more than one core PMU the dummy tracking events make the sample types
+ differ, so IDENTIFIER is asked for even for events whose expectations ask
+ for ID, written on a machine with a single core PMU.
+
+ So treat the two as the same thing: when the expectation asks for either
+ of them, ask for "an id of some sort" on both sides, and when the
+ expectation doesn't ask for any, take the bits out of what got recorded.
+ """
+ try:
+ asked = int(expected, 0) & SAMPLE_ID_BITS
+ except ValueError:
+ # The expectation is a wildcard or a set of alternatives.
+ return expected, recorded
+
+ try:
+ recorded_id_bits = int(recorded, 0) & SAMPLE_ID_BITS
+ except ValueError:
+ # The recorded side is a wildcard or a set of alternatives.
+ return expected, mask_bits(recorded, SAMPLE_ID_BITS)
+
+ if not asked:
+ if single_core_pmu():
+ return expected, recorded
+ return expected, mask_bits(recorded, SAMPLE_ID_BITS)
+
+ return (str(int(expected, 0) & ~SAMPLE_ID_BITS | SAMPLE_ID_EITHER),
+ str(int(recorded, 0) & ~SAMPLE_ID_BITS |
+ (SAMPLE_ID_EITHER if recorded_id_bits else 0)))
+
class Fail(Exception):
def __init__(self, test, msg):
self.msg = msg
@@ -36,17 +223,20 @@ class Fail(Exception):
return '\'%s\' - %s' % (self.test.path, self.msg)
class Notest(Exception):
- def __init__(self, test, arch):
- self.arch = arch
+ def __init__(self, test, reason):
+ self.reason = reason
self.test = test
def getMsg(self):
- return '[%s] \'%s\'' % (self.arch, self.test.path)
+ return '[%s] \'%s\'' % (self.reason, self.test.path)
class Unsup(Exception):
- def __init__(self, test):
+ def __init__(self, test, ret, expected):
self.test = test
+ self.ret = ret
+ self.expected = expected
def getMsg(self):
- return '\'%s\'' % self.test.path
+ return '\'%s\' exit %d, expected %s, no attrs recorded to verify' % (
+ self.test.path, self.ret, self.expected)
class Event(dict):
terms = [
@@ -101,12 +291,76 @@ class Event(dict):
self.add(base)
self.add(data)
+ def hide_core_pmu_type_terms(self, other):
+ """
+ Is this a legacy type event? Then the core PMU type that perf may have
+ put in the top 32 bits of 'config' to tell the kernel which core PMU it
+ wants isn't part of what we asked for, see hide_core_pmu_type().
+ """
+ for event in (self, other):
+ for type in event.get('type', '').split('|'):
+ if type in LEGACY_TYPES:
+ return True
+
+ return False
+
+ def hide_core_pmu_type(self, term, value, other):
+ if term == 'config' and self.hide_core_pmu_type_terms(other):
+ return hide_core_pmu_type(value)
+
+ return value
+
+ def hide_event_count_dependent_terms(self, term, expected, recorded):
+ if term == 'sample_type':
+ return hide_sample_id(expected, recorded)
+ if term == 'read_format':
+ return hide_group_format(expected, recorded)
+
+ return expected, recorded
+
+ def recorded_side(self, other):
+ """
+ Which one is what perf recorded? Both directions get compared, see
+ Test.compare(), so this can't be taken from the arguments' order.
+ """
+ if getattr(other, 'recorded', False):
+ return other
+
+ return self if getattr(self, 'recorded', False) else None
+
+ def hide_tracking_via_dummy(self, term, other):
+ """
+ The tracking terms are set on the tracking event, which is the dummy
+ event when there is more than one core PMU, leaving the requested
+ events without them, nothing to match on then.
+ """
+ if term not in TRACKING_TERMS:
+ return False
+
+ for event in (self, other):
+ if term in getattr(event, 'tracking_moved_to_dummy', ()):
+ return True
+
+ return False
+
def equal(self, other):
for t in Event.terms:
log.debug(" [%s] %s %s", t, self[t], other[t])
if t not in self or t not in other:
return False
- if not data_equal(self[t], other[t]):
+ if self.hide_tracking_via_dummy(t, other):
+ continue
+
+ recorded = self.recorded_side(other)
+ if recorded is self:
+ expected, got_event = other, self
+ else:
+ expected, got_event = self, other
+ exp = self.hide_core_pmu_type(t, expected[t], other)
+ got = self.hide_core_pmu_type(t, got_event[t], other)
+ if recorded is not None:
+ exp, got = self.hide_event_count_dependent_terms(t, exp, got)
+ if not data_equal(exp, got):
return False
return True
@@ -119,14 +373,30 @@ class Event(dict):
for t in Event.terms:
if t not in self or t not in other:
continue
- if not data_equal(self[t], other[t]):
- log.warning("expected %s=%s, got %s", t, self[t], other[t])
+ if self.hide_tracking_via_dummy(t, other):
+ continue
+
+ recorded = self.recorded_side(other)
+ if recorded is self:
+ expected, got_event = other, self
+ else:
+ expected, got_event = self, other
+ exp = self.hide_core_pmu_type(t, expected[t], other)
+ got = self.hide_core_pmu_type(t, got_event[t], other)
+ if recorded is not None:
+ exp, got = self.hide_event_count_dependent_terms(t, exp, got)
+ if not data_equal(exp, got):
+ log.warning("expected %s=%s, got %s", t, expected[t],
+ got_event[t])
def parse_version(version):
if not version:
return None
return [int(v) for v in version.split(".")[0:2]]
+def format_version(version):
+ return ".".join(str(v) for v in version)
+
# Test file description needs to have following sections:
# [config]
# - just single instance in file
@@ -138,6 +408,9 @@ def parse_version(version):
# 'arch' - architecture specific test (optional)
# comma separated list, ! at the beginning
# negates it.
+# 'requires' - Comma separated list of things the test needs, it is
+# skipped when any of them isn't there, currently:
+# core-pmu - the machine can count hardware events.
# 'auxv' - Truthy statement that is evaled in the scope of the auxv map. When false,
# the test is skipped. For example 'auxv["AT_HWCAP"] == 10'. (optional)
# 'kernel_since' - Inclusive kernel version from which the test will start running. Only the
@@ -173,6 +446,7 @@ class Test(object):
self.arch = ''
self.auxv = parser.get('config', 'auxv', fallback=None)
+ self.requires = parser.get('config', 'requires', fallback='')
self.kernel_since = parse_version(parser.get('config', 'kernel_since', fallback=None))
self.kernel_until = parse_version(parser.get('config', 'kernel_until', fallback=None))
self.expect = {}
@@ -188,13 +462,17 @@ class Test(object):
def skip_test_kernel_since(self):
if not self.kernel_since:
- return False
- return not self.kernel_since <= parse_version(platform.release())
+ return None
+ if self.kernel_since <= parse_version(platform.release()):
+ return None
+ return "kernel older than %s" % format_version(self.kernel_since)
def skip_test_kernel_until(self):
if not self.kernel_until:
- return False
- return not parse_version(platform.release()) < self.kernel_until
+ return None
+ if parse_version(platform.release()) < self.kernel_until:
+ return None
+ return "kernel newer than %s" % format_version(self.kernel_until)
def skip_test_auxv(self):
def new_auxv(a, pattern):
@@ -210,18 +488,20 @@ class Test(object):
return (items[0], value)
if not self.auxv:
- return False
+ return None
auxv = subprocess.check_output("LD_SHOW_AUXV=1 sleep 0", shell=True) \
.decode(sys.stdout.encoding)
pattern = re.compile(r"[: ]+")
auxv = dict([new_auxv(a, pattern) for a in auxv.splitlines()])
- return not eval(self.auxv)
+ if eval(self.auxv):
+ return None
+
+ return "the auxv condition doesn't hold here"
def skip_test_arch(self, myarch):
# If architecture not set always run test
if self.arch == '':
- # log.warning("test for arch %s is ok" % myarch)
- return False
+ return None
# Allow multiple values in assignment separated by ','
arch_list = self.arch.split(',')
@@ -231,16 +511,26 @@ class Test(object):
arch_list[0] = arch_list[0][1:]
log.warning("excluded architecture list %s", arch_list)
for arch_item in arch_list:
- # log.warning("test for %s arch is %s" % (arch_item, myarch))
if arch_item == myarch:
- return True
- return False
+ return "arch = %s, machine = %s" % (self.arch, myarch)
+ return None
for arch_item in arch_list:
- # log.warning("test for architecture '%s' current '%s'" % (arch_item, myarch))
if arch_item == myarch:
- return False
- return True
+ return None
+
+ return "arch = %s, machine = %s" % (self.arch, myarch)
+
+ def skip_test_requires(self):
+ for requirement in filter(None, map(str.strip, self.requires.split(','))):
+ if requirement == 'core-pmu':
+ if not core_pmu_available():
+ log.warning("test limitation 'requires %s'", requirement)
+ return "needs a core PMU, none here"
+ else:
+ log.warning("unknown requirement '%s', ignored", requirement)
+
+ return None
def restore_sample_rate(self, value=10000):
try:
@@ -284,17 +574,36 @@ class Test(object):
def run_cmd(self, tempdir):
_junk1, _junk2, _junk3, _junk4, myarch = (os.uname())
- if self.skip_test_arch(myarch):
- raise Notest(self, myarch)
-
- if self.skip_test_auxv():
- raise Notest(self, "auxv skip")
-
- if self.skip_test_kernel_since():
- raise Notest(self, "old kernel skip")
-
- if self.skip_test_kernel_until():
- raise Notest(self, "new kernel skip")
+ arch = self.skip_test_arch(myarch)
+ if arch:
+ log.debug(" detail: the test file has arch = %s, the machine reports %s",
+ self.arch, myarch)
+ raise Notest(self, arch)
+
+ auxv = self.skip_test_auxv()
+ if auxv:
+ log.debug(" detail: the auxv condition %s doesn't hold, the bits come "
+ "from the LD_SHOW_AUXV=1 output", self.auxv)
+ raise Notest(self, auxv)
+
+ kernel = self.skip_test_kernel_since()
+ if kernel:
+ log.debug(" detail: the test file's kernel_since = %s, this kernel is %s",
+ format_version(self.kernel_since), platform.release())
+ raise Notest(self, kernel)
+
+ kernel = self.skip_test_kernel_until()
+ if kernel:
+ log.debug(" detail: the test file's kernel_until = %s, this kernel is %s",
+ format_version(self.kernel_until), platform.release())
+ raise Notest(self, kernel)
+
+ requirement = self.skip_test_requires()
+ if requirement:
+ log.debug(" detail: requires core-pmu, nothing countable here, check "
+ "kernel.perf_event_paranoid and the PMU nodes under "
+ "/sys/bus/event_source/")
+ raise Notest(self, requirement)
self.restore_sample_rate()
cmd = "PERF_TEST_ATTR=%s %s %s -o %s/perf.data %s" % (tempdir,
@@ -304,12 +613,80 @@ class Test(object):
log.info(" '%s' ret '%s', expected '%s'", cmd, str(ret), str(self.ret))
if not data_equal(str(ret), str(self.ret)):
+ plain_cmd = "%s %s -o /dev/null %s" % (
+ self.perf, self.command,
+ self.args.split('>/dev/null')[0].rstrip())
if self.test_ret:
raise Fail(self, "Perf exit code failure")
else:
- raise Unsup(self)
+ log.debug(" detail: '%s' exited with %d, the expectations want the "
+ "workload's %s exit, so there are no attrs recorded to "
+ "verify; this is normally a capability problem, run '%s' "
+ "to see why", plain_cmd, ret, self.ret, plain_cmd)
+ raise Unsup(self, ret, str(self.ret))
+
+ def drop_dummy_tracking_events(self):
+ """
+ Leave out the dummy event perf itself opened, see
+ is_dummy_tracking_event(), unless the expectations ask for one.
+ """
+ if any(is_dummy_tracking_event(e) for e in self.expect.values()):
+ return
+
+ if not any(is_dummy_tracking_event(e) for e in self.result.values()):
+ return
+
+ moved = set()
+ for name, event in list(self.result.items()):
+ if is_dummy_tracking_event(event):
+ log.debug(" dropping dummy tracking event [%s]", name)
+ moved.update(t for t in TRACKING_TERMS if event.get(t) == '1')
+ del self.result[name]
+
+ # The tracking terms the dropped dummies carried, flag the remaining
+ # events so that the match leaves only those out.
+ for event in self.result.values():
+ event.tracking_moved_to_dummy = moved
+
+ def drop_other_core_pmu_events(self, devices='/sys/bus/event_source/devices'):
+ """
+ Leave out the events of the other core PMUs, which have their own
+ encodings, when the expectations name one of the core PMUs.
+ """
+ asked = set()
+ for event in self.expect.values():
+ types = event.get('type', '').split('|')
+ if '*' in types:
+ return
+ asked.update(types)
- def compare(self, expect, result):
+ try:
+ names = os.listdir(devices)
+ except OSError:
+ return
+
+ core = set()
+ for name in names:
+ path = os.path.join(devices, name)
+ has_cpus = os.path.exists(os.path.join(path, 'cpus'))
+ if name not in ('cpu', 'cpum_cf') and not has_cpus:
+ continue
+ try:
+ with open(os.path.join(path, 'type'), encoding='utf-8') as f:
+ core.add(f.read().strip())
+ except OSError:
+ pass
+
+ if not core & asked:
+ return
+
+ others = core - asked
+ for name, event in list(self.result.items()):
+ if event.get('type') in others:
+ log.debug(" dropping other core PMU event [%s]", name)
+ del self.result[name]
+
+ def compare(self, expect, result, strict_groups=True):
match = {}
log.debug(" compare")
@@ -343,8 +720,9 @@ class Test(object):
match[exp_name] = exp_list
- # For each defined group in the expected events
- # check we match the same group in the result.
+ # Check that each expected group is matched in the result; an event
+ # opened in a group may match an expectation that doesn't declare
+ # one, as those only verify the event attributes.
for exp_name, exp_event in expect.items():
group = exp_event.group
@@ -353,6 +731,8 @@ class Test(object):
for res_name in match[exp_name]:
res_group = result[res_name].group
+ if res_group == '' and not strict_groups:
+ continue
if res_group not in match[group]:
raise Fail(self, 'group failure')
@@ -384,6 +764,11 @@ class Test(object):
log.debug(" loading result events")
for f in glob.glob(tempdir + '/event*'):
self.load_events(f, self.result)
+ for event in self.result.values():
+ event.recorded = True
+
+ self.drop_dummy_tracking_events()
+ self.drop_other_core_pmu_events()
# resolve group_fd to event names
self.resolve_groups(self.expect)
@@ -391,7 +776,7 @@ class Test(object):
# do the expectation - results matching - both ways
self.compare(self.expect, self.result)
- self.compare(self.result, self.expect)
+ self.compare(self.result, self.expect, strict_groups=False)
finally:
# cleanup