xref: /aosp_15_r20/external/skia/infra/bots/recipe_modules/flavor/android.py (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
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