From: Simon Glass <simon.glass@canonical.com> Move the size-display methods from Builder to ResultHandler: - print_size_summary(): Print architecture-level size summaries - print_size_detail(): Print board-level size details - print_func_size_detail(): Print function-level bloat analysis - calc_size_changes(): Calculate size changes across boards - calc_image_size_changes(): Calculate per-image size changes - colour_num(): Format numbers with colour Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- tools/buildman/builder.py | 262 +----------------------------- tools/buildman/main.py | 1 + tools/buildman/resulthandler.py | 280 +++++++++++++++++++++++++++++++- tools/buildman/test_builder.py | 16 +- 4 files changed, 289 insertions(+), 270 deletions(-) diff --git a/tools/buildman/builder.py b/tools/buildman/builder.py index db8d980e83e..4b6106b881b 100644 --- a/tools/buildman/builder.py +++ b/tools/buildman/builder.py @@ -24,6 +24,7 @@ from buildman.cfgutil import Config, process_config from buildman.outcome import (BoardStatus, DisplayOptions, ErrLine, Outcome, OUTCOME_OK, OUTCOME_WARNING, OUTCOME_ERROR, OUTCOME_UNKNOWN) +from buildman.resulthandler import ResultHandler from u_boot_pylib import command from u_boot_pylib import gitutil from u_boot_pylib import terminal @@ -1040,21 +1041,6 @@ class Builder: else: arch_list[arch] += text - - def colour_num(self, num): - """Format a number with colour depending on its value - - Args: - num (int): Number to format - - Returns: - str: Formatted string (red if positive, green if negative/zero) - """ - color = self.col.RED if num > 0 else self.col.GREEN - if num == 0: - return '0' - return self.col.build(color, str(num)) - def reset_result_summary(self, board_selected): """Reset the results summary ready for use. @@ -1078,247 +1064,6 @@ class Builder: self._base_config = None self._base_environment = None - def print_func_size_detail(self, fname, old, new): - """Print detailed size information for each function - - Args: - fname (str): Filename to print (e.g. 'u-boot') - old (dict): Dictionary of old function sizes, keyed by function name - new (dict): Dictionary of new function sizes, keyed by function name - """ - grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0 - delta, common = [], {} - - for a in old: - if a in new: - common[a] = 1 - - for name in old: - if name not in common: - remove += 1 - down += old[name] - delta.append([-old[name], name]) - - for name in new: - if name not in common: - add += 1 - up += new[name] - delta.append([new[name], name]) - - for name in common: - diff = new.get(name, 0) - old.get(name, 0) - if diff > 0: - grow, up = grow + 1, up + diff - elif diff < 0: - shrink, down = shrink + 1, down - diff - delta.append([diff, name]) - - delta.sort() - delta.reverse() - - args = [add, -remove, grow, -shrink, up, -down, up - down] - if max(args) == 0 and min(args) == 0: - return - args = [self.colour_num(x) for x in args] - indent = ' ' * 15 - tprint(f'{indent}{self.col.build(self.col.YELLOW, fname)}: add: ' - f'{args[0]}/{args[1]}, grow: {args[2]}/{args[3]} bytes: ' - f'{args[4]}/{args[5]} ({args[6]})') - tprint(f'{indent} {"function":<38s} {"old":>7s} {"new":>7s} ' - f'{"delta":>7s}') - for diff, name in delta: - if diff: - color = self.col.RED if diff > 0 else self.col.GREEN - msg = (f'{indent} {name:<38s} {old.get(name, "-"):>7} ' - f'{new.get(name, "-"):>7} {diff:+7d}') - tprint(msg, colour=color) - - - def print_size_detail(self, target_list, show_bloat): - """Show details size information for each board - - Args: - target_list (list): List of targets, each a dict containing: - 'target': Target name - 'total_diff': Total difference in bytes across all areas - <part_name>: Difference for that part - show_bloat (bool): Show detail for each function - """ - targets_by_diff = sorted(target_list, reverse=True, - key=lambda x: x['_total_diff']) - for result in targets_by_diff: - printed_target = False - for name in sorted(result): - diff = result[name] - if name.startswith('_'): - continue - colour = self.col.RED if diff > 0 else self.col.GREEN - msg = f' {name} {diff:+d}' - if not printed_target: - tprint(f'{"":10s} {result["_target"]:<15s}:', - newline=False) - printed_target = True - tprint(msg, colour=colour, newline=False) - if printed_target: - tprint() - if show_bloat: - target = result['_target'] - outcome = result['_outcome'] - base_outcome = self._base_board_dict[target] - for fname in outcome.func_sizes: - self.print_func_size_detail(fname, - base_outcome.func_sizes[fname], - outcome.func_sizes[fname]) - - - @staticmethod - def _calc_image_size_changes(target, sizes, base_sizes): - """Calculate size changes for each image/part - - Args: - target (str): Target board name - sizes (dict): Dict of image sizes, keyed by image name - base_sizes (dict): Dict of base image sizes, keyed by image name - - Returns: - dict: Size changes, e.g.: - {'_target': 'snapper9g45', 'data': 5, 'u-boot-spl:text': -4} - meaning U-Boot data increased by 5 bytes, SPL text decreased - by 4 - """ - err = {'_target' : target} - for image in sizes: - if image in base_sizes: - base_image = base_sizes[image] - # Loop through the text, data, bss parts - for part in sorted(sizes[image]): - diff = sizes[image][part] - base_image[part] - if diff: - if image == 'u-boot': - name = part - else: - name = image + ':' + part - err[name] = diff - return err - - def _calc_size_changes(self, board_selected, board_dict): - """Calculate changes in size for different image parts - - The previous sizes are in Board.sizes, for each board - - Args: - board_selected (dict): Dict containing boards to summarise, keyed - by board.target - board_dict (dict): Dict containing boards for which we built this - commit, keyed by board.target. The value is an Outcome object. - - Returns: - tuple: (arch_list, arch_count) where: - arch_list: dict keyed by arch name, containing a list of - size-change dicts - arch_count: dict keyed by arch name, containing the number of - boards for that arch - """ - arch_list = {} - arch_count = {} - for target in board_dict: - if target not in board_selected: - continue - base_sizes = self._base_board_dict[target].sizes - outcome = board_dict[target] - sizes = outcome.sizes - err = self._calc_image_size_changes(target, sizes, base_sizes) - arch = board_selected[target].arch - if not arch in arch_count: - arch_count[arch] = 1 - else: - arch_count[arch] += 1 - if not sizes: - pass # Only add to our list when we have some stats - elif not arch in arch_list: - arch_list[arch] = [err] - else: - arch_list[arch].append(err) - return arch_list, arch_count - - def print_size_summary(self, board_selected, board_dict, show_detail, - show_bloat): - """Print a summary of image sizes broken down by section. - - The summary takes the form of one line per architecture. The - line contains deltas for each of the sections (+ means the section - got bigger, - means smaller). The numbers are the average number - of bytes that a board in this section increased by. - - For example: - powerpc: (622 boards) text -0.0 - arm: (285 boards) text -0.0 - - Args: - board_selected (dict): Dict containing boards to summarise, keyed - by board.target - board_dict (dict): Dict containing boards for which we built this - commit, keyed by board.target. The value is an Outcome object. - show_detail (bool): Show size delta detail for each board - show_bloat (bool): Show detail for each function - """ - arch_list, arch_count = self._calc_size_changes(board_selected, - board_dict) - - # We now have a list of image size changes sorted by arch - # Print out a summary of these - for arch, target_list in arch_list.items(): - # Get total difference for each type - totals = {} - for result in target_list: - total = 0 - for name, diff in result.items(): - if name.startswith('_'): - continue - total += diff - if name in totals: - totals[name] += diff - else: - totals[name] = diff - result['_total_diff'] = total - result['_outcome'] = board_dict[result['_target']] - - self._print_arch_size_summary(arch, target_list, arch_count, - totals, show_detail, show_bloat) - - def _print_arch_size_summary(self, arch, target_list, arch_count, totals, - show_detail, show_bloat): - """Print size summary for a single architecture - - Args: - arch (str): Architecture name - target_list (list): List of size-change dicts for this arch - arch_count (dict): Dict of arch name to board count - totals (dict): Dict of name to total size diff - show_detail (bool): Show size delta detail for each board - show_bloat (bool): Show detail for each function - """ - count = len(target_list) - printed_arch = False - for name in sorted(totals): - diff = totals[name] - if diff: - # Display the average difference in this name for this - # architecture - avg_diff = float(diff) / count - color = self.col.RED if avg_diff > 0 else self.col.GREEN - msg = f' {name} {avg_diff:+1.1f}' - if not printed_arch: - tprint(f'{arch:>10s}: (for {count}/{arch_count[arch]} ' - 'boards)', newline=False) - printed_arch = True - tprint(msg, colour=color, newline=False) - - if printed_arch: - tprint() - if show_detail: - self.print_size_detail(target_list, show_bloat) - def _classify_boards(self, board_selected, board_dict): """Classify boards into outcome categories @@ -1789,8 +1534,9 @@ class Builder: worse_err, better_warn, worse_warn) if show_sizes: - self.print_size_summary(board_selected, board_dict, show_detail, - show_bloat) + self._result_handler.print_size_summary( + board_selected, board_dict, self._base_board_dict, + show_detail, show_bloat) if show_environment and self._base_environment: self._show_environment_changes(board_selected, board_dict, diff --git a/tools/buildman/main.py b/tools/buildman/main.py index c8502c370f9..af289a46508 100755 --- a/tools/buildman/main.py +++ b/tools/buildman/main.py @@ -79,6 +79,7 @@ def run_test_coverage(): 'tools/buildman/builderthread.py', 'tools/buildman/cfgutil.py', 'tools/buildman/control.py', + 'tools/buildman/resulthandler.py', 'tools/buildman/toolchain.py']) diff --git a/tools/buildman/resulthandler.py b/tools/buildman/resulthandler.py index 86f95f3eea5..4c9b3c2f9cc 100644 --- a/tools/buildman/resulthandler.py +++ b/tools/buildman/resulthandler.py @@ -1,14 +1,23 @@ # SPDX-License-Identifier: GPL-2.0+ # Copyright (c) 2013 The Chromium OS Authors. +# +# Bloat-o-meter code used here Copyright 2004 Matt Mackall <mpm@selenic.com> +# -"""Result handler for buildman build results""" +"""Result writer for buildman build results""" + +from u_boot_pylib.terminal import tprint class ResultHandler: - """Handles display of build results and summaries + """Handles display of build size results and summaries + + This class is responsible for displaying size information from builds, + including per-architecture summaries, per-board details, and per-function + bloat analysis. - This class is responsible for displaying build results, including - size information, errors, warnings, and configuration changes. + Attributes: + col: terminal.Color object for coloured output """ def __init__(self, col, opts): @@ -29,3 +38,266 @@ class ResultHandler: builder (Builder): Builder object to use for getting results """ self._builder = builder + + def colour_num(self, num): + """Format a number with colour depending on its value + + Args: + num (int): Number to format + + Returns: + str: Formatted string (red if positive, green if negative/zero) + """ + color = self._col.RED if num > 0 else self._col.GREEN + if num == 0: + return '0' + return self._col.build(color, str(num)) + + def print_func_size_detail(self, fname, old, new): + """Print detailed size information for each function + + Args: + fname (str): Filename to print (e.g. 'u-boot') + old (dict): Dictionary of old function sizes, keyed by function name + new (dict): Dictionary of new function sizes, keyed by function name + """ + grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0 + delta, common = [], {} + + for a in old: + if a in new: + common[a] = 1 + + for name in old: + if name not in common: + remove += 1 + down += old[name] + delta.append([-old[name], name]) + + for name in new: + if name not in common: + add += 1 + up += new[name] + delta.append([new[name], name]) + + for name in common: + diff = new.get(name, 0) - old.get(name, 0) + if diff > 0: + grow, up = grow + 1, up + diff + elif diff < 0: + shrink, down = shrink + 1, down - diff + delta.append([diff, name]) + + delta.sort() + delta.reverse() + + args = [add, -remove, grow, -shrink, up, -down, up - down] + if max(args) == 0 and min(args) == 0: + return + args = [self.colour_num(x) for x in args] + indent = ' ' * 15 + tprint(f'{indent}{self._col.build(self._col.YELLOW, fname)}: add: ' + f'{args[0]}/{args[1]}, grow: {args[2]}/{args[3]} bytes: ' + f'{args[4]}/{args[5]} ({args[6]})') + tprint(f'{indent} {"function":<38s} {"old":>7s} {"new":>7s} ' + f'{"delta":>7s}') + for diff, name in delta: + if diff: + color = self._col.RED if diff > 0 else self._col.GREEN + msg = (f'{indent} {name:<38s} {old.get(name, "-"):>7} ' + f'{new.get(name, "-"):>7} {diff:+7d}') + tprint(msg, colour=color) + + def print_size_detail(self, target_list, base_board_dict, board_dict, + show_bloat): + """Show detailed size information for each board + + Args: + target_list (list): List of targets, each a dict containing: + 'target': Target name + 'total_diff': Total difference in bytes across all areas + <part_name>: Difference for that part + base_board_dict (dict): Dict of base board outcomes + board_dict (dict): Dict of current board outcomes + show_bloat (bool): Show detail for each function + """ + targets_by_diff = sorted(target_list, reverse=True, + key=lambda x: x['_total_diff']) + for result in targets_by_diff: + printed_target = False + for name in sorted(result): + diff = result[name] + if name.startswith('_'): + continue + colour = self._col.RED if diff > 0 else self._col.GREEN + msg = f' {name} {diff:+d}' + if not printed_target: + tprint(f'{"":10s} {result["_target"]:<15s}:', + newline=False) + printed_target = True + tprint(msg, colour=colour, newline=False) + if printed_target: + tprint() + if show_bloat: + target = result['_target'] + outcome = board_dict[target] + base_outcome = base_board_dict[target] + for fname in outcome.func_sizes: + self.print_func_size_detail(fname, + base_outcome.func_sizes[fname], + outcome.func_sizes[fname]) + + @staticmethod + def calc_image_size_changes(target, sizes, base_sizes): + """Calculate size changes for each image/part + + Args: + target (str): Target board name + sizes (dict): Dict of image sizes, keyed by image name + base_sizes (dict): Dict of base image sizes, keyed by image name + + Returns: + dict: Size changes, e.g.: + {'_target': 'snapper9g45', 'data': 5, 'u-boot-spl:text': -4} + meaning U-Boot data increased by 5 bytes, SPL text decreased + by 4 + """ + err = {'_target' : target} + for image in sizes: + if image in base_sizes: + base_image = base_sizes[image] + # Loop through the text, data, bss parts + for part in sorted(sizes[image]): + diff = sizes[image][part] - base_image[part] + if diff: + if image == 'u-boot': + name = part + else: + name = image + ':' + part + err[name] = diff + return err + + def calc_size_changes(self, board_selected, board_dict, base_board_dict): + """Calculate changes in size for different image parts + + The previous sizes are in Board.sizes, for each board + + Args: + board_selected (dict): Dict containing boards to summarise, keyed + by board.target + board_dict (dict): Dict containing boards for which we built this + commit, keyed by board.target. The value is an Outcome object. + base_board_dict (dict): Dict of base board outcomes + + Returns: + tuple: (arch_list, arch_count) where: + arch_list: dict keyed by arch name, containing a list of + size-change dicts + arch_count: dict keyed by arch name, containing the number of + boards for that arch + """ + arch_list = {} + arch_count = {} + for target in board_dict: + if target not in board_selected: + continue + base_sizes = base_board_dict[target].sizes + outcome = board_dict[target] + sizes = outcome.sizes + err = self.calc_image_size_changes(target, sizes, base_sizes) + arch = board_selected[target].arch + if not arch in arch_count: + arch_count[arch] = 1 + else: + arch_count[arch] += 1 + if not sizes: + pass # Only add to our list when we have some stats + elif not arch in arch_list: + arch_list[arch] = [err] + else: + arch_list[arch].append(err) + return arch_list, arch_count + + def print_size_summary(self, board_selected, board_dict, base_board_dict, + show_detail, show_bloat): + """Print a summary of image sizes broken down by section. + + The summary takes the form of one line per architecture. The + line contains deltas for each of the sections (+ means the section + got bigger, - means smaller). The numbers are the average number + of bytes that a board in this section increased by. + + For example: + powerpc: (622 boards) text -0.0 + arm: (285 boards) text -0.0 + + Args: + board_selected (dict): Dict containing boards to summarise, keyed + by board.target + board_dict (dict): Dict containing boards for which we built this + commit, keyed by board.target. The value is an Outcome object. + base_board_dict (dict): Dict of base board outcomes + show_detail (bool): Show size delta detail for each board + show_bloat (bool): Show detail for each function + """ + arch_list, arch_count = self.calc_size_changes(board_selected, + board_dict, + base_board_dict) + + # We now have a list of image size changes sorted by arch + # Print out a summary of these + for arch, target_list in arch_list.items(): + # Get total difference for each type + totals = {} + for result in target_list: + total = 0 + for name, diff in result.items(): + if name.startswith('_'): + continue + total += diff + if name in totals: + totals[name] += diff + else: + totals[name] = diff + result['_total_diff'] = total + + self._print_arch_size_summary(arch, target_list, arch_count, + totals, base_board_dict, board_dict, + show_detail, show_bloat) + + def _print_arch_size_summary(self, arch, target_list, arch_count, totals, + base_board_dict, board_dict, + show_detail, show_bloat): + """Print size summary for a single architecture + + Args: + arch (str): Architecture name + target_list (list): List of size-change dicts for this arch + arch_count (dict): Dict of arch name to board count + totals (dict): Dict of name to total size diff + base_board_dict (dict): Dict of base board outcomes + board_dict (dict): Dict of current board outcomes + show_detail (bool): Show size delta detail for each board + show_bloat (bool): Show detail for each function + """ + count = len(target_list) + printed_arch = False + for name in sorted(totals): + diff = totals[name] + if diff: + # Display the average difference in this name for this + # architecture + avg_diff = float(diff) / count + color = self._col.RED if avg_diff > 0 else self._col.GREEN + msg = f' {name} {avg_diff:+1.1f}' + if not printed_arch: + tprint(f'{arch:>10s}: (for {count}/{arch_count[arch]} ' + 'boards)', newline=False) + printed_arch = True + tprint(msg, colour=color, newline=False) + + if printed_arch: + tprint() + if show_detail: + self.print_size_detail(target_list, base_board_dict, board_dict, + show_bloat) diff --git a/tools/buildman/test_builder.py b/tools/buildman/test_builder.py index 40132c1b46f..69e9e324c53 100644 --- a/tools/buildman/test_builder.py +++ b/tools/buildman/test_builder.py @@ -20,7 +20,7 @@ from u_boot_pylib import terminal class TestPrintFuncSizeDetail(unittest.TestCase): - """Tests for Builder.print_func_size_detail()""" + """Tests for ResultHandler.print_func_size_detail()""" def setUp(self): """Set up test fixtures""" @@ -46,7 +46,7 @@ class TestPrintFuncSizeDetail(unittest.TestCase): new = {'func_a': 100, 'func_b': 200} terminal.get_print_test_lines() # Clear - self.builder.print_func_size_detail('u-boot', old, new) + self.result_handler.print_func_size_detail('u-boot', old, new) lines = terminal.get_print_test_lines() # No output when there are no changes @@ -58,7 +58,7 @@ class TestPrintFuncSizeDetail(unittest.TestCase): new = {'func_a': 150} terminal.get_print_test_lines() # Clear - self.builder.print_func_size_detail('u-boot', old, new) + self.result_handler.print_func_size_detail('u-boot', old, new) lines = terminal.get_print_test_lines() text = '\n'.join(line.text for line in lines) @@ -76,7 +76,7 @@ class TestPrintFuncSizeDetail(unittest.TestCase): new = {'func_a': 150} terminal.get_print_test_lines() # Clear - self.builder.print_func_size_detail('u-boot', old, new) + self.result_handler.print_func_size_detail('u-boot', old, new) lines = terminal.get_print_test_lines() text = '\n'.join(line.text for line in lines) @@ -89,7 +89,7 @@ class TestPrintFuncSizeDetail(unittest.TestCase): new = {'func_a': 100, 'func_b': 200} terminal.get_print_test_lines() # Clear - self.builder.print_func_size_detail('u-boot', old, new) + self.result_handler.print_func_size_detail('u-boot', old, new) lines = terminal.get_print_test_lines() text = '\n'.join(line.text for line in lines) @@ -105,7 +105,7 @@ class TestPrintFuncSizeDetail(unittest.TestCase): new = {'func_a': 100} terminal.get_print_test_lines() # Clear - self.builder.print_func_size_detail('u-boot', old, new) + self.result_handler.print_func_size_detail('u-boot', old, new) lines = terminal.get_print_test_lines() text = '\n'.join(line.text for line in lines) @@ -129,7 +129,7 @@ class TestPrintFuncSizeDetail(unittest.TestCase): } terminal.get_print_test_lines() # Clear - self.builder.print_func_size_detail('u-boot', old, new) + self.result_handler.print_func_size_detail('u-boot', old, new) lines = terminal.get_print_test_lines() text = '\n'.join(line.text for line in lines) @@ -148,7 +148,7 @@ class TestPrintFuncSizeDetail(unittest.TestCase): def test_empty_dicts(self): """Test with empty dictionaries""" terminal.get_print_test_lines() # Clear - self.builder.print_func_size_detail('u-boot', {}, {}) + self.result_handler.print_func_size_detail('u-boot', {}, {}) lines = terminal.get_print_test_lines() # No output when both dicts are empty -- 2.43.0