binary_search_perforce.py revision 77efb771911728cb6911f4f1b1741a31341ed2da
1#!/usr/bin/python2 2"""Module of binary serch for perforce.""" 3from __future__ import print_function 4 5import math 6import argparse 7import os 8import re 9import sys 10import tempfile 11 12from cros_utils import command_executer 13from cros_utils import logger 14 15verbose = True 16 17def _GetP4ClientSpec(client_name, p4_paths): 18 p4_string = "" 19 for p4_path in p4_paths: 20 if " " not in p4_path: 21 p4_string += " -a %s" % p4_path 22 else: 23 p4_string += " -a \"" + (" //" + client_name + "/").join(p4_path) + "\"" 24 25 return p4_string 26 27 28def GetP4Command(client_name, p4_port, p4_paths, checkoutdir, 29 p4_snapshot=""): 30 command = "" 31 32 if p4_snapshot: 33 command += "mkdir -p " + checkoutdir 34 for p4_path in p4_paths: 35 real_path = p4_path[1] 36 if real_path.endswith("..."): 37 real_path = real_path.replace("/...", "") 38 command += ("; mkdir -p " + checkoutdir + "/" + 39 os.path.dirname(real_path)) 40 command += ("&& rsync -lr " + p4_snapshot + "/" + real_path + 41 " " + checkoutdir + "/" + os.path.dirname(real_path)) 42 return command 43 44 command += " export P4CONFIG=.p4config" 45 command += " && mkdir -p " + checkoutdir 46 command += " && cd " + checkoutdir 47 command += " && cp ${HOME}/.p4config ." 48 command += " && chmod u+w .p4config" 49 command += " && echo \"P4PORT=" + p4_port + "\" >> .p4config" 50 command += " && echo \"P4CLIENT=" + client_name + "\" >> .p4config" 51 command += (" && g4 client " + 52 _GetP4ClientSpec(client_name, p4_paths)) 53 command += " && g4 sync " 54 command += " && cd -" 55 return command 56 57class BinarySearchPoint(object): 58 """Class of binary search point.""" 59 def __init__(self, revision, status, tag=None): 60 self.revision = revision 61 self.status = status 62 self.tag = tag 63 64class BinarySearcher(object): 65 """Class of binary searcher.""" 66 def __init__(self, logger_to_set=None): 67 self.sorted_list = [] 68 self.index_log = [] 69 self.status_log = [] 70 self.skipped_indices = [] 71 self.current = 0 72 self.points = {} 73 self.lo = 0 74 self.hi = 0 75 if logger_to_set is not None: 76 self.logger = logger_to_set 77 else: 78 self.logger = logger.GetLogger() 79 80 def SetSortedList(self, sorted_list): 81 assert len(sorted_list) > 0 82 self.sorted_list = sorted_list 83 self.index_log = [] 84 self.hi = len(sorted_list) - 1 85 self.lo = 0 86 self.points = {} 87 for i in range(len(self.sorted_list)): 88 bsp = BinarySearchPoint(self.sorted_list[i], -1, "Not yet done.") 89 self.points[i] = bsp 90 91 def SetStatus(self, status, tag=None): 92 message = ("Revision: %s index: %d returned: %d" % 93 (self.sorted_list[self.current], 94 self.current, 95 status)) 96 self.logger.LogOutput(message, print_to_console=verbose) 97 assert status == 0 or status == 1 or status == 2 98 self.index_log.append(self.current) 99 self.status_log.append(status) 100 bsp = BinarySearchPoint(self.sorted_list[self.current], status, tag) 101 self.points[self.current] = bsp 102 103 if status == 2: 104 self.skipped_indices.append(self.current) 105 106 if status == 0 or status == 1: 107 if status == 0: 108 self.lo = self.current + 1 109 elif status == 1: 110 self.hi = self.current 111 self.logger.LogOutput("lo: %d hi: %d\n" % (self.lo, self.hi)) 112 self.current = (self.lo + self.hi)/2 113 114 if self.lo == self.hi: 115 message = ("Search complete. First bad version: %s" 116 " at index: %d" % 117 (self.sorted_list[self.current], 118 self.lo)) 119 self.logger.LogOutput(message) 120 return True 121 122 for index in range(self.lo, self.hi): 123 if index not in self.skipped_indices: 124 return False 125 self.logger.LogOutput( 126 "All skipped indices between: %d and %d\n" % (self.lo, self.hi), 127 print_to_console=verbose) 128 return True 129 130 # Does a better job with chromeos flakiness. 131 def GetNextFlakyBinary(self): 132 t = (self.lo, self.current, self.hi) 133 q = [t] 134 while len(q): 135 element = q.pop(0) 136 if element[1] in self.skipped_indices: 137 # Go top 138 to_add = (element[0], (element[0] + element[1]) / 2, element[1]) 139 q.append(to_add) 140 # Go bottom 141 to_add = (element[1], (element[1] + element[2]) / 2, element[2]) 142 q.append(to_add) 143 else: 144 self.current = element[1] 145 return 146 assert len(q), "Queue should never be 0-size!" 147 148 def GetNextFlakyLinear(self): 149 current_hi = self.current 150 current_lo = self.current 151 while True: 152 if current_hi < self.hi and current_hi not in self.skipped_indices: 153 self.current = current_hi 154 break 155 if current_lo >= self.lo and current_lo not in self.skipped_indices: 156 self.current = current_lo 157 break 158 if current_lo < self.lo and current_hi >= self.hi: 159 break 160 161 current_hi += 1 162 current_lo -= 1 163 164 def GetNext(self): 165 self.current = (self.hi + self.lo) / 2 166 # Try going forward if current is skipped. 167 if self.current in self.skipped_indices: 168 self.GetNextFlakyBinary() 169 170 # TODO: Add an estimated time remaining as well. 171 message = ("Estimated tries: min: %d max: %d\n" % 172 (1 + math.log(self.hi - self.lo, 2), 173 self.hi - self.lo - len(self.skipped_indices))) 174 self.logger.LogOutput(message, print_to_console=verbose) 175 message = ("lo: %d hi: %d current: %d version: %s\n" % 176 (self.lo, self.hi, self.current, 177 self.sorted_list[self.current])) 178 self.logger.LogOutput(message, print_to_console=verbose) 179 self.logger.LogOutput(str(self), print_to_console=verbose) 180 return self.sorted_list[self.current] 181 182 def SetLoRevision(self, lo_revision): 183 self.lo = self.sorted_list.index(lo_revision) 184 def SetHiRevision(self, hi_revision): 185 self.hi = self.sorted_list.index(hi_revision) 186 def GetAllPoints(self): 187 to_return = "" 188 for i in range(len(self.sorted_list)): 189 to_return += ("%d %d %s\n" % 190 (self.points[i].status, 191 i, 192 self.points[i].revision)) 193 194 return to_return 195 def __str__(self): 196 to_return = "" 197 to_return += "Current: %d\n" % self.current 198 to_return += str(self.index_log) + "\n" 199 revision_log = [] 200 for index in self.index_log: 201 revision_log.append(self.sorted_list[index]) 202 to_return += str(revision_log) + "\n" 203 to_return += str(self.status_log) + "\n" 204 to_return += "Skipped indices:\n" 205 to_return += str(self.skipped_indices) + "\n" 206 to_return += self.GetAllPoints() 207 return to_return 208 209class RevisionInfo(object): 210 """Class of reversion info.""" 211 def __init__(self, date, client, description): 212 self.date = date 213 self.client = client 214 self.description = description 215 self.status = -1 216 217class VCSBinarySearcher(object): 218 """Class of VCS binary searcher.""" 219 def __init__(self): 220 self.bs = BinarySearcher() 221 self.rim = {} 222 self.current_ce = None 223 self.checkout_dir = None 224 self.current_revision = None 225 226 def Initialize(self): 227 pass 228 def GetNextRevision(self): 229 pass 230 def CheckoutRevision(self, revision): 231 pass 232 def SetStatus(self, status): 233 pass 234 def Cleanup(self): 235 pass 236 def SetGoodRevision(self, revision): 237 if revision is None: 238 return 239 assert revision in self.bs.sorted_list 240 self.bs.SetLoRevision(revision) 241 def SetBadRevision(self, revision): 242 if revision is None: 243 return 244 assert revision in self.bs.sorted_list 245 self.bs.SetHiRevision(revision) 246 247class P4BinarySearcher(VCSBinarySearcher): 248 """Class of P4 binary searcher.""" 249 def __init__(self, p4_port, p4_paths, test_command): 250 VCSBinarySearcher.__init__(self) 251 self.p4_port = p4_port 252 self.p4_paths = p4_paths 253 self.test_command = test_command 254 self.checkout_dir = tempfile.mkdtemp() 255 self.ce = command_executer.GetCommandExecuter() 256 self.client_name = "binary-searcher-$HOSTNAME-$USER" 257 self.job_log_root = "/home/asharif/www/coreboot_triage/" 258 self.changes = None 259 260 def Initialize(self): 261 self.Cleanup() 262 command = GetP4Command(self.client_name, self.p4_port, 263 self.p4_paths, 1, self.checkout_dir) 264 self.ce.RunCommand(command) 265 command = "cd %s && g4 changes ..." % self.checkout_dir 266 _, out, _ = self.ce.RunCommandWOutput(command) 267 self.changes = re.findall(r"Change (\d+)", out) 268 change_infos = re.findall(r"Change (\d+) on ([\d/]+) by " 269 r"([^\s]+) ('[^']*')", out) 270 for change_info in change_infos: 271 ri = RevisionInfo(change_info[1], change_info[2], change_info[3]) 272 self.rim[change_info[0]] = ri 273 # g4 gives changes in reverse chronological order. 274 self.changes.reverse() 275 self.bs.SetSortedList(self.changes) 276 def SetStatus(self, status): 277 self.rim[self.current_revision].status = status 278 return self.bs.SetStatus(status) 279 def GetNextRevision(self): 280 next_revision = self.bs.GetNext() 281 self.current_revision = next_revision 282 return next_revision 283 def CleanupCLs(self): 284 if not os.path.isfile(self.checkout_dir + "/.p4config"): 285 command = "cd %s" % self.checkout_dir 286 command += " && cp ${HOME}/.p4config ." 287 command += " && echo \"P4PORT=" + self.p4_port + "\" >> .p4config" 288 command += " && echo \"P4CLIENT=" + self.client_name + "\" >> .p4config" 289 self.ce.RunCommand(command) 290 command = "cd %s" % self.checkout_dir 291 command += "; g4 changes -c %s" % self.client_name 292 _, out, _ = self.ce.RunCommandWOUTPUOT(command) 293 changes = re.findall(r"Change (\d+)", out) 294 if len(changes) != 0: 295 command = "cd %s" % self.checkout_dir 296 for change in changes: 297 command += "; g4 revert -c %s" % change 298 self.ce.RunCommand(command) 299 300 def CleanupClient(self): 301 command = "cd %s" % self.checkout_dir 302 command += "; g4 revert ..." 303 command += "; g4 client -d %s" % self.client_name 304 self.ce.RunCommand(command) 305 306 def Cleanup(self): 307 self.CleanupCLs() 308 self.CleanupClient() 309 310 def __str__(self): 311 to_return = "" 312 for change in self.changes: 313 ri = self.rim[change] 314 if ri.status == -1: 315 to_return = "%s\t%d\n" % (change, ri.status) 316 else: 317 to_return += ("%s\t%d\t%s\t%s\t%s\t%s\t%s\t%s\n" % 318 (change, 319 ri.status, 320 ri.date, 321 ri.client, 322 ri.description, 323 self.job_log_root + change + ".cmd", 324 self.job_log_root + change + ".out", 325 self.job_log_root + change + ".err")) 326 return to_return 327 328 329 330class P4GCCBinarySearcher(P4BinarySearcher): 331 """Class of P4 gcc binary searcher.""" 332 # TODO: eventually get these patches from g4 instead of creating them manually 333 def HandleBrokenCLs(self, current_revision): 334 cr = int(current_revision) 335 problematic_ranges = [] 336 problematic_ranges.append([44528, 44539]) 337 problematic_ranges.append([44528, 44760]) 338 problematic_ranges.append([44335, 44882]) 339 command = "pwd" 340 for pr in problematic_ranges: 341 if cr in range(pr[0], pr[1]): 342 patch_file = "/home/asharif/triage_tool/%d-%d.patch" % (pr[0], pr[1]) 343 f = open(patch_file) 344 patch = f.read() 345 f.close() 346 files = re.findall("--- (//.*)", patch) 347 command += "; cd %s" % self.checkout_dir 348 for f in files: 349 command += "; g4 open %s" % f 350 command += "; patch -p2 < %s" % patch_file 351 self.current_ce.RunCommand(command) 352 353 def CheckoutRevision(self, current_revision): 354 job_logger = logger.Logger(self.job_log_root, 355 current_revision, 356 True, subdir="") 357 self.current_ce = command_executer.GetCommandExecuter(job_logger) 358 359 self.CleanupCLs() 360 # Change the revision of only the gcc part of the toolchain. 361 command = ("cd %s/gcctools/google_vendor_src_branch/gcc " 362 "&& g4 revert ...; g4 sync @%s" % 363 (self.checkout_dir, current_revision)) 364 self.current_ce.RunCommand(command) 365 366 self.HandleBrokenCLs(current_revision) 367 368 369def Main(argv): 370 """The main function.""" 371 # Common initializations 372### command_executer.InitCommandExecuter(True) 373 ce = command_executer.GetCommandExecuter() 374 375 parser = argparse.ArgumentParser() 376 parser.add_argument("-n", "--num_tries", dest="num_tries", 377 default="100", 378 help="Number of tries.") 379 parser.add_argument("-g", "--good_revision", dest="good_revision", 380 help="Last known good revision.") 381 parser.add_argument("-b", "--bad_revision", dest="bad_revision", 382 help="Last known bad revision.") 383 parser.add_argument("-s", 384 "--script", 385 dest="script", 386 help="Script to run for every version.") 387 options = parser.parse_args(argv) 388 # First get all revisions 389 p4_paths = ["//depot2/gcctools/google_vendor_src_branch/gcc/gcc-4.4.3/...", 390 "//depot2/gcctools/google_vendor_src_branch/binutils/" 391 "binutils-2.20.1-mobile/...", 392 "//depot2/gcctools/google_vendor_src_branch/" 393 "binutils/binutils-20100303/..."] 394 p4gccbs = P4GCCBinarySearcher("perforce2:2666", p4_paths, "") 395 396 397 # Main loop: 398 terminated = False 399 num_tries = int(options.num_tries) 400 script = os.path.expanduser(options.script) 401 402 try: 403 p4gccbs.Initialize() 404 p4gccbs.SetGoodRevision(options.good_revision) 405 p4gccbs.SetBadRevision(options.bad_revision) 406 while not terminated and num_tries > 0: 407 current_revision = p4gccbs.GetNextRevision() 408 409 # Now run command to get the status 410 ce = command_executer.GetCommandExecuter() 411 command = "%s %s" % (script, p4gccbs.checkout_dir) 412 status = ce.RunCommand(command) 413 message = ("Revision: %s produced: %d status\n" % 414 (current_revision, status)) 415 logger.GetLogger().LogOutput(message, print_to_console=verbose) 416 terminated = p4gccbs.SetStatus(status) 417 num_tries -= 1 418 logger.GetLogger().LogOutput(str(p4gccbs), print_to_console=verbose) 419 420 if not terminated: 421 logger.GetLogger().LogOutput("Tries: %d expired." % num_tries, 422 print_to_console=verbose) 423 logger.GetLogger().LogOutput(str(p4gccbs.bs), 424 print_to_console=verbose) 425 except (KeyboardInterrupt, SystemExit): 426 logger.GetLogger().LogOutput("Cleaning up...") 427 finally: 428 logger.GetLogger().LogOutput(str(p4gccbs.bs), 429 print_to_console=verbose) 430 status = p4gccbs.Cleanup() 431 432 433if __name__ == "__main__": 434 Main(sys.argv[1:]) 435