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