• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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