blob: b90d7e1f58b409ae0a4894c7d87d141a4e7cc14b [file] [log] [blame]
Simon Glassc05694f2013-04-03 11:07:16 +00001# Copyright (c) 2013 The Chromium OS Authors.
2#
3# Bloat-o-meter code used here Copyright 2004 Matt Mackall <mpm@selenic.com>
4#
Wolfgang Denkd79de1d2013-07-08 09:37:19 +02005# SPDX-License-Identifier: GPL-2.0+
Simon Glassc05694f2013-04-03 11:07:16 +00006#
7
8import collections
Simon Glassc05694f2013-04-03 11:07:16 +00009from datetime import datetime, timedelta
10import glob
11import os
12import re
13import Queue
14import shutil
15import string
16import sys
Simon Glassc05694f2013-04-03 11:07:16 +000017import time
18
Simon Glass4a1e88b2014-08-09 15:33:00 -060019import builderthread
Simon Glassc05694f2013-04-03 11:07:16 +000020import command
21import gitutil
22import terminal
23import toolchain
24
25
26"""
27Theory of Operation
28
29Please see README for user documentation, and you should be familiar with
30that before trying to make sense of this.
31
32Buildman works by keeping the machine as busy as possible, building different
33commits for different boards on multiple CPUs at once.
34
35The source repo (self.git_dir) contains all the commits to be built. Each
36thread works on a single board at a time. It checks out the first commit,
37configures it for that board, then builds it. Then it checks out the next
38commit and builds it (typically without re-configuring). When it runs out
39of commits, it gets another job from the builder and starts again with that
40board.
41
42Clearly the builder threads could work either way - they could check out a
43commit and then built it for all boards. Using separate directories for each
44commit/board pair they could leave their build product around afterwards
45also.
46
47The intent behind building a single board for multiple commits, is to make
48use of incremental builds. Since each commit is built incrementally from
49the previous one, builds are faster. Reconfiguring for a different board
50removes all intermediate object files.
51
52Many threads can be working at once, but each has its own working directory.
53When a thread finishes a build, it puts the output files into a result
54directory.
55
56The base directory used by buildman is normally '../<branch>', i.e.
57a directory higher than the source repository and named after the branch
58being built.
59
60Within the base directory, we have one subdirectory for each commit. Within
61that is one subdirectory for each board. Within that is the build output for
62that commit/board combination.
63
64Buildman also create working directories for each thread, in a .bm-work/
65subdirectory in the base dir.
66
67As an example, say we are building branch 'us-net' for boards 'sandbox' and
68'seaboard', and say that us-net has two commits. We will have directories
69like this:
70
71us-net/ base directory
72 01_of_02_g4ed4ebc_net--Add-tftp-speed-/
73 sandbox/
74 u-boot.bin
75 seaboard/
76 u-boot.bin
77 02_of_02_g4ed4ebc_net--Check-tftp-comp/
78 sandbox/
79 u-boot.bin
80 seaboard/
81 u-boot.bin
82 .bm-work/
83 00/ working directory for thread 0 (contains source checkout)
84 build/ build output
85 01/ working directory for thread 1
86 build/ build output
87 ...
88u-boot/ source directory
89 .git/ repository
90"""
91
92# Possible build outcomes
93OUTCOME_OK, OUTCOME_WARNING, OUTCOME_ERROR, OUTCOME_UNKNOWN = range(4)
94
95# Translate a commit subject into a valid filename
96trans_valid_chars = string.maketrans("/: ", "---")
97
98
Simon Glassc05694f2013-04-03 11:07:16 +000099class Builder:
100 """Class for building U-Boot for a particular commit.
101
102 Public members: (many should ->private)
103 active: True if the builder is active and has not been stopped
104 already_done: Number of builds already completed
105 base_dir: Base directory to use for builder
106 checkout: True to check out source, False to skip that step.
107 This is used for testing.
108 col: terminal.Color() object
109 count: Number of commits to build
110 do_make: Method to call to invoke Make
111 fail: Number of builds that failed due to error
112 force_build: Force building even if a build already exists
113 force_config_on_failure: If a commit fails for a board, disable
114 incremental building for the next commit we build for that
115 board, so that we will see all warnings/errors again.
Simon Glass7041c392014-07-13 12:22:31 -0600116 force_build_failures: If a previously-built build (i.e. built on
117 a previous run of buildman) is marked as failed, rebuild it.
Simon Glassc05694f2013-04-03 11:07:16 +0000118 git_dir: Git directory containing source repository
119 last_line_len: Length of the last line we printed (used for erasing
120 it with new progress information)
121 num_jobs: Number of jobs to run at once (passed to make as -j)
122 num_threads: Number of builder threads to run
123 out_queue: Queue of results to process
124 re_make_err: Compiled regular expression for ignore_lines
125 queue: Queue of jobs to run
126 threads: List of active threads
127 toolchains: Toolchains object to use for building
128 upto: Current commit number we are building (0.count-1)
129 warned: Number of builds that produced at least one warning
Simon Glassf3018b7a2014-07-14 17:51:02 -0600130 force_reconfig: Reconfigure U-Boot on each comiit. This disables
131 incremental building, where buildman reconfigures on the first
132 commit for a baord, and then just does an incremental build for
133 the following commits. In fact buildman will reconfigure and
134 retry for any failing commits, so generally the only effect of
135 this option is to slow things down.
Simon Glass38df2e22014-07-14 17:51:03 -0600136 in_tree: Build U-Boot in-tree instead of specifying an output
137 directory separate from the source code. This option is really
138 only useful for testing in-tree builds.
Simon Glassc05694f2013-04-03 11:07:16 +0000139
140 Private members:
141 _base_board_dict: Last-summarised Dict of boards
142 _base_err_lines: Last-summarised list of errors
143 _build_period_us: Time taken for a single build (float object).
144 _complete_delay: Expected delay until completion (timedelta)
145 _next_delay_update: Next time we plan to display a progress update
146 (datatime)
147 _show_unknown: Show unknown boards (those not built) in summary
148 _timestamps: List of timestamps for the completion of the last
149 last _timestamp_count builds. Each is a datetime object.
150 _timestamp_count: Number of timestamps to keep in our list.
151 _working_dir: Base working directory containing all threads
152 """
153 class Outcome:
154 """Records a build outcome for a single make invocation
155
156 Public Members:
157 rc: Outcome value (OUTCOME_...)
158 err_lines: List of error lines or [] if none
159 sizes: Dictionary of image size information, keyed by filename
160 - Each value is itself a dictionary containing
161 values for 'text', 'data' and 'bss', being the integer
162 size in bytes of each section.
163 func_sizes: Dictionary keyed by filename - e.g. 'u-boot'. Each
164 value is itself a dictionary:
165 key: function name
166 value: Size of function in bytes
167 """
168 def __init__(self, rc, err_lines, sizes, func_sizes):
169 self.rc = rc
170 self.err_lines = err_lines
171 self.sizes = sizes
172 self.func_sizes = func_sizes
173
174 def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs,
Masahiro Yamada1fe610d2014-07-22 11:19:09 +0900175 gnu_make='make', checkout=True, show_unknown=True, step=1):
Simon Glassc05694f2013-04-03 11:07:16 +0000176 """Create a new Builder object
177
178 Args:
179 toolchains: Toolchains object to use for building
180 base_dir: Base directory to use for builder
181 git_dir: Git directory containing source repository
182 num_threads: Number of builder threads to run
183 num_jobs: Number of jobs to run at once (passed to make as -j)
Masahiro Yamada1fe610d2014-07-22 11:19:09 +0900184 gnu_make: the command name of GNU Make.
Simon Glassc05694f2013-04-03 11:07:16 +0000185 checkout: True to check out source, False to skip that step.
186 This is used for testing.
187 show_unknown: Show unknown boards (those not built) in summary
188 step: 1 to process every commit, n to process every nth commit
189 """
190 self.toolchains = toolchains
191 self.base_dir = base_dir
192 self._working_dir = os.path.join(base_dir, '.bm-work')
193 self.threads = []
194 self.active = True
195 self.do_make = self.Make
Masahiro Yamada1fe610d2014-07-22 11:19:09 +0900196 self.gnu_make = gnu_make
Simon Glassc05694f2013-04-03 11:07:16 +0000197 self.checkout = checkout
198 self.num_threads = num_threads
199 self.num_jobs = num_jobs
200 self.already_done = 0
201 self.force_build = False
202 self.git_dir = git_dir
203 self._show_unknown = show_unknown
204 self._timestamp_count = 10
205 self._build_period_us = None
206 self._complete_delay = None
207 self._next_delay_update = datetime.now()
208 self.force_config_on_failure = True
Simon Glass7041c392014-07-13 12:22:31 -0600209 self.force_build_failures = False
Simon Glassf3018b7a2014-07-14 17:51:02 -0600210 self.force_reconfig = False
Simon Glassc05694f2013-04-03 11:07:16 +0000211 self._step = step
Simon Glass38df2e22014-07-14 17:51:03 -0600212 self.in_tree = False
Simon Glassbb4dffb2014-08-09 15:33:06 -0600213 self._error_lines = 0
Simon Glassc05694f2013-04-03 11:07:16 +0000214
215 self.col = terminal.Color()
216
217 self.queue = Queue.Queue()
218 self.out_queue = Queue.Queue()
219 for i in range(self.num_threads):
Simon Glass4a1e88b2014-08-09 15:33:00 -0600220 t = builderthread.BuilderThread(self, i)
Simon Glassc05694f2013-04-03 11:07:16 +0000221 t.setDaemon(True)
222 t.start()
223 self.threads.append(t)
224
225 self.last_line_len = 0
Simon Glass4a1e88b2014-08-09 15:33:00 -0600226 t = builderthread.ResultThread(self)
Simon Glassc05694f2013-04-03 11:07:16 +0000227 t.setDaemon(True)
228 t.start()
229 self.threads.append(t)
230
231 ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
232 self.re_make_err = re.compile('|'.join(ignore_lines))
233
234 def __del__(self):
235 """Get rid of all threads created by the builder"""
236 for t in self.threads:
237 del t
238
Simon Glasseb48bbc2014-08-09 15:33:02 -0600239 def SetDisplayOptions(self, show_errors=False, show_sizes=False,
Simon Glass3394c9f2014-08-28 09:43:43 -0600240 show_detail=False, show_bloat=False,
241 list_error_boards=False):
Simon Glasseb48bbc2014-08-09 15:33:02 -0600242 """Setup display options for the builder.
243
244 show_errors: True to show summarised error/warning info
245 show_sizes: Show size deltas
246 show_detail: Show detail for each board
247 show_bloat: Show detail for each function
Simon Glass3394c9f2014-08-28 09:43:43 -0600248 list_error_boards: Show the boards which caused each error/warning
Simon Glasseb48bbc2014-08-09 15:33:02 -0600249 """
250 self._show_errors = show_errors
251 self._show_sizes = show_sizes
252 self._show_detail = show_detail
253 self._show_bloat = show_bloat
Simon Glass3394c9f2014-08-28 09:43:43 -0600254 self._list_error_boards = list_error_boards
Simon Glasseb48bbc2014-08-09 15:33:02 -0600255
Simon Glassc05694f2013-04-03 11:07:16 +0000256 def _AddTimestamp(self):
257 """Add a new timestamp to the list and record the build period.
258
259 The build period is the length of time taken to perform a single
260 build (one board, one commit).
261 """
262 now = datetime.now()
263 self._timestamps.append(now)
264 count = len(self._timestamps)
265 delta = self._timestamps[-1] - self._timestamps[0]
266 seconds = delta.total_seconds()
267
268 # If we have enough data, estimate build period (time taken for a
269 # single build) and therefore completion time.
270 if count > 1 and self._next_delay_update < now:
271 self._next_delay_update = now + timedelta(seconds=2)
272 if seconds > 0:
273 self._build_period = float(seconds) / count
274 todo = self.count - self.upto
275 self._complete_delay = timedelta(microseconds=
276 self._build_period * todo * 1000000)
277 # Round it
278 self._complete_delay -= timedelta(
279 microseconds=self._complete_delay.microseconds)
280
281 if seconds > 60:
282 self._timestamps.popleft()
283 count -= 1
284
285 def ClearLine(self, length):
286 """Clear any characters on the current line
287
288 Make way for a new line of length 'length', by outputting enough
289 spaces to clear out the old line. Then remember the new length for
290 next time.
291
292 Args:
293 length: Length of new line, in characters
294 """
295 if length < self.last_line_len:
296 print ' ' * (self.last_line_len - length),
297 print '\r',
298 self.last_line_len = length
299 sys.stdout.flush()
300
301 def SelectCommit(self, commit, checkout=True):
302 """Checkout the selected commit for this build
303 """
304 self.commit = commit
305 if checkout and self.checkout:
306 gitutil.Checkout(commit.hash)
307
308 def Make(self, commit, brd, stage, cwd, *args, **kwargs):
309 """Run make
310
311 Args:
312 commit: Commit object that is being built
313 brd: Board object that is being built
Roger Meiere0a0e552014-08-20 22:10:29 +0200314 stage: Stage that we are at (mrproper, config, build)
Simon Glassc05694f2013-04-03 11:07:16 +0000315 cwd: Directory where make should be run
316 args: Arguments to pass to make
317 kwargs: Arguments to pass to command.RunPipe()
318 """
Masahiro Yamada1fe610d2014-07-22 11:19:09 +0900319 cmd = [self.gnu_make] + list(args)
Simon Glassc05694f2013-04-03 11:07:16 +0000320 result = command.RunPipe([cmd], capture=True, capture_stderr=True,
321 cwd=cwd, raise_on_error=False, **kwargs)
322 return result
323
324 def ProcessResult(self, result):
325 """Process the result of a build, showing progress information
326
327 Args:
Simon Glass78e418e2014-08-09 15:33:03 -0600328 result: A CommandResult object, which indicates the result for
329 a single build
Simon Glassc05694f2013-04-03 11:07:16 +0000330 """
331 col = terminal.Color()
332 if result:
333 target = result.brd.target
334
335 if result.return_code < 0:
336 self.active = False
337 command.StopAll()
338 return
339
340 self.upto += 1
341 if result.return_code != 0:
342 self.fail += 1
343 elif result.stderr:
344 self.warned += 1
345 if result.already_done:
346 self.already_done += 1
Simon Glass78e418e2014-08-09 15:33:03 -0600347 if self._verbose:
348 print '\r',
349 self.ClearLine(0)
350 boards_selected = {target : result.brd}
351 self.ResetResultSummary(boards_selected)
352 self.ProduceResultSummary(result.commit_upto, self.commits,
353 boards_selected)
Simon Glassc05694f2013-04-03 11:07:16 +0000354 else:
355 target = '(starting)'
356
357 # Display separate counts for ok, warned and fail
358 ok = self.upto - self.warned - self.fail
359 line = '\r' + self.col.Color(self.col.GREEN, '%5d' % ok)
360 line += self.col.Color(self.col.YELLOW, '%5d' % self.warned)
361 line += self.col.Color(self.col.RED, '%5d' % self.fail)
362
363 name = ' /%-5d ' % self.count
364
365 # Add our current completion time estimate
366 self._AddTimestamp()
367 if self._complete_delay:
368 name += '%s : ' % self._complete_delay
369 # When building all boards for a commit, we can print a commit
370 # progress message.
371 if result and result.commit_upto is None:
372 name += 'commit %2d/%-3d' % (self.commit_upto + 1,
373 self.commit_count)
374
375 name += target
376 print line + name,
Simon Glass78e418e2014-08-09 15:33:03 -0600377 length = 14 + len(name)
Simon Glassc05694f2013-04-03 11:07:16 +0000378 self.ClearLine(length)
379
380 def _GetOutputDir(self, commit_upto):
381 """Get the name of the output directory for a commit number
382
383 The output directory is typically .../<branch>/<commit>.
384
385 Args:
386 commit_upto: Commit number to use (0..self.count-1)
387 """
Simon Glassd326ad72014-08-09 15:32:59 -0600388 if self.commits:
389 commit = self.commits[commit_upto]
390 subject = commit.subject.translate(trans_valid_chars)
391 commit_dir = ('%02d_of_%02d_g%s_%s' % (commit_upto + 1,
392 self.commit_count, commit.hash, subject[:20]))
393 else:
394 commit_dir = 'current'
Simon Glassc05694f2013-04-03 11:07:16 +0000395 output_dir = os.path.join(self.base_dir, commit_dir)
396 return output_dir
397
398 def GetBuildDir(self, commit_upto, target):
399 """Get the name of the build directory for a commit number
400
401 The build directory is typically .../<branch>/<commit>/<target>.
402
403 Args:
404 commit_upto: Commit number to use (0..self.count-1)
405 target: Target name
406 """
407 output_dir = self._GetOutputDir(commit_upto)
408 return os.path.join(output_dir, target)
409
410 def GetDoneFile(self, commit_upto, target):
411 """Get the name of the done file for a commit number
412
413 Args:
414 commit_upto: Commit number to use (0..self.count-1)
415 target: Target name
416 """
417 return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
418
419 def GetSizesFile(self, commit_upto, target):
420 """Get the name of the sizes file for a commit number
421
422 Args:
423 commit_upto: Commit number to use (0..self.count-1)
424 target: Target name
425 """
426 return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes')
427
428 def GetFuncSizesFile(self, commit_upto, target, elf_fname):
429 """Get the name of the funcsizes file for a commit number and ELF file
430
431 Args:
432 commit_upto: Commit number to use (0..self.count-1)
433 target: Target name
434 elf_fname: Filename of elf image
435 """
436 return os.path.join(self.GetBuildDir(commit_upto, target),
437 '%s.sizes' % elf_fname.replace('/', '-'))
438
439 def GetObjdumpFile(self, commit_upto, target, elf_fname):
440 """Get the name of the objdump file for a commit number and ELF file
441
442 Args:
443 commit_upto: Commit number to use (0..self.count-1)
444 target: Target name
445 elf_fname: Filename of elf image
446 """
447 return os.path.join(self.GetBuildDir(commit_upto, target),
448 '%s.objdump' % elf_fname.replace('/', '-'))
449
450 def GetErrFile(self, commit_upto, target):
451 """Get the name of the err file for a commit number
452
453 Args:
454 commit_upto: Commit number to use (0..self.count-1)
455 target: Target name
456 """
457 output_dir = self.GetBuildDir(commit_upto, target)
458 return os.path.join(output_dir, 'err')
459
460 def FilterErrors(self, lines):
461 """Filter out errors in which we have no interest
462
463 We should probably use map().
464
465 Args:
466 lines: List of error lines, each a string
467 Returns:
468 New list with only interesting lines included
469 """
470 out_lines = []
471 for line in lines:
472 if not self.re_make_err.search(line):
473 out_lines.append(line)
474 return out_lines
475
476 def ReadFuncSizes(self, fname, fd):
477 """Read function sizes from the output of 'nm'
478
479 Args:
480 fd: File containing data to read
481 fname: Filename we are reading from (just for errors)
482
483 Returns:
484 Dictionary containing size of each function in bytes, indexed by
485 function name.
486 """
487 sym = {}
488 for line in fd.readlines():
489 try:
490 size, type, name = line[:-1].split()
491 except:
492 print "Invalid line in file '%s': '%s'" % (fname, line[:-1])
493 continue
494 if type in 'tTdDbB':
495 # function names begin with '.' on 64-bit powerpc
496 if '.' in name[1:]:
497 name = 'static.' + name.split('.')[0]
498 sym[name] = sym.get(name, 0) + int(size, 16)
499 return sym
500
501 def GetBuildOutcome(self, commit_upto, target, read_func_sizes):
502 """Work out the outcome of a build.
503
504 Args:
505 commit_upto: Commit number to check (0..n-1)
506 target: Target board to check
507 read_func_sizes: True to read function size information
508
509 Returns:
510 Outcome object
511 """
512 done_file = self.GetDoneFile(commit_upto, target)
513 sizes_file = self.GetSizesFile(commit_upto, target)
514 sizes = {}
515 func_sizes = {}
516 if os.path.exists(done_file):
517 with open(done_file, 'r') as fd:
518 return_code = int(fd.readline())
519 err_lines = []
520 err_file = self.GetErrFile(commit_upto, target)
521 if os.path.exists(err_file):
522 with open(err_file, 'r') as fd:
523 err_lines = self.FilterErrors(fd.readlines())
524
525 # Decide whether the build was ok, failed or created warnings
526 if return_code:
527 rc = OUTCOME_ERROR
528 elif len(err_lines):
529 rc = OUTCOME_WARNING
530 else:
531 rc = OUTCOME_OK
532
533 # Convert size information to our simple format
534 if os.path.exists(sizes_file):
535 with open(sizes_file, 'r') as fd:
536 for line in fd.readlines():
537 values = line.split()
538 rodata = 0
539 if len(values) > 6:
540 rodata = int(values[6], 16)
541 size_dict = {
542 'all' : int(values[0]) + int(values[1]) +
543 int(values[2]),
544 'text' : int(values[0]) - rodata,
545 'data' : int(values[1]),
546 'bss' : int(values[2]),
547 'rodata' : rodata,
548 }
549 sizes[values[5]] = size_dict
550
551 if read_func_sizes:
552 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
553 for fname in glob.glob(pattern):
554 with open(fname, 'r') as fd:
555 dict_name = os.path.basename(fname).replace('.sizes',
556 '')
557 func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
558
559 return Builder.Outcome(rc, err_lines, sizes, func_sizes)
560
561 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {})
562
563 def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes):
564 """Calculate a summary of the results of building a commit.
565
566 Args:
567 board_selected: Dict containing boards to summarise
568 commit_upto: Commit number to summarize (0..self.count-1)
569 read_func_sizes: True to read function size information
570
571 Returns:
572 Tuple:
573 Dict containing boards which passed building this commit.
574 keyed by board.target
575 List containing a summary of error/warning lines
Simon Glass3394c9f2014-08-28 09:43:43 -0600576 Dict keyed by error line, containing a list of the Board
577 objects with that error
Simon Glassc05694f2013-04-03 11:07:16 +0000578 """
579 board_dict = {}
580 err_lines_summary = []
Simon Glass3394c9f2014-08-28 09:43:43 -0600581 err_lines_boards = {}
Simon Glassc05694f2013-04-03 11:07:16 +0000582
583 for board in boards_selected.itervalues():
584 outcome = self.GetBuildOutcome(commit_upto, board.target,
585 read_func_sizes)
586 board_dict[board.target] = outcome
587 for err in outcome.err_lines:
Simon Glass3394c9f2014-08-28 09:43:43 -0600588 if err:
589 err = err.rstrip()
590 if err in err_lines_boards:
591 err_lines_boards[err].append(board)
592 else:
593 err_lines_boards[err] = [board]
594 err_lines_summary.append(err.rstrip())
595 return board_dict, err_lines_summary, err_lines_boards
Simon Glassc05694f2013-04-03 11:07:16 +0000596
597 def AddOutcome(self, board_dict, arch_list, changes, char, color):
598 """Add an output to our list of outcomes for each architecture
599
600 This simple function adds failing boards (changes) to the
601 relevant architecture string, so we can print the results out
602 sorted by architecture.
603
604 Args:
605 board_dict: Dict containing all boards
606 arch_list: Dict keyed by arch name. Value is a string containing
607 a list of board names which failed for that arch.
608 changes: List of boards to add to arch_list
609 color: terminal.Colour object
610 """
611 done_arch = {}
612 for target in changes:
613 if target in board_dict:
614 arch = board_dict[target].arch
615 else:
616 arch = 'unknown'
617 str = self.col.Color(color, ' ' + target)
618 if not arch in done_arch:
619 str = self.col.Color(color, char) + ' ' + str
620 done_arch[arch] = True
621 if not arch in arch_list:
622 arch_list[arch] = str
623 else:
624 arch_list[arch] += str
625
626
627 def ColourNum(self, num):
628 color = self.col.RED if num > 0 else self.col.GREEN
629 if num == 0:
630 return '0'
631 return self.col.Color(color, str(num))
632
633 def ResetResultSummary(self, board_selected):
634 """Reset the results summary ready for use.
635
636 Set up the base board list to be all those selected, and set the
637 error lines to empty.
638
639 Following this, calls to PrintResultSummary() will use this
640 information to work out what has changed.
641
642 Args:
643 board_selected: Dict containing boards to summarise, keyed by
644 board.target
645 """
646 self._base_board_dict = {}
647 for board in board_selected:
648 self._base_board_dict[board] = Builder.Outcome(0, [], [], {})
649 self._base_err_lines = []
650
651 def PrintFuncSizeDetail(self, fname, old, new):
652 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
653 delta, common = [], {}
654
655 for a in old:
656 if a in new:
657 common[a] = 1
658
659 for name in old:
660 if name not in common:
661 remove += 1
662 down += old[name]
663 delta.append([-old[name], name])
664
665 for name in new:
666 if name not in common:
667 add += 1
668 up += new[name]
669 delta.append([new[name], name])
670
671 for name in common:
672 diff = new.get(name, 0) - old.get(name, 0)
673 if diff > 0:
674 grow, up = grow + 1, up + diff
675 elif diff < 0:
676 shrink, down = shrink + 1, down - diff
677 delta.append([diff, name])
678
679 delta.sort()
680 delta.reverse()
681
682 args = [add, -remove, grow, -shrink, up, -down, up - down]
683 if max(args) == 0:
684 return
685 args = [self.ColourNum(x) for x in args]
686 indent = ' ' * 15
687 print ('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
688 tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args))
689 print '%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
690 'delta')
691 for diff, name in delta:
692 if diff:
693 color = self.col.RED if diff > 0 else self.col.GREEN
694 msg = '%s %-38s %7s %7s %+7d' % (indent, name,
695 old.get(name, '-'), new.get(name,'-'), diff)
696 print self.col.Color(color, msg)
697
698
699 def PrintSizeDetail(self, target_list, show_bloat):
700 """Show details size information for each board
701
702 Args:
703 target_list: List of targets, each a dict containing:
704 'target': Target name
705 'total_diff': Total difference in bytes across all areas
706 <part_name>: Difference for that part
707 show_bloat: Show detail for each function
708 """
709 targets_by_diff = sorted(target_list, reverse=True,
710 key=lambda x: x['_total_diff'])
711 for result in targets_by_diff:
712 printed_target = False
713 for name in sorted(result):
714 diff = result[name]
715 if name.startswith('_'):
716 continue
717 if diff != 0:
718 color = self.col.RED if diff > 0 else self.col.GREEN
719 msg = ' %s %+d' % (name, diff)
720 if not printed_target:
721 print '%10s %-15s:' % ('', result['_target']),
722 printed_target = True
723 print self.col.Color(color, msg),
724 if printed_target:
725 print
726 if show_bloat:
727 target = result['_target']
728 outcome = result['_outcome']
729 base_outcome = self._base_board_dict[target]
730 for fname in outcome.func_sizes:
731 self.PrintFuncSizeDetail(fname,
732 base_outcome.func_sizes[fname],
733 outcome.func_sizes[fname])
734
735
736 def PrintSizeSummary(self, board_selected, board_dict, show_detail,
737 show_bloat):
738 """Print a summary of image sizes broken down by section.
739
740 The summary takes the form of one line per architecture. The
741 line contains deltas for each of the sections (+ means the section
742 got bigger, - means smaller). The nunmbers are the average number
743 of bytes that a board in this section increased by.
744
745 For example:
746 powerpc: (622 boards) text -0.0
747 arm: (285 boards) text -0.0
748 nds32: (3 boards) text -8.0
749
750 Args:
751 board_selected: Dict containing boards to summarise, keyed by
752 board.target
753 board_dict: Dict containing boards for which we built this
754 commit, keyed by board.target. The value is an Outcome object.
755 show_detail: Show detail for each board
756 show_bloat: Show detail for each function
757 """
758 arch_list = {}
759 arch_count = {}
760
761 # Calculate changes in size for different image parts
762 # The previous sizes are in Board.sizes, for each board
763 for target in board_dict:
764 if target not in board_selected:
765 continue
766 base_sizes = self._base_board_dict[target].sizes
767 outcome = board_dict[target]
768 sizes = outcome.sizes
769
770 # Loop through the list of images, creating a dict of size
771 # changes for each image/part. We end up with something like
772 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
773 # which means that U-Boot data increased by 5 bytes and SPL
774 # text decreased by 4.
775 err = {'_target' : target}
776 for image in sizes:
777 if image in base_sizes:
778 base_image = base_sizes[image]
779 # Loop through the text, data, bss parts
780 for part in sorted(sizes[image]):
781 diff = sizes[image][part] - base_image[part]
782 col = None
783 if diff:
784 if image == 'u-boot':
785 name = part
786 else:
787 name = image + ':' + part
788 err[name] = diff
789 arch = board_selected[target].arch
790 if not arch in arch_count:
791 arch_count[arch] = 1
792 else:
793 arch_count[arch] += 1
794 if not sizes:
795 pass # Only add to our list when we have some stats
796 elif not arch in arch_list:
797 arch_list[arch] = [err]
798 else:
799 arch_list[arch].append(err)
800
801 # We now have a list of image size changes sorted by arch
802 # Print out a summary of these
803 for arch, target_list in arch_list.iteritems():
804 # Get total difference for each type
805 totals = {}
806 for result in target_list:
807 total = 0
808 for name, diff in result.iteritems():
809 if name.startswith('_'):
810 continue
811 total += diff
812 if name in totals:
813 totals[name] += diff
814 else:
815 totals[name] = diff
816 result['_total_diff'] = total
817 result['_outcome'] = board_dict[result['_target']]
818
819 count = len(target_list)
820 printed_arch = False
821 for name in sorted(totals):
822 diff = totals[name]
823 if diff:
824 # Display the average difference in this name for this
825 # architecture
826 avg_diff = float(diff) / count
827 color = self.col.RED if avg_diff > 0 else self.col.GREEN
828 msg = ' %s %+1.1f' % (name, avg_diff)
829 if not printed_arch:
830 print '%10s: (for %d/%d boards)' % (arch, count,
831 arch_count[arch]),
832 printed_arch = True
833 print self.col.Color(color, msg),
834
835 if printed_arch:
836 print
837 if show_detail:
838 self.PrintSizeDetail(target_list, show_bloat)
839
840
841 def PrintResultSummary(self, board_selected, board_dict, err_lines,
Simon Glass3394c9f2014-08-28 09:43:43 -0600842 err_line_boards, show_sizes, show_detail,
843 show_bloat):
Simon Glassc05694f2013-04-03 11:07:16 +0000844 """Compare results with the base results and display delta.
845
846 Only boards mentioned in board_selected will be considered. This
847 function is intended to be called repeatedly with the results of
848 each commit. It therefore shows a 'diff' between what it saw in
849 the last call and what it sees now.
850
851 Args:
852 board_selected: Dict containing boards to summarise, keyed by
853 board.target
854 board_dict: Dict containing boards for which we built this
855 commit, keyed by board.target. The value is an Outcome object.
856 err_lines: A list of errors for this commit, or [] if there is
857 none, or we don't want to print errors
Simon Glass3394c9f2014-08-28 09:43:43 -0600858 err_line_boards: Dict keyed by error line, containing a list of
859 the Board objects with that error
Simon Glassc05694f2013-04-03 11:07:16 +0000860 show_sizes: Show image size deltas
861 show_detail: Show detail for each board
862 show_bloat: Show detail for each function
863 """
Simon Glass3394c9f2014-08-28 09:43:43 -0600864 def _BoardList(line):
865 """Helper function to get a line of boards containing a line
866
867 Args:
868 line: Error line to search for
869 Return:
870 String containing a list of boards with that error line, or
871 '' if the user has not requested such a list
872 """
873 if self._list_error_boards:
874 names = []
875 for board in err_line_boards[line]:
876 names.append(board.target)
877 names_str = '(%s) ' % ','.join(names)
878 else:
879 names_str = ''
880 return names_str
881
Simon Glassc05694f2013-04-03 11:07:16 +0000882 better = [] # List of boards fixed since last commit
883 worse = [] # List of new broken boards since last commit
884 new = [] # List of boards that didn't exist last time
885 unknown = [] # List of boards that were not built
886
887 for target in board_dict:
888 if target not in board_selected:
889 continue
890
891 # If the board was built last time, add its outcome to a list
892 if target in self._base_board_dict:
893 base_outcome = self._base_board_dict[target].rc
894 outcome = board_dict[target]
895 if outcome.rc == OUTCOME_UNKNOWN:
896 unknown.append(target)
897 elif outcome.rc < base_outcome:
898 better.append(target)
899 elif outcome.rc > base_outcome:
900 worse.append(target)
901 else:
902 new.append(target)
903
904 # Get a list of errors that have appeared, and disappeared
905 better_err = []
906 worse_err = []
907 for line in err_lines:
908 if line not in self._base_err_lines:
Simon Glass3394c9f2014-08-28 09:43:43 -0600909 worse_err.append('+' + _BoardList(line) + line)
Simon Glassc05694f2013-04-03 11:07:16 +0000910 for line in self._base_err_lines:
911 if line not in err_lines:
912 better_err.append('-' + line)
913
914 # Display results by arch
915 if better or worse or unknown or new or worse_err or better_err:
916 arch_list = {}
917 self.AddOutcome(board_selected, arch_list, better, '',
918 self.col.GREEN)
919 self.AddOutcome(board_selected, arch_list, worse, '+',
920 self.col.RED)
921 self.AddOutcome(board_selected, arch_list, new, '*', self.col.BLUE)
922 if self._show_unknown:
923 self.AddOutcome(board_selected, arch_list, unknown, '?',
924 self.col.MAGENTA)
925 for arch, target_list in arch_list.iteritems():
926 print '%10s: %s' % (arch, target_list)
Simon Glassbb4dffb2014-08-09 15:33:06 -0600927 self._error_lines += 1
Simon Glassc05694f2013-04-03 11:07:16 +0000928 if better_err:
929 print self.col.Color(self.col.GREEN, '\n'.join(better_err))
Simon Glassbb4dffb2014-08-09 15:33:06 -0600930 self._error_lines += 1
Simon Glassc05694f2013-04-03 11:07:16 +0000931 if worse_err:
932 print self.col.Color(self.col.RED, '\n'.join(worse_err))
Simon Glassbb4dffb2014-08-09 15:33:06 -0600933 self._error_lines += 1
Simon Glassc05694f2013-04-03 11:07:16 +0000934
935 if show_sizes:
936 self.PrintSizeSummary(board_selected, board_dict, show_detail,
937 show_bloat)
938
939 # Save our updated information for the next call to this function
940 self._base_board_dict = board_dict
941 self._base_err_lines = err_lines
942
943 # Get a list of boards that did not get built, if needed
944 not_built = []
945 for board in board_selected:
946 if not board in board_dict:
947 not_built.append(board)
948 if not_built:
949 print "Boards not built (%d): %s" % (len(not_built),
950 ', '.join(not_built))
951
Simon Glasseb48bbc2014-08-09 15:33:02 -0600952 def ProduceResultSummary(self, commit_upto, commits, board_selected):
Simon Glass3394c9f2014-08-28 09:43:43 -0600953 board_dict, err_lines, err_line_boards = self.GetResultSummary(
954 board_selected, commit_upto,
955 read_func_sizes=self._show_bloat)
Simon Glasseb48bbc2014-08-09 15:33:02 -0600956 if commits:
957 msg = '%02d: %s' % (commit_upto + 1,
958 commits[commit_upto].subject)
959 print self.col.Color(self.col.BLUE, msg)
960 self.PrintResultSummary(board_selected, board_dict,
Simon Glass3394c9f2014-08-28 09:43:43 -0600961 err_lines if self._show_errors else [], err_line_boards,
Simon Glasseb48bbc2014-08-09 15:33:02 -0600962 self._show_sizes, self._show_detail, self._show_bloat)
Simon Glassc05694f2013-04-03 11:07:16 +0000963
Simon Glasseb48bbc2014-08-09 15:33:02 -0600964 def ShowSummary(self, commits, board_selected):
Simon Glassc05694f2013-04-03 11:07:16 +0000965 """Show a build summary for U-Boot for a given board list.
966
967 Reset the result summary, then repeatedly call GetResultSummary on
968 each commit's results, then display the differences we see.
969
970 Args:
971 commit: Commit objects to summarise
972 board_selected: Dict containing boards to summarise
Simon Glassc05694f2013-04-03 11:07:16 +0000973 """
Simon Glassd326ad72014-08-09 15:32:59 -0600974 self.commit_count = len(commits) if commits else 1
Simon Glassc05694f2013-04-03 11:07:16 +0000975 self.commits = commits
976 self.ResetResultSummary(board_selected)
Simon Glassbb4dffb2014-08-09 15:33:06 -0600977 self._error_lines = 0
Simon Glassc05694f2013-04-03 11:07:16 +0000978
979 for commit_upto in range(0, self.commit_count, self._step):
Simon Glasseb48bbc2014-08-09 15:33:02 -0600980 self.ProduceResultSummary(commit_upto, commits, board_selected)
Simon Glassbb4dffb2014-08-09 15:33:06 -0600981 if not self._error_lines:
982 print self.col.Color(self.col.GREEN, '(no errors to report)')
Simon Glassc05694f2013-04-03 11:07:16 +0000983
984
985 def SetupBuild(self, board_selected, commits):
986 """Set up ready to start a build.
987
988 Args:
989 board_selected: Selected boards to build
990 commits: Selected commits to build
991 """
992 # First work out how many commits we will build
Simon Glassd326ad72014-08-09 15:32:59 -0600993 count = (self.commit_count + self._step - 1) / self._step
Simon Glassc05694f2013-04-03 11:07:16 +0000994 self.count = len(board_selected) * count
995 self.upto = self.warned = self.fail = 0
996 self._timestamps = collections.deque()
997
Simon Glassc05694f2013-04-03 11:07:16 +0000998 def GetThreadDir(self, thread_num):
999 """Get the directory path to the working dir for a thread.
1000
1001 Args:
1002 thread_num: Number of thread to check.
1003 """
1004 return os.path.join(self._working_dir, '%02d' % thread_num)
1005
Simon Glassd326ad72014-08-09 15:32:59 -06001006 def _PrepareThread(self, thread_num, setup_git):
Simon Glassc05694f2013-04-03 11:07:16 +00001007 """Prepare the working directory for a thread.
1008
1009 This clones or fetches the repo into the thread's work directory.
1010
1011 Args:
1012 thread_num: Thread number (0, 1, ...)
Simon Glassd326ad72014-08-09 15:32:59 -06001013 setup_git: True to set up a git repo clone
Simon Glassc05694f2013-04-03 11:07:16 +00001014 """
1015 thread_dir = self.GetThreadDir(thread_num)
Simon Glass4a1e88b2014-08-09 15:33:00 -06001016 builderthread.Mkdir(thread_dir)
Simon Glassc05694f2013-04-03 11:07:16 +00001017 git_dir = os.path.join(thread_dir, '.git')
1018
1019 # Clone the repo if it doesn't already exist
1020 # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so
1021 # we have a private index but uses the origin repo's contents?
Simon Glassd326ad72014-08-09 15:32:59 -06001022 if setup_git and self.git_dir:
Simon Glassc05694f2013-04-03 11:07:16 +00001023 src_dir = os.path.abspath(self.git_dir)
1024 if os.path.exists(git_dir):
1025 gitutil.Fetch(git_dir, thread_dir)
1026 else:
1027 print 'Cloning repo for thread %d' % thread_num
1028 gitutil.Clone(src_dir, thread_dir)
1029
Simon Glassd326ad72014-08-09 15:32:59 -06001030 def _PrepareWorkingSpace(self, max_threads, setup_git):
Simon Glassc05694f2013-04-03 11:07:16 +00001031 """Prepare the working directory for use.
1032
1033 Set up the git repo for each thread.
1034
1035 Args:
1036 max_threads: Maximum number of threads we expect to need.
Simon Glassd326ad72014-08-09 15:32:59 -06001037 setup_git: True to set up a git repo clone
Simon Glassc05694f2013-04-03 11:07:16 +00001038 """
Simon Glass4a1e88b2014-08-09 15:33:00 -06001039 builderthread.Mkdir(self._working_dir)
Simon Glassc05694f2013-04-03 11:07:16 +00001040 for thread in range(max_threads):
Simon Glassd326ad72014-08-09 15:32:59 -06001041 self._PrepareThread(thread, setup_git)
Simon Glassc05694f2013-04-03 11:07:16 +00001042
1043 def _PrepareOutputSpace(self):
1044 """Get the output directories ready to receive files.
1045
1046 We delete any output directories which look like ones we need to
1047 create. Having left over directories is confusing when the user wants
1048 to check the output manually.
1049 """
1050 dir_list = []
1051 for commit_upto in range(self.commit_count):
1052 dir_list.append(self._GetOutputDir(commit_upto))
1053
1054 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1055 if dirname not in dir_list:
1056 shutil.rmtree(dirname)
1057
Simon Glass78e418e2014-08-09 15:33:03 -06001058 def BuildBoards(self, commits, board_selected, keep_outputs, verbose):
Simon Glassc05694f2013-04-03 11:07:16 +00001059 """Build all commits for a list of boards
1060
1061 Args:
1062 commits: List of commits to be build, each a Commit object
1063 boards_selected: Dict of selected boards, key is target name,
1064 value is Board object
Simon Glassc05694f2013-04-03 11:07:16 +00001065 keep_outputs: True to save build output files
Simon Glass78e418e2014-08-09 15:33:03 -06001066 verbose: Display build results as they are completed
Simon Glassc2f91072014-08-28 09:43:39 -06001067 Returns:
1068 Tuple containing:
1069 - number of boards that failed to build
1070 - number of boards that issued warnings
Simon Glassc05694f2013-04-03 11:07:16 +00001071 """
Simon Glassd326ad72014-08-09 15:32:59 -06001072 self.commit_count = len(commits) if commits else 1
Simon Glassc05694f2013-04-03 11:07:16 +00001073 self.commits = commits
Simon Glass78e418e2014-08-09 15:33:03 -06001074 self._verbose = verbose
Simon Glassc05694f2013-04-03 11:07:16 +00001075
1076 self.ResetResultSummary(board_selected)
Simon Glass4a1e88b2014-08-09 15:33:00 -06001077 builderthread.Mkdir(self.base_dir)
Simon Glassd326ad72014-08-09 15:32:59 -06001078 self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)),
1079 commits is not None)
Simon Glassc05694f2013-04-03 11:07:16 +00001080 self._PrepareOutputSpace()
1081 self.SetupBuild(board_selected, commits)
1082 self.ProcessResult(None)
1083
1084 # Create jobs to build all commits for each board
1085 for brd in board_selected.itervalues():
Simon Glass4a1e88b2014-08-09 15:33:00 -06001086 job = builderthread.BuilderJob()
Simon Glassc05694f2013-04-03 11:07:16 +00001087 job.board = brd
1088 job.commits = commits
1089 job.keep_outputs = keep_outputs
1090 job.step = self._step
1091 self.queue.put(job)
1092
1093 # Wait until all jobs are started
1094 self.queue.join()
1095
1096 # Wait until we have processed all output
1097 self.out_queue.join()
1098 print
1099 self.ClearLine(0)
Simon Glassc2f91072014-08-28 09:43:39 -06001100 return (self.fail, self.warned)