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