• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# SPDX-License-Identifier: Apache-2.0
2#
3# Copyright (C) 2016, ARM Limited and contributors.
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16#
17
18from collections import namedtuple, OrderedDict
19from itertools import product
20import logging
21import operator
22import re
23
24import pandas as pd
25import numpy as np
26
27from devlib.utils.misc import memoized, mask_to_list
28from devlib import TargetError
29
30"""Classes for modeling and estimating energy usage of CPU systems"""
31
32def read_multiple_oneline_files(target, glob_patterns):
33    """
34    Quickly read many single-line files that match a glob pattern
35
36    Finds all the files that match any of the glob patterns and, assuming that
37    they each contain exactly 1 line of text, read them all at once. When the
38    target or connection is slow this saves a lot of time when reading a large
39    number of files.
40
41    This will only work safely on stationary files, don't try to use it where
42    the glob expansion will change often - for example /proc/**/autogroup would
43    not work because /proc/ entries will likely appear & disappear while we're
44    reading them.
45
46    :param target: devlib target object to read from
47    :param glob_pattern: Unix glob pattern matching the files to read
48    :returns: A dictionary mapping matched paths to the values read. ``{}`` if
49              no paths matched the globs.
50    """
51    find_cmd = 'find ' + ' '.join(glob_patterns)
52    try:
53        paths = target.execute(find_cmd).split()
54    except TargetError:
55        return {}
56
57    cmd = '{} | {} xargs cat'.format(find_cmd, target.busybox)
58    contents = target.execute(cmd).splitlines()
59
60    if len(contents) != len(paths):
61        raise RuntimeError('File count mismatch while reading multiple files')
62
63    return dict(zip(paths, contents))
64
65class EnergyModelCapacityError(Exception):
66    """Used by :meth:`EnergyModel.get_optimal_placements`"""
67    pass
68
69class ActiveState(namedtuple('ActiveState', ['capacity', 'power'])):
70    """Represents power and compute capacity at a given frequency
71
72    :param capacity: Relative compute capacity at frequency
73    :param power: Power usage at frequency
74    """
75    def __new__(cls, capacity=None, power=None):
76        return super(ActiveState, cls).__new__(cls, capacity, power)
77
78class _CpuTree(object):
79    """Internal class. Abstract representation of a CPU topology.
80
81    Each node contains either a single CPU or a set of child nodes.
82    """
83    def __init__(self, cpu, children):
84        if (cpu is None) == (children is None):
85            raise ValueError('Provide exactly one of: cpu or children')
86
87        self.parent = None
88        self.cpu = cpu
89
90        if cpu is not None:
91            self.cpus = (cpu,)
92            self.children = []
93        else:
94            if len(children) == 0:
95                raise ValueError('children cannot be empty')
96            self.cpus = tuple(sorted(set().union(*[n.cpus for n in children])))
97            self.children = children
98            for child in children:
99                child.parent = self
100
101        self.name = None
102
103    def __repr__(self):
104        name_bit = ''
105        if self.name:
106            name_bit = 'name="{}", '.format(self.name)
107
108        if self.children:
109            return '{}({}children={})'.format(
110                self.__class__.__name__, name_bit, self.children)
111        else:
112            return '{}({}cpus={})'.format(
113                self.__class__.__name__, name_bit, self.cpus)
114
115    def _iter(self, include_non_leaves):
116        for child in self.children:
117            for child_i in child._iter(include_non_leaves):
118                yield child_i
119        if include_non_leaves or not self.children:
120            yield self
121
122    def iter_nodes(self):
123        """Iterate over nodes depth-first, post-order"""
124        return self._iter(True)
125
126    def iter_leaves(self):
127        """Iterate over leaves"""
128        return self._iter(False)
129
130class EnergyModelNode(_CpuTree):
131    """Describes topology and energy data for an EnergyModel.
132
133    Represents a CPU topology with energy data. The active and idle state data
134    represents the power usage of just the hardware resources of this topology
135    level, not its children. e.g. If the node represents a cluster, the power
136    numbers should not include power used by the CPU - that power should be
137    included the data of the child nodes.
138
139    Exactly one of ``cpu`` and ``children`` must be given.
140
141    :param active_states: Dict mapping frequencies to :class:`ActiveState`
142                          values. Compute capacity data is optional for
143                          non-leaf nodes.
144    :param idle_states: Dict mapping idle state names to power usage values
145    :param cpu: The CPU this node represents. If provided, this is a leaf node.
146    :type cpus: tuple(int)
147    :param children: Non-empty list of child :class:`EnergyModelNode` objects
148    :param name: Optional human-readable name for this node. Leaf (CPU) nodes
149                 have a default name of "cpuN" where N is the cpu number.
150
151    :ivar cpus: CPUs contained in this node. Includes those of child nodes.
152    :ivar cpu: For convenience, this holds the single CPU contained by leaf
153               nodes. ``None`` for non-leaf nodes.
154    """
155    def __init__(self, active_states, idle_states,
156                 cpu=None, children=None, name=None):
157        super(EnergyModelNode, self).__init__(cpu, children)
158
159        self._log = logging.getLogger('EnergyModel')
160
161        def is_monotonic(l, decreasing=False):
162            op = operator.ge if decreasing else operator.le
163            return all(op(a, b) for a, b in zip(l, l[1:]))
164
165        if active_states:
166            # Sanity check for active_states's frequencies
167            freqs = active_states.keys()
168            if not is_monotonic(freqs):
169                self._log.warning(
170                    'Active states frequencies are expected to be '
171                    'monotonically increasing. Freqs: {}'.format(freqs))
172
173            # Sanity check for active_states's powers
174            power_vals = [s.power for s in active_states.values()]
175            if not is_monotonic(power_vals):
176                self._log.warning(
177                    'Active states powers are expected to be '
178                    'monotonically increasing. Values: {}'.format(power_vals))
179
180        # Sanity check for idle_states powers
181        if idle_states:
182            power_vals = idle_states.values()
183            if not is_monotonic(power_vals, decreasing=True):
184                self._log.warning(
185                    'Idle states powers are expected to be '
186                    'monotonically decreasing. Values: {}'.format(power_vals))
187
188        if cpu is not None and not name:
189            name = 'cpu' + str(cpu)
190
191        self.name = name
192        self.active_states = active_states
193        self.idle_states = idle_states
194
195    @property
196    def max_capacity(self):
197        """Compute capacity at highest frequency"""
198        return max(s.capacity for s in self.active_states.values())
199
200class EnergyModelRoot(EnergyModelNode):
201    """
202    Convenience class for root of an EnergyModelNode tree.
203
204    Just like EnergyModelNode except that ``active_states`` and ``idle_states``
205    aren't required.
206    """
207    def __init__(self, active_states=None, idle_states=None,
208                 cpu=None, children=None, name=None):
209        return super(EnergyModelRoot, self).__init__(
210            active_states, idle_states, cpu, children, name)
211
212class PowerDomain(_CpuTree):
213    """Describes the power domain hierarchy for an EnergyModel.
214
215    Power domains are a description of the topological dependencies in hardware
216    for entering idle states. "Composite" states such as cluster-sleep states
217    require a set of CPUs to all be idle before that state can be entered. In
218    that case those CPUs can be grouped into a power domain, and that composite
219    state attached to the power domain. Note that cpuidle is not aware of these
220    dependencies; they are typically handled by the platform firmware.
221
222    Exactly one of ``cpu`` and ``children`` must be given. That is, leaves of
223    the PowerDomain tree always contain exactly one CPU - each CPU is
224    represented as being in a power domain of its own. This represents the
225    assumption that all CPUs have at least one idle state (such as ARM WFI) that
226    they can enter independently of other CPUs.
227
228    :param idle_states: List of names of idle states for this power domain. Does
229                        not store power data - these names are used as keys into
230                        the ``idle_states`` field of :class:`EnergyModelNode`
231                        objects.
232    :type idle_states: list(str)
233    :param cpu: The CPU this node represents. If provided, this is a leaf node.
234    :type cpu:  int
235    :param children: Non-empty list of child :class:`PowerDomain` objects
236    :type children:  list(PowerDomain)
237
238    :ivar cpus: CPUs contained in this node. Includes those of child nodes.
239    :type cpus: tuple(int)
240    """
241    def __init__(self, idle_states, cpu=None, children=None):
242        if idle_states is None:
243            raise ValueError('idle_states cannot be None (but may be empty)')
244        super(PowerDomain, self).__init__(cpu, children)
245        self.idle_states = idle_states
246
247class EnergyModel(object):
248    """Represents hierarchical CPU topology with power and capacity data
249
250    An energy model consists of
251
252    - A CPU topology, representing the physical (cache/interconnect) topology of
253      the CPUs.  Each node stores the energy usage of that node's hardware when
254      it is in each active or idle state. They also store a compute capacity at
255      each frequency, but this is only meaningful for leaf nodes (CPUs) and may
256      be None at higher levels. These capacity values are relative; the maximum
257      capacity would usually be 1024, the value of SCHED_CAPACITY_SCALE in the
258      Linux kernel scheduler.  Use EnergyModelNodes to describe this.
259
260    - A power domain topology, representing the hierarchy of areas that can be
261      powered down (idled).
262      The power domains are a single tree. Leaf nodes must contain exactly one
263      CPU and the root node must indirectly contain every CPU. Each power domain
264      has a list (maybe empty) of names of idle states that that domain can
265      enter.
266      Use PowerDomains to describe this.
267
268    - A set of frequency domains, representing groups of CPUs whose clock
269      frequencies must be equal (probably because they share a clock). The
270      frequency domains must be a partition of the CPUs.
271
272    :ivar cpu_nodes: List of leaf (CPU) :class:`EnergyModelNode`
273    :ivar cpus: List of logical CPU numbers in the system
274
275    :param root_node: Root of :class:`EnergyModelNode` tree
276    :param root_power_domain: Root of :class:`PowerDomain` tree
277    :param freq_domains: Collection of collections of logical CPU numbers
278                         representing frequency (clock) domains.
279
280    .. note::
281      The most signficant shortcomings of the model are:
282
283        1. Voltage domains are assumed to be congruent to frequency domains
284
285        2. Idle state power is assumed to be independent of voltage
286
287        3. Temperature is ignored entirely
288
289    .. _cpu-utils:
290
291    .. admonition:: ``cpu_utils``: CPU util distributions
292
293        Used throughout this module: A ``cpu_utils`` is a list ``u`` where
294        ``u[N]`` is the sum of the frequency-invariant, capacity-invariant
295        utilization of tasks placed on CPU N. That is, the quantity represented
296        by a CPU runqueue's util_avg in the Linux kernel scheduler's
297        load-tracking system with EAS features enabled.
298
299        The range of utilization values is 0 -
300        :attr:`EnergyModel.capacity_scale`.
301
302        This represents a static utilization, assuming that tasks don't change
303        in size (for example representing a set of fixed periodic RT-App
304        workloads). For workloads that change over time, a series of
305        ``cpu_utils`` items would be needed to describe the utilization, with a
306        distinct estimation for each item in the series.
307    """
308
309    capacity_scale = 1024
310    """The relative computational capacity of the most powerful CPU at its
311    highest available frequency.
312    """
313
314    def __init__(self, root_node, root_power_domain, freq_domains):
315        self.cpus = root_node.cpus
316        if self.cpus != tuple(range(len(self.cpus))):
317            raise ValueError('CPU IDs [{}] are sparse'.format(self.cpus))
318
319        # Check that freq_domains is a partition of the CPUs
320        fd_intersection = set().intersection(*freq_domains)
321        if fd_intersection:
322            raise ValueError('CPUs {} exist in multiple freq domains'.format(
323                fd_intersection))
324        fd_difference = set(self.cpus) - set().union(*freq_domains)
325        if fd_difference:
326            raise ValueError('CPUs {} not in any frequency domain'.format(
327                fd_difference))
328        self.freq_domains = freq_domains
329
330        # Check that nodes with energy data are all within a frequency domain
331        for node in root_node.iter_nodes():
332            if not node.active_states or node.idle_states:
333                continue
334            cpu_freq_doms = []
335            for cpu in node.cpus:
336                [cpu_freq_dom] = [d for d in freq_domains if cpu in d]
337                cpu_freq_doms.append(cpu_freq_dom)
338            if not all(d == cpu_freq_doms[0] for d in cpu_freq_doms[1:]):
339                raise ValueError(
340                    'Node {} (CPUs {}) '
341                    'has energy data and overlaps freq domains'.format(
342                        node.name, node.cpus))
343
344        def sorted_leaves(root):
345            # Get a list of the leaf (cpu) nodes of a _CpuTree in order of the
346            # CPU ID
347            ret = sorted(list(root.iter_leaves()), key=lambda n: n.cpus[0])
348            assert all(len(n.cpus) == 1 for n in ret)
349            return ret
350
351        self.root = root_node
352        self.cpu_nodes = sorted_leaves(root_node)
353        self.cpu_pds = sorted_leaves(root_power_domain)
354        assert len(self.cpu_pds) == len(self.cpu_nodes)
355
356        self._log = logging.getLogger('EnergyModel')
357
358        max_cap = max(n.max_capacity for n in self.cpu_nodes)
359        if max_cap != self.capacity_scale:
360            self._log.warning(
361                'Unusual max capacity (%s), overriding capacity_scale', max_cap)
362            self.capacity_scale = max_cap
363
364    def _cpus_with_capacity(self, cap):
365        """
366        Helper method to find the CPUs whose max capacity equals cap
367        """
368        return [c for c in self.cpus
369                if self.cpu_nodes[c].max_capacity == cap]
370
371    @property
372    @memoized
373    def biggest_cpus(self):
374        """
375        The CPUs with the highest compute capacity at their highest frequency
376        """
377        return self._cpus_with_capacity(self.capacity_scale)
378
379    @property
380    @memoized
381    def littlest_cpus(self):
382        """
383        The CPUs with the lowest compute capacity at their highest frequency
384        """
385        min_cap = min(n.max_capacity for n in self.cpu_nodes)
386        return self._cpus_with_capacity(min_cap)
387
388    @property
389    @memoized
390    def is_heterogeneous(self):
391        """
392        True iff CPUs do not all have the same efficiency and OPP range
393        """
394        states = self.cpu_nodes[0].active_states
395        return any(c.active_states != states for c in self.cpu_nodes[1:])
396
397    @property
398    @memoized
399    def cpu_groups(self):
400        """
401        List of lists of CPUs who share the same active state values
402        """
403        groups = []
404        for node in self.cpu_nodes:
405            for group in groups:
406                group_states = self.cpu_nodes[group[0]].active_states
407                if node.active_states == group_states:
408                    group.append(node.cpu)
409                    break
410            else:
411                groups.append([node.cpu])
412        return groups
413
414    def _guess_idle_states(self, cpus_active):
415        def find_deepest(pd):
416            if not any(cpus_active[c] for c in pd.cpus):
417                if pd.parent:
418                    parent_state = find_deepest(pd.parent)
419                    if parent_state:
420                        return parent_state
421                return pd.idle_states[-1] if len(pd.idle_states) else None
422            return None
423
424        return [find_deepest(pd) for pd in self.cpu_pds]
425
426    def get_cpu_capacity(self, cpu, freq=None):
427        """Convenience method to get the capacity of a CPU at a given frequency
428
429        :param cpu: CPU to get capacity for
430        :param freq: Frequency to get the CPU capacity at. Default is max
431                     capacity.
432        """
433        if freq is None:
434            return self.cpu_nodes[cpu].max_capacity
435        return self.cpu_nodes[cpu].active_states[freq].capacity
436
437    def guess_idle_states(self, cpus_active):
438        """Pessimistically guess the idle states that each CPU may enter
439
440        If a CPU has any tasks it is estimated that it may only enter its
441        shallowest idle state in between task activations. If all the CPUs
442        within a power domain have no tasks, they will all be judged able to
443        enter that domain's deepest idle state. If any CPU in a domain has work,
444        no CPUs in that domain are assumed to enter any domain shared state.
445
446        e.g. Consider a system with
447
448        - two power domains PD0 and PD1
449
450        - 4 CPUs, with CPUs [0, 1] in PD0 and CPUs [2, 3] in PD1
451
452        - 4 idle states: "WFI", "cpu-sleep", "cluster-sleep-0" and
453          "cluster-sleep-1", where the "cluster-sleep-*" states domain states,
454          i.e. a CPU can only enter those states when both CPUs in the domain
455          are idle.
456
457        Then here are some example inputs and outputs:
458
459        ::
460
461          # All CPUs idle:
462          [0, 0, 0, 0] -> ["cluster-sleep-1", "cluster-sleep-1",
463                           "cluster-sleep-1", "cluster-sleep-1"]
464
465          # All CPUs have work
466          [1, 1, 1, 1] -> ["WFI","WFI","WFI", "WFI"]
467
468          # One power domain active, the other idle
469          [0, 0, 1, 1] -> ["cluster-sleep-1", "cluster-sleep-1", "WFI","WFI"]
470
471          # One CPU active.
472          # Note that CPU 2 has no work but is assumed to never be able to enter
473          # any "cluster" state.
474          [0, 0, 0, 1] -> ["cluster-sleep-1", "cluster-sleep-1",
475                           "cpu-sleep","WFI"]
476
477        :param cpus_active: list where bool(cpus_active[N]) is False iff no
478                            tasks will run on CPU N.
479        :returns: List ``ret`` where ``ret[N]`` is the name of the estimated
480                  idle state that CPU N can enter during idle periods.
481
482        """
483        states = self._guess_idle_states(cpus_active)
484        return [s or c.idle_states.keys()[0]
485                for s, c in zip(states, self.cpu_nodes)]
486
487    def _guess_freqs(self, cpu_utils):
488        overutilized = False
489        # Find what frequency each CPU would need if it was alone in its
490        # frequency domain
491        ideal_freqs = [0 for _ in self.cpus]
492        for node in self.cpu_nodes:
493            [cpu] = node.cpus
494            required_cap = cpu_utils[cpu]
495
496            possible_freqs = [f for f, s in node.active_states.iteritems()
497                              if s.capacity >= required_cap]
498
499            if possible_freqs:
500                ideal_freqs[cpu] = min(possible_freqs)
501            else:
502                # CPU cannot provide required capacity, use max freq
503                ideal_freqs[cpu] = max(node.active_states.keys())
504                overutilized = True
505
506        # Rectify the frequencies among domains
507        freqs = [0 for _ in ideal_freqs]
508        for domain in self.freq_domains:
509            domain_freq = max(ideal_freqs[c] for c in domain)
510            for cpu in domain:
511                freqs[cpu] = domain_freq
512
513        return freqs, overutilized
514
515    def guess_freqs(self, cpu_utils):
516        """Work out CPU frequencies required to execute a workload
517
518        Find the lowest possible frequency for each CPU that provides enough
519        capacity to satisfy the utilization, taking into account frequency
520        domains.
521
522        :param cpu_utils: Utilization distribution, see
523                             :ref:`cpu_utils <cpu-utils>`
524        :returns: List ``ret`` where ``ret[N]`` is the frequency that CPU N must
525                  run at
526        """
527        freqs, _ = self._guess_freqs(cpu_utils)
528        return freqs
529
530    def _estimate_from_active_time(self, cpu_active_time, freqs, idle_states,
531                                   combine):
532        """Helper for estimate_from_cpu_util
533
534        Like estimate_from_cpu_util but uses active time i.e. proportion of time
535        spent not-idle in the range 0.0 - 1.0.
536
537        If combine=False, return idle and active power as separate components.
538        """
539        power = 0
540        ret = {}
541
542        assert all(0.0 <= a <= 1.0 for a in cpu_active_time)
543
544        for node in self.root.iter_nodes():
545            # Some nodes might not have energy model data, they could just be
546            # used to group other nodes (likely the root node, for example).
547            if not node.active_states or not node.idle_states:
548                continue
549
550            cpus = tuple(node.cpus)
551            # For now we assume topology nodes with energy models do not overlap
552            # with frequency domains
553            freq = freqs[cpus[0]]
554            assert all(freqs[c] == freq for c in cpus[1:])
555
556            # The active time of a node is estimated as the max of the active
557            # times of its children.
558            # This works great for the synthetic periodic workloads we use in
559            # LISA (where all threads wake up at the same time) but is probably
560            # no good for real workloads.
561            active_time = max(cpu_active_time[c] for c in cpus)
562            active_power = node.active_states[freq].power * active_time
563
564            _idle_power = max(node.idle_states[idle_states[c]] for c in cpus)
565            idle_power = _idle_power * (1 - active_time)
566
567            if combine:
568                ret[cpus] = active_power + idle_power
569            else:
570                ret[cpus] = {}
571                ret[cpus]["active"] = active_power
572                ret[cpus]["idle"] = idle_power
573
574        return ret
575
576    def estimate_from_cpu_util(self, cpu_utils, freqs=None, idle_states=None):
577        """
578        Estimate the energy usage of the system under a utilization distribution
579
580        Optionally also take freqs; a list of frequencies at which each CPU is
581        assumed to run, and idle_states, the idle states that each CPU can enter
582        between activations. If not provided, they will be estimated assuming an
583        ideal selection system (i.e. perfect cpufreq & cpuidle governors).
584
585        :param cpu_utils: Utilization distribution, see
586                             :ref:`cpu_utils <cpu-utils>`
587        :param freqs: List of CPU frequencies. Got from :meth:`guess_freqs` by
588                      default.
589        :param idle_states: List of CPU frequencies. Got from
590                            :meth:`guess_idle_states` by default.
591
592        :returns: Dict with power in bogo-Watts (bW), with contributions from
593                  each system component keyed with a tuple of the CPUs
594                  comprising that component (i.e. :attr:EnergyModelNode.cpus)
595
596                  ::
597
598                    {
599                        (0,)    : 10,
600                        (1,)    : 10,
601                        (0, 1)  : 5,
602                    }
603
604                  This represents CPUs 0 and 1 each using 10bW and their shared
605                  resources using 5bW for a total of 25bW.
606        """
607        if len(cpu_utils) != len(self.cpus):
608            raise ValueError(
609                'cpu_utils length ({}) must equal CPU count ({})'.format(
610                    len(cpu_utils), len(self.cpus)))
611
612        if freqs is None:
613            freqs = self.guess_freqs(cpu_utils)
614        if idle_states is None:
615            idle_states = self.guess_idle_states(cpu_utils)
616
617        cpu_active_time = []
618        for cpu, node in enumerate(self.cpu_nodes):
619            assert (cpu,) == node.cpus
620            cap = node.active_states[freqs[cpu]].capacity
621            cpu_active_time.append(min(float(cpu_utils[cpu]) / cap, 1.0))
622
623        return self._estimate_from_active_time(cpu_active_time,
624                                               freqs, idle_states, combine=True)
625
626    def get_optimal_placements(self, capacities):
627        """Find the optimal distribution of work for a set of tasks
628
629        Find a list of candidates which are estimated to be optimal in terms of
630        power consumption, but that do not result in any CPU becoming
631        over-utilized.
632
633        If no such candidates exist, i.e. the system being modeled cannot
634        satisfy the workload's throughput requirements, an
635        :class:`EnergyModelCapacityError` is raised. For example, if e was an
636        EnergyModel modeling two CPUs with capacity 1024, this error would be
637        raised by:
638
639        ::
640
641          e.get_optimal_placements({"t1": 800, "t2": 800, "t3: "800"})
642
643        This estimation assumes an ideal system of selecting OPPs and idle
644        states for CPUs.
645
646        .. note::
647            This is a brute force search taking time exponential wrt. the number
648            of tasks.
649
650        :param capacities: Dict mapping tasks to expected utilization
651                           values. These tasks are assumed not to change; they
652                           have a single static utilization value. A set of
653                           single-phase periodic RT-App tasks is an example of a
654                           suitable workload for this model.
655        :returns: List of ``cpu_utils`` items representing distributions of work
656                  under optimal task placements, see
657                  :ref:`cpu_utils <cpu-utils>`. Multiple task placements
658                  that result in the same CPU utilizations are considered
659                  equivalent.
660        """
661        tasks = capacities.keys()
662
663        num_candidates = len(self.cpus) ** len(tasks)
664        self._log.debug(
665            '%14s - Searching %d configurations for optimal task placement...',
666            'EnergyModel', num_candidates)
667
668        candidates = {}
669        excluded = []
670        for cpus in product(self.cpus, repeat=len(tasks)):
671            placement = {task: cpu for task, cpu in zip(tasks, cpus)}
672
673            util = [0 for _ in self.cpus]
674            for task, cpu in placement.items():
675                util[cpu] += capacities[task]
676            util = tuple(util)
677
678            # Filter out candidate placements that have tasks greater than max
679            # or that we have already determined that we cannot place.
680            if (any(u > self.capacity_scale for u in util) or util in excluded):
681                continue
682
683            if util not in candidates:
684                freqs, overutilized = self._guess_freqs(util)
685                if overutilized:
686                    # This isn't a valid placement
687                    excluded.append(util)
688                else:
689                    power = self.estimate_from_cpu_util(util, freqs=freqs)
690                    candidates[util] = sum(power.values())
691
692        if not candidates:
693            # The system can't provide full throughput to this workload.
694            raise EnergyModelCapacityError(
695                "Can't handle workload - total cap = {}".format(
696                    sum(capacities.values())))
697
698        # Whittle down to those that give the lowest energy estimate
699        min_power = min(p for p in candidates.itervalues())
700        ret = [u for u, p in candidates.iteritems() if p == min_power]
701
702        self._log.debug('%14s - Done', 'EnergyModel')
703        return ret
704
705    @classmethod
706    def _find_core_groups(cls, target):
707        """
708        Read the core_siblings masks for each CPU from sysfs
709
710        :param target: Devlib Target object to read masks from
711        :returns: A list of tuples of ints, representing the partition of core
712                  siblings
713        """
714        cpus = range(target.number_of_cpus)
715
716        topology_base = '/sys/devices/system/cpu/'
717
718        # We only care about core_siblings, but let's check *_siblings, so we
719        # can throw an error if a CPU's thread_siblings isn't just itself, or if
720        # there's a topology level we don't understand.
721
722        # Since we might have to read a lot of files, read everything we need in
723        # one go to avoid taking too long.
724        mask_glob = topology_base + 'cpu**/topology/*_siblings'
725        file_values = read_multiple_oneline_files(target, [mask_glob])
726
727        regex = re.compile(
728            topology_base + r'cpu([0-9]+)/topology/([a-z]+)_siblings')
729
730        ret = set()
731
732        for path, mask_str in file_values.iteritems():
733            match = regex.match(path)
734            cpu = int(match.groups()[0])
735            level = match.groups()[1]
736            # mask_to_list returns the values in descending order, so we'll sort
737            # them ascending. This isn't strictly necessary but it's nicer.
738            siblings = tuple(sorted(mask_to_list(int(mask_str, 16))))
739
740            if level == 'thread':
741                if siblings != (cpu,):
742                    # SMT systems aren't supported
743                    raise RuntimeError('CPU{} thread_siblings is {}. '
744                                       'expected {}'.format(cpu, siblings, [cpu]))
745                continue
746            if level != 'core':
747                # The only other levels we should expect to find are 'book' and
748                # 'shelf', which are not used by architectures we support.
749                raise RuntimeError(
750                    'Unrecognised topology level "{}"'.format(level))
751
752            ret.add(siblings)
753
754        # Sort core groups so that the lowest-numbered cores are first
755        # Again, not strictly necessary, just more pleasant.
756        return sorted(ret, key=lambda x: x[0])
757
758    @classmethod
759    def from_target(cls, target):
760        """
761        Create an EnergyModel by reading a target filesystem
762
763        This uses the sysctl added by EAS pathces to exposes the cap_states and
764        idle_states fields for each sched_group. This feature depends on
765        CONFIG_SCHED_DEBUG, and is not upstream in mainline Linux (as of v4.11),
766        so this method is only tested with Android kernels.
767
768        The kernel doesn't have an power domain data, so this method assumes
769        that all CPUs are totally independent wrt. idle states - the EnergyModel
770        constructed won't be aware of the topological dependencies for entering
771        "cluster" idle states.
772
773        Assumes the energy model has two-levels (plus the root) - a level for
774        CPUs and a level for 'clusters'.
775
776        :param target: Devlib target object to read filesystem from. Must have
777                       cpufreq and cpuidle modules enabled.
778        :returns: Constructed EnergyModel object based on the parameters
779                  reported by the target.
780        """
781        if 'cpufreq' not in target.modules:
782            raise TargetError('Requires cpufreq devlib module. Please ensure '
783                               '"cpufreq" is listed in your target/test modules')
784        if 'cpuidle' not in target.modules:
785            raise TargetError('Requires cpuidle devlib module. Please ensure '
786                               '"cpuidle" is listed in your target/test modules')
787
788        def sge_path(cpu, domain, group, field):
789            f = '/proc/sys/kernel/sched_domain/cpu{}/domain{}/group{}/energy/{}'
790            return f.format(cpu, domain, group, field)
791
792        # Read all the files we might need in one go, otherwise this will take
793        # ages.
794        sge_globs = [sge_path('**', '**', '**', 'cap_states'),
795                     sge_path('**', '**', '**', 'idle_states')]
796        sge_file_values = read_multiple_oneline_files(target, sge_globs)
797
798        if not sge_file_values:
799            raise TargetError('Energy Model not exposed in sysfs. '
800                              'Check CONFIG_SCHED_DEBUG is enabled.')
801
802        # These functions read the cap_states and idle_states vectors for the
803        # first sched_group in the sched_domain for a given CPU at a given
804        # level. That first group will include the given CPU. So
805        # read_active_states(0, 0) will give the CPU-level active_states for
806        # CPU0 and read_active_states(0, 1) will give the "cluster"-level
807        # active_states for the "cluster" that contains CPU0.
808
809        def read_sge_file(path):
810            try:
811                return sge_file_values[path]
812            except KeyError as e:
813                raise TargetError('No such file: {}'.format(e))
814
815        def read_active_states(cpu, domain_level):
816            cap_states_path = sge_path(cpu, domain_level, 0, 'cap_states')
817            cap_states_strs = read_sge_file(cap_states_path).split()
818
819            # cap_states lists the capacity of each state followed by its power,
820            # in increasing order. The `zip` call does this:
821            #   [c0, p0, c1, p1, c2, p2] -> [(c0, p0), (c1, p1), (c2, p2)]
822            cap_states = [ActiveState(capacity=int(c), power=int(p))
823                          for c, p in zip(cap_states_strs[0::2],
824                                          cap_states_strs[1::2])]
825            freqs = target.cpufreq.list_frequencies(cpu)
826            return OrderedDict(zip(sorted(freqs), cap_states))
827
828        def read_idle_states(cpu, domain_level):
829            idle_states_path = sge_path(cpu, domain_level, 0, 'idle_states')
830            idle_states_strs = read_sge_file(idle_states_path).split()
831
832            # get_states should return the state names in increasing depth order
833            names = [s.name for s in target.cpuidle.get_states(cpu)]
834            # idle_states is a list of power values in increasing order of
835            # idle-depth/decreasing order of power.
836            return OrderedDict(zip(names, [int(p) for p in idle_states_strs]))
837
838        # Read the CPU-level data from sched_domain level 0
839        cpus = range(target.number_of_cpus)
840        cpu_nodes = []
841        for cpu in cpus:
842            node = EnergyModelNode(
843                cpu=cpu,
844                active_states=read_active_states(cpu, 0),
845                idle_states=read_idle_states(cpu, 0))
846            cpu_nodes.append(node)
847
848        # Read the "cluster" level data from sched_domain level 1
849        core_group_nodes = []
850        for core_group in cls._find_core_groups(target):
851            node=EnergyModelNode(
852                children=[cpu_nodes[c] for c in core_group],
853                active_states=read_active_states(core_group[0], 1),
854                idle_states=read_idle_states(core_group[0], 1))
855            core_group_nodes.append(node)
856
857        root = EnergyModelRoot(children=core_group_nodes)
858
859        # Use cpufreq to figure out the frequency domains
860        freq_domains = []
861        remaining_cpus = set(cpus)
862        while remaining_cpus:
863            cpu = next(iter(remaining_cpus))
864            dom = target.cpufreq.get_domain_cpus(cpu)
865            freq_domains.append(dom)
866            remaining_cpus = remaining_cpus.difference(dom)
867
868        # We don't have a way to read the power domains from sysfs (the kernel
869        # isn't even aware of them) so we'll just have to assume each CPU is its
870        # own power domain and all idle states are independent of each other.
871        cpu_pds = []
872        for cpu in cpus:
873            names = [s.name for s in target.cpuidle.get_states(cpu)]
874            cpu_pds.append(PowerDomain(cpu=cpu, idle_states=names))
875
876        root_pd=PowerDomain(children=cpu_pds, idle_states=[])
877
878        return cls(root_node=root,
879                   root_power_domain=root_pd,
880                   freq_domains=freq_domains)
881