1#    Copyright 2015-2016 ARM Limited
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14#
15
16"""A library for asserting scheduler scenarios based on the
17statistics aggregation framework"""
18
19import re
20import inspect
21import trappy
22from bart.sched import functions as sched_funcs
23from bart.sched.SchedAssert import SchedAssert
24from bart.common import Utils
25
26class SchedMultiAssert(object):
27    """This is vector assertion class built on top of
28    :mod:`bart.sched.SchedAssert.SchedAssert`
29
30    :param ftrace: A single trappy.FTrace object
31        or a path that can be passed to trappy.FTrace
32    :type ftrace: :mod:`trappy.ftrace.FTrace`
33
34    :param topology: A topology that describes the arrangement of
35        CPU's on a system. This is useful for multi-cluster systems
36        where data needs to be aggregated at different topological
37        levels
38    :type topology: :mod:`trappy.stats.Topology.Topology`
39
40    :param execnames: The execnames of the task to be analysed
41
42        A single execname or a list of execnames can be passed.
43        There can be multiple processes associated with a single
44        execname parameter. The execnames are searched using a prefix
45        match.
46    :type execname: list, str
47
48    :param pids: The process IDs of the tasks to be analysed
49    :type pids: list, int
50
51    Consider the following processes which need to be analysed
52
53        ===== ==============
54         PID    execname
55        ===== ==============
56         11     task_1
57         22     task_2
58         33     task_3
59        ===== ==============
60
61    A :mod:`bart.sched.SchedMultiAssert.SchedMultiAssert` instance be created
62    following different ways:
63
64        - Using execname prefix match
65          ::
66
67            SchedMultiAssert(ftrace, topology, execnames="task_")
68
69        - Individual Task names
70          ::
71
72            SchedMultiAssert(ftrace, topology, execnames=["task_1", "task_2", "task_3"])
73
74        - Using Process IDs
75          ::
76
77            SchedMultiAssert(ftrace, topology, pids=[11, 22, 33])
78
79
80    All the functionality provided in :mod:`bart.sched.SchedAssert.SchedAssert` is available
81    in this class with the addition of handling vector assertions.
82
83    For example consider the use of :func:`getDutyCycle`
84    ::
85
86        >>> s = SchedMultiAssert(ftrace, topology, execnames="task_")
87        >>> s.getDutyCycle(window=(start, end))
88        {
89            "11": {
90                "task_name": "task_1",
91                "dutycycle": 10.0
92            },
93            "22": {
94                "task_name": "task_2",
95                "dutycycle": 20.0
96            },
97            "33": {
98                "task_name": "task_3",
99                "dutycycle": 30.0
100            },
101        }
102
103    The assertions can be used in a similar way
104    ::
105
106        >>> import operator as op
107        >>> s = SchedMultiAssert(ftrace, topology, execnames="task_")
108        >>> s.assertDutyCycle(15, op.ge, window=(start, end))
109        {
110            "11": {
111                "task_name": "task_1",
112                "dutycycle": False
113            },
114            "22": {
115                "task_name": "task_2",
116                "dutycycle": True
117            },
118            "33": {
119                "task_name": "task_3",
120                "dutycycle": True
121            },
122        }
123
124    The above result can be coalesced using a :code:`rank` parameter
125    As we know that only 2 processes have duty cycles greater than 15%
126    we can do the following:
127    ::
128
129        >>> import operator as op
130        >>> s = SchedMultiAssert(ftrace, topology, execnames="task_")
131        >>> s.assertDutyCycle(15, op.ge, window=(start, end), rank=2)
132        True
133
134    See :mod:`bart.sched.SchedAssert.SchedAssert` for the available
135    functionality
136    """
137
138    def __init__(self, ftrace, topology, execnames=None, pids=None):
139
140        self._ftrace = Utils.init_ftrace(ftrace)
141        self._topology = topology
142
143        if execnames and pids:
144            raise ValueError('Either pids or execnames must be specified')
145        if execnames:
146            self._execnames = Utils.listify(execnames)
147            self._pids = self._populate_pids()
148        elif pids:
149            self._pids = pids
150        else:
151            raise ValueError('One of PIDs or execnames must be specified')
152
153        self._asserts = self._populate_asserts()
154        self._populate_methods()
155
156    def _populate_asserts(self):
157        """Populate SchedAsserts for the PIDs"""
158
159        asserts = {}
160
161        for pid in self._pids:
162            asserts[pid] = SchedAssert(self._ftrace, self._topology, pid=pid)
163
164        return asserts
165
166    def _populate_pids(self):
167        """Map the input execnames to PIDs"""
168
169        if len(self._execnames) == 1:
170            return sched_funcs.get_pids_for_process(self._ftrace, self._execnames[0])
171
172        pids = []
173
174        for proc in self._execnames:
175            pids += sched_funcs.get_pids_for_process(self._ftrace, proc)
176
177        return list(set(pids))
178
179    def _create_method(self, attr_name):
180        """A wrapper function to create a dispatch function"""
181
182        return lambda *args, **kwargs: self._dispatch(attr_name, *args, **kwargs)
183
184    def _populate_methods(self):
185        """Populate Methods from SchedAssert"""
186
187        for attr_name in dir(SchedAssert):
188            attr = getattr(SchedAssert, attr_name)
189
190            valid_method = attr_name.startswith("get") or \
191                           attr_name.startswith("assert")
192            if inspect.ismethod(attr) and valid_method:
193                func = self._create_method(attr_name)
194                setattr(self, attr_name, func)
195
196    def get_task_name(self, pid):
197        """Get task name for the PID"""
198        return self._asserts[pid].execname
199
200
201    def _dispatch(self, func_name, *args, **kwargs):
202        """The dispatch function to call into the SchedAssert
203           Method
204        """
205
206        assert_func = func_name.startswith("assert")
207        num_true = 0
208
209        rank = kwargs.pop("rank", None)
210        result = kwargs.pop("result", {})
211        param = kwargs.pop("param", re.sub(r"assert|get", "", func_name, count=1).lower())
212
213        for pid in self._pids:
214
215            if pid not in result:
216                result[pid] = {}
217                result[pid]["task_name"] = self.get_task_name(pid)
218
219            attr = getattr(self._asserts[pid], func_name)
220            result[pid][param] = attr(*args, **kwargs)
221
222            if assert_func and result[pid][param]:
223                num_true += 1
224
225        if assert_func and rank:
226            return num_true == rank
227        else:
228            return result
229
230    def getCPUBusyTime(self, level, node, window=None, percent=False):
231        """Get the amount of time the cpus in the system were busy executing the
232        tasks
233
234        :param level: The topological level to which the group belongs
235        :type level: string
236
237        :param node: The group of CPUs for which to calculate busy time
238        :type node: list
239
240        :param window: A (start, end) tuple to limit the scope of the
241        calculation.
242        :type window: tuple
243
244        :param percent: If True the result is normalized to the total
245        time of the period, either the window or the full lenght of
246        the trace.
247        :type percent: bool
248
249        .. math::
250
251            R = \\frac{T_{busy} \\times 100}{T_{total}}
252
253        """
254        residencies = self.getResidency(level, node, window=window)
255
256        busy_time = sum(v["residency"] for v in residencies.itervalues())
257
258        if percent:
259            if window:
260                total_time = window[1] - window[0]
261            else:
262                total_time = self._ftrace.get_duration()
263            num_cpus = len(node)
264            return busy_time / (total_time * num_cpus) * 100
265        else:
266            return busy_time
267
268    def generate_events(self, level, window=None):
269        """Generate Events for the trace plot
270
271        .. note::
272            This is an internal function for plotting data
273        """
274
275        events = {}
276        for s_assert in self._asserts.values():
277            events[s_assert.name] = s_assert.generate_events(level, window=window)
278
279        return events
280
281    def plot(self, level="cpu", window=None, xlim=None):
282        """
283        :return: :mod:`trappy.plotter.AbstractDataPlotter` instance
284            Call :func:`view` to draw the graph
285        """
286
287        if not xlim:
288            if not window:
289                xlim = [0, self._ftrace.get_duration()]
290            else:
291                xlim = list(window)
292
293        events = self.generate_events(level, window)
294        names = [s.name for s in self._asserts.values()]
295        num_lanes = self._topology.level_span(level)
296        lane_prefix = level.upper() + ": "
297        return trappy.EventPlot(events, names, xlim,
298                                lane_prefix=lane_prefix,
299                                num_lanes=num_lanes)
300