1# Copyright 2016 The Chromium Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5 6from recipe_engine import recipe_api 7from recipe_engine import recipe_test_api 8 9from . import default 10import subprocess # TODO(borenet): No! Remove this. 11 12 13"""Android flavor, used for running code on Android.""" 14 15 16class AndroidFlavor(default.DefaultFlavor): 17 def __init__(self, m, app_name): 18 super(AndroidFlavor, self).__init__(m, app_name) 19 self._ever_ran_adb = False 20 self.ADB_BINARY = '/usr/bin/adb.1.0.35' 21 self.ADB_PUB_KEY = '/home/chrome-bot/.android/adbkey' 22 if 'skia' not in self.m.vars.swarming_bot_id: 23 self.ADB_BINARY = '/opt/infra-android/tools/adb' 24 self.ADB_PUB_KEY = ('/home/chrome-bot/.android/' 25 'chrome_infrastructure_adbkey') 26 27 # Data should go in android_data_dir, which may be preserved across runs. 28 android_data_dir = '/sdcard/revenge_of_the_skiabot/' 29 self.device_dirs = default.DeviceDirs( 30 bin_dir = '/data/local/tmp/', 31 dm_dir = android_data_dir + 'dm_out', 32 perf_data_dir = android_data_dir + 'perf', 33 resource_dir = android_data_dir + 'resources', 34 fonts_dir = 'NOT_SUPPORTED', 35 images_dir = android_data_dir + 'images', 36 lotties_dir = android_data_dir + 'lotties', 37 skp_dir = android_data_dir + 'skps', 38 svg_dir = android_data_dir + 'svgs', 39 tmp_dir = android_data_dir, 40 texttraces_dir = android_data_dir + 'text_blob_traces') 41 42 # A list of devices we can't root. If rooting fails and a device is not 43 # on the list, we fail the task to avoid perf inconsistencies. 44 self.cant_root = ['GalaxyS7_G930FD', 'GalaxyS9', 45 'GalaxyS20', 'MotoG4', 'NVIDIA_Shield', 46 'P30', 'Pixel4','Pixel4XL', 'Pixel5', 'TecnoSpark3Pro', 'JioNext'] 47 48 # Maps device type -> CPU ids that should be scaled for nanobench. 49 # Many devices have two (or more) different CPUs (e.g. big.LITTLE 50 # on Nexus5x). The CPUs listed are the biggest cpus on the device. 51 # The CPUs are grouped together, so we only need to scale one of them 52 # (the one listed) in order to scale them all. 53 # E.g. Nexus5x has cpu0-3 as one chip and cpu4-5 as the other. Thus, 54 # if one wants to run a single-threaded application (e.g. nanobench), one 55 # can disable cpu0-3 and scale cpu 4 to have only cpu4 and 5 at the same 56 # frequency. See also disable_for_nanobench. 57 self.cpus_to_scale = { 58 'Nexus5x': [4], 59 'Pixel': [2], 60 'Pixel2XL': [4] 61 } 62 63 # Maps device type -> CPU ids that should be turned off when running 64 # single-threaded applications like nanobench. The devices listed have 65 # multiple, differnt CPUs. We notice a lot of noise that seems to be 66 # caused by nanobench running on the slow CPU, then the big CPU. By 67 # disabling this, we see less of that noise by forcing the same CPU 68 # to be used for the performance testing every time. 69 self.disable_for_nanobench = { 70 'Nexus5x': range(0, 4), 71 'Pixel': range(0, 2), 72 'Pixel2XL': range(0, 4), 73 'Pixel6': range(4,8), # Only use the 4 small cores. 74 'Pixel7': range(4,8), 75 } 76 77 self.gpu_scaling = { 78 "Nexus5": 450000000, 79 "Nexus5x": 600000000, 80 } 81 82 def _wait_for_device(self, title, attempt): 83 self.m.run(self.m.step, 84 'adb kill-server after failure of \'%s\' (attempt %d)' % ( 85 title, attempt), 86 cmd=[self.ADB_BINARY, 'kill-server'], 87 infra_step=True, timeout=30, abort_on_failure=False, 88 fail_build_on_failure=False) 89 self.m.run(self.m.step, 90 'wait for device after failure of \'%s\' (attempt %d)' % ( 91 title, attempt), 92 cmd=[self.ADB_BINARY, 'wait-for-device'], infra_step=True, 93 timeout=180, abort_on_failure=False, 94 fail_build_on_failure=False) 95 self.m.run(self.m.step, 96 'adb devices -l after failure of \'%s\' (attempt %d)' % ( 97 title, attempt), 98 cmd=[self.ADB_BINARY, 'devices', '-l'], 99 infra_step=True, timeout=30, abort_on_failure=False, 100 fail_build_on_failure=False) 101 self.m.run(self.m.step, 102 'adb reboot device after failure of \'%s\' (attempt %d)' % ( 103 title, attempt), 104 cmd=[self.ADB_BINARY, 'reboot'], 105 infra_step=True, timeout=30, abort_on_failure=False, 106 fail_build_on_failure=False) 107 self.m.run(self.m.step, 108 'wait for device after failure of \'%s\' (attempt %d)' % ( 109 title, attempt), 110 cmd=[ 111 self.ADB_BINARY, 'wait-for-device', 'shell', 112 # Wait until the boot is actually complete. 113 # https://android.stackexchange.com/a/164050 114 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done', 115 ], 116 timeout=180, abort_on_failure=False, 117 fail_build_on_failure=False) 118 device = self.m.vars.builder_cfg.get('model') 119 if (device in self.cant_root): # pragma: nocover 120 return 121 self.m.run(self.m.step, 122 'adb root', 123 cmd=[ 124 self.ADB_BINARY, 'root' 125 ], 126 timeout=180, abort_on_failure=False, 127 fail_build_on_failure=False) 128 129 def _adb(self, title, *cmd, **kwargs): 130 # The only non-infra adb steps (dm / nanobench) happen to not use _adb(). 131 if 'infra_step' not in kwargs: 132 kwargs['infra_step'] = True 133 134 self._ever_ran_adb = True 135 # ADB seems to be occasionally flaky on every device, so always retry. 136 attempts = kwargs.pop('attempts', 3) 137 138 def wait_for_device(attempt): 139 return self._wait_for_device(title, attempt) 140 141 with self.m.context(cwd=self.m.path.start_dir.join('skia')): 142 with self.m.env({'ADB_VENDOR_KEYS': self.ADB_PUB_KEY}): 143 return self.m.run.with_retry(self.m.step, title, attempts, 144 cmd=[self.ADB_BINARY]+list(cmd), 145 between_attempts_fn=wait_for_device, 146 **kwargs) 147 148 def _scale_for_dm(self): 149 device = self.m.vars.builder_cfg.get('model') 150 if (device in self.cant_root or 151 self.m.vars.internal_hardware_label): 152 return 153 154 # This is paranoia... any CPUs we disabled while running nanobench 155 # ought to be back online now that we've restarted the device. 156 for i in self.disable_for_nanobench.get(device, []): 157 self._set_cpu_online(i, 1) # enable 158 159 scale_up = self.cpus_to_scale.get(device, [0]) 160 # For big.LITTLE devices, make sure we scale the LITTLE cores up; 161 # there is a chance they are still in powersave mode from when 162 # swarming slows things down for cooling down and charging. 163 if 0 not in scale_up: 164 scale_up.append(0) 165 for i in scale_up: 166 # AndroidOne doesn't support ondemand governor. hotplug is similar. 167 if device == 'AndroidOne': 168 self._set_governor(i, 'hotplug') 169 elif device in ['Pixel3a', 'Pixel4', 'Pixel4a', 'Wembley', 'Pixel6', 'Pixel7']: 170 # Pixel3a/4/4a have userspace powersave performance schedutil. 171 # performance seems like a reasonable choice. 172 self._set_governor(i, 'performance') 173 else: 174 self._set_governor(i, 'ondemand') 175 176 def _scale_for_nanobench(self): 177 device = self.m.vars.builder_cfg.get('model') 178 if (device in self.cant_root or 179 self.m.vars.internal_hardware_label): 180 return 181 182 # Set to 'powersave' for Pixel6 and Pixel7. 183 for i in self.cpus_to_scale.get(device, [0]): 184 if device in ['Pixel6', 'Pixel7']: 185 self._set_governor(i, 'powersave') 186 else: 187 self._set_governor(i, 'userspace') 188 self._scale_cpu(i, 0.6) 189 190 for i in self.disable_for_nanobench.get(device, []): 191 self._set_cpu_online(i, 0) # disable 192 193 if device in self.gpu_scaling: 194 #https://developer.qualcomm.com/qfile/28823/lm80-p0436-11_adb_commands.pdf 195 # Section 3.2.1 Commands to put the GPU in performance mode 196 # Nexus 5 is 320000000 by default 197 # Nexus 5x is 180000000 by default 198 gpu_freq = self.gpu_scaling[device] 199 script = self.module.resource('set_gpu_scaling.py') 200 self.m.run.with_retry(self.m.step, 201 "Lock GPU to %d (and other perf tweaks)" % gpu_freq, 202 3, # attempts 203 cmd=['python3', script, self.ADB_BINARY, gpu_freq], 204 infra_step=True, 205 timeout=30) 206 207 def _set_governor(self, cpu, gov): 208 self._ever_ran_adb = True 209 script = self.module.resource('set_cpu_scaling_governor.py') 210 self.m.run.with_retry(self.m.step, 211 "Set CPU %d's governor to %s" % (cpu, gov), 212 3, # attempts 213 cmd=['python3', script, self.ADB_BINARY, cpu, gov], 214 infra_step=True, 215 timeout=30) 216 217 218 def _set_cpu_online(self, cpu, value): 219 """Set /sys/devices/system/cpu/cpu{N}/online to value (0 or 1).""" 220 self._ever_ran_adb = True 221 msg = 'Disabling' 222 if value: 223 msg = 'Enabling' 224 225 def wait_for_device(attempt): 226 return self._wait_for_device("set cpu online", attempt) # pragma: nocover 227 228 script = self.module.resource('set_cpu_online.py') 229 self.m.run.with_retry(self.m.step, 230 '%s CPU %d' % (msg, cpu), 231 3, # attempts 232 cmd=['python3', script, self.ADB_BINARY, cpu, value], 233 infra_step=True, 234 between_attempts_fn=wait_for_device, 235 timeout=30) 236 237 238 def _scale_cpu(self, cpu, target_percent): 239 self._ever_ran_adb = True 240 241 def wait_for_device(attempt): 242 return self._wait_for_device("scale cpu", attempt) 243 244 script = self.module.resource('scale_cpu.py') 245 self.m.run.with_retry(self.m.step, 246 'Scale CPU %d to %f' % (cpu, target_percent), 247 3, # attempts 248 cmd=['python3', script, self.ADB_BINARY, str(target_percent), cpu], 249 infra_step=True, 250 between_attempts_fn=wait_for_device, 251 timeout=30) 252 253 254 def _asan_setup_path(self): 255 return self.m.vars.workdir.join( 256 'android_ndk_linux', 'toolchains', 'llvm', 'prebuilt', 'linux-x86_64', 257 'lib', 'clang', '17', 'bin', 'asan_device_setup') 258 259 260 def install(self): 261 self._adb('mkdir ' + self.device_dirs.resource_dir, 262 'shell', 'mkdir', '-p', self.device_dirs.resource_dir) 263 if self.m.vars.builder_cfg.get('model') in ['GalaxyS20', 'GalaxyS9']: 264 # See skia:10184, should be moot once upgraded to Android 11? 265 self._adb('cp libGLES_mali.so to ' + self.device_dirs.bin_dir, 266 'shell', 'cp', 267 '/vendor/lib64/egl/libGLES_mali.so', 268 self.device_dirs.bin_dir + 'libvulkan.so') 269 if 'ASAN' in self.m.vars.extra_tokens: 270 self._ever_ran_adb = True 271 script = self.module.resource('setup_device_for_asan.py') 272 self.m.run( 273 self.m.step, 'Setting up device to run ASAN', 274 cmd=['python3', script, self.ADB_BINARY, self._asan_setup_path()], 275 infra_step=True, 276 timeout=300, 277 abort_on_failure=True) 278 if self.app_name: 279 if (self.app_name == 'nanobench'): 280 self._scale_for_nanobench() 281 else: 282 self._scale_for_dm() 283 app_path = self.host_dirs.bin_dir.join(self.app_name) 284 self._adb('push %s' % self.app_name, 285 'push', app_path, self.device_dirs.bin_dir) 286 287 288 289 def cleanup_steps(self): 290 self.m.run(self.m.step, 291 'adb reboot device', 292 cmd=[self.ADB_BINARY, 'reboot'], 293 infra_step=True, timeout=30, abort_on_failure=False, 294 fail_build_on_failure=False) 295 self.m.run(self.m.step, 296 'wait for device after rebooting', 297 cmd=[ 298 self.ADB_BINARY, 'wait-for-device', 'shell', 299 # Wait until the boot is actually complete. 300 # https://android.stackexchange.com/a/164050 301 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done', 302 ], 303 timeout=180, abort_on_failure=False, 304 fail_build_on_failure=False) 305 306 if 'ASAN' in self.m.vars.extra_tokens: 307 self._ever_ran_adb = True 308 # Remove ASAN. 309 self.m.run(self.m.step, 310 'wait for device before uninstalling ASAN', 311 cmd=[self.ADB_BINARY, 'wait-for-device', 'shell', 312 # Wait until the boot is actually complete. 313 # https://android.stackexchange.com/a/164050 314 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done', 315 ], infra_step=True, 316 timeout=180, abort_on_failure=False, 317 fail_build_on_failure=False) 318 self.m.run(self.m.step, 'uninstall ASAN', 319 cmd=[self._asan_setup_path(), '--revert'], 320 infra_step=True, timeout=300, 321 abort_on_failure=False, fail_build_on_failure=False) 322 323 if self._ever_ran_adb: 324 script = self.module.resource('dump_adb_log.py') 325 self.m.run(self.m.step, 'dump log', 326 cmd=['python3', script, self.host_dirs.bin_dir, self.ADB_BINARY], 327 infra_step=True, 328 timeout=300, 329 abort_on_failure=False) 330 331 # Only quarantine the bot if the first failed step 332 # is an infra step. If, instead, we did this for any infra failures, we 333 # would do this too much. For example, if a Nexus 10 died during dm 334 # and the following pull step would also fail "device not found" - causing 335 # us to run the shutdown command when the device was probably not in a 336 # broken state; it was just rebooting. 337 if (self.m.run.failed_steps and 338 isinstance(self.m.run.failed_steps[0], recipe_api.InfraFailure)): 339 bot_id = self.m.vars.swarming_bot_id 340 self.m.file.write_text('Quarantining Bot', 341 '/home/chrome-bot/%s.force_quarantine' % bot_id, 342 ' ') 343 344 # if self._ever_ran_adb: 345 # self._adb('kill adb server', 'kill-server') 346 347 def step(self, name, cmd): 348 sh = '%s.sh' % cmd[0] 349 self.m.run.writefile(self.m.vars.tmp_dir.join(sh), 350 'set -x; LD_LIBRARY_PATH=%s %s%s; echo $? >%src' % ( 351 self.device_dirs.bin_dir, 352 self.device_dirs.bin_dir, subprocess.list2cmdline(map(str, cmd)), 353 self.device_dirs.bin_dir)) 354 self._adb('push %s' % sh, 355 'push', self.m.vars.tmp_dir.join(sh), self.device_dirs.bin_dir) 356 357 self._adb('clear log', 'logcat', '-c') 358 script = self.module.resource('run_sh.py') 359 self.m.step('%s' % cmd[0], 360 cmd=['python3', script, self.device_dirs.bin_dir, sh, self.ADB_BINARY]) 361 362 def copy_file_to_device(self, host, device): 363 self._adb('push %s %s' % (host, device), 'push', host, device) 364 365 def copy_directory_contents_to_device(self, host, device): 366 contents = self.m.file.glob_paths('ls %s/*' % host, 367 host, '*', 368 test_data=['foo.png', 'bar.jpg']) 369 args = contents + [device] 370 self._adb('push %s/* %s' % (host, device), 'push', *args) 371 372 def copy_directory_contents_to_host(self, device, host): 373 # TODO(borenet): When all of our devices are on Android 6.0 and up, we can 374 # switch to using tar to zip up the results before pulling. 375 with self.m.step.nest('adb pull'): 376 tmp = self.m.path.mkdtemp('adb_pull') 377 self._adb('pull %s' % device, 'pull', device, tmp) 378 paths = self.m.file.glob_paths( 379 'list pulled files', 380 tmp, 381 self.m.path.basename(device) + self.m.path.sep + '*', 382 test_data=['%d.png' % i for i in (1, 2)]) 383 for p in paths: 384 self.m.file.copy('copy %s' % self.m.path.basename(p), p, host) 385 386 def read_file_on_device(self, path, **kwargs): 387 testKwargs = { 388 'attempts': 1, 389 'abort_on_failure': False, 390 'fail_build_on_failure': False, 391 } 392 rv = self._adb('check if %s exists' % path, 393 'shell', 'test', '-f', path, **testKwargs) 394 if not rv: # pragma: nocover 395 return None 396 397 rv = self._adb('read %s' % path, 398 'shell', 'cat', path, stdout=self.m.raw_io.output(), 399 **kwargs) 400 return rv.stdout.decode('utf-8').rstrip() if rv and rv.stdout else None 401 402 def remove_file_on_device(self, path): 403 script = self.module.resource('remove_file_on_device.py') 404 self.m.run.with_retry(self.m.step, 'rm %s' % path, 3, 405 cmd=['python3', script, self.ADB_BINARY, path], 406 infra_step=True) 407 408 def create_clean_device_dir(self, path): 409 self.remove_file_on_device(path) 410 self._adb('mkdir %s' % path, 'shell', 'mkdir', '-p', path) 411