#!/usr/bin/env python # Copyright (c) 2014 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """greenify.py: standalone script to correct flaky bench expectations. Requires Rietveld credentials on the running machine. Usage: Copy script to a separate dir outside Skia repo. The script will create a skia dir on the first run to host the repo, and will create/delete temp dirs as needed. ./greenify.py --url """ import argparse import filecmp import os import re import shutil import subprocess import time import urllib2 # Regular expression for matching exception data. EXCEPTION_RE = ('Bench (\S+) out of range \[(\d+.\d+), (\d+.\d+)\] \((\d+.\d+) ' 'vs (\d+.\d+), ') EXCEPTION_RE_COMPILED = re.compile(EXCEPTION_RE) def clean_dir(d): if os.path.exists(d): shutil.rmtree(d) os.makedirs(d) def checkout_or_update_skia(repo_dir): status = True old_cwd = os.getcwd() os.chdir(repo_dir) print 'CHECK SKIA REPO...' if subprocess.call(['git', 'pull'], stderr=subprocess.PIPE): print 'Checking out Skia from git, please be patient...' os.chdir(old_cwd) clean_dir(repo_dir) os.chdir(repo_dir) if subprocess.call(['git', 'clone', '-q', '--depth=50', '--single-branch', 'https://skia.googlesource.com/skia.git', '.']): status = False subprocess.call(['git', 'checkout', 'master']) subprocess.call(['git', 'pull']) os.chdir(old_cwd) return status def git_commit_expectations(repo_dir, exp_dir, bot, build, commit): commit_msg = """Greenify bench bot %s at build %s TBR=bsalomon@google.com Bypassing trybots: NOTRY=true""" % (bot, build) old_cwd = os.getcwd() os.chdir(repo_dir) upload = ['git', 'cl', 'upload', '-f', '--bypass-hooks', '--bypass-watchlists', '-m', commit_msg] if commit: upload.append('--use-commit-queue') branch = exp_dir[exp_dir.rfind('/') + 1:] filename = 'bench_expectations_%s.txt' % bot cmds = ([['git', 'checkout', 'master'], ['git', 'pull'], ['git', 'checkout', '-b', branch, '-t', 'origin/master'], ['cp', '%s/%s' % (exp_dir, filename), 'expectations/bench'], ['git', 'add', 'expectations/bench/' + filename], ['git', 'commit', '-m', commit_msg], upload, ['git', 'checkout', 'master'], ['git', 'branch', '-D', branch], ]) status = True for cmd in cmds: print 'Running ' + ' '.join(cmd) if subprocess.call(cmd): print 'FAILED. Please check if skia git repo is present.' subprocess.call(['git', 'checkout', 'master']) status = False break os.chdir(old_cwd) return status def delete_dirs(li): for d in li: print 'Deleting directory %s' % d shutil.rmtree(d) def widen_bench_ranges(url, bot, repo_dir, exp_dir): fname = 'bench_expectations_%s.txt' % bot src = os.path.join(repo_dir, 'expectations', 'bench', fname) if not os.path.isfile(src): print 'This bot has no expectations! %s' % bot return False row_dic = {} for l in urllib2.urlopen(url).read().split('\n'): data = EXCEPTION_RE_COMPILED.search(l) if data: row = data.group(1) lb = float(data.group(2)) ub = float(data.group(3)) actual = float(data.group(4)) exp = float(data.group(5)) avg = (actual + exp) / 2 shift = avg - exp lb = lb + shift ub = ub + shift # In case outlier really fluctuates a lot if actual < lb: lb = actual - abs(shift) * 0.1 + 0.5 elif actual > ub: ub = actual + abs(shift) * 0.1 + 0.5 row_dic[row] = '%.2f,%.2f,%.2f' % (avg, lb, ub) if not row_dic: print 'NO out-of-range benches found at %s' % url return False changed = 0 li = [] for l in open(src).readlines(): parts = l.strip().split(',') if parts[0].startswith('#') or len(parts) != 5: li.append(l.strip()) continue if ','.join(parts[:2]) in row_dic: li.append(','.join(parts[:2]) + ',' + row_dic[','.join(parts[:2])]) changed += 1 else: li.append(l.strip()) if not changed: print 'Not in source file:\n' + '\n'.join(row_dic.keys()) return False dst = os.path.join(exp_dir, fname) with open(dst, 'w+') as f: f.write('\n'.join(li)) return True def main(): d = os.path.dirname(os.path.abspath(__file__)) os.chdir(d) if not subprocess.call(['git', 'rev-parse'], stderr=subprocess.PIPE): print 'Please copy script to a separate dir outside git repos to use.' return ts_str = '%s' % time.time() parser = argparse.ArgumentParser() parser.add_argument('--url', help='Broken bench build CheckForRegressions page url.') parser.add_argument('--commit', action='store_true', help='Whether to commit changes automatically.') args = parser.parse_args() repo_dir = os.path.join(d, 'skia') if not os.path.exists(repo_dir): os.makedirs(repo_dir) if not checkout_or_update_skia(repo_dir): print 'ERROR setting up Skia repo at %s' % repo_dir return 1 file_in_repo = os.path.join(d, 'skia/experimental/benchtools/greenify.py') if not filecmp.cmp(__file__, file_in_repo): shutil.copy(file_in_repo, __file__) print 'Updated this script from repo; please run again.' return if not args.url: raise Exception('Please provide a url with broken CheckForRegressions.') path = args.url.split('/') if len(path) != 11 or not path[6].isdigit(): raise Exception('Unexpected url format: %s' % args.url) bot = path[4] build = path[6] commit = False if args.commit: commit = True exp_dir = os.path.join(d, 'exp' + ts_str) clean_dir(exp_dir) if not widen_bench_ranges(args.url, bot, repo_dir, exp_dir): print 'NO bench exceptions found! %s' % args.url elif not git_commit_expectations( repo_dir, exp_dir, bot, build, commit): print 'ERROR uploading expectations using git.' elif not commit: print 'CL created. Please take a look at the link above.' else: print 'New bench baselines should be in CQ now.' delete_dirs([exp_dir]) if __name__ == "__main__": main()