1# SPDX-License-Identifier: Apache-2.0
2#
3# Copyright (C) 2015, 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
18import logging
19import os
20import re
21import webbrowser
22import time
23from collections import namedtuple
24
25from gfxinfo import GfxInfo
26from surfaceflinger import SurfaceFlinger
27from devlib.utils.android_build import Build
28
29from . import System
30
31class Workload(object):
32    """
33    Base class for Android related workloads
34    """
35
36    _packages = None
37    _availables = {}
38
39    WorkloadPackage = namedtuple("WorkloadPackage", "package_name apk_path src_path")
40
41    def __init__(self, test_env):
42        """
43        Initialized workloads available on the specified test environment
44
45        test_env: target test environment
46        """
47        self._te = test_env
48        self._target = test_env.target
49        self._log = logging.getLogger('Workload')
50
51        # Set of data reported in output of each run
52        self.trace_file = None
53        self.nrg_report = None
54
55        # Hooks to run at different points of workload execution
56        self.hooks = {}
57
58    def _adb(self, cmd):
59        return 'adb -s {} {}'.format(self._target.adb_name, cmd)
60
61    @classmethod
62    def _packages_installed(cls, sc, allow_install):
63        # If workload does not have packages just return
64        if not hasattr(sc, 'packages'):
65            return True
66        # Require package to be installed unless it can be installed when allowed
67        if allow_install:
68            required_packages = [package.package_name for package in sc.packages if package.apk_path==None]
69        else:
70            required_packages = [package.package_name for package in sc.packages]
71        return all(p in cls._packages for p in required_packages)
72
73    @classmethod
74    def _build_packages(cls, sc, te):
75        bld = Build(te)
76        for p in sc.packages:
77            if p.src_path != None:
78                bld.build_module(p.src_path)
79        return True
80
81    @classmethod
82    def _install_packages(cls, sc, te):
83        for p in sc.packages:
84            System.install_apk(te.target,
85                '{}/{}'.format(te.ANDROID_PRODUCT_OUT, p.apk_path))
86        return True;
87
88    @classmethod
89    def _subclasses(cls):
90        """
91        Recursively get all subclasses
92        """
93        nodes = cls.__subclasses__()
94        return nodes + [child for node in nodes for child in node._subclasses()]
95
96    @classmethod
97    def _check_availables(cls, test_env):
98        """
99        List the supported android workloads which are available on the target
100        """
101
102        _log = logging.getLogger('Workload')
103
104        # Getting the list of installed packages
105        cls._packages = test_env.target.list_packages()
106        _log.debug('Packages:\n%s', cls._packages)
107
108        _log.debug('Building list of available workloads...')
109        for sc in Workload._subclasses():
110            _log.debug('Checking workload [%s]...', sc.__name__)
111
112            # Check if all required packages are installed or can be installed
113            if cls._packages_installed(sc, True):
114                cls._availables[sc.__name__.lower()] = sc
115
116        _log.info('Supported workloads available on target:')
117        _log.info('  %s', ', '.join(cls._availables.keys()))
118
119    @classmethod
120    def getInstance(cls, test_env, name, reinstall=False):
121        """
122        Get a reference to the specified Android workload
123
124        :param test_env: target test environment
125        :type test_env: TestEnv
126
127        :param name: workload name
128        :type name: str
129
130        :param reinstall: flag to reinstall workload applications
131        :type reinstall: boolean
132
133        """
134
135        # Initialize list of available workloads
136        if cls._packages is None:
137            cls._check_availables(test_env)
138
139        if name.lower() not in cls._availables:
140            msg = 'Workload [{}] not available on target'.format(name)
141            raise ValueError(msg)
142
143        sc = cls._availables[name.lower()]
144
145        if (reinstall or not cls._packages_installed(sc, False)):
146            if (not cls._build_packages(sc, test_env) or
147                not cls._install_packages(sc, test_env)):
148                msg = 'Unable to install packages required for [{}] workload'.format(name)
149                raise RuntimeError(msg)
150
151        ret_cls = sc(test_env)
152
153        # Add generic support for cgroup tracing (detect if cgroup module exists)
154        if ('modules' in test_env.conf) and ('cgroups' in test_env.conf['modules']):
155                # Enable dumping support (which happens after systrace starts)
156                ret_cls._log.info('Enabling CGroup support for dumping schedtune/cpuset events')
157                ret_cls.add_hook('post_collect_start', ret_cls.post_collect_start_cgroup)
158                # Also update the extra ftrace points needed
159                if not 'systrace' in test_env.conf:
160                    test_env.conf['systrace'] = { 'extra_events': ['cgroup_attach_task', 'sched_process_fork'] }
161                else:
162                    if not 'extra_events' in test_env.conf['systrace']:
163                        test_env.conf['systrace']['extra_events'] = ['cgroup_attach_task', 'sched_process_fork']
164                    else:
165                        test_env.conf['systrace']['extra_events'].extend(['cgroup_attach_task', 'sched_process_fork'])
166
167        return ret_cls
168
169    def trace_cgroup(self, controller, cgroup):
170        cgroup = self._te.target.cgroups.controllers[controller].cgroup('/' + cgroup)
171        cgroup.trace_cgroup_tasks()
172
173    def post_collect_start_cgroup(self):
174        # Since systrace starts asynchronously, wait for trace to start
175        while True:
176            if self._te.target.execute('cat /d/tracing/tracing_on')[0] == "0":
177                time.sleep(0.1)
178                continue
179            break
180
181        self.trace_cgroup('schedtune', '')           # root
182        self.trace_cgroup('schedtune', 'top-app')
183        self.trace_cgroup('schedtune', 'foreground')
184        self.trace_cgroup('schedtune', 'background')
185        self.trace_cgroup('schedtune', 'rt')
186
187        self.trace_cgroup('cpuset', '')              # root
188        self.trace_cgroup('cpuset', 'top-app')
189        self.trace_cgroup('cpuset', 'foreground')
190        self.trace_cgroup('cpuset', 'background')
191        self.trace_cgroup('cpuset', 'system-background')
192
193    def add_hook(self, hook, hook_fn):
194        allowed = ['post_collect_start']
195        if hook not in allowed:
196            return
197        self.hooks[hook] = hook_fn
198
199    def run(self, out_dir, collect='',
200            **kwargs):
201        raise RuntimeError('Not implemented')
202
203    def tracingStart(self, screen_always_on=True):
204        # Keep the screen on during any data collection
205        if screen_always_on:
206            System.screen_always_on(self._target, enable=True)
207        # Reset the dumpsys data for the package
208        if 'gfxinfo' in self.collect:
209            System.gfxinfo_reset(self._target, self.package)
210        if 'surfaceflinger' in self.collect:
211            System.surfaceflinger_reset(self._target, self.package)
212        if 'logcat' in self.collect:
213            System.logcat_reset(self._target)
214        # Make sure ftrace and systrace are not both specified to be collected
215        if 'ftrace' in self.collect and 'systrace' in self.collect:
216            msg = 'ftrace and systrace cannot be used at the same time'
217            raise ValueError(msg)
218        # Start FTrace
219        if 'ftrace' in self.collect:
220            self.trace_file = os.path.join(self.out_dir, 'trace.dat')
221            self._log.info('FTrace START')
222            self._te.ftrace.start()
223        # Start Systrace (mutually exclusive with ftrace)
224        elif 'systrace' in self.collect:
225            self.trace_file = os.path.join(self.out_dir, 'trace.html')
226            # Get the systrace time
227            match = re.search(r'systrace_([0-9]+)', self.collect)
228            self._trace_time = match.group(1) if match else None
229            self._log.info('Systrace START')
230            self._target.execute('echo 0 > /d/tracing/tracing_on')
231            self._systrace_output = System.systrace_start(
232                self._te, self.trace_file, self._trace_time, conf=self._te.conf)
233            if 'energy' in self.collect:
234                # Wait for systrace to start before cutting off USB
235                while True:
236                    if self._target.execute('cat /d/tracing/tracing_on')[0] == "0":
237                        time.sleep(0.1)
238                        continue
239                    break
240        # Initializing frequency times
241        if 'time_in_state' in self.collect:
242            self._time_in_state_start = self._te.target.cpufreq.get_time_in_state(
243                    self._te.topology.get_level('cluster'))
244        # Initialize energy meter results
245        if 'energy' in self.collect and self._te.emeter:
246            self._te.emeter.reset()
247            self._log.info('Energy meter STARTED')
248        # Run post collect hooks passed added by the user of wload object
249        if 'post_collect_start' in self.hooks:
250            hookfn = self.hooks['post_collect_start']
251            self._log.info("Running post collect startup hook {}".format(hookfn.__name__))
252            hookfn()
253
254    def tracingStop(self, screen_always_on=True):
255        # Collect energy meter results
256        if 'energy' in self.collect and self._te.emeter:
257            self.nrg_report = self._te.emeter.report(self.out_dir)
258            self._log.info('Energy meter STOPPED')
259        # Calculate the delta in frequency times
260        if 'time_in_state' in self.collect:
261            self._te.target.cpufreq.dump_time_in_state_delta(
262                    self._time_in_state_start,
263                    self._te.topology.get_level('cluster'),
264                    os.path.join(self.out_dir, 'time_in_state.json'))
265        # Stop FTrace
266        if 'ftrace' in self.collect:
267            self._te.ftrace.stop()
268            self._log.info('FTrace STOP')
269            self._te.ftrace.get_trace(self.trace_file)
270        # Stop Systrace (mutually exclusive with ftrace)
271        elif 'systrace' in self.collect:
272            if not self._systrace_output:
273                self._log.warning('Systrace is not running!')
274            else:
275                self._log.info('Waiting systrace report [%s]...',
276                                 self.trace_file)
277                if self._trace_time is None:
278                    # Systrace expects <enter>
279                    self._systrace_output.sendline('')
280                self._systrace_output.wait()
281        # Parse the data gathered from dumpsys gfxinfo
282        if 'gfxinfo' in self.collect:
283            dump_file = os.path.join(self.out_dir, 'dumpsys_gfxinfo.txt')
284            System.gfxinfo_get(self._target, self.package, dump_file)
285            self.gfxinfo = GfxInfo(dump_file)
286        # Parse the data gathered from dumpsys SurfaceFlinger
287        if 'surfaceflinger' in self.collect:
288            dump_file = os.path.join(self.out_dir, 'dumpsys_surfaceflinger.txt')
289            System.surfaceflinger_get(self._target, self.package, dump_file)
290            self.surfaceflinger = SurfaceFlinger(dump_file)
291        if 'logcat' in self.collect:
292            dump_file = os.path.join(self.out_dir, 'logcat.txt')
293            System.logcat_get(self._target, dump_file)
294        # Dump a platform description
295        self._te.platform_dump(self.out_dir)
296        # Restore automatic screen off
297        if screen_always_on:
298            System.screen_always_on(self._target, enable=False)
299
300    def traceShow(self):
301        """
302        Open the collected trace using the most appropriate native viewer.
303
304        The native viewer depends on the specified trace format:
305        - ftrace: open using kernelshark
306        - systrace: open using a browser
307
308        In both cases the native viewer is assumed to be available in the host
309        machine.
310        """
311
312        if 'ftrace' in self.collect:
313            os.popen("kernelshark {}".format(self.trace_file))
314            return
315
316        if 'systrace' in self.collect:
317            webbrowser.open(self.trace_file)
318            return
319
320        self._log.warning('No trace collected since last run')
321
322# vim :set tabstop=4 shiftwidth=4 expandtab
323