| #!/usr/bin/env python3 |
| # vim: set syntax=python ts=4 : |
| # |
| # Copyright (c) 2018 Intel Corporation |
| # SPDX-License-Identifier: Apache-2.0 |
| |
| import subprocess |
| import sys |
| import os |
| import re |
| import typing |
| import logging |
| from twisterlib.error import TwisterRuntimeError |
| |
| logger = logging.getLogger('twister') |
| logger.setLevel(logging.DEBUG) |
| |
| class SizeCalculator: |
| alloc_sections = [ |
| "bss", |
| "noinit", |
| "app_bss", |
| "app_noinit", |
| "ccm_bss", |
| "ccm_noinit" |
| ] |
| |
| rw_sections = [ |
| "datas", |
| "initlevel", |
| "exceptions", |
| "_static_thread_data_area", |
| "k_timer_area", |
| "k_mem_slab_area", |
| "sw_isr_table", |
| "k_sem_area", |
| "k_mutex_area", |
| "app_shmem_regions", |
| "_k_fifo_area", |
| "_k_lifo_area", |
| "k_stack_area", |
| "k_msgq_area", |
| "k_mbox_area", |
| "k_pipe_area", |
| "net_if_area", |
| "net_if_dev_area", |
| "net_l2_area", |
| "net_l2_data", |
| "k_queue_area", |
| "_net_buf_pool_area", |
| "app_datas", |
| "kobject_data", |
| "mmu_tables", |
| "app_pad", |
| "priv_stacks", |
| "ccm_data", |
| "usb_descriptor", |
| "usb_data", "usb_bos_desc", |
| "uart_mux", |
| 'log_backends_sections', |
| 'log_dynamic_sections', |
| 'log_const_sections', |
| "app_smem", |
| 'shell_root_cmds_sections', |
| 'log_const_sections', |
| "priv_stacks_noinit", |
| "_GCOV_BSS_SECTION_NAME", |
| "gcov", |
| "nocache", |
| "devices", |
| "k_heap_area", |
| ] |
| |
| # These get copied into RAM only on non-XIP |
| ro_sections = [ |
| "rom_start", |
| "text", |
| "ctors", |
| "init_array", |
| "reset", |
| "z_object_assignment_area", |
| "rodata", |
| "net_l2", |
| "vector", |
| "sw_isr_table", |
| "settings_handler_static_area", |
| "bt_l2cap_fixed_chan_area", |
| "bt_l2cap_br_fixed_chan_area", |
| "bt_gatt_service_static_area", |
| "vectors", |
| "net_socket_register_area", |
| "net_ppp_proto", |
| "shell_area", |
| "tracing_backend_area", |
| "ppp_protocol_handler_area", |
| ] |
| |
| # Variable below is stored for calculating size using build.log |
| USEFUL_LINES_AMOUNT = 4 |
| |
| def __init__(self, elf_filename: str,\ |
| extra_sections: typing.List[str],\ |
| buildlog_filepath: str = '',\ |
| generate_warning: bool = True): |
| """Constructor |
| |
| @param elf_filename (str) Path to the output binary |
| parsed by objdump to determine section sizes. |
| @param extra_sections (list[str]) List of extra, |
| unexpected sections, which Twister should not |
| report as error and not include in the |
| size calculation. |
| @param buildlog_filepath (str, default: '') Path to the |
| output build.log |
| @param generate_warning (bool, default: True) Twister should |
| (or not) warning about missing the information about |
| footprint. |
| """ |
| self.elf_filename = elf_filename |
| self.buildlog_filename = buildlog_filepath |
| self.sections = [] |
| self.used_rom = 0 |
| self.used_ram = 0 |
| self.available_ram = 0 |
| self.available_rom = 0 |
| self.extra_sections = extra_sections |
| self.is_xip = True |
| self.generate_warning = generate_warning |
| |
| self._calculate_sizes() |
| |
| def size_report(self): |
| print(self.elf_filename) |
| print("SECTION NAME VMA LMA SIZE HEX SZ TYPE") |
| for v in self.sections: |
| print("%-17s 0x%08x 0x%08x %8d 0x%05x %-7s" % |
| (v["name"], v["virt_addr"], v["load_addr"], v["size"], v["size"], |
| v["type"])) |
| |
| print("Totals: %d bytes (ROM), %d bytes (RAM)" % |
| (self.used_rom, self.used_ram)) |
| print("") |
| |
| def get_used_ram(self): |
| """Get the amount of RAM the application will use up on the device |
| |
| @return amount of RAM, in bytes |
| """ |
| return self.used_ram |
| |
| def get_used_rom(self): |
| """Get the size of the data that this application uses on device's flash |
| |
| @return amount of ROM, in bytes |
| """ |
| return self.used_rom |
| |
| def unrecognized_sections(self): |
| """Get a list of sections inside the binary that weren't recognized |
| |
| @return list of unrecognized section names |
| """ |
| slist = [] |
| for v in self.sections: |
| if not v["recognized"]: |
| slist.append(v["name"]) |
| return slist |
| |
| def get_available_ram(self) -> int: |
| """Get the total available RAM. |
| |
| @return total available RAM, in bytes (int) |
| """ |
| return self.available_ram |
| |
| def get_available_rom(self) -> int: |
| """Get the total available ROM. |
| |
| @return total available ROM, in bytes (int) |
| """ |
| return self.available_rom |
| |
| def _calculate_sizes(self): |
| """ELF file is analyzed, even if the option to read memory |
| footprint from the build.log file is set. |
| This is to detect potential problems contained in |
| unrecognized sections of the file. |
| """ |
| self._analyze_elf_file() |
| if self.buildlog_filename.endswith("build.log"): |
| self._get_footprint_from_buildlog() |
| |
| def _check_elf_file(self) -> None: |
| # Make sure this is an ELF binary |
| with open(self.elf_filename, "rb") as f: |
| magic = f.read(4) |
| |
| try: |
| if magic != b'\x7fELF': |
| raise TwisterRuntimeError("%s is not an ELF binary" % self.elf_filename) |
| except Exception as e: |
| print(str(e)) |
| sys.exit(2) |
| |
| def _check_is_xip(self) -> None: |
| # Search for CONFIG_XIP in the ELF's list of symbols using NM and AWK. |
| # GREP can not be used as it returns an error if the symbol is not |
| # found. |
| is_xip_command = "nm " + self.elf_filename + \ |
| " | awk '/CONFIG_XIP/ { print $3 }'" |
| is_xip_output = subprocess.check_output( |
| is_xip_command, shell=True, stderr=subprocess.STDOUT).decode( |
| "utf-8").strip() |
| try: |
| if is_xip_output.endswith("no symbols"): |
| raise TwisterRuntimeError("%s has no symbol information" % self.elf_filename) |
| except Exception as e: |
| print(str(e)) |
| sys.exit(2) |
| |
| self.is_xip = len(is_xip_output) != 0 |
| |
| def _get_info_elf_sections(self) -> None: |
| """Calculate RAM and ROM usage and information about issues by section""" |
| objdump_command = "objdump -h " + self.elf_filename |
| objdump_output = subprocess.check_output( |
| objdump_command, shell=True).decode("utf-8").splitlines() |
| |
| for line in objdump_output: |
| words = line.split() |
| |
| if not words: # Skip lines that are too short |
| continue |
| |
| index = words[0] |
| if not index[0].isdigit(): # Skip lines that do not start |
| continue # with a digit |
| |
| name = words[1] # Skip lines with section names |
| if name[0] == '.': # starting with '.' |
| continue |
| |
| # TODO this doesn't actually reflect the size in flash or RAM as |
| # it doesn't include linker-imposed padding between sections. |
| # It is close though. |
| size = int(words[2], 16) |
| if size == 0: |
| continue |
| |
| load_addr = int(words[4], 16) |
| virt_addr = int(words[3], 16) |
| |
| # Add section to memory use totals (for both non-XIP and XIP scenarios) |
| # Unrecognized section names are not included in the calculations. |
| recognized = True |
| |
| # If build.log file exists, check errors (unrecognized sections |
| # in ELF file). |
| if self.buildlog_filename: |
| if name in SizeCalculator.alloc_sections or\ |
| SizeCalculator.rw_sections or\ |
| SizeCalculator.ro_sections: |
| continue |
| else: |
| stype = "unknown" |
| if name not in self.extra_sections: |
| recognized = False |
| else: |
| if name in SizeCalculator.alloc_sections: |
| self.used_ram += size |
| stype = "alloc" |
| elif name in SizeCalculator.rw_sections: |
| self.used_ram += size |
| self.used_rom += size |
| stype = "rw" |
| elif name in SizeCalculator.ro_sections: |
| self.used_rom += size |
| if not self.is_xip: |
| self.used_ram += size |
| stype = "ro" |
| else: |
| stype = "unknown" |
| if name not in self.extra_sections: |
| recognized = False |
| |
| self.sections.append({"name": name, "load_addr": load_addr, |
| "size": size, "virt_addr": virt_addr, |
| "type": stype, "recognized": recognized}) |
| |
| def _analyze_elf_file(self) -> None: |
| self._check_elf_file() |
| self._check_is_xip() |
| self._get_info_elf_sections() |
| |
| def _get_buildlog_file_content(self) -> typing.List[str]: |
| """Get content of the build.log file. |
| |
| @return Content of the build.log file (list[str]) |
| """ |
| if os.path.exists(path=self.buildlog_filename): |
| with open(file=self.buildlog_filename, mode='r') as file: |
| file_content = file.readlines() |
| else: |
| if self.generate_warning: |
| logger.error(msg=f"Incorrect path to build.log file to analyze footprints. Please check the path {self.buildlog_filename}.") |
| file_content = [] |
| return file_content |
| |
| def _find_offset_of_last_pattern_occurrence(self, file_content: typing.List[str]) -> int: |
| """Find the offset from which the information about the memory footprint is read. |
| |
| @param file_content (list[str]) Content of build.log. |
| @return Offset with information about the memory footprint (int) |
| """ |
| result = -1 |
| if len(file_content) == 0: |
| logger.warning("Build.log file is empty.") |
| else: |
| # Pattern to first line with information about memory footprint |
| PATTERN_SEARCHED_LINE = "Memory region" |
| # Check the file in reverse order. |
| for idx, line in enumerate(reversed(file_content)): |
| # Condition is fulfilled if the pattern matches with the start of the line. |
| if re.match(pattern=PATTERN_SEARCHED_LINE, string=line): |
| result = idx + 1 |
| break |
| # If the file does not contain information about memory footprint, the warning is raised. |
| if result == -1: |
| logger.warning(msg=f"Information about memory footprint for this test configuration is not found. Please check file {self.buildlog_filename}.") |
| return result |
| |
| def _get_lines_with_footprint(self, start_offset: int, file_content: typing.List[str]) -> typing.List[str]: |
| """Get lines from the file with a memory footprint. |
| |
| @param start_offset (int) Offset with the first line of the information about memory footprint. |
| @param file_content (list[str]) Content of the build.log file. |
| @return Lines with information about memory footprint (list[str]) |
| """ |
| if len(file_content) == 0: |
| result = [] |
| else: |
| if start_offset > len(file_content) or start_offset <= 0: |
| info_line_idx_start = len(file_content) - 1 |
| else: |
| info_line_idx_start = len(file_content) - start_offset |
| |
| info_line_idx_stop = info_line_idx_start + self.USEFUL_LINES_AMOUNT |
| if info_line_idx_stop > len(file_content): |
| info_line_idx_stop = len(file_content) |
| |
| result = file_content[info_line_idx_start:info_line_idx_stop] |
| return result |
| |
| def _clear_whitespaces_from_lines(self, text_lines: typing.List[str]) -> typing.List[str]: |
| """Clear text lines from whitespaces. |
| |
| @param text_lines (list[str]) Lines with useful information. |
| @return Cleared text lines with information (list[str]) |
| """ |
| return [line.strip("\n").rstrip("%") for line in text_lines] if text_lines else [] |
| |
| def _divide_text_lines_into_columns(self, text_lines: typing.List[str]) -> typing.List[typing.List[str]]: |
| """Divide lines of text into columns. |
| |
| @param lines (list[list[str]]) Lines with information about memory footprint. |
| @return Lines divided into columns (list[list[str]]) |
| """ |
| if text_lines: |
| result = [] |
| PATTERN_SPLIT_COLUMNS = " +" |
| for line in text_lines: |
| line = [column.rstrip(":") for column in re.split(pattern=PATTERN_SPLIT_COLUMNS, string=line)] |
| result.append(list(filter(None, line))) |
| else: |
| result = [[]] |
| |
| return result |
| |
| def _unify_prefixes_on_all_values(self, data_lines: typing.List[typing.List[str]]) -> typing.List[typing.List[str]]: |
| """Convert all values in the table to unified order of magnitude. |
| |
| @param data_lines (list[list[str]]) Lines with information about memory footprint. |
| @return Lines with unified values (list[list[str]]) |
| """ |
| if len(data_lines) != self.USEFUL_LINES_AMOUNT: |
| data_lines = [[]] |
| if self.generate_warning: |
| logger.warning(msg=f"Incomplete information about memory footprint. Please check file {self.buildlog_filename}") |
| else: |
| for idx, line in enumerate(data_lines): |
| # Line with description of the columns |
| if idx == 0: |
| continue |
| line_to_replace = list(map(self._binary_prefix_converter, line)) |
| data_lines[idx] = line_to_replace |
| |
| return data_lines |
| |
| def _binary_prefix_converter(self, value: str) -> str: |
| """Convert specific value to particular prefix. |
| |
| @param value (str) Value to convert. |
| @return Converted value to output prefix (str) |
| """ |
| PATTERN_VALUE = r"([0-9]?\s.?B\Z)" |
| |
| if not re.search(pattern=PATTERN_VALUE, string=value): |
| converted_value = value.rstrip() |
| else: |
| PREFIX_POWER = {'B': 0, 'KB': 10, 'MB': 20, 'GB': 30} |
| DEFAULT_DATA_PREFIX = 'B' |
| |
| data_parts = value.split() |
| numeric_value = int(data_parts[0]) |
| unit = data_parts[1] |
| shift = PREFIX_POWER.get(unit, 0) - PREFIX_POWER.get(DEFAULT_DATA_PREFIX, 0) |
| unit_predictor = pow(2, shift) |
| converted_value = str(numeric_value * unit_predictor) |
| return converted_value |
| |
| def _create_data_table(self) -> typing.List[typing.List[str]]: |
| """Create table with information about memory footprint. |
| |
| @return Table with information about memory usage (list[list[str]]) |
| """ |
| file_content = self._get_buildlog_file_content() |
| data_line_start_idx = self._find_offset_of_last_pattern_occurrence(file_content=file_content) |
| |
| if data_line_start_idx < 0: |
| data_from_content = [[]] |
| else: |
| # Clean lines and separate information to columns |
| information_lines = self._get_lines_with_footprint(start_offset=data_line_start_idx, file_content=file_content) |
| information_lines = self._clear_whitespaces_from_lines(text_lines=information_lines) |
| data_from_content = self._divide_text_lines_into_columns(text_lines=information_lines) |
| data_from_content = self._unify_prefixes_on_all_values(data_lines=data_from_content) |
| |
| return data_from_content |
| |
| def _get_footprint_from_buildlog(self) -> None: |
| """Get memory footprint from build.log""" |
| data_from_file = self._create_data_table() |
| |
| if data_from_file == [[]] or not data_from_file: |
| self.used_ram = 0 |
| self.used_rom = 0 |
| self.available_ram = 0 |
| self.available_rom = 0 |
| if self.generate_warning: |
| logger.warning(msg=f"Missing information about memory footprint. Check file {self.buildlog_filename}.") |
| else: |
| ROW_RAM_IDX = 2 |
| ROW_ROM_IDX = 1 |
| COLUMN_USED_SIZE_IDX = 1 |
| COLUMN_AVAILABLE_SIZE_IDX = 2 |
| |
| self.used_ram = int(data_from_file[ROW_RAM_IDX][COLUMN_USED_SIZE_IDX]) |
| self.used_rom = int(data_from_file[ROW_ROM_IDX][COLUMN_USED_SIZE_IDX]) |
| self.available_ram = int(data_from_file[ROW_RAM_IDX][COLUMN_AVAILABLE_SIZE_IDX]) |
| self.available_rom = int(data_from_file[ROW_ROM_IDX][COLUMN_AVAILABLE_SIZE_IDX]) |