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