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 19 import argparse 20 import filecmp 21 import os 22 import re 23 import shutil 24 import subprocess 25 import time 26 import urllib2 27 28 29 # googlesource url that has most recent Skia git hash info. 30 SKIA_GIT_HEAD_URL = 'https://skia.googlesource.com/skia/+log/HEAD' 31 32 # Google Storage bench file prefix. 33 GS_PREFIX = 'gs://chromium-skia-gm/perfdata' 34 35 # Regular expression for matching githash data. 36 HA_RE = '<a href="/skia/\+/([0-9a-f]+)">' 37 HA_RE_COMPILED = re.compile(HA_RE) 38 39 40 def get_git_hashes(): 41 print 'Getting recent git hashes...' 42 hashes = HA_RE_COMPILED.findall( 43 urllib2.urlopen(SKIA_GIT_HEAD_URL).read()) 44 45 return hashes 46 47 def filter_file(f): 48 if f.find('_msaa') > 0 or f.find('_record') > 0: 49 return True 50 51 return False 52 53 def clean_dir(d): 54 if os.path.exists(d): 55 shutil.rmtree(d) 56 os.makedirs(d) 57 58 def get_gs_filelist(p, h): 59 print 'Looking up for the closest bench files in Google Storage...' 60 proc = subprocess.Popen(['gsutil', 'ls', 61 '/'.join([GS_PREFIX, p, 'bench_' + h + '_data_skp_*'])], 62 stdout=subprocess.PIPE) 63 out, err = proc.communicate() 64 if err or not out: 65 return [] 66 return [i for i in out.strip().split('\n') if not filter_file(i)] 67 68 def download_gs_files(p, h, gs_dir): 69 print 'Downloading raw bench files from Google Storage...' 70 proc = subprocess.Popen(['gsutil', 'cp', 71 '/'.join([GS_PREFIX, p, 'bench_' + h + '_data_skp_*']), 72 '%s/%s' % (gs_dir, p)], 73 stdout=subprocess.PIPE) 74 out, err = proc.communicate() 75 if err: 76 clean_dir(gs_dir) 77 return False 78 files = 0 79 for f in os.listdir(os.path.join(gs_dir, p)): 80 if filter_file(f): 81 os.remove(os.path.join(gs_dir, p, f)) 82 else: 83 files += 1 84 if files: 85 return True 86 return False 87 88 def get_expectations_dict(f): 89 """Given an expectations file f, returns a dictionary of data.""" 90 # maps row_key to (expected, lower_bound, upper_bound) float tuple. 91 dic = {} 92 for l in open(f).readlines(): 93 line_parts = l.strip().split(',') 94 if line_parts[0].startswith('#') or len(line_parts) != 5: 95 continue 96 dic[','.join(line_parts[:2])] = (float(line_parts[2]), float(line_parts[3]), 97 float(line_parts[4])) 98 99 return dic 100 101 def calc_expectations(p, h, gs_dir, exp_dir, repo_dir, extra_dir, extra_hash): 102 exp_filename = 'bench_expectations_%s.txt' % p 103 exp_fullname = os.path.join(exp_dir, exp_filename) 104 proc = subprocess.Popen(['python', 'skia/bench/gen_bench_expectations.py', 105 '-r', h, '-b', p, '-d', os.path.join(gs_dir, p), '-o', exp_fullname], 106 stdout=subprocess.PIPE) 107 out, err = proc.communicate() 108 if err: 109 print 'ERR_CALCULATING_EXPECTATIONS: ' + err 110 return False 111 print 'CALCULATED_EXPECTATIONS: ' + out 112 if extra_dir: # Adjust data with the ones in extra_dir 113 print 'USE_EXTRA_DATA_FOR_ADJUSTMENT.' 114 proc = subprocess.Popen(['python', 'skia/bench/gen_bench_expectations.py', 115 '-r', extra_hash, '-b', p, '-d', os.path.join(extra_dir, p), '-o', 116 os.path.join(extra_dir, exp_filename)], 117 stdout=subprocess.PIPE) 118 out, err = proc.communicate() 119 if err: 120 print 'ERR_CALCULATING_EXTRA_EXPECTATIONS: ' + err 121 return False 122 extra_dic = get_expectations_dict(os.path.join(extra_dir, exp_filename)) 123 output_lines = [] 124 for l in open(exp_fullname).readlines(): 125 parts = l.strip().split(',') 126 if parts[0].startswith('#') or len(parts) != 5: 127 output_lines.append(l.strip()) 128 continue 129 key = ','.join(parts[:2]) 130 if key in extra_dic: 131 exp, lb, ub = (float(parts[2]), float(parts[3]), float(parts[4])) 132 alt, _, _ = extra_dic[key] 133 avg = (exp + alt) / 2 134 # Keeps the extra range in lower/upper bounds from two actual values. 135 new_lb = min(exp, alt) - (exp - lb) 136 new_ub = max(exp, alt) + (ub - exp) 137 output_lines.append('%s,%.2f,%.2f,%.2f' % (key, avg, new_lb, new_ub)) 138 else: 139 output_lines.append(l.strip()) 140 with open(exp_fullname, 'w') as f: 141 f.write('\n'.join(output_lines)) 142 143 repo_file = os.path.join(repo_dir, 'expectations', 'bench', exp_filename) 144 if (os.path.isfile(repo_file) and 145 filecmp.cmp(repo_file, os.path.join(exp_dir, exp_filename))): 146 print 'NO CHANGE ON %s' % repo_file 147 return False 148 return True 149 150 def checkout_or_update_skia(repo_dir): 151 status = True 152 old_cwd = os.getcwd() 153 os.chdir(repo_dir) 154 print 'CHECK SKIA REPO...' 155 if subprocess.call(['git', 'pull'], 156 stderr=subprocess.PIPE): 157 print 'Checking out Skia from git, please be patient...' 158 os.chdir(old_cwd) 159 clean_dir(repo_dir) 160 os.chdir(repo_dir) 161 if subprocess.call(['git', 'clone', '-q', '--depth=50', '--single-branch', 162 'https://skia.googlesource.com/skia.git', '.']): 163 status = False 164 subprocess.call(['git', 'checkout', 'master']) 165 subprocess.call(['git', 'pull']) 166 os.chdir(old_cwd) 167 return status 168 169 def git_commit_expectations(repo_dir, exp_dir, update_li, h, commit, 170 extra_hash): 171 if extra_hash: 172 extra_hash = ', adjusted with ' + extra_hash 173 commit_msg = """manual bench rebase after %s%s 174 175 TBR=robertphillips (at] google.com 176 177 Bypassing trybots: 178 NOTRY=true""" % (h, extra_hash) 179 old_cwd = os.getcwd() 180 os.chdir(repo_dir) 181 upload = ['git', 'cl', 'upload', '-f', '--bypass-hooks', 182 '--bypass-watchlists', '-m', commit_msg] 183 branch = exp_dir.split('/')[-1] 184 if commit: 185 upload.append('--use-commit-queue') 186 cmds = ([['git', 'checkout', 'master'], 187 ['git', 'pull'], 188 ['git', 'checkout', '-b', branch, '-t', 'origin/master']] + 189 [['cp', '%s/%s' % (exp_dir, f), 'expectations/bench'] for f in 190 update_li] + 191 [['git', 'add'] + ['expectations/bench/%s' % i for i in update_li], 192 ['git', 'commit', '-m', commit_msg], 193 upload, 194 ['git', 'checkout', 'master'], 195 ['git', 'branch', '-D', branch], 196 ]) 197 status = True 198 for cmd in cmds: 199 print 'Running ' + ' '.join(cmd) 200 if subprocess.call(cmd): 201 print 'FAILED. Please check if skia git repo is present.' 202 subprocess.call(['git', 'checkout', 'master']) 203 status = False 204 break 205 os.chdir(old_cwd) 206 return status 207 208 def delete_dirs(li): 209 for d in li: 210 print 'Deleting directory %s' % d 211 shutil.rmtree(d) 212 213 214 def main(): 215 d = os.path.dirname(os.path.abspath(__file__)) 216 os.chdir(d) 217 if not subprocess.call(['git', 'rev-parse'], stderr=subprocess.PIPE): 218 print 'Please copy script to a separate dir outside git repos to use.' 219 return 220 parser = argparse.ArgumentParser() 221 parser.add_argument('--githash', 222 help=('Githash prefix (7+ chars) to rebaseline to. If ' 223 'a second one is supplied after comma, and it has ' 224 'corresponding bench data, will shift the range ' 225 'center to the average of two expected values.')) 226 parser.add_argument('--bots', 227 help=('Comma-separated list of bots to work on. If no ' 228 'matching bots are found in the list, will default ' 229 'to processing all bots.')) 230 parser.add_argument('--commit', action='store_true', 231 help='Whether to commit changes automatically.') 232 args = parser.parse_args() 233 234 repo_dir = os.path.join(d, 'skia') 235 if not os.path.exists(repo_dir): 236 os.makedirs(repo_dir) 237 if not checkout_or_update_skia(repo_dir): 238 print 'ERROR setting up Skia repo at %s' % repo_dir 239 return 1 240 241 file_in_repo = os.path.join(d, 'skia/experimental/benchtools/rebase.py') 242 if not filecmp.cmp(__file__, file_in_repo): 243 shutil.copy(file_in_repo, __file__) 244 print 'Updated this script from repo; please run again.' 245 return 246 247 all_platforms = [] # Find existing list of platforms with expectations. 248 for item in os.listdir(os.path.join(d, 'skia/expectations/bench')): 249 all_platforms.append( 250 item.replace('bench_expectations_', '').replace('.txt', '')) 251 252 platforms = [] 253 # If at least one given bot is in all_platforms, use list of valid args.bots. 254 if args.bots: 255 bots = args.bots.strip().split(',') 256 for bot in bots: 257 if bot in all_platforms: # Filters platforms with given bot list. 258 platforms.append(bot) 259 if not platforms: # Include all existing platforms with expectations. 260 platforms = all_platforms 261 262 if not args.githash or len(args.githash) < 7: 263 raise Exception('Please provide --githash with a longer prefix (7+).') 264 githashes = args.githash.strip().split(',') 265 if len(githashes[0]) < 7: 266 raise Exception('Please provide --githash with longer prefixes (7+).') 267 commit = False 268 if args.commit: 269 commit = True 270 rebase_hash = githashes[0][:7] 271 extra_hash = '' 272 if len(githashes) == 2: 273 extra_hash = githashes[1][:7] 274 hashes = get_git_hashes() 275 short_hashes = [h[:7] for h in hashes] 276 if (rebase_hash not in short_hashes or 277 (extra_hash and extra_hash not in short_hashes) or 278 rebase_hash == extra_hash): 279 raise Exception('Provided --githashes not found, or identical!') 280 if extra_hash: 281 extra_hash = hashes[short_hashes.index(extra_hash)] 282 hashes = hashes[:short_hashes.index(rebase_hash) + 1] 283 update_li = [] 284 285 ts_str = '%s' % time.time() 286 gs_dir = os.path.join(d, 'gs' + ts_str) 287 exp_dir = os.path.join(d, 'exp' + ts_str) 288 extra_dir = os.path.join(d, 'extra' + ts_str) 289 clean_dir(gs_dir) 290 clean_dir(exp_dir) 291 clean_dir(extra_dir) 292 for p in platforms: 293 clean_dir(os.path.join(gs_dir, p)) 294 clean_dir(os.path.join(extra_dir, p)) 295 hash_to_use = '' 296 for h in reversed(hashes): 297 li = get_gs_filelist(p, h) 298 if not len(li): # no data 299 continue 300 if download_gs_files(p, h, gs_dir): 301 print 'Copied %s/%s' % (p, h) 302 hash_to_use = h 303 break 304 else: 305 print 'DOWNLOAD BENCH FAILED %s/%s' % (p, h) 306 break 307 if hash_to_use: 308 if extra_hash and download_gs_files(p, extra_hash, extra_dir): 309 print 'Copied extra data %s/%s' % (p, extra_hash) 310 if calc_expectations(p, h, gs_dir, exp_dir, repo_dir, extra_dir, 311 extra_hash): 312 update_li.append('bench_expectations_%s.txt' % p) 313 elif calc_expectations(p, h, gs_dir, exp_dir, repo_dir, '', ''): 314 update_li.append('bench_expectations_%s.txt' % p) 315 if not update_li: 316 print 'No bench data to update after %s!' % args.githash 317 elif not git_commit_expectations( 318 repo_dir, exp_dir, update_li, rebase_hash, commit, extra_hash): 319 print 'ERROR uploading expectations using git.' 320 elif not commit: 321 print 'CL created. Please take a look at the link above.' 322 else: 323 print 'New bench baselines should be in CQ now.' 324 delete_dirs([gs_dir, exp_dir, extra_dir]) 325 326 327 if __name__ == "__main__": 328 main() 329