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 7 8from . import default 9import subprocess # TODO(borenet): No! Remove this. 10 11 12"""Android flavor, used for running code on Android.""" 13 14 15class AndroidFlavor(default.DefaultFlavor): 16 def __init__(self, m): 17 super(AndroidFlavor, self).__init__(m) 18 self._ever_ran_adb = False 19 self.ADB_BINARY = '/usr/bin/adb.1.0.35' 20 self.ADB_PUB_KEY = '/home/chrome-bot/.android/adbkey' 21 if 'skia' not in self.m.vars.swarming_bot_id: 22 self.ADB_BINARY = '/opt/infra-android/tools/adb' 23 self.ADB_PUB_KEY = ('/home/chrome-bot/.android/' 24 'chrome_infrastructure_adbkey') 25 26 # Data should go in android_data_dir, which may be preserved across runs. 27 android_data_dir = '/sdcard/revenge_of_the_skiabot/' 28 self.device_dirs = default.DeviceDirs( 29 bin_dir = '/data/local/tmp/', 30 dm_dir = android_data_dir + 'dm_out', 31 perf_data_dir = android_data_dir + 'perf', 32 resource_dir = android_data_dir + 'resources', 33 images_dir = android_data_dir + 'images', 34 lotties_dir = android_data_dir + 'lotties', 35 skp_dir = android_data_dir + 'skps', 36 svg_dir = android_data_dir + 'svgs', 37 mskp_dir = android_data_dir + 'mskp', 38 tmp_dir = android_data_dir) 39 40 # A list of devices we can't root. If rooting fails and a device is not 41 # on the list, we fail the task to avoid perf inconsistencies. 42 self.rootable_blacklist = ['GalaxyS6', 'GalaxyS7_G930FD', 'GalaxyS9', 43 'MotoG4', 'NVIDIA_Shield', 'P30', 44 'TecnoSpark3Pro'] 45 46 # Maps device type -> CPU ids that should be scaled for nanobench. 47 # Many devices have two (or more) different CPUs (e.g. big.LITTLE 48 # on Nexus5x). The CPUs listed are the biggest cpus on the device. 49 # The CPUs are grouped together, so we only need to scale one of them 50 # (the one listed) in order to scale them all. 51 # E.g. Nexus5x has cpu0-3 as one chip and cpu4-5 as the other. Thus, 52 # if one wants to run a single-threaded application (e.g. nanobench), one 53 # can disable cpu0-3 and scale cpu 4 to have only cpu4 and 5 at the same 54 # frequency. See also disable_for_nanobench. 55 self.cpus_to_scale = { 56 'Nexus5x': [4], 57 'Pixel': [2], 58 'Pixel2XL': [4] 59 } 60 61 # Maps device type -> CPU ids that should be turned off when running 62 # single-threaded applications like nanobench. The devices listed have 63 # multiple, differnt CPUs. We notice a lot of noise that seems to be 64 # caused by nanobench running on the slow CPU, then the big CPU. By 65 # disabling this, we see less of that noise by forcing the same CPU 66 # to be used for the performance testing every time. 67 self.disable_for_nanobench = { 68 'Nexus5x': range(0, 4), 69 'Pixel': range(0, 2), 70 'Pixel2XL': range(0, 4) 71 } 72 73 self.gpu_scaling = { 74 "Nexus5": 450000000, 75 "Nexus5x": 600000000, 76 } 77 78 def _run(self, title, *cmd, **kwargs): 79 with self.m.context(cwd=self.m.path['start_dir'].join('skia')): 80 return self.m.run(self.m.step, title, cmd=list(cmd), **kwargs) 81 82 def _adb(self, title, *cmd, **kwargs): 83 # The only non-infra adb steps (dm / nanobench) happen to not use _adb(). 84 if 'infra_step' not in kwargs: 85 kwargs['infra_step'] = True 86 87 self._ever_ran_adb = True 88 # ADB seems to be occasionally flaky on every device, so always retry. 89 attempts = 3 90 91 def wait_for_device(attempt): 92 self.m.run(self.m.step, 93 'kill adb server after failure of \'%s\' (attempt %d)' % ( 94 title, attempt), 95 cmd=[self.ADB_BINARY, 'kill-server'], 96 infra_step=True, timeout=30, abort_on_failure=False, 97 fail_build_on_failure=False) 98 self.m.run(self.m.step, 99 'wait for device after failure of \'%s\' (attempt %d)' % ( 100 title, attempt), 101 cmd=[self.ADB_BINARY, 'wait-for-device'], infra_step=True, 102 timeout=180, abort_on_failure=False, 103 fail_build_on_failure=False) 104 105 with self.m.context(cwd=self.m.path['start_dir'].join('skia')): 106 with self.m.env({'ADB_VENDOR_KEYS': self.ADB_PUB_KEY}): 107 return self.m.run.with_retry(self.m.step, title, attempts, 108 cmd=[self.ADB_BINARY]+list(cmd), 109 between_attempts_fn=wait_for_device, 110 **kwargs) 111 112 def _scale_for_dm(self): 113 device = self.m.vars.builder_cfg.get('model') 114 if (device in self.rootable_blacklist or 115 self.m.vars.internal_hardware_label): 116 return 117 118 # This is paranoia... any CPUs we disabled while running nanobench 119 # ought to be back online now that we've restarted the device. 120 for i in self.disable_for_nanobench.get(device, []): 121 self._set_cpu_online(i, 1) # enable 122 123 scale_up = self.cpus_to_scale.get(device, [0]) 124 # For big.LITTLE devices, make sure we scale the LITTLE cores up; 125 # there is a chance they are still in powersave mode from when 126 # swarming slows things down for cooling down and charging. 127 if 0 not in scale_up: 128 scale_up.append(0) 129 for i in scale_up: 130 # AndroidOne doesn't support ondemand governor. hotplug is similar. 131 if device == 'AndroidOne': 132 self._set_governor(i, 'hotplug') 133 else: 134 self._set_governor(i, 'ondemand') 135 136 def _scale_for_nanobench(self): 137 device = self.m.vars.builder_cfg.get('model') 138 if (device in self.rootable_blacklist or 139 self.m.vars.internal_hardware_label): 140 return 141 142 for i in self.cpus_to_scale.get(device, [0]): 143 self._set_governor(i, 'userspace') 144 self._scale_cpu(i, 0.6) 145 146 for i in self.disable_for_nanobench.get(device, []): 147 self._set_cpu_online(i, 0) # disable 148 149 if device in self.gpu_scaling: 150 #https://developer.qualcomm.com/qfile/28823/lm80-p0436-11_adb_commands.pdf 151 # Section 3.2.1 Commands to put the GPU in performance mode 152 # Nexus 5 is 320000000 by default 153 # Nexus 5x is 180000000 by default 154 gpu_freq = self.gpu_scaling[device] 155 self.m.run.with_retry(self.m.python.inline, 156 "Lock GPU to %d (and other perf tweaks)" % gpu_freq, 157 3, # attempts 158 program=""" 159import os 160import subprocess 161import sys 162import time 163ADB = sys.argv[1] 164freq = sys.argv[2] 165idle_timer = "10000" 166 167log = subprocess.check_output([ADB, 'root']) 168# check for message like 'adbd cannot run as root in production builds' 169print log 170if 'cannot' in log: 171 raise Exception('adb root failed') 172 173subprocess.check_output([ADB, 'shell', 'stop', 'thermald']) 174 175subprocess.check_output([ADB, 'shell', 'echo "%s" > ' 176 '/sys/class/kgsl/kgsl-3d0/gpuclk' % freq]) 177 178actual_freq = subprocess.check_output([ADB, 'shell', 'cat ' 179 '/sys/class/kgsl/kgsl-3d0/gpuclk']).strip() 180if actual_freq != freq: 181 raise Exception('Frequency (actual, expected) (%s, %s)' 182 % (actual_freq, freq)) 183 184subprocess.check_output([ADB, 'shell', 'echo "%s" > ' 185 '/sys/class/kgsl/kgsl-3d0/idle_timer' % idle_timer]) 186 187actual_timer = subprocess.check_output([ADB, 'shell', 'cat ' 188 '/sys/class/kgsl/kgsl-3d0/idle_timer']).strip() 189if actual_timer != idle_timer: 190 raise Exception('idle_timer (actual, expected) (%s, %s)' 191 % (actual_timer, idle_timer)) 192 193for s in ['force_bus_on', 'force_rail_on', 'force_clk_on']: 194 subprocess.check_output([ADB, 'shell', 'echo "1" > ' 195 '/sys/class/kgsl/kgsl-3d0/%s' % s]) 196 actual_set = subprocess.check_output([ADB, 'shell', 'cat ' 197 '/sys/class/kgsl/kgsl-3d0/%s' % s]).strip() 198 if actual_set != "1": 199 raise Exception('%s (actual, expected) (%s, 1)' 200 % (s, actual_set)) 201""", 202 args = [self.ADB_BINARY, gpu_freq], 203 infra_step=True, 204 timeout=30) 205 206 def _set_governor(self, cpu, gov): 207 self._ever_ran_adb = True 208 self.m.run.with_retry(self.m.python.inline, 209 "Set CPU %d's governor to %s" % (cpu, gov), 210 3, # attempts 211 program=""" 212import os 213import subprocess 214import sys 215import time 216ADB = sys.argv[1] 217cpu = int(sys.argv[2]) 218gov = sys.argv[3] 219 220log = subprocess.check_output([ADB, 'root']) 221# check for message like 'adbd cannot run as root in production builds' 222print log 223if 'cannot' in log: 224 raise Exception('adb root failed') 225 226subprocess.check_output([ADB, 'shell', 'echo "%s" > ' 227 '/sys/devices/system/cpu/cpu%d/cpufreq/scaling_governor' % (gov, cpu)]) 228actual_gov = subprocess.check_output([ADB, 'shell', 'cat ' 229 '/sys/devices/system/cpu/cpu%d/cpufreq/scaling_governor' % cpu]).strip() 230if actual_gov != gov: 231 raise Exception('(actual, expected) (%s, %s)' 232 % (actual_gov, gov)) 233""", 234 args = [self.ADB_BINARY, cpu, gov], 235 infra_step=True, 236 timeout=30) 237 238 239 def _set_cpu_online(self, cpu, value): 240 """Set /sys/devices/system/cpu/cpu{N}/online to value (0 or 1).""" 241 self._ever_ran_adb = True 242 msg = 'Disabling' 243 if value: 244 msg = 'Enabling' 245 self.m.run.with_retry(self.m.python.inline, 246 '%s CPU %d' % (msg, cpu), 247 3, # attempts 248 program=""" 249import os 250import subprocess 251import sys 252import time 253ADB = sys.argv[1] 254cpu = int(sys.argv[2]) 255value = int(sys.argv[3]) 256 257log = subprocess.check_output([ADB, 'root']) 258# check for message like 'adbd cannot run as root in production builds' 259print log 260if 'cannot' in log: 261 raise Exception('adb root failed') 262 263# If we try to echo 1 to an already online cpu, adb returns exit code 1. 264# So, check the value before trying to write it. 265prior_status = subprocess.check_output([ADB, 'shell', 'cat ' 266 '/sys/devices/system/cpu/cpu%d/online' % cpu]).strip() 267if prior_status == str(value): 268 print 'CPU %d online already %d' % (cpu, value) 269 sys.exit() 270 271subprocess.check_output([ADB, 'shell', 'echo %s > ' 272 '/sys/devices/system/cpu/cpu%d/online' % (value, cpu)]) 273actual_status = subprocess.check_output([ADB, 'shell', 'cat ' 274 '/sys/devices/system/cpu/cpu%d/online' % cpu]).strip() 275if actual_status != str(value): 276 raise Exception('(actual, expected) (%s, %d)' 277 % (actual_status, value)) 278""", 279 args = [self.ADB_BINARY, cpu, value], 280 infra_step=True, 281 timeout=30) 282 283 284 def _scale_cpu(self, cpu, target_percent): 285 self._ever_ran_adb = True 286 self.m.run.with_retry(self.m.python.inline, 287 'Scale CPU %d to %f' % (cpu, target_percent), 288 3, # attempts 289 program=""" 290import os 291import subprocess 292import sys 293import time 294ADB = sys.argv[1] 295target_percent = float(sys.argv[2]) 296cpu = int(sys.argv[3]) 297log = subprocess.check_output([ADB, 'root']) 298# check for message like 'adbd cannot run as root in production builds' 299print log 300if 'cannot' in log: 301 raise Exception('adb root failed') 302 303root = '/sys/devices/system/cpu/cpu%d/cpufreq' %cpu 304 305# All devices we test on give a list of their available frequencies. 306available_freqs = subprocess.check_output([ADB, 'shell', 307 'cat %s/scaling_available_frequencies' % root]) 308 309# Check for message like '/system/bin/sh: file not found' 310if available_freqs and '/system/bin/sh' not in available_freqs: 311 available_freqs = sorted( 312 int(i) for i in available_freqs.strip().split()) 313else: 314 raise Exception('Could not get list of available frequencies: %s' % 315 available_freqs) 316 317maxfreq = available_freqs[-1] 318target = int(round(maxfreq * target_percent)) 319freq = maxfreq 320for f in reversed(available_freqs): 321 if f <= target: 322 freq = f 323 break 324 325print 'Setting frequency to %d' % freq 326 327# If scaling_max_freq is lower than our attempted setting, it won't take. 328# We must set min first, because if we try to set max to be less than min 329# (which sometimes happens after certain devices reboot) it returns a 330# perplexing permissions error. 331subprocess.check_output([ADB, 'shell', 'echo 0 > ' 332 '%s/scaling_min_freq' % root]) 333subprocess.check_output([ADB, 'shell', 'echo %d > ' 334 '%s/scaling_max_freq' % (freq, root)]) 335subprocess.check_output([ADB, 'shell', 'echo %d > ' 336 '%s/scaling_setspeed' % (freq, root)]) 337time.sleep(5) 338actual_freq = subprocess.check_output([ADB, 'shell', 'cat ' 339 '%s/scaling_cur_freq' % root]).strip() 340if actual_freq != str(freq): 341 raise Exception('(actual, expected) (%s, %d)' 342 % (actual_freq, freq)) 343""", 344 args = [self.ADB_BINARY, str(target_percent), cpu], 345 infra_step=True, 346 timeout=30) 347 348 349 def install(self): 350 self._adb('mkdir ' + self.device_dirs.resource_dir, 351 'shell', 'mkdir', '-p', self.device_dirs.resource_dir) 352 if 'ASAN' in self.m.vars.extra_tokens: 353 self._ever_ran_adb = True 354 asan_setup = self.m.vars.slave_dir.join( 355 'android_ndk_linux', 'toolchains', 'llvm', 'prebuilt', 356 'linux-x86_64', 'lib64', 'clang', '8.0.7', 'bin', 357 'asan_device_setup') 358 self.m.run(self.m.python.inline, 'Setting up device to run ASAN', 359 program=""" 360import os 361import subprocess 362import sys 363import time 364ADB = sys.argv[1] 365ASAN_SETUP = sys.argv[2] 366 367def wait_for_device(): 368 while True: 369 time.sleep(5) 370 print 'Waiting for device' 371 subprocess.check_output([ADB, 'wait-for-device']) 372 bit1 = subprocess.check_output([ADB, 'shell', 'getprop', 373 'dev.bootcomplete']) 374 bit2 = subprocess.check_output([ADB, 'shell', 'getprop', 375 'sys.boot_completed']) 376 if '1' in bit1 and '1' in bit2: 377 print 'Device detected' 378 break 379 380log = subprocess.check_output([ADB, 'root']) 381# check for message like 'adbd cannot run as root in production builds' 382print log 383if 'cannot' in log: 384 raise Exception('adb root failed') 385 386output = subprocess.check_output([ADB, 'disable-verity']) 387print output 388 389if 'already disabled' not in output: 390 print 'Rebooting device' 391 subprocess.check_output([ADB, 'reboot']) 392 wait_for_device() 393 394def installASAN(revert=False): 395 # ASAN setup script is idempotent, either it installs it or 396 # says it's installed. Returns True on success, false otherwise. 397 out = subprocess.check_output([ADB, 'wait-for-device']) 398 print out 399 cmd = [ASAN_SETUP] 400 if revert: 401 cmd = [ASAN_SETUP, '--revert'] 402 process = subprocess.Popen(cmd, env={'ADB': ADB}, 403 stdout=subprocess.PIPE, stderr=subprocess.PIPE) 404 405 # this also blocks until command finishes 406 (stdout, stderr) = process.communicate() 407 print stdout 408 print 'Stderr: %s' % stderr 409 return process.returncode == 0 410 411if not installASAN(): 412 print 'Trying to revert the ASAN install and then re-install' 413 # ASAN script sometimes has issues if it was interrupted or partially applied 414 # Try reverting it, then re-enabling it 415 if not installASAN(revert=True): 416 raise Exception('reverting ASAN install failed') 417 418 # Sleep because device does not reboot instantly 419 time.sleep(10) 420 421 if not installASAN(): 422 raise Exception('Tried twice to setup ASAN and failed.') 423 424# Sleep because device does not reboot instantly 425time.sleep(10) 426wait_for_device() 427# Sleep again to hopefully avoid error "secure_mkdirs failed: No such file or 428# directory" when pushing resources to the device. 429time.sleep(60) 430""", 431 args = [self.ADB_BINARY, asan_setup], 432 infra_step=True, 433 timeout=300, 434 abort_on_failure=True) 435 436 437 def cleanup_steps(self): 438 if 'ASAN' in self.m.vars.extra_tokens: 439 self._ever_ran_adb = True 440 # Remove ASAN. 441 asan_setup = self.m.vars.slave_dir.join( 442 'android_ndk_linux', 'toolchains', 'llvm', 'prebuilt', 443 'linux-x86_64', 'lib64', 'clang', '8.0.2', 'bin', 444 'asan_device_setup') 445 self.m.run(self.m.step, 446 'wait for device before uninstalling ASAN', 447 cmd=[self.ADB_BINARY, 'wait-for-device'], infra_step=True, 448 timeout=180, abort_on_failure=False, 449 fail_build_on_failure=False) 450 self.m.run(self.m.step, 'uninstall ASAN', 451 cmd=[asan_setup, '--revert'], infra_step=True, timeout=300, 452 abort_on_failure=False, fail_build_on_failure=False) 453 454 if self._ever_ran_adb: 455 self.m.run(self.m.python.inline, 'dump log', program=""" 456 import os 457 import subprocess 458 import sys 459 out = sys.argv[1] 460 log = subprocess.check_output(['%s', 'logcat', '-d']) 461 for line in log.split('\\n'): 462 tokens = line.split() 463 if len(tokens) == 11 and tokens[-7] == 'F' and tokens[-3] == 'pc': 464 addr, path = tokens[-2:] 465 local = os.path.join(out, os.path.basename(path)) 466 if os.path.exists(local): 467 sym = subprocess.check_output(['addr2line', '-Cfpe', local, addr]) 468 line = line.replace(addr, addr + ' ' + sym.strip()) 469 print line 470 """ % self.ADB_BINARY, 471 args=[self.host_dirs.bin_dir], 472 infra_step=True, 473 timeout=300, 474 abort_on_failure=False) 475 476 # Only quarantine the bot if the first failed step 477 # is an infra step. If, instead, we did this for any infra failures, we 478 # would do this too much. For example, if a Nexus 10 died during dm 479 # and the following pull step would also fail "device not found" - causing 480 # us to run the shutdown command when the device was probably not in a 481 # broken state; it was just rebooting. 482 if (self.m.run.failed_steps and 483 isinstance(self.m.run.failed_steps[0], recipe_api.InfraFailure)): 484 bot_id = self.m.vars.swarming_bot_id 485 self.m.file.write_text('Quarantining Bot', 486 '/home/chrome-bot/%s.force_quarantine' % bot_id, 487 ' ') 488 489 if self._ever_ran_adb: 490 self._adb('kill adb server', 'kill-server') 491 492 def step(self, name, cmd, **kwargs): 493 if not kwargs.get('skip_binary_push', False): 494 if (cmd[0] == 'nanobench'): 495 self._scale_for_nanobench() 496 else: 497 self._scale_for_dm() 498 app = self.host_dirs.bin_dir.join(cmd[0]) 499 self._adb('push %s' % cmd[0], 500 'push', app, self.device_dirs.bin_dir) 501 502 sh = '%s.sh' % cmd[0] 503 self.m.run.writefile(self.m.vars.tmp_dir.join(sh), 504 'set -x; %s%s; echo $? >%src' % ( 505 self.device_dirs.bin_dir, subprocess.list2cmdline(map(str, cmd)), 506 self.device_dirs.bin_dir)) 507 self._adb('push %s' % sh, 508 'push', self.m.vars.tmp_dir.join(sh), self.device_dirs.bin_dir) 509 510 self._adb('clear log', 'logcat', '-c') 511 self.m.python.inline('%s' % cmd[0], """ 512 import subprocess 513 import sys 514 bin_dir = sys.argv[1] 515 sh = sys.argv[2] 516 subprocess.check_call(['%s', 'shell', 'sh', bin_dir + sh]) 517 try: 518 sys.exit(int(subprocess.check_output(['%s', 'shell', 'cat', 519 bin_dir + 'rc']))) 520 except ValueError: 521 print "Couldn't read the return code. Probably killed for OOM." 522 sys.exit(1) 523 """ % (self.ADB_BINARY, self.ADB_BINARY), 524 args=[self.device_dirs.bin_dir, sh]) 525 526 def copy_file_to_device(self, host, device): 527 self._adb('push %s %s' % (host, device), 'push', host, device) 528 529 def copy_directory_contents_to_device(self, host, device): 530 # Copy the tree, avoiding hidden directories and resolving symlinks. 531 self.m.run(self.m.python.inline, 'push %s/* %s' % (host, device), 532 program=""" 533 import os 534 import subprocess 535 import sys 536 host = sys.argv[1] 537 device = sys.argv[2] 538 for d, _, fs in os.walk(host): 539 p = os.path.relpath(d, host) 540 if p != '.' and p.startswith('.'): 541 continue 542 for f in fs: 543 print os.path.join(p,f) 544 subprocess.check_call(['%s', 'push', 545 os.path.realpath(os.path.join(host, p, f)), 546 os.path.join(device, p, f)]) 547 """ % self.ADB_BINARY, args=[host, device], infra_step=True) 548 549 def copy_directory_contents_to_host(self, device, host): 550 # TODO(borenet): When all of our devices are on Android 6.0 and up, we can 551 # switch to using tar to zip up the results before pulling. 552 with self.m.step.nest('adb pull'): 553 with self.m.tempfile.temp_dir('adb_pull') as tmp: 554 self._adb('pull %s' % device, 'pull', device, tmp) 555 paths = self.m.file.glob_paths( 556 'list pulled files', 557 tmp, 558 self.m.path.basename(device) + self.m.path.sep + '*', 559 test_data=['%d.png' % i for i in (1, 2)]) 560 for p in paths: 561 self.m.file.copy('copy %s' % self.m.path.basename(p), p, host) 562 563 def read_file_on_device(self, path, **kwargs): 564 rv = self._adb('read %s' % path, 565 'shell', 'cat', path, stdout=self.m.raw_io.output(), 566 **kwargs) 567 return rv.stdout.rstrip() if rv and rv.stdout else None 568 569 def remove_file_on_device(self, path): 570 self._adb('rm %s' % path, 'shell', 'rm', '-f', path) 571 572 def create_clean_device_dir(self, path): 573 self._adb('rm %s' % path, 'shell', 'rm', '-rf', path) 574 self._adb('mkdir %s' % path, 'shell', 'mkdir', '-p', path) 575