From 4bb8690199ab0cc985528c232584452ea799c2d5 Mon Sep 17 00:00:00 2001 From: Brendan Jackman Date: Wed, 22 Mar 2017 17:36:43 +0000 Subject: [PATCH 1/6] juno_energy: Re-order clusters so the lower-numbered CPUs come first --- libs/utils/platforms/juno_energy.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/libs/utils/platforms/juno_energy.py b/libs/utils/platforms/juno_energy.py index 49c75b4a1..cb2e5afe7 100644 --- a/libs/utils/platforms/juno_energy.py +++ b/libs/utils/platforms/juno_energy.py @@ -93,16 +93,16 @@ def a57_cpu_node(cpu): juno_energy = EnergyModel( root_node=EnergyModelRoot( children=[ - EnergyModelNode( - name="cluster_a57", - active_states=a57_cluster_active_states, - idle_states=a57_cluster_idle_states, - children=[a57_cpu_node(c) for c in a57s]), EnergyModelNode( name="cluster_a53", active_states=a53_cluster_active_states, idle_states=a53_cluster_idle_states, - children=[a53_cpu_node(c) for c in a53s])]), + children=[a53_cpu_node(c) for c in a53s]), + EnergyModelNode( + name="cluster_a57", + active_states=a57_cluster_active_states, + idle_states=a57_cluster_idle_states, + children=[a57_cpu_node(c) for c in a57s])]), root_power_domain=PowerDomain(idle_states=[], children=[ PowerDomain( idle_states=["cluster-sleep-0"], -- GitLab From 5e8ea271bc91518e797dd6c64c0b3d908a89f9fe Mon Sep 17 00:00:00 2001 From: Brendan Jackman Date: Wed, 22 Mar 2017 19:03:00 +0000 Subject: [PATCH 2/6] hikey_energy: Fix off-by-one power number This is just to align LISA's data with the energy model that's been deployed. --- libs/utils/platforms/hikey_energy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/utils/platforms/hikey_energy.py b/libs/utils/platforms/hikey_energy.py index 7f3cea7f8..823d6155d 100644 --- a/libs/utils/platforms/hikey_energy.py +++ b/libs/utils/platforms/hikey_energy.py @@ -35,7 +35,7 @@ cluster_idle_states = OrderedDict([ cpu_active_states = OrderedDict([ ( 208000, ActiveState(capacity=178, power=69)), - ( 432000, ActiveState(capacity=369, power=125)), + ( 432000, ActiveState(capacity=369, power=124)), ( 729000, ActiveState(capacity=622, power=224)), ( 960000, ActiveState(capacity=819, power=367)), (1200000, ActiveState(capacity=1024, power=670)) -- GitLab From 4cbdeff26d87053fbd6c4c057aff15ec6a24a4e7 Mon Sep 17 00:00:00 2001 From: Brendan Jackman Date: Wed, 22 Mar 2017 18:15:50 +0000 Subject: [PATCH 3/6] juno_energy: Fix max capacity --- libs/utils/platforms/juno_energy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/utils/platforms/juno_energy.py b/libs/utils/platforms/juno_energy.py index cb2e5afe7..69c7b7e46 100644 --- a/libs/utils/platforms/juno_energy.py +++ b/libs/utils/platforms/juno_energy.py @@ -74,7 +74,7 @@ a57_cpu_active_states = OrderedDict([ (625000, ActiveState(capacity=579, power=251)), (800000, ActiveState(capacity=744, power=359)), (950000, ActiveState(capacity=883, power=479)), - (1100000, ActiveState(capacity=1024, power=616)), + (1100000, ActiveState(capacity=1023, power=616)), ]) a57_cpu_idle_states = OrderedDict([ -- GitLab From aead1dbf4b8e09f0189ec66c458c75fe1c1d6013 Mon Sep 17 00:00:00 2001 From: Brendan Jackman Date: Wed, 22 Mar 2017 15:55:37 +0000 Subject: [PATCH 4/6] energy_model: Add method for building EnergyModel from target data --- libs/utils/energy_model.py | 199 ++++++++++++++++++++++++++++++++++++- 1 file changed, 197 insertions(+), 2 deletions(-) diff --git a/libs/utils/energy_model.py b/libs/utils/energy_model.py index 7517dd24c..afeacce7e 100644 --- a/libs/utils/energy_model.py +++ b/libs/utils/energy_model.py @@ -15,18 +15,44 @@ # limitations under the License. # -from collections import namedtuple +from collections import namedtuple, OrderedDict from itertools import product import logging import operator +import re import pandas as pd import numpy as np -from devlib.utils.misc import memoized +from devlib.utils.misc import memoized, mask_to_list +from devlib import TargetError """Classes for modeling and estimating energy usage of CPU systems""" +def read_multiple_oneline_files(target, glob_patterns): + """ + Quickly read many single-line files that match a glob pattern + + Finds all the files that match any of the glob patterns and, assuming that + they each contain exactly 1 line of text, read them all at once. When the + target or connection is slow this saves a lot of time when reading a large + number of files. + + :param target: devlib target object to read from + :param glob_pattern: Unix glob pattern matching the files to read + :returns: A dictionary mapping matched paths to the values read. ``{}`` if + no paths matched the globs. + """ + try: + paths = target.execute('find ' + ' '.join(glob_patterns)).split() + except TargetError: + return {} + + cmd = 'cat ' + ' '.join(glob_patterns) + contents = target.execute(cmd).splitlines() + + return dict(zip(paths, contents)) + class EnergyModelCapacityError(Exception): """Used by :meth:`EnergyModel.get_optimal_placements`""" pass @@ -664,3 +690,172 @@ class EnergyModel(object): self._log.debug('%14s - Done', 'EnergyModel') return ret + + @classmethod + def _find_core_groups(cls, target): + """ + Read the core_siblings masks for each CPU from sysfs + + :param target: Devlib Target object to read masks from + :returns: A list of tuples of ints, representing the partition of core + siblings + """ + cpus = range(target.number_of_cpus) + + topology_base = '/sys/devices/system/cpu/' + + # We only care about core_siblings, but let's check *_siblings, so we + # can throw an error if a CPU's thread_siblings isn't just itself, or if + # there's a topology level we don't understand. + + # Since we might have to read a lot of files, read everything we need in + # one go to avoid taking too long. + mask_glob = topology_base + 'cpu**/topology/*_siblings' + file_values = read_multiple_oneline_files(target, [mask_glob]) + + regex = re.compile( + topology_base + r'cpu([0-9]+)/topology/([a-z]+)_siblings') + + ret = set() + + for path, mask_str in file_values.iteritems(): + match = regex.match(path) + cpu = int(match.groups()[0]) + level = match.groups()[1] + # mask_to_list returns the values in descending order, so we'll sort + # them ascending. This isn't strictly necessary but it's nicer. + siblings = tuple(sorted(mask_to_list(int(mask_str, 16)))) + + if level == 'thread': + if siblings != (cpu,): + # SMT systems aren't supported + raise RuntimeError('CPU{} thread_siblings is {}. ' + 'expected {}'.format(cpu, siblings, [cpu])) + continue + if level != 'core': + # The only other levels we should expect to find are 'book' and + # 'shelf', which are not used by architectures we support. + raise RuntimeError( + 'Unrecognised topology level "{}"'.format(level)) + + ret.add(siblings) + + # Sort core groups so that the lowest-numbered cores are first + # Again, not strictly necessary, just more pleasant. + return sorted(ret, key=lambda x: x[0]) + + @classmethod + def from_target(cls, target): + """ + Create an EnergyModel by reading a target filesystem + + This uses the sysctl added by EAS pathces to exposes the cap_states and + idle_states fields for each sched_group. This feature depends on + CONFIG_SCHED_DEBUG, and is not upstream in mainline Linux (as of v4.11), + so this method is only tested with Android kernels. + + The kernel doesn't have an power domain data, so this method assumes + that all CPUs are totally independent wrt. idle states - the EnergyModel + constructed won't be aware of the topological dependencies for entering + "cluster" idle states. + + Assumes the energy model has two-levels (plus the root) - a level for + CPUs and a level for 'clusters'. + + :param target: Devlib target object to read filesystem from. Must have + cpufreq and cpuidle modules enabled. + :returns: Constructed EnergyModel object based on the parameters + reported by the target. + """ + if 'cpufreq' not in target.modules: + raise TargetError('Requires cpufreq devlib module. Please ensure ' + '"cpufreq" is listed in your target/test modules') + if 'cpuidle' not in target.modules: + raise TargetError('Requires cpuidle devlib module. Please ensure ' + '"cpuidle" is listed in your target/test modules') + + def sge_path(cpu, domain, group, field): + f = '/proc/sys/kernel/sched_domain/cpu{}/domain{}/group{}/energy/{}' + return f.format(cpu, domain, group, field) + + # Read all the files we might need in one go, otherwise this will take + # ages. + sge_globs = [sge_path('**', '**', '**', 'cap_states'), + sge_path('**', '**', '**', 'idle_states')] + sge_file_values = read_multiple_oneline_files(target, sge_globs) + + if not sge_file_values: + raise TargetError('Energy Model not exposed in sysfs. ' + 'Check CONFIG_SCHED_DEBUG is enabled.') + + # These functions read the cap_states and idle_states vectors for the + # first sched_group in the sched_domain for a given CPU at a given + # level. That first group will include the given CPU. So + # read_active_states(0, 0) will give the CPU-level active_states for + # CPU0 and read_active_states(0, 1) will give the "cluster"-level + # active_states for the "cluster" that contains CPU0. + + def read_active_states(cpu, domain_level): + cap_states_path = sge_path(cpu, domain_level, 0, 'cap_states') + cap_states_strs = sge_file_values[cap_states_path].split() + # cap_states lists the capacity of each state followed by its power, + # in increasing order. The `zip` call does this: + # [c0, p0, c1, p1, c2, p2] -> [(c0, p0), (c1, p1), (c2, p2)] + cap_states = [ActiveState(capacity=int(c), power=int(p)) + for c, p in zip(cap_states_strs[0::2], + cap_states_strs[1::2])] + freqs = target.cpufreq.list_frequencies(cpu) + return OrderedDict(zip(sorted(freqs), cap_states)) + + def read_idle_states(cpu, domain_level): + idle_states_path = sge_path(cpu, domain_level, 0, 'idle_states') + idle_states_strs = sge_file_values[idle_states_path].split() + # get_states should return the state names in increasing depth order + names = [s.name for s in target.cpuidle.get_states(cpu)] + # idle_states is a list of power values in increasing order of + # idle-depth/decreasing order of power. + return OrderedDict(zip(names, [int(p) for p in idle_states_strs])) + + # Read the CPU-level data from sched_domain level 0 + cpus = range(target.number_of_cpus) + cpu_nodes = [] + for cpu in cpus: + node = EnergyModelNode( + cpu=cpu, + active_states=read_active_states(cpu, 0), + idle_states=read_idle_states(cpu, 0)) + cpu_nodes.append(node) + + # Read the "cluster" level data from sched_domain level 1 + core_group_nodes = [] + for core_group in cls._find_core_groups(target): + node=EnergyModelNode( + children=[cpu_nodes[c] for c in core_group], + active_states=read_active_states(core_group[0], 1), + idle_states=read_idle_states(core_group[0], 1)) + core_group_nodes.append(node) + + root = EnergyModelRoot(children=core_group_nodes) + + # Use cpufreq to figure out the frequency domains + freq_domains = [] + remaining_cpus = set(cpus) + while remaining_cpus: + cpu = next(iter(remaining_cpus)) + dom = target.cpufreq.get_domain_cpus(cpu) + freq_domains.append(dom) + remaining_cpus = remaining_cpus.difference(dom) + + # We don't have a way to read the power domains from sysfs (the kernel + # isn't even aware of them) so we'll just have to assume each CPU is its + # own power domain and all idle states are independent of each other. + cpu_pds = [] + for cpu in cpus: + names = [s.name for s in target.cpuidle.get_states(cpu)] + cpu_pds.append(PowerDomain(cpu=cpu, idle_states=names)) + + root_pd=PowerDomain(children=cpu_pds, idle_states=[]) + + return cls(root_node=root, + root_power_domain=root_pd, + freq_domains=freq_domains) -- GitLab From a9589d7b10137a0bbc978e681b3bcfe03fa33488 Mon Sep 17 00:00:00 2001 From: Brendan Jackman Date: Thu, 23 Mar 2017 12:20:02 +0000 Subject: [PATCH 5/6] TestEnv: Use EnergyModel.from_target where we don't have a built-in EnergyModel --- libs/utils/env.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/libs/utils/env.py b/libs/utils/env.py index 487ef0d02..3ab72dd20 100644 --- a/libs/utils/env.py +++ b/libs/utils/env.py @@ -27,11 +27,12 @@ import unittest import devlib from devlib.utils.misc import memoized -from devlib import Platform +from devlib import Platform, TargetError from trappy.stats.Topology import Topology from wlgen import RTA from energy import EnergyMeter +from energy_model import EnergyModel from conf import JsonConf from platforms.juno_energy import juno_energy from platforms.hikey_energy import hikey_energy @@ -404,6 +405,7 @@ class TestEnv(ShareState): ######################################################################## # Setup board default if not specified by configuration + self.nrg_model = None platform = None self.__modules = [] if 'board' not in self.conf: @@ -545,6 +547,13 @@ class TestEnv(ShareState): raise RuntimeError('Failed to initialized [{}] module, ' 'update your kernel or test configurations'.format(module)) + if not self.nrg_model: + try: + self._log.info('Attempting to read energy model from target') + self.nrg_model = EnergyModel.from_target(self.target) + except (TargetError, RuntimeError) as e: + self._log.error("Couldn't read target energy model: %s", e) + def install_tools(self, tools): """ Install tools additional to those specified in the test config 'tools' -- GitLab From e4e4b7cc8831a7c7fb96b2cf3dc8bc4d476b4607 Mon Sep 17 00:00:00 2001 From: Brendan Jackman Date: Thu, 23 Mar 2017 13:29:04 +0000 Subject: [PATCH 6/6] tests/eas/generic: Check and error if TestEnv.nrg_model not present --- tests/eas/generic.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/eas/generic.py b/tests/eas/generic.py index 35b6ca6bb..19443e9d0 100644 --- a/tests/eas/generic.py +++ b/tests/eas/generic.py @@ -22,10 +22,12 @@ import pandas as pd from bart.common.Utils import area_under_curve -from energy_model import EnergyModelCapacityError +from energy_model import EnergyModel, EnergyModelCapacityError from perf_analysis import PerfAnalysis from test import LisaTest, experiment_test from trace import Trace +from . import _EasTest, energy_aware_conf, WORKLOAD_PERIOD_MS +from unittest import SkipTest energy_aware_conf = { "tag" : "energy_aware", @@ -78,7 +80,16 @@ class _EnergyModelTest(LisaTest): super(_EnergyModelTest, cls).runExperiments(*args, **kwargs) @classmethod - def _getExperimentsConf(cls, *args, **kwargs): + def _getExperimentsConf(cls, test_env): + if not test_env.nrg_model: + try: + test_env.nrg_model = EnergyModel.from_target(test_env.target) + except Exception as e: + raise SkipTest( + 'This test requires an EnergyModel for the platform. ' + 'Either provide one manually or ensure it can be read ' + 'from the filesystem: {}'.format(e)) + return { 'wloads' : cls.workloads, 'confs' : [energy_aware_conf] -- GitLab