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