1# Copyright (c) 2013 The Chromium OS Authors. 2# 3# Bloat-o-meter code used here Copyright 2004 Matt Mackall <mpm@selenic.com> 4# 5# SPDX-License-Identifier: GPL-2.0+ 6# 7 8import collections 9import errno 10from datetime import datetime, timedelta 11import glob 12import os 13import re 14import Queue 15import shutil 16import string 17import sys 18import threading 19import time 20 21import command 22import gitutil 23import terminal 24import toolchain 25 26 27""" 28Theory of Operation 29 30Please see README for user documentation, and you should be familiar with 31that before trying to make sense of this. 32 33Buildman works by keeping the machine as busy as possible, building different 34commits for different boards on multiple CPUs at once. 35 36The source repo (self.git_dir) contains all the commits to be built. Each 37thread works on a single board at a time. It checks out the first commit, 38configures it for that board, then builds it. Then it checks out the next 39commit and builds it (typically without re-configuring). When it runs out 40of commits, it gets another job from the builder and starts again with that 41board. 42 43Clearly the builder threads could work either way - they could check out a 44commit and then built it for all boards. Using separate directories for each 45commit/board pair they could leave their build product around afterwards 46also. 47 48The intent behind building a single board for multiple commits, is to make 49use of incremental builds. Since each commit is built incrementally from 50the previous one, builds are faster. Reconfiguring for a different board 51removes all intermediate object files. 52 53Many threads can be working at once, but each has its own working directory. 54When a thread finishes a build, it puts the output files into a result 55directory. 56 57The base directory used by buildman is normally '../<branch>', i.e. 58a directory higher than the source repository and named after the branch 59being built. 60 61Within the base directory, we have one subdirectory for each commit. Within 62that is one subdirectory for each board. Within that is the build output for 63that commit/board combination. 64 65Buildman also create working directories for each thread, in a .bm-work/ 66subdirectory in the base dir. 67 68As an example, say we are building branch 'us-net' for boards 'sandbox' and 69'seaboard', and say that us-net has two commits. We will have directories 70like this: 71 72us-net/ base directory 73 01_of_02_g4ed4ebc_net--Add-tftp-speed-/ 74 sandbox/ 75 u-boot.bin 76 seaboard/ 77 u-boot.bin 78 02_of_02_g4ed4ebc_net--Check-tftp-comp/ 79 sandbox/ 80 u-boot.bin 81 seaboard/ 82 u-boot.bin 83 .bm-work/ 84 00/ working directory for thread 0 (contains source checkout) 85 build/ build output 86 01/ working directory for thread 1 87 build/ build output 88 ... 89u-boot/ source directory 90 .git/ repository 91""" 92 93# Possible build outcomes 94OUTCOME_OK, OUTCOME_WARNING, OUTCOME_ERROR, OUTCOME_UNKNOWN = range(4) 95 96# Translate a commit subject into a valid filename 97trans_valid_chars = string.maketrans("/: ", "---") 98 99 100def Mkdir(dirname): 101 """Make a directory if it doesn't already exist. 102 103 Args: 104 dirname: Directory to create 105 """ 106 try: 107 os.mkdir(dirname) 108 except OSError as err: 109 if err.errno == errno.EEXIST: 110 pass 111 else: 112 raise 113 114class BuilderJob: 115 """Holds information about a job to be performed by a thread 116 117 Members: 118 board: Board object to build 119 commits: List of commit options to build. 120 """ 121 def __init__(self): 122 self.board = None 123 self.commits = [] 124 125 126class ResultThread(threading.Thread): 127 """This thread processes results from builder threads. 128 129 It simply passes the results on to the builder. There is only one 130 result thread, and this helps to serialise the build output. 131 """ 132 def __init__(self, builder): 133 """Set up a new result thread 134 135 Args: 136 builder: Builder which will be sent each result 137 """ 138 threading.Thread.__init__(self) 139 self.builder = builder 140 141 def run(self): 142 """Called to start up the result thread. 143 144 We collect the next result job and pass it on to the build. 145 """ 146 while True: 147 result = self.builder.out_queue.get() 148 self.builder.ProcessResult(result) 149 self.builder.out_queue.task_done() 150 151 152class BuilderThread(threading.Thread): 153 """This thread builds U-Boot for a particular board. 154 155 An input queue provides each new job. We run 'make' to build U-Boot 156 and then pass the results on to the output queue. 157 158 Members: 159 builder: The builder which contains information we might need 160 thread_num: Our thread number (0-n-1), used to decide on a 161 temporary directory 162 """ 163 def __init__(self, builder, thread_num): 164 """Set up a new builder thread""" 165 threading.Thread.__init__(self) 166 self.builder = builder 167 self.thread_num = thread_num 168 169 def Make(self, commit, brd, stage, cwd, *args, **kwargs): 170 """Run 'make' on a particular commit and board. 171 172 The source code will already be checked out, so the 'commit' 173 argument is only for information. 174 175 Args: 176 commit: Commit object that is being built 177 brd: Board object that is being built 178 stage: Stage of the build. Valid stages are: 179 distclean - can be called to clean source 180 config - called to configure for a board 181 build - the main make invocation - it does the build 182 args: A list of arguments to pass to 'make' 183 kwargs: A list of keyword arguments to pass to command.RunPipe() 184 185 Returns: 186 CommandResult object 187 """ 188 return self.builder.do_make(commit, brd, stage, cwd, *args, 189 **kwargs) 190 191 def RunCommit(self, commit_upto, brd, work_dir, do_config, force_build, 192 force_build_failures): 193 """Build a particular commit. 194 195 If the build is already done, and we are not forcing a build, we skip 196 the build and just return the previously-saved results. 197 198 Args: 199 commit_upto: Commit number to build (0...n-1) 200 brd: Board object to build 201 work_dir: Directory to which the source will be checked out 202 do_config: True to run a make <board>_config on the source 203 force_build: Force a build even if one was previously done 204 force_build_failures: Force a bulid if the previous result showed 205 failure 206 207 Returns: 208 tuple containing: 209 - CommandResult object containing the results of the build 210 - boolean indicating whether 'make config' is still needed 211 """ 212 # Create a default result - it will be overwritte by the call to 213 # self.Make() below, in the event that we do a build. 214 result = command.CommandResult() 215 result.return_code = 0 216 if self.builder.in_tree: 217 out_dir = work_dir 218 else: 219 out_dir = os.path.join(work_dir, 'build') 220 221 # Check if the job was already completed last time 222 done_file = self.builder.GetDoneFile(commit_upto, brd.target) 223 result.already_done = os.path.exists(done_file) 224 will_build = (force_build or force_build_failures or 225 not result.already_done) 226 if result.already_done and will_build: 227 # Get the return code from that build and use it 228 with open(done_file, 'r') as fd: 229 result.return_code = int(fd.readline()) 230 err_file = self.builder.GetErrFile(commit_upto, brd.target) 231 if os.path.exists(err_file) and os.stat(err_file).st_size: 232 result.stderr = 'bad' 233 elif not force_build: 234 # The build passed, so no need to build it again 235 will_build = False 236 237 if will_build: 238 # We are going to have to build it. First, get a toolchain 239 if not self.toolchain: 240 try: 241 self.toolchain = self.builder.toolchains.Select(brd.arch) 242 except ValueError as err: 243 result.return_code = 10 244 result.stdout = '' 245 result.stderr = str(err) 246 # TODO(sjg@chromium.org): This gets swallowed, but needs 247 # to be reported. 248 249 if self.toolchain: 250 # Checkout the right commit 251 if commit_upto is not None: 252 commit = self.builder.commits[commit_upto] 253 if self.builder.checkout: 254 git_dir = os.path.join(work_dir, '.git') 255 gitutil.Checkout(commit.hash, git_dir, work_dir, 256 force=True) 257 else: 258 commit = self.builder.commit # Ick, fix this for BuildCommits() 259 260 # Set up the environment and command line 261 env = self.toolchain.MakeEnvironment() 262 Mkdir(out_dir) 263 args = [] 264 if not self.builder.in_tree: 265 args.append('O=build') 266 args.append('-s') 267 if self.builder.num_jobs is not None: 268 args.extend(['-j', str(self.builder.num_jobs)]) 269 config_args = ['%s_config' % brd.target] 270 config_out = '' 271 args.extend(self.builder.toolchains.GetMakeArguments(brd)) 272 273 # If we need to reconfigure, do that now 274 if do_config: 275 result = self.Make(commit, brd, 'distclean', work_dir, 276 'distclean', *args, env=env) 277 result = self.Make(commit, brd, 'config', work_dir, 278 *(args + config_args), env=env) 279 config_out = result.combined 280 do_config = False # No need to configure next time 281 if result.return_code == 0: 282 result = self.Make(commit, brd, 'build', work_dir, *args, 283 env=env) 284 result.stdout = config_out + result.stdout 285 else: 286 result.return_code = 1 287 result.stderr = 'No tool chain for %s\n' % brd.arch 288 result.already_done = False 289 290 result.toolchain = self.toolchain 291 result.brd = brd 292 result.commit_upto = commit_upto 293 result.out_dir = out_dir 294 return result, do_config 295 296 def _WriteResult(self, result, keep_outputs): 297 """Write a built result to the output directory. 298 299 Args: 300 result: CommandResult object containing result to write 301 keep_outputs: True to store the output binaries, False 302 to delete them 303 """ 304 # Fatal error 305 if result.return_code < 0: 306 return 307 308 # Aborted? 309 if result.stderr and 'No child processes' in result.stderr: 310 return 311 312 if result.already_done: 313 return 314 315 # Write the output and stderr 316 output_dir = self.builder._GetOutputDir(result.commit_upto) 317 Mkdir(output_dir) 318 build_dir = self.builder.GetBuildDir(result.commit_upto, 319 result.brd.target) 320 Mkdir(build_dir) 321 322 outfile = os.path.join(build_dir, 'log') 323 with open(outfile, 'w') as fd: 324 if result.stdout: 325 fd.write(result.stdout) 326 327 errfile = self.builder.GetErrFile(result.commit_upto, 328 result.brd.target) 329 if result.stderr: 330 with open(errfile, 'w') as fd: 331 fd.write(result.stderr) 332 elif os.path.exists(errfile): 333 os.remove(errfile) 334 335 if result.toolchain: 336 # Write the build result and toolchain information. 337 done_file = self.builder.GetDoneFile(result.commit_upto, 338 result.brd.target) 339 with open(done_file, 'w') as fd: 340 fd.write('%s' % result.return_code) 341 with open(os.path.join(build_dir, 'toolchain'), 'w') as fd: 342 print >>fd, 'gcc', result.toolchain.gcc 343 print >>fd, 'path', result.toolchain.path 344 print >>fd, 'cross', result.toolchain.cross 345 print >>fd, 'arch', result.toolchain.arch 346 fd.write('%s' % result.return_code) 347 348 with open(os.path.join(build_dir, 'toolchain'), 'w') as fd: 349 print >>fd, 'gcc', result.toolchain.gcc 350 print >>fd, 'path', result.toolchain.path 351 352 # Write out the image and function size information and an objdump 353 env = result.toolchain.MakeEnvironment() 354 lines = [] 355 for fname in ['u-boot', 'spl/u-boot-spl']: 356 cmd = ['%snm' % self.toolchain.cross, '--size-sort', fname] 357 nm_result = command.RunPipe([cmd], capture=True, 358 capture_stderr=True, cwd=result.out_dir, 359 raise_on_error=False, env=env) 360 if nm_result.stdout: 361 nm = self.builder.GetFuncSizesFile(result.commit_upto, 362 result.brd.target, fname) 363 with open(nm, 'w') as fd: 364 print >>fd, nm_result.stdout, 365 366 cmd = ['%sobjdump' % self.toolchain.cross, '-h', fname] 367 dump_result = command.RunPipe([cmd], capture=True, 368 capture_stderr=True, cwd=result.out_dir, 369 raise_on_error=False, env=env) 370 rodata_size = '' 371 if dump_result.stdout: 372 objdump = self.builder.GetObjdumpFile(result.commit_upto, 373 result.brd.target, fname) 374 with open(objdump, 'w') as fd: 375 print >>fd, dump_result.stdout, 376 for line in dump_result.stdout.splitlines(): 377 fields = line.split() 378 if len(fields) > 5 and fields[1] == '.rodata': 379 rodata_size = fields[2] 380 381 cmd = ['%ssize' % self.toolchain.cross, fname] 382 size_result = command.RunPipe([cmd], capture=True, 383 capture_stderr=True, cwd=result.out_dir, 384 raise_on_error=False, env=env) 385 if size_result.stdout: 386 lines.append(size_result.stdout.splitlines()[1] + ' ' + 387 rodata_size) 388 389 # Write out the image sizes file. This is similar to the output 390 # of binutil's 'size' utility, but it omits the header line and 391 # adds an additional hex value at the end of each line for the 392 # rodata size 393 if len(lines): 394 sizes = self.builder.GetSizesFile(result.commit_upto, 395 result.brd.target) 396 with open(sizes, 'w') as fd: 397 print >>fd, '\n'.join(lines) 398 399 # Now write the actual build output 400 if keep_outputs: 401 patterns = ['u-boot', '*.bin', 'u-boot.dtb', '*.map', 402 'include/autoconf.mk', 'spl/u-boot-spl', 403 'spl/u-boot-spl.bin'] 404 for pattern in patterns: 405 file_list = glob.glob(os.path.join(result.out_dir, pattern)) 406 for fname in file_list: 407 shutil.copy(fname, build_dir) 408 409 410 def RunJob(self, job): 411 """Run a single job 412 413 A job consists of a building a list of commits for a particular board. 414 415 Args: 416 job: Job to build 417 """ 418 brd = job.board 419 work_dir = self.builder.GetThreadDir(self.thread_num) 420 self.toolchain = None 421 if job.commits: 422 # Run 'make board_config' on the first commit 423 do_config = True 424 commit_upto = 0 425 force_build = False 426 for commit_upto in range(0, len(job.commits), job.step): 427 result, request_config = self.RunCommit(commit_upto, brd, 428 work_dir, do_config, 429 force_build or self.builder.force_build, 430 self.builder.force_build_failures) 431 failed = result.return_code or result.stderr 432 did_config = do_config 433 if failed and not do_config: 434 # If our incremental build failed, try building again 435 # with a reconfig. 436 if self.builder.force_config_on_failure: 437 result, request_config = self.RunCommit(commit_upto, 438 brd, work_dir, True, True, False) 439 did_config = True 440 if not self.builder.force_reconfig: 441 do_config = request_config 442 443 # If we built that commit, then config is done. But if we got 444 # an warning, reconfig next time to force it to build the same 445 # files that created warnings this time. Otherwise an 446 # incremental build may not build the same file, and we will 447 # think that the warning has gone away. 448 # We could avoid this by using -Werror everywhere... 449 # For errors, the problem doesn't happen, since presumably 450 # the build stopped and didn't generate output, so will retry 451 # that file next time. So we could detect warnings and deal 452 # with them specially here. For now, we just reconfigure if 453 # anything goes work. 454 # Of course this is substantially slower if there are build 455 # errors/warnings (e.g. 2-3x slower even if only 10% of builds 456 # have problems). 457 if (failed and not result.already_done and not did_config and 458 self.builder.force_config_on_failure): 459 # If this build failed, try the next one with a 460 # reconfigure. 461 # Sometimes if the board_config.h file changes it can mess 462 # with dependencies, and we get: 463 # make: *** No rule to make target `include/autoconf.mk', 464 # needed by `depend'. 465 do_config = True 466 force_build = True 467 else: 468 force_build = False 469 if self.builder.force_config_on_failure: 470 if failed: 471 do_config = True 472 result.commit_upto = commit_upto 473 if result.return_code < 0: 474 raise ValueError('Interrupt') 475 476 # We have the build results, so output the result 477 self._WriteResult(result, job.keep_outputs) 478 self.builder.out_queue.put(result) 479 else: 480 # Just build the currently checked-out build 481 result = self.RunCommit(None, True) 482 result.commit_upto = self.builder.upto 483 self.builder.out_queue.put(result) 484 485 def run(self): 486 """Our thread's run function 487 488 This thread picks a job from the queue, runs it, and then goes to the 489 next job. 490 """ 491 alive = True 492 while True: 493 job = self.builder.queue.get() 494 try: 495 if self.builder.active and alive: 496 self.RunJob(job) 497 except Exception as err: 498 alive = False 499 print err 500 self.builder.queue.task_done() 501 502 503class Builder: 504 """Class for building U-Boot for a particular commit. 505 506 Public members: (many should ->private) 507 active: True if the builder is active and has not been stopped 508 already_done: Number of builds already completed 509 base_dir: Base directory to use for builder 510 checkout: True to check out source, False to skip that step. 511 This is used for testing. 512 col: terminal.Color() object 513 count: Number of commits to build 514 do_make: Method to call to invoke Make 515 fail: Number of builds that failed due to error 516 force_build: Force building even if a build already exists 517 force_config_on_failure: If a commit fails for a board, disable 518 incremental building for the next commit we build for that 519 board, so that we will see all warnings/errors again. 520 force_build_failures: If a previously-built build (i.e. built on 521 a previous run of buildman) is marked as failed, rebuild it. 522 git_dir: Git directory containing source repository 523 last_line_len: Length of the last line we printed (used for erasing 524 it with new progress information) 525 num_jobs: Number of jobs to run at once (passed to make as -j) 526 num_threads: Number of builder threads to run 527 out_queue: Queue of results to process 528 re_make_err: Compiled regular expression for ignore_lines 529 queue: Queue of jobs to run 530 threads: List of active threads 531 toolchains: Toolchains object to use for building 532 upto: Current commit number we are building (0.count-1) 533 warned: Number of builds that produced at least one warning 534 force_reconfig: Reconfigure U-Boot on each comiit. This disables 535 incremental building, where buildman reconfigures on the first 536 commit for a baord, and then just does an incremental build for 537 the following commits. In fact buildman will reconfigure and 538 retry for any failing commits, so generally the only effect of 539 this option is to slow things down. 540 in_tree: Build U-Boot in-tree instead of specifying an output 541 directory separate from the source code. This option is really 542 only useful for testing in-tree builds. 543 544 Private members: 545 _base_board_dict: Last-summarised Dict of boards 546 _base_err_lines: Last-summarised list of errors 547 _build_period_us: Time taken for a single build (float object). 548 _complete_delay: Expected delay until completion (timedelta) 549 _next_delay_update: Next time we plan to display a progress update 550 (datatime) 551 _show_unknown: Show unknown boards (those not built) in summary 552 _timestamps: List of timestamps for the completion of the last 553 last _timestamp_count builds. Each is a datetime object. 554 _timestamp_count: Number of timestamps to keep in our list. 555 _working_dir: Base working directory containing all threads 556 """ 557 class Outcome: 558 """Records a build outcome for a single make invocation 559 560 Public Members: 561 rc: Outcome value (OUTCOME_...) 562 err_lines: List of error lines or [] if none 563 sizes: Dictionary of image size information, keyed by filename 564 - Each value is itself a dictionary containing 565 values for 'text', 'data' and 'bss', being the integer 566 size in bytes of each section. 567 func_sizes: Dictionary keyed by filename - e.g. 'u-boot'. Each 568 value is itself a dictionary: 569 key: function name 570 value: Size of function in bytes 571 """ 572 def __init__(self, rc, err_lines, sizes, func_sizes): 573 self.rc = rc 574 self.err_lines = err_lines 575 self.sizes = sizes 576 self.func_sizes = func_sizes 577 578 def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs, 579 gnu_make='make', checkout=True, show_unknown=True, step=1): 580 """Create a new Builder object 581 582 Args: 583 toolchains: Toolchains object to use for building 584 base_dir: Base directory to use for builder 585 git_dir: Git directory containing source repository 586 num_threads: Number of builder threads to run 587 num_jobs: Number of jobs to run at once (passed to make as -j) 588 gnu_make: the command name of GNU Make. 589 checkout: True to check out source, False to skip that step. 590 This is used for testing. 591 show_unknown: Show unknown boards (those not built) in summary 592 step: 1 to process every commit, n to process every nth commit 593 """ 594 self.toolchains = toolchains 595 self.base_dir = base_dir 596 self._working_dir = os.path.join(base_dir, '.bm-work') 597 self.threads = [] 598 self.active = True 599 self.do_make = self.Make 600 self.gnu_make = gnu_make 601 self.checkout = checkout 602 self.num_threads = num_threads 603 self.num_jobs = num_jobs 604 self.already_done = 0 605 self.force_build = False 606 self.git_dir = git_dir 607 self._show_unknown = show_unknown 608 self._timestamp_count = 10 609 self._build_period_us = None 610 self._complete_delay = None 611 self._next_delay_update = datetime.now() 612 self.force_config_on_failure = True 613 self.force_build_failures = False 614 self.force_reconfig = False 615 self._step = step 616 self.in_tree = False 617 618 self.col = terminal.Color() 619 620 self.queue = Queue.Queue() 621 self.out_queue = Queue.Queue() 622 for i in range(self.num_threads): 623 t = BuilderThread(self, i) 624 t.setDaemon(True) 625 t.start() 626 self.threads.append(t) 627 628 self.last_line_len = 0 629 t = ResultThread(self) 630 t.setDaemon(True) 631 t.start() 632 self.threads.append(t) 633 634 ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)'] 635 self.re_make_err = re.compile('|'.join(ignore_lines)) 636 637 def __del__(self): 638 """Get rid of all threads created by the builder""" 639 for t in self.threads: 640 del t 641 642 def _AddTimestamp(self): 643 """Add a new timestamp to the list and record the build period. 644 645 The build period is the length of time taken to perform a single 646 build (one board, one commit). 647 """ 648 now = datetime.now() 649 self._timestamps.append(now) 650 count = len(self._timestamps) 651 delta = self._timestamps[-1] - self._timestamps[0] 652 seconds = delta.total_seconds() 653 654 # If we have enough data, estimate build period (time taken for a 655 # single build) and therefore completion time. 656 if count > 1 and self._next_delay_update < now: 657 self._next_delay_update = now + timedelta(seconds=2) 658 if seconds > 0: 659 self._build_period = float(seconds) / count 660 todo = self.count - self.upto 661 self._complete_delay = timedelta(microseconds= 662 self._build_period * todo * 1000000) 663 # Round it 664 self._complete_delay -= timedelta( 665 microseconds=self._complete_delay.microseconds) 666 667 if seconds > 60: 668 self._timestamps.popleft() 669 count -= 1 670 671 def ClearLine(self, length): 672 """Clear any characters on the current line 673 674 Make way for a new line of length 'length', by outputting enough 675 spaces to clear out the old line. Then remember the new length for 676 next time. 677 678 Args: 679 length: Length of new line, in characters 680 """ 681 if length < self.last_line_len: 682 print ' ' * (self.last_line_len - length), 683 print '\r', 684 self.last_line_len = length 685 sys.stdout.flush() 686 687 def SelectCommit(self, commit, checkout=True): 688 """Checkout the selected commit for this build 689 """ 690 self.commit = commit 691 if checkout and self.checkout: 692 gitutil.Checkout(commit.hash) 693 694 def Make(self, commit, brd, stage, cwd, *args, **kwargs): 695 """Run make 696 697 Args: 698 commit: Commit object that is being built 699 brd: Board object that is being built 700 stage: Stage that we are at (distclean, config, build) 701 cwd: Directory where make should be run 702 args: Arguments to pass to make 703 kwargs: Arguments to pass to command.RunPipe() 704 """ 705 cmd = [self.gnu_make] + list(args) 706 result = command.RunPipe([cmd], capture=True, capture_stderr=True, 707 cwd=cwd, raise_on_error=False, **kwargs) 708 return result 709 710 def ProcessResult(self, result): 711 """Process the result of a build, showing progress information 712 713 Args: 714 result: A CommandResult object 715 """ 716 col = terminal.Color() 717 if result: 718 target = result.brd.target 719 720 if result.return_code < 0: 721 self.active = False 722 command.StopAll() 723 return 724 725 self.upto += 1 726 if result.return_code != 0: 727 self.fail += 1 728 elif result.stderr: 729 self.warned += 1 730 if result.already_done: 731 self.already_done += 1 732 else: 733 target = '(starting)' 734 735 # Display separate counts for ok, warned and fail 736 ok = self.upto - self.warned - self.fail 737 line = '\r' + self.col.Color(self.col.GREEN, '%5d' % ok) 738 line += self.col.Color(self.col.YELLOW, '%5d' % self.warned) 739 line += self.col.Color(self.col.RED, '%5d' % self.fail) 740 741 name = ' /%-5d ' % self.count 742 743 # Add our current completion time estimate 744 self._AddTimestamp() 745 if self._complete_delay: 746 name += '%s : ' % self._complete_delay 747 # When building all boards for a commit, we can print a commit 748 # progress message. 749 if result and result.commit_upto is None: 750 name += 'commit %2d/%-3d' % (self.commit_upto + 1, 751 self.commit_count) 752 753 name += target 754 print line + name, 755 length = 13 + len(name) 756 self.ClearLine(length) 757 758 def _GetOutputDir(self, commit_upto): 759 """Get the name of the output directory for a commit number 760 761 The output directory is typically .../<branch>/<commit>. 762 763 Args: 764 commit_upto: Commit number to use (0..self.count-1) 765 """ 766 commit = self.commits[commit_upto] 767 subject = commit.subject.translate(trans_valid_chars) 768 commit_dir = ('%02d_of_%02d_g%s_%s' % (commit_upto + 1, 769 self.commit_count, commit.hash, subject[:20])) 770 output_dir = os.path.join(self.base_dir, commit_dir) 771 return output_dir 772 773 def GetBuildDir(self, commit_upto, target): 774 """Get the name of the build directory for a commit number 775 776 The build directory is typically .../<branch>/<commit>/<target>. 777 778 Args: 779 commit_upto: Commit number to use (0..self.count-1) 780 target: Target name 781 """ 782 output_dir = self._GetOutputDir(commit_upto) 783 return os.path.join(output_dir, target) 784 785 def GetDoneFile(self, commit_upto, target): 786 """Get the name of the done file for a commit number 787 788 Args: 789 commit_upto: Commit number to use (0..self.count-1) 790 target: Target name 791 """ 792 return os.path.join(self.GetBuildDir(commit_upto, target), 'done') 793 794 def GetSizesFile(self, commit_upto, target): 795 """Get the name of the sizes file for a commit number 796 797 Args: 798 commit_upto: Commit number to use (0..self.count-1) 799 target: Target name 800 """ 801 return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes') 802 803 def GetFuncSizesFile(self, commit_upto, target, elf_fname): 804 """Get the name of the funcsizes file for a commit number and ELF file 805 806 Args: 807 commit_upto: Commit number to use (0..self.count-1) 808 target: Target name 809 elf_fname: Filename of elf image 810 """ 811 return os.path.join(self.GetBuildDir(commit_upto, target), 812 '%s.sizes' % elf_fname.replace('/', '-')) 813 814 def GetObjdumpFile(self, commit_upto, target, elf_fname): 815 """Get the name of the objdump file for a commit number and ELF file 816 817 Args: 818 commit_upto: Commit number to use (0..self.count-1) 819 target: Target name 820 elf_fname: Filename of elf image 821 """ 822 return os.path.join(self.GetBuildDir(commit_upto, target), 823 '%s.objdump' % elf_fname.replace('/', '-')) 824 825 def GetErrFile(self, commit_upto, target): 826 """Get the name of the err file for a commit number 827 828 Args: 829 commit_upto: Commit number to use (0..self.count-1) 830 target: Target name 831 """ 832 output_dir = self.GetBuildDir(commit_upto, target) 833 return os.path.join(output_dir, 'err') 834 835 def FilterErrors(self, lines): 836 """Filter out errors in which we have no interest 837 838 We should probably use map(). 839 840 Args: 841 lines: List of error lines, each a string 842 Returns: 843 New list with only interesting lines included 844 """ 845 out_lines = [] 846 for line in lines: 847 if not self.re_make_err.search(line): 848 out_lines.append(line) 849 return out_lines 850 851 def ReadFuncSizes(self, fname, fd): 852 """Read function sizes from the output of 'nm' 853 854 Args: 855 fd: File containing data to read 856 fname: Filename we are reading from (just for errors) 857 858 Returns: 859 Dictionary containing size of each function in bytes, indexed by 860 function name. 861 """ 862 sym = {} 863 for line in fd.readlines(): 864 try: 865 size, type, name = line[:-1].split() 866 except: 867 print "Invalid line in file '%s': '%s'" % (fname, line[:-1]) 868 continue 869 if type in 'tTdDbB': 870 # function names begin with '.' on 64-bit powerpc 871 if '.' in name[1:]: 872 name = 'static.' + name.split('.')[0] 873 sym[name] = sym.get(name, 0) + int(size, 16) 874 return sym 875 876 def GetBuildOutcome(self, commit_upto, target, read_func_sizes): 877 """Work out the outcome of a build. 878 879 Args: 880 commit_upto: Commit number to check (0..n-1) 881 target: Target board to check 882 read_func_sizes: True to read function size information 883 884 Returns: 885 Outcome object 886 """ 887 done_file = self.GetDoneFile(commit_upto, target) 888 sizes_file = self.GetSizesFile(commit_upto, target) 889 sizes = {} 890 func_sizes = {} 891 if os.path.exists(done_file): 892 with open(done_file, 'r') as fd: 893 return_code = int(fd.readline()) 894 err_lines = [] 895 err_file = self.GetErrFile(commit_upto, target) 896 if os.path.exists(err_file): 897 with open(err_file, 'r') as fd: 898 err_lines = self.FilterErrors(fd.readlines()) 899 900 # Decide whether the build was ok, failed or created warnings 901 if return_code: 902 rc = OUTCOME_ERROR 903 elif len(err_lines): 904 rc = OUTCOME_WARNING 905 else: 906 rc = OUTCOME_OK 907 908 # Convert size information to our simple format 909 if os.path.exists(sizes_file): 910 with open(sizes_file, 'r') as fd: 911 for line in fd.readlines(): 912 values = line.split() 913 rodata = 0 914 if len(values) > 6: 915 rodata = int(values[6], 16) 916 size_dict = { 917 'all' : int(values[0]) + int(values[1]) + 918 int(values[2]), 919 'text' : int(values[0]) - rodata, 920 'data' : int(values[1]), 921 'bss' : int(values[2]), 922 'rodata' : rodata, 923 } 924 sizes[values[5]] = size_dict 925 926 if read_func_sizes: 927 pattern = self.GetFuncSizesFile(commit_upto, target, '*') 928 for fname in glob.glob(pattern): 929 with open(fname, 'r') as fd: 930 dict_name = os.path.basename(fname).replace('.sizes', 931 '') 932 func_sizes[dict_name] = self.ReadFuncSizes(fname, fd) 933 934 return Builder.Outcome(rc, err_lines, sizes, func_sizes) 935 936 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {}) 937 938 def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes): 939 """Calculate a summary of the results of building a commit. 940 941 Args: 942 board_selected: Dict containing boards to summarise 943 commit_upto: Commit number to summarize (0..self.count-1) 944 read_func_sizes: True to read function size information 945 946 Returns: 947 Tuple: 948 Dict containing boards which passed building this commit. 949 keyed by board.target 950 List containing a summary of error/warning lines 951 """ 952 board_dict = {} 953 err_lines_summary = [] 954 955 for board in boards_selected.itervalues(): 956 outcome = self.GetBuildOutcome(commit_upto, board.target, 957 read_func_sizes) 958 board_dict[board.target] = outcome 959 for err in outcome.err_lines: 960 if err and not err.rstrip() in err_lines_summary: 961 err_lines_summary.append(err.rstrip()) 962 return board_dict, err_lines_summary 963 964 def AddOutcome(self, board_dict, arch_list, changes, char, color): 965 """Add an output to our list of outcomes for each architecture 966 967 This simple function adds failing boards (changes) to the 968 relevant architecture string, so we can print the results out 969 sorted by architecture. 970 971 Args: 972 board_dict: Dict containing all boards 973 arch_list: Dict keyed by arch name. Value is a string containing 974 a list of board names which failed for that arch. 975 changes: List of boards to add to arch_list 976 color: terminal.Colour object 977 """ 978 done_arch = {} 979 for target in changes: 980 if target in board_dict: 981 arch = board_dict[target].arch 982 else: 983 arch = 'unknown' 984 str = self.col.Color(color, ' ' + target) 985 if not arch in done_arch: 986 str = self.col.Color(color, char) + ' ' + str 987 done_arch[arch] = True 988 if not arch in arch_list: 989 arch_list[arch] = str 990 else: 991 arch_list[arch] += str 992 993 994 def ColourNum(self, num): 995 color = self.col.RED if num > 0 else self.col.GREEN 996 if num == 0: 997 return '0' 998 return self.col.Color(color, str(num)) 999 1000 def ResetResultSummary(self, board_selected): 1001 """Reset the results summary ready for use. 1002 1003 Set up the base board list to be all those selected, and set the 1004 error lines to empty. 1005 1006 Following this, calls to PrintResultSummary() will use this 1007 information to work out what has changed. 1008 1009 Args: 1010 board_selected: Dict containing boards to summarise, keyed by 1011 board.target 1012 """ 1013 self._base_board_dict = {} 1014 for board in board_selected: 1015 self._base_board_dict[board] = Builder.Outcome(0, [], [], {}) 1016 self._base_err_lines = [] 1017 1018 def PrintFuncSizeDetail(self, fname, old, new): 1019 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0 1020 delta, common = [], {} 1021 1022 for a in old: 1023 if a in new: 1024 common[a] = 1 1025 1026 for name in old: 1027 if name not in common: 1028 remove += 1 1029 down += old[name] 1030 delta.append([-old[name], name]) 1031 1032 for name in new: 1033 if name not in common: 1034 add += 1 1035 up += new[name] 1036 delta.append([new[name], name]) 1037 1038 for name in common: 1039 diff = new.get(name, 0) - old.get(name, 0) 1040 if diff > 0: 1041 grow, up = grow + 1, up + diff 1042 elif diff < 0: 1043 shrink, down = shrink + 1, down - diff 1044 delta.append([diff, name]) 1045 1046 delta.sort() 1047 delta.reverse() 1048 1049 args = [add, -remove, grow, -shrink, up, -down, up - down] 1050 if max(args) == 0: 1051 return 1052 args = [self.ColourNum(x) for x in args] 1053 indent = ' ' * 15 1054 print ('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' % 1055 tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args)) 1056 print '%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new', 1057 'delta') 1058 for diff, name in delta: 1059 if diff: 1060 color = self.col.RED if diff > 0 else self.col.GREEN 1061 msg = '%s %-38s %7s %7s %+7d' % (indent, name, 1062 old.get(name, '-'), new.get(name,'-'), diff) 1063 print self.col.Color(color, msg) 1064 1065 1066 def PrintSizeDetail(self, target_list, show_bloat): 1067 """Show details size information for each board 1068 1069 Args: 1070 target_list: List of targets, each a dict containing: 1071 'target': Target name 1072 'total_diff': Total difference in bytes across all areas 1073 <part_name>: Difference for that part 1074 show_bloat: Show detail for each function 1075 """ 1076 targets_by_diff = sorted(target_list, reverse=True, 1077 key=lambda x: x['_total_diff']) 1078 for result in targets_by_diff: 1079 printed_target = False 1080 for name in sorted(result): 1081 diff = result[name] 1082 if name.startswith('_'): 1083 continue 1084 if diff != 0: 1085 color = self.col.RED if diff > 0 else self.col.GREEN 1086 msg = ' %s %+d' % (name, diff) 1087 if not printed_target: 1088 print '%10s %-15s:' % ('', result['_target']), 1089 printed_target = True 1090 print self.col.Color(color, msg), 1091 if printed_target: 1092 print 1093 if show_bloat: 1094 target = result['_target'] 1095 outcome = result['_outcome'] 1096 base_outcome = self._base_board_dict[target] 1097 for fname in outcome.func_sizes: 1098 self.PrintFuncSizeDetail(fname, 1099 base_outcome.func_sizes[fname], 1100 outcome.func_sizes[fname]) 1101 1102 1103 def PrintSizeSummary(self, board_selected, board_dict, show_detail, 1104 show_bloat): 1105 """Print a summary of image sizes broken down by section. 1106 1107 The summary takes the form of one line per architecture. The 1108 line contains deltas for each of the sections (+ means the section 1109 got bigger, - means smaller). The nunmbers are the average number 1110 of bytes that a board in this section increased by. 1111 1112 For example: 1113 powerpc: (622 boards) text -0.0 1114 arm: (285 boards) text -0.0 1115 nds32: (3 boards) text -8.0 1116 1117 Args: 1118 board_selected: Dict containing boards to summarise, keyed by 1119 board.target 1120 board_dict: Dict containing boards for which we built this 1121 commit, keyed by board.target. The value is an Outcome object. 1122 show_detail: Show detail for each board 1123 show_bloat: Show detail for each function 1124 """ 1125 arch_list = {} 1126 arch_count = {} 1127 1128 # Calculate changes in size for different image parts 1129 # The previous sizes are in Board.sizes, for each board 1130 for target in board_dict: 1131 if target not in board_selected: 1132 continue 1133 base_sizes = self._base_board_dict[target].sizes 1134 outcome = board_dict[target] 1135 sizes = outcome.sizes 1136 1137 # Loop through the list of images, creating a dict of size 1138 # changes for each image/part. We end up with something like 1139 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4} 1140 # which means that U-Boot data increased by 5 bytes and SPL 1141 # text decreased by 4. 1142 err = {'_target' : target} 1143 for image in sizes: 1144 if image in base_sizes: 1145 base_image = base_sizes[image] 1146 # Loop through the text, data, bss parts 1147 for part in sorted(sizes[image]): 1148 diff = sizes[image][part] - base_image[part] 1149 col = None 1150 if diff: 1151 if image == 'u-boot': 1152 name = part 1153 else: 1154 name = image + ':' + part 1155 err[name] = diff 1156 arch = board_selected[target].arch 1157 if not arch in arch_count: 1158 arch_count[arch] = 1 1159 else: 1160 arch_count[arch] += 1 1161 if not sizes: 1162 pass # Only add to our list when we have some stats 1163 elif not arch in arch_list: 1164 arch_list[arch] = [err] 1165 else: 1166 arch_list[arch].append(err) 1167 1168 # We now have a list of image size changes sorted by arch 1169 # Print out a summary of these 1170 for arch, target_list in arch_list.iteritems(): 1171 # Get total difference for each type 1172 totals = {} 1173 for result in target_list: 1174 total = 0 1175 for name, diff in result.iteritems(): 1176 if name.startswith('_'): 1177 continue 1178 total += diff 1179 if name in totals: 1180 totals[name] += diff 1181 else: 1182 totals[name] = diff 1183 result['_total_diff'] = total 1184 result['_outcome'] = board_dict[result['_target']] 1185 1186 count = len(target_list) 1187 printed_arch = False 1188 for name in sorted(totals): 1189 diff = totals[name] 1190 if diff: 1191 # Display the average difference in this name for this 1192 # architecture 1193 avg_diff = float(diff) / count 1194 color = self.col.RED if avg_diff > 0 else self.col.GREEN 1195 msg = ' %s %+1.1f' % (name, avg_diff) 1196 if not printed_arch: 1197 print '%10s: (for %d/%d boards)' % (arch, count, 1198 arch_count[arch]), 1199 printed_arch = True 1200 print self.col.Color(color, msg), 1201 1202 if printed_arch: 1203 print 1204 if show_detail: 1205 self.PrintSizeDetail(target_list, show_bloat) 1206 1207 1208 def PrintResultSummary(self, board_selected, board_dict, err_lines, 1209 show_sizes, show_detail, show_bloat): 1210 """Compare results with the base results and display delta. 1211 1212 Only boards mentioned in board_selected will be considered. This 1213 function is intended to be called repeatedly with the results of 1214 each commit. It therefore shows a 'diff' between what it saw in 1215 the last call and what it sees now. 1216 1217 Args: 1218 board_selected: Dict containing boards to summarise, keyed by 1219 board.target 1220 board_dict: Dict containing boards for which we built this 1221 commit, keyed by board.target. The value is an Outcome object. 1222 err_lines: A list of errors for this commit, or [] if there is 1223 none, or we don't want to print errors 1224 show_sizes: Show image size deltas 1225 show_detail: Show detail for each board 1226 show_bloat: Show detail for each function 1227 """ 1228 better = [] # List of boards fixed since last commit 1229 worse = [] # List of new broken boards since last commit 1230 new = [] # List of boards that didn't exist last time 1231 unknown = [] # List of boards that were not built 1232 1233 for target in board_dict: 1234 if target not in board_selected: 1235 continue 1236 1237 # If the board was built last time, add its outcome to a list 1238 if target in self._base_board_dict: 1239 base_outcome = self._base_board_dict[target].rc 1240 outcome = board_dict[target] 1241 if outcome.rc == OUTCOME_UNKNOWN: 1242 unknown.append(target) 1243 elif outcome.rc < base_outcome: 1244 better.append(target) 1245 elif outcome.rc > base_outcome: 1246 worse.append(target) 1247 else: 1248 new.append(target) 1249 1250 # Get a list of errors that have appeared, and disappeared 1251 better_err = [] 1252 worse_err = [] 1253 for line in err_lines: 1254 if line not in self._base_err_lines: 1255 worse_err.append('+' + line) 1256 for line in self._base_err_lines: 1257 if line not in err_lines: 1258 better_err.append('-' + line) 1259 1260 # Display results by arch 1261 if better or worse or unknown or new or worse_err or better_err: 1262 arch_list = {} 1263 self.AddOutcome(board_selected, arch_list, better, '', 1264 self.col.GREEN) 1265 self.AddOutcome(board_selected, arch_list, worse, '+', 1266 self.col.RED) 1267 self.AddOutcome(board_selected, arch_list, new, '*', self.col.BLUE) 1268 if self._show_unknown: 1269 self.AddOutcome(board_selected, arch_list, unknown, '?', 1270 self.col.MAGENTA) 1271 for arch, target_list in arch_list.iteritems(): 1272 print '%10s: %s' % (arch, target_list) 1273 if better_err: 1274 print self.col.Color(self.col.GREEN, '\n'.join(better_err)) 1275 if worse_err: 1276 print self.col.Color(self.col.RED, '\n'.join(worse_err)) 1277 1278 if show_sizes: 1279 self.PrintSizeSummary(board_selected, board_dict, show_detail, 1280 show_bloat) 1281 1282 # Save our updated information for the next call to this function 1283 self._base_board_dict = board_dict 1284 self._base_err_lines = err_lines 1285 1286 # Get a list of boards that did not get built, if needed 1287 not_built = [] 1288 for board in board_selected: 1289 if not board in board_dict: 1290 not_built.append(board) 1291 if not_built: 1292 print "Boards not built (%d): %s" % (len(not_built), 1293 ', '.join(not_built)) 1294 1295 1296 def ShowSummary(self, commits, board_selected, show_errors, show_sizes, 1297 show_detail, show_bloat): 1298 """Show a build summary for U-Boot for a given board list. 1299 1300 Reset the result summary, then repeatedly call GetResultSummary on 1301 each commit's results, then display the differences we see. 1302 1303 Args: 1304 commit: Commit objects to summarise 1305 board_selected: Dict containing boards to summarise 1306 show_errors: Show errors that occured 1307 show_sizes: Show size deltas 1308 show_detail: Show detail for each board 1309 show_bloat: Show detail for each function 1310 """ 1311 self.commit_count = len(commits) 1312 self.commits = commits 1313 self.ResetResultSummary(board_selected) 1314 1315 for commit_upto in range(0, self.commit_count, self._step): 1316 board_dict, err_lines = self.GetResultSummary(board_selected, 1317 commit_upto, read_func_sizes=show_bloat) 1318 msg = '%02d: %s' % (commit_upto + 1, commits[commit_upto].subject) 1319 print self.col.Color(self.col.BLUE, msg) 1320 self.PrintResultSummary(board_selected, board_dict, 1321 err_lines if show_errors else [], show_sizes, show_detail, 1322 show_bloat) 1323 1324 1325 def SetupBuild(self, board_selected, commits): 1326 """Set up ready to start a build. 1327 1328 Args: 1329 board_selected: Selected boards to build 1330 commits: Selected commits to build 1331 """ 1332 # First work out how many commits we will build 1333 count = (len(commits) + self._step - 1) / self._step 1334 self.count = len(board_selected) * count 1335 self.upto = self.warned = self.fail = 0 1336 self._timestamps = collections.deque() 1337 1338 def BuildBoardsForCommit(self, board_selected, keep_outputs): 1339 """Build all boards for a single commit""" 1340 self.SetupBuild(board_selected) 1341 self.count = len(board_selected) 1342 for brd in board_selected.itervalues(): 1343 job = BuilderJob() 1344 job.board = brd 1345 job.commits = None 1346 job.keep_outputs = keep_outputs 1347 self.queue.put(brd) 1348 1349 self.queue.join() 1350 self.out_queue.join() 1351 print 1352 self.ClearLine(0) 1353 1354 def BuildCommits(self, commits, board_selected, show_errors, keep_outputs): 1355 """Build all boards for all commits (non-incremental)""" 1356 self.commit_count = len(commits) 1357 1358 self.ResetResultSummary(board_selected) 1359 for self.commit_upto in range(self.commit_count): 1360 self.SelectCommit(commits[self.commit_upto]) 1361 self.SelectOutputDir() 1362 Mkdir(self.output_dir) 1363 1364 self.BuildBoardsForCommit(board_selected, keep_outputs) 1365 board_dict, err_lines = self.GetResultSummary() 1366 self.PrintResultSummary(board_selected, board_dict, 1367 err_lines if show_errors else []) 1368 1369 if self.already_done: 1370 print '%d builds already done' % self.already_done 1371 1372 def GetThreadDir(self, thread_num): 1373 """Get the directory path to the working dir for a thread. 1374 1375 Args: 1376 thread_num: Number of thread to check. 1377 """ 1378 return os.path.join(self._working_dir, '%02d' % thread_num) 1379 1380 def _PrepareThread(self, thread_num): 1381 """Prepare the working directory for a thread. 1382 1383 This clones or fetches the repo into the thread's work directory. 1384 1385 Args: 1386 thread_num: Thread number (0, 1, ...) 1387 """ 1388 thread_dir = self.GetThreadDir(thread_num) 1389 Mkdir(thread_dir) 1390 git_dir = os.path.join(thread_dir, '.git') 1391 1392 # Clone the repo if it doesn't already exist 1393 # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so 1394 # we have a private index but uses the origin repo's contents? 1395 if self.git_dir: 1396 src_dir = os.path.abspath(self.git_dir) 1397 if os.path.exists(git_dir): 1398 gitutil.Fetch(git_dir, thread_dir) 1399 else: 1400 print 'Cloning repo for thread %d' % thread_num 1401 gitutil.Clone(src_dir, thread_dir) 1402 1403 def _PrepareWorkingSpace(self, max_threads): 1404 """Prepare the working directory for use. 1405 1406 Set up the git repo for each thread. 1407 1408 Args: 1409 max_threads: Maximum number of threads we expect to need. 1410 """ 1411 Mkdir(self._working_dir) 1412 for thread in range(max_threads): 1413 self._PrepareThread(thread) 1414 1415 def _PrepareOutputSpace(self): 1416 """Get the output directories ready to receive files. 1417 1418 We delete any output directories which look like ones we need to 1419 create. Having left over directories is confusing when the user wants 1420 to check the output manually. 1421 """ 1422 dir_list = [] 1423 for commit_upto in range(self.commit_count): 1424 dir_list.append(self._GetOutputDir(commit_upto)) 1425 1426 for dirname in glob.glob(os.path.join(self.base_dir, '*')): 1427 if dirname not in dir_list: 1428 shutil.rmtree(dirname) 1429 1430 def BuildBoards(self, commits, board_selected, show_errors, keep_outputs): 1431 """Build all commits for a list of boards 1432 1433 Args: 1434 commits: List of commits to be build, each a Commit object 1435 boards_selected: Dict of selected boards, key is target name, 1436 value is Board object 1437 show_errors: True to show summarised error/warning info 1438 keep_outputs: True to save build output files 1439 """ 1440 self.commit_count = len(commits) 1441 self.commits = commits 1442 1443 self.ResetResultSummary(board_selected) 1444 Mkdir(self.base_dir) 1445 self._PrepareWorkingSpace(min(self.num_threads, len(board_selected))) 1446 self._PrepareOutputSpace() 1447 self.SetupBuild(board_selected, commits) 1448 self.ProcessResult(None) 1449 1450 # Create jobs to build all commits for each board 1451 for brd in board_selected.itervalues(): 1452 job = BuilderJob() 1453 job.board = brd 1454 job.commits = commits 1455 job.keep_outputs = keep_outputs 1456 job.step = self._step 1457 self.queue.put(job) 1458 1459 # Wait until all jobs are started 1460 self.queue.join() 1461 1462 # Wait until we have processed all output 1463 self.out_queue.join() 1464 print 1465 self.ClearLine(0) 1466