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