Home | History | Annotate | Download | only in android
      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 
     18 import logging
     19 import os
     20 import re
     21 import webbrowser
     22 import time
     23 from collections import namedtuple
     24 
     25 from gfxinfo import GfxInfo
     26 from surfaceflinger import SurfaceFlinger
     27 from devlib.utils.android_build import Build
     28 
     29 from . import System
     30 
     31 class 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