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