| # Copyright 2022 The Pigweed Authors |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| # use this file except in compliance with the License. You may obtain a copy of |
| # the License at |
| # |
| # https://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| # License for the specific language governing permissions and limitations under |
| # the License. |
| """Module for size report ASCII tables from DataSourceMaps.""" |
| |
| import enum |
| from typing import ( |
| Iterable, |
| Tuple, |
| Union, |
| Type, |
| List, |
| Optional, |
| NamedTuple, |
| cast, |
| ) |
| |
| from pw_bloat.label import DataSourceMap, DiffDataSourceMap, Label |
| |
| |
| class AsciiCharset(enum.Enum): |
| """Set of ASCII characters for drawing tables.""" |
| |
| TL = '+' |
| TM = '+' |
| TR = '+' |
| ML = '+' |
| MM = '+' |
| MR = '+' |
| BL = '+' |
| BM = '+' |
| BR = '+' |
| V = '|' |
| H = '-' |
| HH = '=' |
| |
| |
| class LineCharset(enum.Enum): |
| """Set of line-drawing characters for tables.""" |
| |
| TL = '┌' |
| TM = '┬' |
| TR = '┐' |
| ML = '├' |
| MM = '┼' |
| MR = '┤' |
| BL = '└' |
| BM = '┴' |
| BR = '┘' |
| V = '│' |
| H = '─' |
| HH = '═' |
| |
| |
| class _Align(enum.Enum): |
| CENTER = 0 |
| LEFT = 1 |
| RIGHT = 2 |
| |
| |
| def get_label_status(curr_label: Label) -> str: |
| if curr_label.is_new(): |
| return 'NEW' |
| if curr_label.is_del(): |
| return 'DEL' |
| return '' |
| |
| |
| def diff_sign_sizes(size: int, diff_mode: bool) -> str: |
| if diff_mode: |
| size_sign = '+' if size > 0 else '' |
| return f"{size_sign}{size:,}" |
| return f"{size:,}" |
| |
| |
| class BloatTableOutput: |
| """ASCII Table generator from DataSourceMap.""" |
| |
| _RST_PADDING_WIDTH = 6 |
| _DEFAULT_MAX_WIDTH = 80 |
| |
| class _LabelContent(NamedTuple): |
| name: str |
| size: int |
| label_status: str |
| |
| def __init__( |
| self, |
| ds_map: Union[DiffDataSourceMap, DataSourceMap], |
| col_max_width: int = _DEFAULT_MAX_WIDTH, |
| charset: Union[Type[AsciiCharset], Type[LineCharset]] = AsciiCharset, |
| rst_output: bool = False, |
| diff_label: Optional[str] = None, |
| ): |
| self._data_source_map = ds_map |
| self._cs = charset |
| self._total_size = 0 |
| col_names = [*self._data_source_map.get_ds_names(), 'sizes'] |
| self._diff_mode = False |
| self._diff_label = diff_label |
| if isinstance(self._data_source_map, DiffDataSourceMap): |
| col_names = ['diff', *col_names] |
| self._diff_mode = True |
| self._col_names = col_names |
| self._additional_padding = 0 |
| self._ascii_table_rows: List[str] = [] |
| self._rst_output = rst_output |
| self._total_divider = self._cs.HH.value |
| if self._rst_output: |
| self._total_divider = self._cs.H.value |
| self._additional_padding = self._RST_PADDING_WIDTH |
| |
| self._col_widths = self._generate_col_width(col_max_width) |
| |
| def _generate_col_width(self, col_max_width: int) -> List[int]: |
| """Find column width for all data sources and sizes.""" |
| max_len_size = 0 |
| diff_len_col_width = 0 |
| |
| col_list = [ |
| len(ds_name) for ds_name in self._data_source_map.get_ds_names() |
| ] |
| for curr_label in self._data_source_map.labels(): |
| self._total_size += curr_label.size |
| max_len_size = max( |
| len(diff_sign_sizes(self._total_size, self._diff_mode)), |
| len(diff_sign_sizes(curr_label.size, self._diff_mode)), |
| max_len_size, |
| ) |
| for index, parent_label in enumerate( |
| [*curr_label.parents, curr_label.name] |
| ): |
| if len(parent_label) > col_max_width: |
| col_list[index] = col_max_width |
| elif len(parent_label) > col_list[index]: |
| col_list[index] = len(parent_label) |
| |
| diff_same = 0 |
| if self._diff_mode: |
| col_list = [len('Total'), *col_list] |
| diff_same = len('(SAME)') |
| col_list.append(max(max_len_size, len('sizes'), diff_same)) |
| |
| if self._diff_label is not None: |
| sum_all_col_names = sum(col_list) |
| if sum_all_col_names < len(self._diff_label): |
| diff_len_col_width = ( |
| len(self._diff_label) - sum_all_col_names |
| ) // len(self._col_names) |
| |
| return [ |
| (x + self._additional_padding + diff_len_col_width) |
| for x in col_list |
| ] |
| |
| def _diff_label_names( |
| self, |
| old_labels: Optional[Tuple[_LabelContent, ...]], |
| new_labels: Tuple[_LabelContent, ...], |
| ) -> Tuple[_LabelContent, ...]: |
| """Return difference between arrays of labels.""" |
| |
| if old_labels is None: |
| return new_labels |
| diff_list = [] |
| for new_lb, old_lb in zip(new_labels, old_labels): |
| if (new_lb.name == old_lb.name) and (new_lb.size == old_lb.size): |
| diff_list.append(self._LabelContent('', 0, '')) |
| else: |
| diff_list.append(new_lb) |
| |
| return tuple(diff_list) |
| |
| def _label_title_row(self) -> List[str]: |
| label_rows = [] |
| label_cells = '' |
| divider_cells = '' |
| for width in self._col_widths: |
| label_cells += ' ' * width + ' ' |
| divider_cells += (self._cs.H.value * width) + self._cs.H.value |
| if self._diff_label is not None: |
| label_cells = self._diff_label.center(len(label_cells[:-1]), ' ') |
| label_rows.extend( |
| [ |
| f"{self._cs.TL.value}{divider_cells[:-1]}{self._cs.TR.value}", |
| f"{self._cs.V.value}{label_cells}{self._cs.V.value}", |
| f"{self._cs.ML.value}{divider_cells[:-1]}{self._cs.MR.value}", |
| ] |
| ) |
| return label_rows |
| |
| def create_table(self) -> str: |
| """Parse DataSourceMap to create ASCII table.""" |
| curr_lb_hierarchy = None |
| last_diff_name = '' |
| if self._diff_mode: |
| self._ascii_table_rows.extend([*self._label_title_row()]) |
| else: |
| self._ascii_table_rows.extend( |
| [self._create_border(True, self._cs.H.value)] |
| ) |
| self._ascii_table_rows.extend([*self._create_title_row()]) |
| |
| has_entries = False |
| |
| for curr_label in self._data_source_map.labels(): |
| if curr_label.size == 0: |
| continue |
| |
| has_entries = True |
| |
| new_lb_hierarchy = tuple( |
| [ |
| *self._get_ds_label_size(curr_label.parents), |
| self._LabelContent( |
| curr_label.name, |
| curr_label.size, |
| get_label_status(curr_label), |
| ), |
| ] |
| ) |
| diff_list = self._diff_label_names( |
| curr_lb_hierarchy, new_lb_hierarchy |
| ) |
| curr_lb_hierarchy = new_lb_hierarchy |
| |
| if curr_label.parents and curr_label.parents[0] == last_diff_name: |
| continue |
| if ( |
| self._diff_mode |
| and diff_list[0].name |
| and ( |
| not cast( |
| DiffDataSourceMap, self._data_source_map |
| ).has_diff_sublabels(diff_list[0].name) |
| ) |
| ): |
| if (len(self._ascii_table_rows) > 5) and ( |
| self._ascii_table_rows[-1][0] != '+' |
| ): |
| self._ascii_table_rows.append( |
| self._row_divider( |
| len(self._col_names), self._cs.H.value |
| ) |
| ) |
| self._ascii_table_rows.append( |
| self._create_same_label_row(1, diff_list[0].name) |
| ) |
| |
| last_diff_name = curr_label.parents[0] |
| else: |
| self._ascii_table_rows += self._create_diff_rows(diff_list) |
| |
| if self._rst_output and self._ascii_table_rows[-1][0] == '+': |
| self._ascii_table_rows.pop() |
| |
| self._ascii_table_rows.extend( |
| [*self._create_total_row(is_empty=not has_entries)] |
| ) |
| |
| return '\n'.join(self._ascii_table_rows) + '\n' |
| |
| def _create_same_label_row(self, col_index: int, label: str) -> str: |
| label_row = '' |
| for col in range(len(self._col_names) - 1): |
| if col == col_index: |
| curr_cell = self._create_cell(label, False, col, _Align.LEFT) |
| else: |
| curr_cell = self._create_cell('', False, col) |
| label_row += curr_cell |
| label_row += self._create_cell( |
| "(SAME)", True, len(self._col_widths) - 1, _Align.RIGHT |
| ) |
| return label_row |
| |
| def _get_ds_label_size( |
| self, parent_labels: Tuple[str, ...] |
| ) -> Iterable[_LabelContent]: |
| """Produce label, size pairs from parent label names.""" |
| parent_label_sizes = [] |
| for index, target_label in enumerate(parent_labels): |
| for curr_label in self._data_source_map.labels(index): |
| if curr_label.name == target_label: |
| diff_label = get_label_status(curr_label) |
| parent_label_sizes.append( |
| self._LabelContent( |
| curr_label.name, curr_label.size, diff_label |
| ) |
| ) |
| break |
| return parent_label_sizes |
| |
| def _create_total_row(self, is_empty: bool = False) -> Iterable[str]: |
| complete_total_rows = [] |
| |
| if self._diff_mode and is_empty: |
| # When diffing two identical binaries, output a row indicating that |
| # the two are the same. |
| no_diff_row = '' |
| for i in range(len(self._col_names)): |
| if i == 0: |
| no_diff_row += self._create_cell( |
| 'N/A', False, i, _Align.CENTER |
| ) |
| elif i == len(self._col_names) - 1: |
| no_diff_row += self._create_cell('0', True, i) |
| else: |
| no_diff_row += self._create_cell( |
| '(same)', False, i, _Align.CENTER |
| ) |
| complete_total_rows.append(no_diff_row) |
| |
| complete_total_rows.append( |
| self._row_divider(len(self._col_names), self._total_divider) |
| ) |
| total_row = '' |
| |
| for i in range(len(self._col_names)): |
| if i == 0: |
| total_row += self._create_cell('Total', False, i, _Align.LEFT) |
| elif i == len(self._col_names) - 1: |
| total_size_str = diff_sign_sizes( |
| self._total_size, self._diff_mode |
| ) |
| total_row += self._create_cell(total_size_str, True, i) |
| else: |
| total_row += self._create_cell('', False, i, _Align.CENTER) |
| |
| complete_total_rows.extend( |
| [total_row, self._create_border(False, self._cs.H.value)] |
| ) |
| return complete_total_rows |
| |
| def _create_diff_rows( |
| self, diff_list: Tuple[_LabelContent, ...] |
| ) -> Iterable[str]: |
| """Create rows for each label according to its index in diff_list.""" |
| curr_row = '' |
| diff_index = 0 |
| diff_rows = [] |
| for index, label_content in enumerate(diff_list): |
| if label_content.name: |
| if self._diff_mode: |
| curr_row += self._create_cell( |
| label_content.label_status, False, 0 |
| ) |
| diff_index = 1 |
| for cell_index in range( |
| diff_index, len(diff_list) + diff_index |
| ): |
| if cell_index == index + diff_index: |
| if ( |
| cell_index == diff_index |
| and len(self._ascii_table_rows) > 5 |
| and not self._rst_output |
| ): |
| diff_rows.append( |
| self._row_divider( |
| len(self._col_names), self._cs.H.value |
| ) |
| ) |
| if ( |
| len(label_content.name) + self._additional_padding |
| ) > self._col_widths[cell_index]: |
| curr_row = self._multi_row_label( |
| label_content.name, cell_index |
| ) |
| break |
| curr_row += self._create_cell( |
| label_content.name, False, cell_index, _Align.LEFT |
| ) |
| else: |
| curr_row += self._create_cell('', False, cell_index) |
| |
| # Add size end of current row. |
| curr_size = diff_sign_sizes(label_content.size, self._diff_mode) |
| curr_row += self._create_cell( |
| curr_size, True, len(self._col_widths) - 1, _Align.RIGHT |
| ) |
| diff_rows.append(curr_row) |
| if self._rst_output: |
| diff_rows.append( |
| self._row_divider( |
| len(self._col_names), self._cs.H.value |
| ) |
| ) |
| curr_row = '' |
| |
| return diff_rows |
| |
| def _create_cell( |
| self, |
| content: str, |
| last_cell: bool, |
| col_index: int, |
| align: Optional[_Align] = _Align.RIGHT, |
| ) -> str: |
| v_border = self._cs.V.value |
| if self._rst_output and content: |
| content = content.replace('_', '\\_') |
| pad_diff = self._col_widths[col_index] - len(content) |
| padding = (pad_diff // 2) * ' ' |
| odd_pad = ' ' if pad_diff % 2 == 1 else '' |
| string_cell = '' |
| |
| if align == _Align.CENTER: |
| string_cell = f'{v_border}{odd_pad}{padding}{content}{padding}' |
| elif align == _Align.LEFT: |
| string_cell = f'{v_border}{content}{padding*2}{odd_pad}' |
| elif align == _Align.RIGHT: |
| string_cell = f'{v_border}{padding*2}{odd_pad}{content}' |
| |
| if last_cell: |
| string_cell += self._cs.V.value |
| return string_cell |
| |
| def _multi_row_label(self, content: str, target_col_index: int) -> str: |
| """Split content name into multiple rows within correct column.""" |
| max_len = self._col_widths[target_col_index] - self._additional_padding |
| split_content = '...'.join( |
| content[max_len:][i : i + max_len - 3] |
| for i in range(0, len(content[max_len:]), max_len - 3) |
| ) |
| split_content = f"{content[:max_len]}...{split_content}" |
| split_tab_content = [ |
| split_content[i : i + max_len] |
| for i in range(0, len(split_content), max_len) |
| ] |
| multi_label = [] |
| curr_row = '' |
| for index, cut_content in enumerate(split_tab_content): |
| last_cell = False |
| for blank_cell_index in range(len(self._col_names)): |
| if blank_cell_index == target_col_index: |
| curr_row += self._create_cell( |
| cut_content, False, target_col_index, _Align.LEFT |
| ) |
| else: |
| if blank_cell_index == len(self._col_names) - 1: |
| if index == len(split_tab_content) - 1: |
| break |
| last_cell = True |
| curr_row += self._create_cell( |
| '', last_cell, blank_cell_index |
| ) |
| multi_label.append(curr_row) |
| curr_row = '' |
| |
| return '\n'.join(multi_label) |
| |
| def _row_divider(self, col_num: int, h_div: str) -> str: |
| l_border = '' |
| r_border = '' |
| row_div = '' |
| for col in range(col_num): |
| if col == 0: |
| l_border = self._cs.ML.value |
| r_border = '' |
| elif col == (col_num - 1): |
| l_border = self._cs.MM.value |
| r_border = self._cs.MR.value |
| else: |
| l_border = self._cs.MM.value |
| r_border = '' |
| |
| row_div += f"{l_border}{self._col_widths[col] * h_div}{r_border}" |
| return row_div |
| |
| def _create_title_row(self) -> Iterable[str]: |
| title_rows = [] |
| title_cells = '' |
| last_cell = False |
| for index, curr_name in enumerate(self._col_names): |
| if index == len(self._col_names) - 1: |
| last_cell = True |
| title_cells += self._create_cell( |
| curr_name, last_cell, index, _Align.CENTER |
| ) |
| title_rows.extend( |
| [ |
| title_cells, |
| self._row_divider(len(self._col_names), self._cs.HH.value), |
| ] |
| ) |
| return title_rows |
| |
| def _create_border(self, top: bool, h_div: str): |
| """Top or bottom borders of ASCII table.""" |
| row_div = '' |
| for col in range(len(self._col_names)): |
| if top: |
| if col == 0: |
| l_div = self._cs.TL.value |
| r_div = '' |
| elif col == (len(self._col_names) - 1): |
| l_div = self._cs.TM.value |
| r_div = self._cs.TR.value |
| else: |
| l_div = self._cs.TM.value |
| r_div = '' |
| else: |
| if col == 0: |
| l_div = self._cs.BL.value |
| r_div = '' |
| elif col == (len(self._col_names) - 1): |
| l_div = self._cs.BM.value |
| r_div = self._cs.BR.value |
| else: |
| l_div = self._cs.BM.value |
| r_div = '' |
| |
| row_div += f"{l_div}{self._col_widths[col] * h_div}{r_div}" |
| return row_div |
| |
| |
| class RstOutput: |
| """Tabular output in ASCII format, which is also valid RST.""" |
| |
| def __init__( |
| self, ds_map: DataSourceMap, table_label: Optional[str] = None |
| ): |
| self._data_source_map = ds_map |
| self._table_label = table_label |
| self._diff_mode = False |
| if isinstance(self._data_source_map, DiffDataSourceMap): |
| self._diff_mode = True |
| |
| def create_table(self) -> str: |
| """Initializes RST table and builds first row.""" |
| table_builder = [ |
| '\n.. list-table::', |
| ' :widths: auto', |
| ' :header-rows: 1\n', |
| ] |
| header_cols = ['Label', 'Segment', 'Delta'] |
| for i, col_name in enumerate(header_cols): |
| list_space = '*' if i == 0 else ' ' |
| table_builder.append(f" {list_space} - {col_name}") |
| |
| return '\n'.join(table_builder) + f'\n{self.add_report_row()}\n' |
| |
| def _label_status_unchanged(self, parent_lb_name: str) -> bool: |
| """Determines if parent label has no status change in diff mode.""" |
| for curr_lb in self._data_source_map.labels(): |
| if curr_lb.size != 0: |
| if ( |
| curr_lb.parents and (parent_lb_name == curr_lb.parents[0]) |
| ) or (curr_lb.name == parent_lb_name): |
| if get_label_status(curr_lb) != '': |
| return False |
| return True |
| |
| def add_report_row(self) -> str: |
| """Add in new size report row with Label, Segment, and Delta. |
| |
| Returns: |
| RST string that is the current row with a full symbols |
| table breakdown of the corresponding segment. |
| """ |
| table_rows = [] |
| curr_row = [] |
| curr_label_name = '' |
| for parent_lb in self._data_source_map.labels(0): |
| if parent_lb.size != 0: |
| if (self._table_label is not None) and ( |
| curr_label_name != self._table_label |
| ): |
| curr_row.append(f' * - {self._table_label} ') |
| curr_label_name = self._table_label |
| else: |
| curr_row.append(' * -') |
| curr_row.extend( |
| [ |
| f' - .. dropdown:: {parent_lb.name}', |
| ' :animate: fade-in\n', |
| ' .. list-table::', |
| ' :widths: auto\n', |
| ] |
| ) |
| if self._label_status_unchanged(parent_lb.name): |
| skip_status = 1, '*' |
| else: |
| skip_status = 0, ' ' |
| for curr_lb in self._data_source_map.labels(): |
| if (curr_lb.size != 0) and ( |
| ( |
| curr_lb.parents |
| and (parent_lb.name == curr_lb.parents[0]) |
| ) |
| or (curr_lb.name == parent_lb.name) |
| ): |
| sign_size = diff_sign_sizes( |
| curr_lb.size, self._diff_mode |
| ) |
| curr_status = get_label_status(curr_lb) |
| curr_name = curr_lb.name.replace('_', '\\_') |
| to_extend = [ |
| f' * - {curr_status}', |
| f' {skip_status[1]} - {sign_size}', |
| f' - {curr_name}\n', |
| ][skip_status[0] :] |
| curr_row.extend(to_extend) |
| curr_row.append( |
| f' - {diff_sign_sizes(parent_lb.size, self._diff_mode)}' |
| ) |
| table_rows.extend(curr_row) |
| curr_row = [] |
| |
| # No size difference. |
| if len(table_rows) == 0: |
| table_rows.extend( |
| [f'\n * - {self._table_label}', ' - (ALL)', ' - 0'] |
| ) |
| |
| return '\n'.join(table_rows) |