1*3ac0a46fSAndroid Build Coastguard Worker#!/usr/bin/env python3 2*3ac0a46fSAndroid Build Coastguard Worker# Copyright 2017 The PDFium Authors 3*3ac0a46fSAndroid Build Coastguard Worker# Use of this source code is governed by a BSD-style license that can be 4*3ac0a46fSAndroid Build Coastguard Worker# found in the LICENSE file. 5*3ac0a46fSAndroid Build Coastguard Worker"""Looks for performance regressions on all pushes since the last run. 6*3ac0a46fSAndroid Build Coastguard Worker 7*3ac0a46fSAndroid Build Coastguard WorkerRun this nightly to have a periodical check for performance regressions. 8*3ac0a46fSAndroid Build Coastguard Worker 9*3ac0a46fSAndroid Build Coastguard WorkerStores the results for each run and last checkpoint in a results directory. 10*3ac0a46fSAndroid Build Coastguard Worker""" 11*3ac0a46fSAndroid Build Coastguard Worker 12*3ac0a46fSAndroid Build Coastguard Workerimport argparse 13*3ac0a46fSAndroid Build Coastguard Workerimport datetime 14*3ac0a46fSAndroid Build Coastguard Workerimport json 15*3ac0a46fSAndroid Build Coastguard Workerimport os 16*3ac0a46fSAndroid Build Coastguard Workerimport sys 17*3ac0a46fSAndroid Build Coastguard Worker 18*3ac0a46fSAndroid Build Coastguard Workerfrom common import PrintWithTime 19*3ac0a46fSAndroid Build Coastguard Workerfrom common import RunCommandPropagateErr 20*3ac0a46fSAndroid Build Coastguard Workerfrom githelper import GitHelper 21*3ac0a46fSAndroid Build Coastguard Workerfrom safetynet_conclusions import PrintConclusionsDictHumanReadable 22*3ac0a46fSAndroid Build Coastguard Worker 23*3ac0a46fSAndroid Build Coastguard Worker 24*3ac0a46fSAndroid Build Coastguard Workerclass JobContext: 25*3ac0a46fSAndroid Build Coastguard Worker """Context for a single run, including name and directory paths.""" 26*3ac0a46fSAndroid Build Coastguard Worker 27*3ac0a46fSAndroid Build Coastguard Worker def __init__(self, args): 28*3ac0a46fSAndroid Build Coastguard Worker self.datetime = datetime.datetime.now().strftime('%Y-%m-%d-%H-%M-%S') 29*3ac0a46fSAndroid Build Coastguard Worker self.results_dir = args.results_dir 30*3ac0a46fSAndroid Build Coastguard Worker self.last_revision_covered_file = os.path.join(self.results_dir, 31*3ac0a46fSAndroid Build Coastguard Worker 'last_revision_covered') 32*3ac0a46fSAndroid Build Coastguard Worker self.run_output_dir = os.path.join(self.results_dir, 33*3ac0a46fSAndroid Build Coastguard Worker 'profiles_%s' % self.datetime) 34*3ac0a46fSAndroid Build Coastguard Worker self.run_output_log_file = os.path.join(self.results_dir, 35*3ac0a46fSAndroid Build Coastguard Worker '%s.log' % self.datetime) 36*3ac0a46fSAndroid Build Coastguard Worker 37*3ac0a46fSAndroid Build Coastguard Worker 38*3ac0a46fSAndroid Build Coastguard Workerclass JobRun: 39*3ac0a46fSAndroid Build Coastguard Worker """A single run looking for regressions since the last one.""" 40*3ac0a46fSAndroid Build Coastguard Worker 41*3ac0a46fSAndroid Build Coastguard Worker def __init__(self, args, context): 42*3ac0a46fSAndroid Build Coastguard Worker """Constructor. 43*3ac0a46fSAndroid Build Coastguard Worker 44*3ac0a46fSAndroid Build Coastguard Worker Args: 45*3ac0a46fSAndroid Build Coastguard Worker args: Namespace with arguments passed to the script. 46*3ac0a46fSAndroid Build Coastguard Worker context: JobContext for this run. 47*3ac0a46fSAndroid Build Coastguard Worker """ 48*3ac0a46fSAndroid Build Coastguard Worker self.git = GitHelper() 49*3ac0a46fSAndroid Build Coastguard Worker self.args = args 50*3ac0a46fSAndroid Build Coastguard Worker self.context = context 51*3ac0a46fSAndroid Build Coastguard Worker 52*3ac0a46fSAndroid Build Coastguard Worker def Run(self): 53*3ac0a46fSAndroid Build Coastguard Worker """Searches for regressions. 54*3ac0a46fSAndroid Build Coastguard Worker 55*3ac0a46fSAndroid Build Coastguard Worker Will only write a checkpoint when first run, and on all subsequent runs 56*3ac0a46fSAndroid Build Coastguard Worker a comparison is done against the last checkpoint. 57*3ac0a46fSAndroid Build Coastguard Worker 58*3ac0a46fSAndroid Build Coastguard Worker Returns: 59*3ac0a46fSAndroid Build Coastguard Worker Exit code for the script: 0 if no significant changes are found; 1 if 60*3ac0a46fSAndroid Build Coastguard Worker there was an error in the comparison; 3 if there was a regression; 4 if 61*3ac0a46fSAndroid Build Coastguard Worker there was an improvement and no regression. 62*3ac0a46fSAndroid Build Coastguard Worker """ 63*3ac0a46fSAndroid Build Coastguard Worker pdfium_src_dir = os.path.join( 64*3ac0a46fSAndroid Build Coastguard Worker os.path.dirname(__file__), os.path.pardir, os.path.pardir) 65*3ac0a46fSAndroid Build Coastguard Worker os.chdir(pdfium_src_dir) 66*3ac0a46fSAndroid Build Coastguard Worker 67*3ac0a46fSAndroid Build Coastguard Worker branch_to_restore = self.git.GetCurrentBranchName() 68*3ac0a46fSAndroid Build Coastguard Worker 69*3ac0a46fSAndroid Build Coastguard Worker if not self.args.no_checkout: 70*3ac0a46fSAndroid Build Coastguard Worker self.git.FetchOriginMaster() 71*3ac0a46fSAndroid Build Coastguard Worker self.git.Checkout('origin/main') 72*3ac0a46fSAndroid Build Coastguard Worker 73*3ac0a46fSAndroid Build Coastguard Worker # Make sure results dir exists 74*3ac0a46fSAndroid Build Coastguard Worker if not os.path.exists(self.context.results_dir): 75*3ac0a46fSAndroid Build Coastguard Worker os.makedirs(self.context.results_dir) 76*3ac0a46fSAndroid Build Coastguard Worker 77*3ac0a46fSAndroid Build Coastguard Worker if not os.path.exists(self.context.last_revision_covered_file): 78*3ac0a46fSAndroid Build Coastguard Worker result = self._InitialRun() 79*3ac0a46fSAndroid Build Coastguard Worker else: 80*3ac0a46fSAndroid Build Coastguard Worker with open(self.context.last_revision_covered_file) as f: 81*3ac0a46fSAndroid Build Coastguard Worker last_revision_covered = f.read().strip() 82*3ac0a46fSAndroid Build Coastguard Worker result = self._IncrementalRun(last_revision_covered) 83*3ac0a46fSAndroid Build Coastguard Worker 84*3ac0a46fSAndroid Build Coastguard Worker self.git.Checkout(branch_to_restore) 85*3ac0a46fSAndroid Build Coastguard Worker return result 86*3ac0a46fSAndroid Build Coastguard Worker 87*3ac0a46fSAndroid Build Coastguard Worker def _InitialRun(self): 88*3ac0a46fSAndroid Build Coastguard Worker """Initial run, just write a checkpoint. 89*3ac0a46fSAndroid Build Coastguard Worker 90*3ac0a46fSAndroid Build Coastguard Worker Returns: 91*3ac0a46fSAndroid Build Coastguard Worker Exit code for the script. 92*3ac0a46fSAndroid Build Coastguard Worker """ 93*3ac0a46fSAndroid Build Coastguard Worker current = self.git.GetCurrentBranchHash() 94*3ac0a46fSAndroid Build Coastguard Worker 95*3ac0a46fSAndroid Build Coastguard Worker PrintWithTime('Initial run, current is %s' % current) 96*3ac0a46fSAndroid Build Coastguard Worker 97*3ac0a46fSAndroid Build Coastguard Worker self._WriteCheckpoint(current) 98*3ac0a46fSAndroid Build Coastguard Worker 99*3ac0a46fSAndroid Build Coastguard Worker PrintWithTime('All set up, next runs will be incremental and perform ' 100*3ac0a46fSAndroid Build Coastguard Worker 'comparisons') 101*3ac0a46fSAndroid Build Coastguard Worker return 0 102*3ac0a46fSAndroid Build Coastguard Worker 103*3ac0a46fSAndroid Build Coastguard Worker def _IncrementalRun(self, last_revision_covered): 104*3ac0a46fSAndroid Build Coastguard Worker """Incremental run, compare against last checkpoint and update it. 105*3ac0a46fSAndroid Build Coastguard Worker 106*3ac0a46fSAndroid Build Coastguard Worker Args: 107*3ac0a46fSAndroid Build Coastguard Worker last_revision_covered: String with hash for last checkpoint. 108*3ac0a46fSAndroid Build Coastguard Worker 109*3ac0a46fSAndroid Build Coastguard Worker Returns: 110*3ac0a46fSAndroid Build Coastguard Worker Exit code for the script. 111*3ac0a46fSAndroid Build Coastguard Worker """ 112*3ac0a46fSAndroid Build Coastguard Worker current = self.git.GetCurrentBranchHash() 113*3ac0a46fSAndroid Build Coastguard Worker 114*3ac0a46fSAndroid Build Coastguard Worker PrintWithTime('Incremental run, current is %s, last is %s' % 115*3ac0a46fSAndroid Build Coastguard Worker (current, last_revision_covered)) 116*3ac0a46fSAndroid Build Coastguard Worker 117*3ac0a46fSAndroid Build Coastguard Worker if not os.path.exists(self.context.run_output_dir): 118*3ac0a46fSAndroid Build Coastguard Worker os.makedirs(self.context.run_output_dir) 119*3ac0a46fSAndroid Build Coastguard Worker 120*3ac0a46fSAndroid Build Coastguard Worker if current == last_revision_covered: 121*3ac0a46fSAndroid Build Coastguard Worker PrintWithTime('No changes seen, finishing job') 122*3ac0a46fSAndroid Build Coastguard Worker output_info = { 123*3ac0a46fSAndroid Build Coastguard Worker 'metadata': 124*3ac0a46fSAndroid Build Coastguard Worker self._BuildRunMetadata(last_revision_covered, current, False) 125*3ac0a46fSAndroid Build Coastguard Worker } 126*3ac0a46fSAndroid Build Coastguard Worker self._WriteRawJson(output_info) 127*3ac0a46fSAndroid Build Coastguard Worker return 0 128*3ac0a46fSAndroid Build Coastguard Worker 129*3ac0a46fSAndroid Build Coastguard Worker # Run compare 130*3ac0a46fSAndroid Build Coastguard Worker cmd = [ 131*3ac0a46fSAndroid Build Coastguard Worker 'testing/tools/safetynet_compare.py', '--this-repo', 132*3ac0a46fSAndroid Build Coastguard Worker '--machine-readable', 133*3ac0a46fSAndroid Build Coastguard Worker '--branch-before=%s' % last_revision_covered, 134*3ac0a46fSAndroid Build Coastguard Worker '--output-dir=%s' % self.context.run_output_dir 135*3ac0a46fSAndroid Build Coastguard Worker ] 136*3ac0a46fSAndroid Build Coastguard Worker cmd.extend(self.args.input_paths) 137*3ac0a46fSAndroid Build Coastguard Worker 138*3ac0a46fSAndroid Build Coastguard Worker json_output = RunCommandPropagateErr(cmd) 139*3ac0a46fSAndroid Build Coastguard Worker 140*3ac0a46fSAndroid Build Coastguard Worker if json_output is None: 141*3ac0a46fSAndroid Build Coastguard Worker return 1 142*3ac0a46fSAndroid Build Coastguard Worker 143*3ac0a46fSAndroid Build Coastguard Worker output_info = json.loads(json_output) 144*3ac0a46fSAndroid Build Coastguard Worker 145*3ac0a46fSAndroid Build Coastguard Worker run_metadata = self._BuildRunMetadata(last_revision_covered, current, True) 146*3ac0a46fSAndroid Build Coastguard Worker output_info.setdefault('metadata', {}).update(run_metadata) 147*3ac0a46fSAndroid Build Coastguard Worker self._WriteRawJson(output_info) 148*3ac0a46fSAndroid Build Coastguard Worker 149*3ac0a46fSAndroid Build Coastguard Worker PrintConclusionsDictHumanReadable( 150*3ac0a46fSAndroid Build Coastguard Worker output_info, 151*3ac0a46fSAndroid Build Coastguard Worker colored=(not self.args.output_to_log and not self.args.no_color), 152*3ac0a46fSAndroid Build Coastguard Worker key='after') 153*3ac0a46fSAndroid Build Coastguard Worker 154*3ac0a46fSAndroid Build Coastguard Worker status = 0 155*3ac0a46fSAndroid Build Coastguard Worker 156*3ac0a46fSAndroid Build Coastguard Worker if output_info['summary']['improvement']: 157*3ac0a46fSAndroid Build Coastguard Worker PrintWithTime('Improvement detected.') 158*3ac0a46fSAndroid Build Coastguard Worker status = 4 159*3ac0a46fSAndroid Build Coastguard Worker 160*3ac0a46fSAndroid Build Coastguard Worker if output_info['summary']['regression']: 161*3ac0a46fSAndroid Build Coastguard Worker PrintWithTime('Regression detected.') 162*3ac0a46fSAndroid Build Coastguard Worker status = 3 163*3ac0a46fSAndroid Build Coastguard Worker 164*3ac0a46fSAndroid Build Coastguard Worker if status == 0: 165*3ac0a46fSAndroid Build Coastguard Worker PrintWithTime('Nothing detected.') 166*3ac0a46fSAndroid Build Coastguard Worker 167*3ac0a46fSAndroid Build Coastguard Worker self._WriteCheckpoint(current) 168*3ac0a46fSAndroid Build Coastguard Worker 169*3ac0a46fSAndroid Build Coastguard Worker return status 170*3ac0a46fSAndroid Build Coastguard Worker 171*3ac0a46fSAndroid Build Coastguard Worker def _WriteRawJson(self, output_info): 172*3ac0a46fSAndroid Build Coastguard Worker json_output_file = os.path.join(self.context.run_output_dir, 'raw.json') 173*3ac0a46fSAndroid Build Coastguard Worker with open(json_output_file, 'w') as f: 174*3ac0a46fSAndroid Build Coastguard Worker json.dump(output_info, f) 175*3ac0a46fSAndroid Build Coastguard Worker 176*3ac0a46fSAndroid Build Coastguard Worker def _BuildRunMetadata(self, revision_before, revision_after, 177*3ac0a46fSAndroid Build Coastguard Worker comparison_performed): 178*3ac0a46fSAndroid Build Coastguard Worker return { 179*3ac0a46fSAndroid Build Coastguard Worker 'datetime': self.context.datetime, 180*3ac0a46fSAndroid Build Coastguard Worker 'revision_before': revision_before, 181*3ac0a46fSAndroid Build Coastguard Worker 'revision_after': revision_after, 182*3ac0a46fSAndroid Build Coastguard Worker 'comparison_performed': comparison_performed, 183*3ac0a46fSAndroid Build Coastguard Worker } 184*3ac0a46fSAndroid Build Coastguard Worker 185*3ac0a46fSAndroid Build Coastguard Worker def _WriteCheckpoint(self, checkpoint): 186*3ac0a46fSAndroid Build Coastguard Worker if not self.args.no_checkpoint: 187*3ac0a46fSAndroid Build Coastguard Worker with open(self.context.last_revision_covered_file, 'w') as f: 188*3ac0a46fSAndroid Build Coastguard Worker f.write(checkpoint + '\n') 189*3ac0a46fSAndroid Build Coastguard Worker 190*3ac0a46fSAndroid Build Coastguard Worker 191*3ac0a46fSAndroid Build Coastguard Workerdef main(): 192*3ac0a46fSAndroid Build Coastguard Worker parser = argparse.ArgumentParser() 193*3ac0a46fSAndroid Build Coastguard Worker parser.add_argument('results_dir', help='where to write the job results') 194*3ac0a46fSAndroid Build Coastguard Worker parser.add_argument( 195*3ac0a46fSAndroid Build Coastguard Worker 'input_paths', 196*3ac0a46fSAndroid Build Coastguard Worker nargs='+', 197*3ac0a46fSAndroid Build Coastguard Worker help='pdf files or directories to search for pdf files ' 198*3ac0a46fSAndroid Build Coastguard Worker 'to run as test cases') 199*3ac0a46fSAndroid Build Coastguard Worker parser.add_argument( 200*3ac0a46fSAndroid Build Coastguard Worker '--no-checkout', 201*3ac0a46fSAndroid Build Coastguard Worker action='store_true', 202*3ac0a46fSAndroid Build Coastguard Worker help='whether to skip checking out origin/main. Use ' 203*3ac0a46fSAndroid Build Coastguard Worker 'for script debugging.') 204*3ac0a46fSAndroid Build Coastguard Worker parser.add_argument( 205*3ac0a46fSAndroid Build Coastguard Worker '--no-checkpoint', 206*3ac0a46fSAndroid Build Coastguard Worker action='store_true', 207*3ac0a46fSAndroid Build Coastguard Worker help='whether to skip writing the new checkpoint. Use ' 208*3ac0a46fSAndroid Build Coastguard Worker 'for script debugging.') 209*3ac0a46fSAndroid Build Coastguard Worker parser.add_argument( 210*3ac0a46fSAndroid Build Coastguard Worker '--no-color', 211*3ac0a46fSAndroid Build Coastguard Worker action='store_true', 212*3ac0a46fSAndroid Build Coastguard Worker help='whether to write output without color escape ' 213*3ac0a46fSAndroid Build Coastguard Worker 'codes.') 214*3ac0a46fSAndroid Build Coastguard Worker parser.add_argument( 215*3ac0a46fSAndroid Build Coastguard Worker '--output-to-log', 216*3ac0a46fSAndroid Build Coastguard Worker action='store_true', 217*3ac0a46fSAndroid Build Coastguard Worker help='whether to write output to a log file') 218*3ac0a46fSAndroid Build Coastguard Worker args = parser.parse_args() 219*3ac0a46fSAndroid Build Coastguard Worker 220*3ac0a46fSAndroid Build Coastguard Worker job_context = JobContext(args) 221*3ac0a46fSAndroid Build Coastguard Worker 222*3ac0a46fSAndroid Build Coastguard Worker if args.output_to_log: 223*3ac0a46fSAndroid Build Coastguard Worker log_file = open(job_context.run_output_log_file, 'w') 224*3ac0a46fSAndroid Build Coastguard Worker sys.stdout = log_file 225*3ac0a46fSAndroid Build Coastguard Worker sys.stderr = log_file 226*3ac0a46fSAndroid Build Coastguard Worker 227*3ac0a46fSAndroid Build Coastguard Worker run = JobRun(args, job_context) 228*3ac0a46fSAndroid Build Coastguard Worker result = run.Run() 229*3ac0a46fSAndroid Build Coastguard Worker 230*3ac0a46fSAndroid Build Coastguard Worker if args.output_to_log: 231*3ac0a46fSAndroid Build Coastguard Worker log_file.close() 232*3ac0a46fSAndroid Build Coastguard Worker 233*3ac0a46fSAndroid Build Coastguard Worker return result 234*3ac0a46fSAndroid Build Coastguard Worker 235*3ac0a46fSAndroid Build Coastguard Worker 236*3ac0a46fSAndroid Build Coastguard Workerif __name__ == '__main__': 237*3ac0a46fSAndroid Build Coastguard Worker sys.exit(main()) 238