test_toolchains.py revision 748254e9a5dd7227806ca618d1f9a32ab49f4056
1#!/usr/bin/python2
2
3# Script to test different toolchains against ChromeOS benchmarks.
4"""Toolchain team nightly performance test script (local builds)."""
5
6from __future__ import print_function
7
8import argparse
9import datetime
10import os
11import sys
12import build_chromeos
13import setup_chromeos
14import time
15from cros_utils import command_executer
16from cros_utils import misc
17from cros_utils import logger
18
19CROSTC_ROOT = '/usr/local/google/crostc'
20MAIL_PROGRAM = '~/var/bin/mail-sheriff'
21WEEKLY_REPORTS_ROOT = os.path.join(CROSTC_ROOT, 'weekly_test_data')
22PENDING_ARCHIVES_DIR = os.path.join(CROSTC_ROOT, 'pending_archives')
23NIGHTLY_TESTS_DIR = os.path.join(CROSTC_ROOT, 'nightly_test_reports')
24
25
26class GCCConfig(object):
27  """GCC configuration class."""
28
29  def __init__(self, githash):
30    self.githash = githash
31
32
33class ToolchainConfig(object):
34  """Toolchain configuration class."""
35
36  def __init__(self, gcc_config=None):
37    self.gcc_config = gcc_config
38
39
40class ChromeOSCheckout(object):
41  """Main class for checking out, building and testing ChromeOS."""
42
43  def __init__(self, board, chromeos_root):
44    self._board = board
45    self._chromeos_root = chromeos_root
46    self._ce = command_executer.GetCommandExecuter()
47    self._l = logger.GetLogger()
48    self._build_num = None
49
50  def _DeleteChroot(self):
51    command = 'cd %s; cros_sdk --delete' % self._chromeos_root
52    return self._ce.RunCommand(command)
53
54  def _DeleteCcahe(self):
55    # crosbug.com/34956
56    command = 'sudo rm -rf %s' % os.path.join(self._chromeos_root, '.cache')
57    return self._ce.RunCommand(command)
58
59  def _GetBuildNumber(self):
60    """Get the build number of the ChromeOS image from the chroot.
61
62    This function assumes a ChromeOS image has been built in the chroot.
63    It translates the 'latest' symlink in the
64    <chroot>/src/build/images/<board> directory, to find the actual
65    ChromeOS build number for the image that was built.  For example, if
66    src/build/image/lumpy/latest ->  R37-5982.0.2014_06_23_0454-a1, then
67    This function would parse it out and assign 'R37-5982' to self._build_num.
68    This is used to determine the official, vanilla build to use for
69    comparison tests.
70    """
71    # Get the path to 'latest'
72    sym_path = os.path.join(
73        misc.GetImageDir(self._chromeos_root, self._board), 'latest')
74    # Translate the symlink to its 'real' path.
75    real_path = os.path.realpath(sym_path)
76    # Break up the path and get the last piece
77    # (e.g. 'R37-5982.0.2014_06_23_0454-a1"
78    path_pieces = real_path.split('/')
79    last_piece = path_pieces[-1]
80    # Break this piece into the image number + other pieces, and get the
81    # image number [ 'R37-5982', '0', '2014_06_23_0454-a1']
82    image_parts = last_piece.split('.')
83    self._build_num = image_parts[0]
84
85  def _BuildLabelName(self, config):
86    pieces = config.split('/')
87    compiler_version = pieces[-1]
88    label = compiler_version + '_tot_afdo'
89    return label
90
91  def _BuildAndImage(self, label=''):
92    if (not label or
93        not misc.DoesLabelExist(self._chromeos_root, self._board, label)):
94      build_chromeos_args = [build_chromeos.__file__,
95                             '--chromeos_root=%s' % self._chromeos_root,
96                             '--board=%s' % self._board, '--rebuild']
97      if self._public:
98        build_chromeos_args.append('--env=USE=-chrome_internal')
99
100      ret = build_chromeos.Main(build_chromeos_args)
101      if ret != 0:
102        raise RuntimeError("Couldn't build ChromeOS!")
103
104      if not self._build_num:
105        self._GetBuildNumber()
106      # Check to see if we need to create the symbolic link for the vanilla
107      # image, and do so if appropriate.
108      if not misc.DoesLabelExist(self._chromeos_root, self._board, 'vanilla'):
109        build_name = '%s-release/%s.0.0' % (self._board, self._build_num)
110        full_vanilla_path = os.path.join(os.getcwd(), self._chromeos_root,
111                                         'chroot/tmp', build_name)
112        misc.LabelLatestImage(self._chromeos_root, self._board, label,
113                              full_vanilla_path)
114      else:
115        misc.LabelLatestImage(self._chromeos_root, self._board, label)
116    return label
117
118  def _SetupBoard(self, env_dict, usepkg_flag, clobber_flag):
119    env_string = misc.GetEnvStringFromDict(env_dict)
120    command = ('%s %s' % (env_string,
121                          misc.GetSetupBoardCommand(self._board,
122                                                    usepkg=usepkg_flag,
123                                                    force=clobber_flag)))
124    ret = self._ce.ChrootRunCommand(self._chromeos_root, command)
125    error_str = "Could not setup board: '%s'" % command
126    assert ret == 0, error_str
127
128  def _UnInstallToolchain(self):
129    command = ('sudo CLEAN_DELAY=0 emerge -C cross-%s/gcc' %
130               misc.GetCtargetFromBoard(self._board, self._chromeos_root))
131    ret = self._ce.ChrootRunCommand(self._chromeos_root, command)
132    if ret != 0:
133      raise RuntimeError("Couldn't uninstall the toolchain!")
134
135  def _CheckoutChromeOS(self):
136    # TODO(asharif): Setup a fixed ChromeOS version (quarterly snapshot).
137    if not os.path.exists(self._chromeos_root):
138      setup_chromeos_args = [setup_chromeos.__file__,
139                             '--dir=%s' % self._chromeos_root]
140      if self._public:
141        setup_chromeos_args.append('--public')
142      ret = setup_chromeos.Main(setup_chromeos_args)
143      if ret != 0:
144        raise RuntimeError("Couldn't run setup_chromeos!")
145
146  def _BuildToolchain(self, config):
147    # Call setup_board for basic, vanilla setup.
148    self._SetupBoard({}, usepkg_flag=True, clobber_flag=False)
149    # Now uninstall the vanilla compiler and setup/build our custom
150    # compiler.
151    self._UnInstallToolchain()
152    envdict = {'USE': 'git_gcc',
153               'GCC_GITHASH': config.gcc_config.githash,
154               'EMERGE_DEFAULT_OPTS': '--exclude=gcc'}
155    self._SetupBoard(envdict, usepkg_flag=False, clobber_flag=False)
156
157
158class ToolchainComparator(ChromeOSCheckout):
159  """Main class for running tests and generating reports."""
160
161  def __init__(self,
162               board,
163               remotes,
164               configs,
165               clean,
166               public,
167               force_mismatch,
168               noschedv2=False):
169    self._board = board
170    self._remotes = remotes
171    self._chromeos_root = 'chromeos'
172    self._configs = configs
173    self._clean = clean
174    self._public = public
175    self._force_mismatch = force_mismatch
176    self._ce = command_executer.GetCommandExecuter()
177    self._l = logger.GetLogger()
178    timestamp = datetime.datetime.strftime(datetime.datetime.now(),
179                                           '%Y-%m-%d_%H:%M:%S')
180    self._reports_dir = os.path.join(NIGHTLY_TESTS_DIR,
181                                     '%s.%s' % (timestamp, board),)
182    self._noschedv2 = noschedv2
183    ChromeOSCheckout.__init__(self, board, self._chromeos_root)
184
185  def _FinishSetup(self):
186    # Get correct .boto file
187    current_dir = os.getcwd()
188    src = '/usr/local/google/home/mobiletc-prebuild/.boto'
189    dest = os.path.join(current_dir, self._chromeos_root,
190                        'src/private-overlays/chromeos-overlay/'
191                        'googlestorage_account.boto')
192    # Copy the file to the correct place
193    copy_cmd = 'cp %s %s' % (src, dest)
194    retv = self._ce.RunCommand(copy_cmd)
195    if retv != 0:
196      raise RuntimeError("Couldn't copy .boto file for google storage.")
197
198    # Fix protections on ssh key
199    command = ('chmod 600 /var/cache/chromeos-cache/distfiles/target'
200               '/chrome-src-internal/src/third_party/chromite/ssh_keys'
201               '/testing_rsa')
202    retv = self._ce.ChrootRunCommand(self._chromeos_root, command)
203    if retv != 0:
204      raise RuntimeError('chmod for testing_rsa failed')
205
206  def _TestLabels(self, labels):
207    experiment_file = 'toolchain_experiment.txt'
208    image_args = ''
209    if self._force_mismatch:
210      image_args = '--force-mismatch'
211    experiment_header = """
212    board: %s
213    remote: %s
214    retries: 1
215    """ % (self._board, self._remotes)
216    experiment_tests = """
217    benchmark: all_toolchain_perf {
218      suite: telemetry_Crosperf
219      iterations: 3
220    }
221    """
222
223    with open(experiment_file, 'w') as f:
224      f.write(experiment_header)
225      f.write(experiment_tests)
226      for label in labels:
227        # TODO(asharif): Fix crosperf so it accepts labels with symbols
228        crosperf_label = label
229        crosperf_label = crosperf_label.replace('-', '_')
230        crosperf_label = crosperf_label.replace('+', '_')
231        crosperf_label = crosperf_label.replace('.', '')
232
233        # Use the official build instead of building vanilla ourselves.
234        if label == 'vanilla':
235          build_name = '%s-release/%s.0.0' % (self._board, self._build_num)
236
237          # Now add 'official build' to test file.
238          official_image = """
239          official_image {
240            chromeos_root: %s
241            build: %s
242          }
243          """ % (self._chromeos_root, build_name)
244          f.write(official_image)
245
246        else:
247          experiment_image = """
248          %s {
249            chromeos_image: %s
250            image_args: %s
251          }
252          """ % (crosperf_label, os.path.join(
253              misc.GetImageDir(self._chromeos_root, self._board), label,
254              'chromiumos_test_image.bin'), image_args)
255          f.write(experiment_image)
256
257    crosperf = os.path.join(os.path.dirname(__file__), 'crosperf', 'crosperf')
258    noschedv2_opts = '--noschedv2' if self._noschedv2 else ''
259    command = ('{crosperf} --no_email=True --results_dir={r_dir} '
260               '--json_report=True {noschedv2_opts} {exp_file}').format(
261                   crosperf=crosperf,
262                   r_dir=self._reports_dir,
263                   noschedv2_opts=noschedv2_opts,
264                   exp_file=experiment_file)
265
266    ret = self._ce.RunCommand(command)
267    if ret != 0:
268      raise RuntimeError("Couldn't run crosperf!")
269    else:
270      # Copy json report to pending archives directory.
271      command = 'cp %s/*.json %s/.' % (self._reports_dir, PENDING_ARCHIVES_DIR)
272      ret = self._ce.RunCommand(command)
273    return
274
275  def _CopyWeeklyReportFiles(self, labels):
276    """Move files into place for creating 7-day reports.
277
278    Create tar files of the custom and official images and copy them
279    to the weekly reports directory, so they exist when the weekly report
280    gets generated.  IMPORTANT NOTE: This function must run *after*
281    crosperf has been run; otherwise the vanilla images will not be there.
282    """
283    images_path = os.path.join(
284        os.path.realpath(self._chromeos_root), 'src/build/images', self._board)
285    weekday = time.strftime('%a')
286    data_dir = os.path.join(WEEKLY_REPORTS_ROOT, self._board)
287    dest_dir = os.path.join(data_dir, weekday)
288    if not os.path.exists(dest_dir):
289      os.makedirs(dest_dir)
290    # Make sure dest_dir is empty (clean out last week's data).
291    cmd = 'cd %s; rm -Rf %s_*_image*' % (dest_dir, weekday)
292    self._ce.RunCommand(cmd)
293    # Now create new tar files and copy them over.
294    for l in labels:
295      test_path = os.path.join(images_path, l)
296      if os.path.exists(test_path):
297        if l != 'vanilla':
298          label_name = 'test'
299        else:
300          label_name = 'vanilla'
301        tar_file_name = '%s_%s_image.tar' % (weekday, label_name)
302        cmd = ('cd %s; tar -cvf %s %s/chromiumos_test_image.bin; '
303               'cp %s %s/.') % (images_path, tar_file_name, l, tar_file_name,
304                                dest_dir)
305        tar_ret = self._ce.RunCommand(cmd)
306        if tar_ret != 0:
307          self._l.LogOutput('Error while creating/copying test tar file(%s).' %
308                            tar_file_name)
309
310  def _SendEmail(self):
311    """Find email msesage generated by crosperf and send it."""
312    filename = os.path.join(self._reports_dir, 'msg_body.html')
313    if (os.path.exists(filename) and
314        os.path.exists(os.path.expanduser(MAIL_PROGRAM))):
315      command = ('cat %s | %s -s "Nightly test results, %s" -team -html' %
316                 (filename, MAIL_PROGRAM, self._board))
317      self._ce.RunCommand(command)
318
319  def DoAll(self):
320    self._CheckoutChromeOS()
321    labels = []
322    labels.append('vanilla')
323    for config in self._configs:
324      label = self._BuildLabelName(config.gcc_config.githash)
325      if not misc.DoesLabelExist(self._chromeos_root, self._board, label):
326        self._BuildToolchain(config)
327        label = self._BuildAndImage(label)
328      labels.append(label)
329    self._FinishSetup()
330    self._TestLabels(labels)
331    self._SendEmail()
332    # Only try to copy the image files if the test runs ran successfully.
333    self._CopyWeeklyReportFiles(labels)
334    if self._clean:
335      ret = self._DeleteChroot()
336      if ret != 0:
337        return ret
338      ret = self._DeleteCcahe()
339      if ret != 0:
340        return ret
341    return 0
342
343
344def Main(argv):
345  """The main function."""
346  # Common initializations
347  ###  command_executer.InitCommandExecuter(True)
348  command_executer.InitCommandExecuter()
349  parser = argparse.ArgumentParser()
350  parser.add_argument('--remote',
351                      dest='remote',
352                      help='Remote machines to run tests on.')
353  parser.add_argument('--board',
354                      dest='board',
355                      default='x86-alex',
356                      help='The target board.')
357  parser.add_argument('--githashes',
358                      dest='githashes',
359                      default='master',
360                      help='The gcc githashes to test.')
361  parser.add_argument('--clean',
362                      dest='clean',
363                      default=False,
364                      action='store_true',
365                      help='Clean the chroot after testing.')
366  parser.add_argument('--public',
367                      dest='public',
368                      default=False,
369                      action='store_true',
370                      help='Use the public checkout/build.')
371  parser.add_argument('--force-mismatch',
372                      dest='force_mismatch',
373                      default='',
374                      help='Force the image regardless of board mismatch')
375  parser.add_argument('--noschedv2',
376                      dest='noschedv2',
377                      action='store_true',
378                      default=False,
379                      help='Pass --noschedv2 to crosperf.')
380  options = parser.parse_args(argv)
381  if not options.board:
382    print('Please give a board.')
383    return 1
384  if not options.remote:
385    print('Please give at least one remote machine.')
386    return 1
387  toolchain_configs = []
388  for githash in options.githashes.split(','):
389    gcc_config = GCCConfig(githash=githash)
390    toolchain_config = ToolchainConfig(gcc_config=gcc_config)
391    toolchain_configs.append(toolchain_config)
392  fc = ToolchainComparator(options.board, options.remote, toolchain_configs,
393                           options.clean, options.public,
394                           options.force_mismatch, options.noschedv2)
395  return fc.DoAll()
396
397
398if __name__ == '__main__':
399  retval = Main(sys.argv[1:])
400  sys.exit(retval)
401