1#!/usr/bin/env python
2# Copyright (c) 2014 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6
7"""rebase.py: standalone script to batch update bench expectations.
8
9    Requires gsutil to access gs://chromium-skia-gm and Rietveld credentials.
10
11    Usage:
12      Copy script to a separate dir outside Skia repo. The script will create a
13          skia dir on the first run to host the repo, and will create/delete
14          temp dirs as needed.
15      ./rebase.py --githash <githash prefix to use for getting bench data>
16"""
17
18
19import argparse
20import filecmp
21import os
22import re
23import shutil
24import subprocess
25import time
26import urllib2
27
28
29# googlesource url that has most recent Skia git hash info.
30SKIA_GIT_HEAD_URL = 'https://skia.googlesource.com/skia/+log/HEAD'
31
32# Google Storage bench file prefix.
33GS_PREFIX = 'gs://chromium-skia-gm/perfdata'
34
35# List of Perf platforms we want to process. Populate from expectations/bench.
36PLATFORMS = []
37
38# Regular expression for matching githash data.
39HA_RE = '<a href="/skia/\+/([0-9a-f]+)">'
40HA_RE_COMPILED = re.compile(HA_RE)
41
42
43def get_git_hashes():
44  print 'Getting recent git hashes...'
45  hashes = HA_RE_COMPILED.findall(
46      urllib2.urlopen(SKIA_GIT_HEAD_URL).read())
47
48  return hashes
49
50def filter_file(f):
51  if f.find('_msaa') > 0 or f.find('_record') > 0:
52    return True
53
54  return False
55
56def clean_dir(d):
57  if os.path.exists(d):
58    shutil.rmtree(d)
59  os.makedirs(d)
60
61def get_gs_filelist(p, h):
62  print 'Looking up for the closest bench files in Google Storage...'
63  proc = subprocess.Popen(['gsutil', 'ls',
64      '/'.join([GS_PREFIX, p, 'bench_' + h + '_data_skp_*'])],
65          stdout=subprocess.PIPE)
66  out, err = proc.communicate()
67  if err or not out:
68    return []
69  return [i for i in out.strip().split('\n') if not filter_file(i)]
70
71def download_gs_files(p, h, gs_dir):
72  print 'Downloading raw bench files from Google Storage...'
73  proc = subprocess.Popen(['gsutil', 'cp',
74      '/'.join([GS_PREFIX, p, 'bench_' + h + '_data_skp_*']),
75          '%s/%s' % (gs_dir, p)],
76          stdout=subprocess.PIPE)
77  out, err = proc.communicate()
78  if err:
79    clean_dir(gs_dir)
80    return False
81  files = 0
82  for f in os.listdir(os.path.join(gs_dir, p)):
83    if filter_file(f):
84      os.remove(os.path.join(gs_dir, p, f))
85    else:
86      files += 1
87  if files:
88    return True
89  return False
90
91def calc_expectations(p, h, gs_dir, exp_dir, repo_dir):
92  exp_filename = 'bench_expectations_%s.txt' % p
93  proc = subprocess.Popen(['python', 'skia/bench/gen_bench_expectations.py',
94      '-r', h, '-b', p, '-d', os.path.join(gs_dir, p), '-o',
95          os.path.join(exp_dir, exp_filename)],
96              stdout=subprocess.PIPE)
97  out, err = proc.communicate()
98  if err:
99    print 'ERR_CALCULATING_EXPECTATIONS: ' + err
100    return False
101  print 'CALCULATED_EXPECTATIONS: ' + out
102  repo_file = os.path.join(repo_dir, 'expectations', 'bench', exp_filename)
103  if (os.path.isfile(repo_file) and
104      filecmp.cmp(repo_file, os.path.join(exp_dir, exp_filename))):
105      print 'NO CHANGE ON %s' % repo_file
106      return False
107  return True
108
109def checkout_or_update_skia(repo_dir):
110  status = True
111  old_cwd = os.getcwd()
112  os.chdir(repo_dir)
113  print 'CHECK SKIA REPO...'
114  if subprocess.call(['git', 'pull'],
115                     stderr=subprocess.PIPE):
116    print 'Checking out Skia from git, please be patient...'
117    os.chdir(old_cwd)
118    clean_dir(repo_dir)
119    os.chdir(repo_dir)
120    if subprocess.call(['git', 'clone', '-q', '--depth=50', '--single-branch',
121                        'https://skia.googlesource.com/skia.git', '.']):
122      status = False
123  subprocess.call(['git', 'checkout', 'master'])
124  subprocess.call(['git', 'pull'])
125  os.chdir(old_cwd)
126  return status
127
128def git_commit_expectations(repo_dir, exp_dir, update_li, h, commit):
129  commit_msg = """manual bench rebase after %s
130
131TBR=robertphillips@google.com
132
133Bypassing trybots:
134NOTRY=true""" % h
135  old_cwd = os.getcwd()
136  os.chdir(repo_dir)
137  upload = ['git', 'cl', 'upload', '-f', '--bypass-hooks',
138            '--bypass-watchlists', '-m', commit_msg]
139  branch = exp_dir.split('/')[-1]
140  if commit:
141    upload.append('--use-commit-queue')
142  cmds = ([['git', 'checkout', 'master'],
143           ['git', 'pull'],
144           ['git', 'checkout', '-b', branch, '-t', 'origin/master']] +
145          [['cp', '%s/%s' % (exp_dir, f), 'expectations/bench'] for f in
146           update_li] +
147          [['git', 'add'] + ['expectations/bench/%s' % i for i in update_li],
148           ['git', 'commit', '-m', commit_msg],
149           upload,
150           ['git', 'checkout', 'master'],
151           ['git', 'branch', '-D', branch],
152          ])
153  status = True
154  for cmd in cmds:
155    print 'Running ' + ' '.join(cmd)
156    if subprocess.call(cmd):
157      print 'FAILED. Please check if skia git repo is present.'
158      subprocess.call(['git', 'checkout', 'master'])
159      status = False
160      break
161  os.chdir(old_cwd)
162  return status
163
164def delete_dirs(li):
165  for d in li:
166    print 'Deleting directory %s' % d
167    shutil.rmtree(d)
168
169
170def main():
171  d = os.path.dirname(os.path.abspath(__file__))
172  os.chdir(d)
173  if not subprocess.call(['git', 'rev-parse'], stderr=subprocess.PIPE):
174    print 'Please copy script to a separate dir outside git repos to use.'
175    return
176  parser = argparse.ArgumentParser()
177  parser.add_argument('--githash',
178                      help='Githash prefix (7+ chars) to rebaseline to.')
179  parser.add_argument('--commit', action='store_true',
180                      help='Whether to commit changes automatically.')
181  args = parser.parse_args()
182
183  repo_dir = os.path.join(d, 'skia')
184  if not os.path.exists(repo_dir):
185    os.makedirs(repo_dir)
186  if not checkout_or_update_skia(repo_dir):
187    print 'ERROR setting up Skia repo at %s' % repo_dir
188    return 1
189
190  file_in_repo = os.path.join(d, 'skia/experimental/benchtools/rebase.py')
191  if not filecmp.cmp(__file__, file_in_repo):
192    shutil.copy(file_in_repo, __file__)
193    print 'Updated this script from repo; please run again.'
194    return
195
196  for item in os.listdir(os.path.join(d, 'skia/expectations/bench')):
197    PLATFORMS.append(
198        item.replace('bench_expectations_', '').replace('.txt', ''))
199
200  if not args.githash or len(args.githash) < 7:
201    raise Exception('Please provide --githash with a longer prefix (7+).')
202  commit = False
203  if args.commit:
204    commit = True
205  rebase_hash = args.githash[:7]
206  hashes = get_git_hashes()
207  short_hashes = [h[:7] for h in hashes]
208  if rebase_hash not in short_hashes:
209    raise Exception('Provided --githash not found in recent history!')
210  hashes = hashes[:short_hashes.index(rebase_hash) + 1]
211  update_li = []
212
213  ts_str = '%s' % time.time()
214  gs_dir = os.path.join(d, 'gs' + ts_str)
215  exp_dir = os.path.join(d, 'exp' + ts_str)
216  clean_dir(gs_dir)
217  clean_dir(exp_dir)
218  for p in PLATFORMS:
219    clean_dir(os.path.join(gs_dir, p))
220    hash_to_use = ''
221    for h in reversed(hashes):
222      li = get_gs_filelist(p, h)
223      if not len(li):  # no data
224        continue
225      if download_gs_files(p, h, gs_dir):
226        print 'Copied %s/%s' % (p, h)
227        hash_to_use = h
228        break
229      else:
230        print 'DOWNLOAD BENCH FAILED %s/%s' % (p, h)
231        break
232    if hash_to_use:
233      if calc_expectations(p, h, gs_dir, exp_dir, repo_dir):
234        update_li.append('bench_expectations_%s.txt' % p)
235  if not update_li:
236    print 'No bench data to update after %s!' % args.githash
237  elif not git_commit_expectations(
238      repo_dir, exp_dir, update_li, args.githash[:7], commit):
239    print 'ERROR uploading expectations using git.'
240  elif not commit:
241    print 'CL created. Please take a look at the link above.'
242  else:
243    print 'New bench baselines should be in CQ now.'
244  delete_dirs([gs_dir, exp_dir])
245
246
247if __name__ == "__main__":
248  main()
249