diff options
Diffstat (limited to 'tools/perf/tests/shell/lib')
| -rw-r--r-- | tools/perf/tests/shell/lib/attr.py | 461 |
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 |
