|  | #!/usr/bin/env python3 | 
|  |  | 
|  | """ | 
|  | This script is for comparing the size of the library files from two | 
|  | different Git revisions within an Mbed TLS repository. | 
|  | The results of the comparison is formatted as csv and stored at a | 
|  | configurable location. | 
|  | Note: must be run from Mbed TLS root. | 
|  | """ | 
|  |  | 
|  | # Copyright The Mbed TLS Contributors | 
|  | # SPDX-License-Identifier: Apache-2.0 | 
|  | # | 
|  | # 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 | 
|  | # | 
|  | # http://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. | 
|  |  | 
|  | import argparse | 
|  | import logging | 
|  | import os | 
|  | import re | 
|  | import shutil | 
|  | import subprocess | 
|  | import sys | 
|  | import typing | 
|  | from enum import Enum | 
|  |  | 
|  | from mbedtls_dev import build_tree | 
|  | from mbedtls_dev import logging_util | 
|  | from mbedtls_dev import typing_util | 
|  |  | 
|  | class SupportedArch(Enum): | 
|  | """Supported architecture for code size measurement.""" | 
|  | AARCH64 = 'aarch64' | 
|  | AARCH32 = 'aarch32' | 
|  | ARMV8_M = 'armv8-m' | 
|  | X86_64 = 'x86_64' | 
|  | X86 = 'x86' | 
|  |  | 
|  |  | 
|  | class SupportedConfig(Enum): | 
|  | """Supported configuration for code size measurement.""" | 
|  | DEFAULT = 'default' | 
|  | TFM_MEDIUM = 'tfm-medium' | 
|  |  | 
|  |  | 
|  | # Static library | 
|  | MBEDTLS_STATIC_LIB = { | 
|  | 'CRYPTO': 'library/libmbedcrypto.a', | 
|  | 'X509': 'library/libmbedx509.a', | 
|  | 'TLS': 'library/libmbedtls.a', | 
|  | } | 
|  |  | 
|  | class CodeSizeDistinctInfo: # pylint: disable=too-few-public-methods | 
|  | """Data structure to store possibly distinct information for code size | 
|  | comparison.""" | 
|  | def __init__( #pylint: disable=too-many-arguments | 
|  | self, | 
|  | version: str, | 
|  | git_rev: str, | 
|  | arch: str, | 
|  | config: str, | 
|  | compiler: str, | 
|  | opt_level: str, | 
|  | ) -> None: | 
|  | """ | 
|  | :param: version: which version to compare with for code size. | 
|  | :param: git_rev: Git revision to calculate code size. | 
|  | :param: arch: architecture to measure code size on. | 
|  | :param: config: Configuration type to calculate code size. | 
|  | (See SupportedConfig) | 
|  | :param: compiler: compiler used to build library/*.o. | 
|  | :param: opt_level: Options that control optimization. (E.g. -Os) | 
|  | """ | 
|  | self.version = version | 
|  | self.git_rev = git_rev | 
|  | self.arch = arch | 
|  | self.config = config | 
|  | self.compiler = compiler | 
|  | self.opt_level = opt_level | 
|  | # Note: Variables below are not initialized by class instantiation. | 
|  | self.pre_make_cmd = [] #type: typing.List[str] | 
|  | self.make_cmd = '' | 
|  |  | 
|  | def get_info_indication(self): | 
|  | """Return a unique string to indicate Code Size Distinct Information.""" | 
|  | return '{git_rev}-{arch}-{config}-{compiler}'.format(**self.__dict__) | 
|  |  | 
|  |  | 
|  | class CodeSizeCommonInfo: # pylint: disable=too-few-public-methods | 
|  | """Data structure to store common information for code size comparison.""" | 
|  | def __init__( | 
|  | self, | 
|  | host_arch: str, | 
|  | measure_cmd: str, | 
|  | ) -> None: | 
|  | """ | 
|  | :param host_arch: host architecture. | 
|  | :param measure_cmd: command to measure code size for library/*.o. | 
|  | """ | 
|  | self.host_arch = host_arch | 
|  | self.measure_cmd = measure_cmd | 
|  |  | 
|  | def get_info_indication(self): | 
|  | """Return a unique string to indicate Code Size Common Information.""" | 
|  | return '{measure_tool}'\ | 
|  | .format(measure_tool=self.measure_cmd.strip().split(' ')[0]) | 
|  |  | 
|  | class CodeSizeResultInfo: # pylint: disable=too-few-public-methods | 
|  | """Data structure to store result options for code size comparison.""" | 
|  | def __init__( #pylint: disable=too-many-arguments | 
|  | self, | 
|  | record_dir: str, | 
|  | comp_dir: str, | 
|  | with_markdown=False, | 
|  | stdout=False, | 
|  | show_all=False, | 
|  | ) -> None: | 
|  | """ | 
|  | :param record_dir: directory to store code size record. | 
|  | :param comp_dir: directory to store results of code size comparision. | 
|  | :param with_markdown: write comparision result into a markdown table. | 
|  | (Default: False) | 
|  | :param stdout: direct comparison result into sys.stdout. | 
|  | (Default False) | 
|  | :param show_all: show all objects in comparison result. (Default False) | 
|  | """ | 
|  | self.record_dir = record_dir | 
|  | self.comp_dir = comp_dir | 
|  | self.with_markdown = with_markdown | 
|  | self.stdout = stdout | 
|  | self.show_all = show_all | 
|  |  | 
|  |  | 
|  | DETECT_ARCH_CMD = "cc -dM -E - < /dev/null" | 
|  | def detect_arch() -> str: | 
|  | """Auto-detect host architecture.""" | 
|  | cc_output = subprocess.check_output(DETECT_ARCH_CMD, shell=True).decode() | 
|  | if '__aarch64__' in cc_output: | 
|  | return SupportedArch.AARCH64.value | 
|  | if '__arm__' in cc_output: | 
|  | return SupportedArch.AARCH32.value | 
|  | if '__x86_64__' in cc_output: | 
|  | return SupportedArch.X86_64.value | 
|  | if '__i386__' in cc_output: | 
|  | return SupportedArch.X86.value | 
|  | else: | 
|  | print("Unknown host architecture, cannot auto-detect arch.") | 
|  | sys.exit(1) | 
|  |  | 
|  | TFM_MEDIUM_CONFIG_H = 'configs/tfm_mbedcrypto_config_profile_medium.h' | 
|  | TFM_MEDIUM_CRYPTO_CONFIG_H = 'configs/crypto_config_profile_medium.h' | 
|  |  | 
|  | CONFIG_H = 'include/mbedtls/mbedtls_config.h' | 
|  | CRYPTO_CONFIG_H = 'include/psa/crypto_config.h' | 
|  | BACKUP_SUFFIX = '.code_size.bak' | 
|  |  | 
|  | class CodeSizeBuildInfo: # pylint: disable=too-few-public-methods | 
|  | """Gather information used to measure code size. | 
|  |  | 
|  | It collects information about architecture, configuration in order to | 
|  | infer build command for code size measurement. | 
|  | """ | 
|  |  | 
|  | SupportedArchConfig = [ | 
|  | '-a ' + SupportedArch.AARCH64.value + ' -c ' + SupportedConfig.DEFAULT.value, | 
|  | '-a ' + SupportedArch.AARCH32.value + ' -c ' + SupportedConfig.DEFAULT.value, | 
|  | '-a ' + SupportedArch.X86_64.value  + ' -c ' + SupportedConfig.DEFAULT.value, | 
|  | '-a ' + SupportedArch.X86.value     + ' -c ' + SupportedConfig.DEFAULT.value, | 
|  | '-a ' + SupportedArch.ARMV8_M.value + ' -c ' + SupportedConfig.TFM_MEDIUM.value, | 
|  | ] | 
|  |  | 
|  | def __init__( | 
|  | self, | 
|  | size_dist_info: CodeSizeDistinctInfo, | 
|  | host_arch: str, | 
|  | logger: logging.Logger, | 
|  | ) -> None: | 
|  | """ | 
|  | :param size_dist_info: | 
|  | CodeSizeDistinctInfo containing info for code size measurement. | 
|  | - size_dist_info.arch: architecture to measure code size on. | 
|  | - size_dist_info.config: configuration type to measure | 
|  | code size with. | 
|  | - size_dist_info.compiler: compiler used to build library/*.o. | 
|  | - size_dist_info.opt_level: Options that control optimization. | 
|  | (E.g. -Os) | 
|  | :param host_arch: host architecture. | 
|  | :param logger: logging module | 
|  | """ | 
|  | self.arch = size_dist_info.arch | 
|  | self.config = size_dist_info.config | 
|  | self.compiler = size_dist_info.compiler | 
|  | self.opt_level = size_dist_info.opt_level | 
|  |  | 
|  | self.make_cmd = ['make', '-j', 'lib'] | 
|  |  | 
|  | self.host_arch = host_arch | 
|  | self.logger = logger | 
|  |  | 
|  | def check_correctness(self) -> bool: | 
|  | """Check whether we are using proper / supported combination | 
|  | of information to build library/*.o.""" | 
|  |  | 
|  | # default config | 
|  | if self.config == SupportedConfig.DEFAULT.value and \ | 
|  | self.arch == self.host_arch: | 
|  | return True | 
|  | # TF-M | 
|  | elif self.arch == SupportedArch.ARMV8_M.value and \ | 
|  | self.config == SupportedConfig.TFM_MEDIUM.value: | 
|  | return True | 
|  |  | 
|  | return False | 
|  |  | 
|  | def infer_pre_make_command(self) -> typing.List[str]: | 
|  | """Infer command to set up proper configuration before running make.""" | 
|  | pre_make_cmd = [] #type: typing.List[str] | 
|  | if self.config == SupportedConfig.TFM_MEDIUM.value: | 
|  | pre_make_cmd.append('cp {src} {dest}' | 
|  | .format(src=TFM_MEDIUM_CONFIG_H, dest=CONFIG_H)) | 
|  | pre_make_cmd.append('cp {src} {dest}' | 
|  | .format(src=TFM_MEDIUM_CRYPTO_CONFIG_H, | 
|  | dest=CRYPTO_CONFIG_H)) | 
|  |  | 
|  | return pre_make_cmd | 
|  |  | 
|  | def infer_make_cflags(self) -> str: | 
|  | """Infer CFLAGS by instance attributes in CodeSizeDistinctInfo.""" | 
|  | cflags = [] #type: typing.List[str] | 
|  |  | 
|  | # set optimization level | 
|  | cflags.append(self.opt_level) | 
|  | # set compiler by config | 
|  | if self.config == SupportedConfig.TFM_MEDIUM.value: | 
|  | self.compiler = 'armclang' | 
|  | cflags.append('-mcpu=cortex-m33') | 
|  | # set target | 
|  | if self.compiler == 'armclang': | 
|  | cflags.append('--target=arm-arm-none-eabi') | 
|  |  | 
|  | return ' '.join(cflags) | 
|  |  | 
|  | def infer_make_command(self) -> str: | 
|  | """Infer make command by CFLAGS and CC.""" | 
|  |  | 
|  | if self.check_correctness(): | 
|  | # set CFLAGS= | 
|  | self.make_cmd.append('CFLAGS=\'{}\''.format(self.infer_make_cflags())) | 
|  | # set CC= | 
|  | self.make_cmd.append('CC={}'.format(self.compiler)) | 
|  | return ' '.join(self.make_cmd) | 
|  | else: | 
|  | self.logger.error("Unsupported combination of architecture: {} " \ | 
|  | "and configuration: {}.\n" | 
|  | .format(self.arch, | 
|  | self.config)) | 
|  | self.logger.error("Please use supported combination of " \ | 
|  | "architecture and configuration:") | 
|  | for comb in CodeSizeBuildInfo.SupportedArchConfig: | 
|  | self.logger.error(comb) | 
|  | self.logger.error("") | 
|  | self.logger.error("For your system, please use:") | 
|  | for comb in CodeSizeBuildInfo.SupportedArchConfig: | 
|  | if "default" in comb and self.host_arch not in comb: | 
|  | continue | 
|  | self.logger.error(comb) | 
|  | sys.exit(1) | 
|  |  | 
|  |  | 
|  | class CodeSizeCalculator: | 
|  | """ A calculator to calculate code size of library/*.o based on | 
|  | Git revision and code size measurement tool. | 
|  | """ | 
|  |  | 
|  | def __init__( #pylint: disable=too-many-arguments | 
|  | self, | 
|  | git_rev: str, | 
|  | pre_make_cmd: typing.List[str], | 
|  | make_cmd: str, | 
|  | measure_cmd: str, | 
|  | logger: logging.Logger, | 
|  | ) -> None: | 
|  | """ | 
|  | :param git_rev: Git revision. (E.g: commit) | 
|  | :param pre_make_cmd: command to set up proper config before running make. | 
|  | :param make_cmd: command to build library/*.o. | 
|  | :param measure_cmd: command to measure code size for library/*.o. | 
|  | :param logger: logging module | 
|  | """ | 
|  | self.repo_path = "." | 
|  | self.git_command = "git" | 
|  | self.make_clean = 'make clean' | 
|  |  | 
|  | self.git_rev = git_rev | 
|  | self.pre_make_cmd = pre_make_cmd | 
|  | self.make_cmd = make_cmd | 
|  | self.measure_cmd = measure_cmd | 
|  | self.logger = logger | 
|  |  | 
|  | @staticmethod | 
|  | def validate_git_revision(git_rev: str) -> str: | 
|  | result = subprocess.check_output(["git", "rev-parse", "--verify", | 
|  | git_rev + "^{commit}"], | 
|  | shell=False, universal_newlines=True) | 
|  | return result[:7] | 
|  |  | 
|  | def _create_git_worktree(self) -> str: | 
|  | """Create a separate worktree for Git revision. | 
|  | If Git revision is current, use current worktree instead.""" | 
|  |  | 
|  | if self.git_rev == 'current': | 
|  | self.logger.debug("Using current work directory.") | 
|  | git_worktree_path = self.repo_path | 
|  | else: | 
|  | self.logger.debug("Creating git worktree for {}." | 
|  | .format(self.git_rev)) | 
|  | git_worktree_path = os.path.join(self.repo_path, | 
|  | "temp-" + self.git_rev) | 
|  | subprocess.check_output( | 
|  | [self.git_command, "worktree", "add", "--detach", | 
|  | git_worktree_path, self.git_rev], cwd=self.repo_path, | 
|  | stderr=subprocess.STDOUT | 
|  | ) | 
|  |  | 
|  | return git_worktree_path | 
|  |  | 
|  | @staticmethod | 
|  | def backup_config_files(restore: bool) -> None: | 
|  | """Backup / Restore config files.""" | 
|  | if restore: | 
|  | shutil.move(CONFIG_H + BACKUP_SUFFIX, CONFIG_H) | 
|  | shutil.move(CRYPTO_CONFIG_H + BACKUP_SUFFIX, CRYPTO_CONFIG_H) | 
|  | else: | 
|  | shutil.copy(CONFIG_H, CONFIG_H + BACKUP_SUFFIX) | 
|  | shutil.copy(CRYPTO_CONFIG_H, CRYPTO_CONFIG_H + BACKUP_SUFFIX) | 
|  |  | 
|  | def _build_libraries(self, git_worktree_path: str) -> None: | 
|  | """Build library/*.o in the specified worktree.""" | 
|  |  | 
|  | self.logger.debug("Building library/*.o for {}." | 
|  | .format(self.git_rev)) | 
|  | my_environment = os.environ.copy() | 
|  | try: | 
|  | if self.git_rev == 'current': | 
|  | self.backup_config_files(restore=False) | 
|  | for pre_cmd in self.pre_make_cmd: | 
|  | subprocess.check_output( | 
|  | pre_cmd, env=my_environment, shell=True, | 
|  | cwd=git_worktree_path, stderr=subprocess.STDOUT, | 
|  | universal_newlines=True | 
|  | ) | 
|  | subprocess.check_output( | 
|  | self.make_clean, env=my_environment, shell=True, | 
|  | cwd=git_worktree_path, stderr=subprocess.STDOUT, | 
|  | universal_newlines=True | 
|  | ) | 
|  | subprocess.check_output( | 
|  | self.make_cmd, env=my_environment, shell=True, | 
|  | cwd=git_worktree_path, stderr=subprocess.STDOUT, | 
|  | universal_newlines=True | 
|  | ) | 
|  | if self.git_rev == 'current': | 
|  | self.backup_config_files(restore=True) | 
|  | except subprocess.CalledProcessError as e: | 
|  | self._handle_called_process_error(e, git_worktree_path) | 
|  |  | 
|  | def _gen_raw_code_size(self, git_worktree_path: str) -> typing.Dict[str, str]: | 
|  | """Measure code size by a tool and return in UTF-8 encoding.""" | 
|  |  | 
|  | self.logger.debug("Measuring code size for {} by `{}`." | 
|  | .format(self.git_rev, | 
|  | self.measure_cmd.strip().split(' ')[0])) | 
|  |  | 
|  | res = {} | 
|  | for mod, st_lib in MBEDTLS_STATIC_LIB.items(): | 
|  | try: | 
|  | result = subprocess.check_output( | 
|  | [self.measure_cmd + ' ' + st_lib], cwd=git_worktree_path, | 
|  | shell=True, universal_newlines=True | 
|  | ) | 
|  | res[mod] = result | 
|  | except subprocess.CalledProcessError as e: | 
|  | self._handle_called_process_error(e, git_worktree_path) | 
|  |  | 
|  | return res | 
|  |  | 
|  | def _remove_worktree(self, git_worktree_path: str) -> None: | 
|  | """Remove temporary worktree.""" | 
|  | if git_worktree_path != self.repo_path: | 
|  | self.logger.debug("Removing temporary worktree {}." | 
|  | .format(git_worktree_path)) | 
|  | subprocess.check_output( | 
|  | [self.git_command, "worktree", "remove", "--force", | 
|  | git_worktree_path], cwd=self.repo_path, | 
|  | stderr=subprocess.STDOUT | 
|  | ) | 
|  |  | 
|  | def _handle_called_process_error(self, e: subprocess.CalledProcessError, | 
|  | git_worktree_path: str) -> None: | 
|  | """Handle a CalledProcessError and quit the program gracefully. | 
|  | Remove any extra worktrees so that the script may be called again.""" | 
|  |  | 
|  | # Tell the user what went wrong | 
|  | self.logger.error(e, exc_info=True) | 
|  | self.logger.error("Process output:\n {}".format(e.output)) | 
|  |  | 
|  | # Quit gracefully by removing the existing worktree | 
|  | self._remove_worktree(git_worktree_path) | 
|  | sys.exit(-1) | 
|  |  | 
|  | def cal_libraries_code_size(self) -> typing.Dict[str, str]: | 
|  | """Do a complete round to calculate code size of library/*.o | 
|  | by measurement tool. | 
|  |  | 
|  | :return A dictionary of measured code size | 
|  | - typing.Dict[mod: str] | 
|  | """ | 
|  |  | 
|  | git_worktree_path = self._create_git_worktree() | 
|  | try: | 
|  | self._build_libraries(git_worktree_path) | 
|  | res = self._gen_raw_code_size(git_worktree_path) | 
|  | finally: | 
|  | self._remove_worktree(git_worktree_path) | 
|  |  | 
|  | return res | 
|  |  | 
|  |  | 
|  | class CodeSizeGenerator: | 
|  | """ A generator based on size measurement tool for library/*.o. | 
|  |  | 
|  | This is an abstract class. To use it, derive a class that implements | 
|  | write_record and write_comparison methods, then call both of them with | 
|  | proper arguments. | 
|  | """ | 
|  | def __init__(self, logger: logging.Logger) -> None: | 
|  | """ | 
|  | :param logger: logging module | 
|  | """ | 
|  | self.logger = logger | 
|  |  | 
|  | def write_record( | 
|  | self, | 
|  | git_rev: str, | 
|  | code_size_text: typing.Dict[str, str], | 
|  | output: typing_util.Writable | 
|  | ) -> None: | 
|  | """Write size record into a file. | 
|  |  | 
|  | :param git_rev: Git revision. (E.g: commit) | 
|  | :param code_size_text: | 
|  | string output (utf-8) from measurement tool of code size. | 
|  | - typing.Dict[mod: str] | 
|  | :param output: output stream which the code size record is written to. | 
|  | (Note: Normally write code size record into File) | 
|  | """ | 
|  | raise NotImplementedError | 
|  |  | 
|  | def write_comparison( #pylint: disable=too-many-arguments | 
|  | self, | 
|  | old_rev: str, | 
|  | new_rev: str, | 
|  | output: typing_util.Writable, | 
|  | with_markdown=False, | 
|  | show_all=False | 
|  | ) -> None: | 
|  | """Write a comparision result into a stream between two Git revisions. | 
|  |  | 
|  | :param old_rev: old Git revision to compared with. | 
|  | :param new_rev: new Git revision to compared with. | 
|  | :param output: output stream which the code size record is written to. | 
|  | (File / sys.stdout) | 
|  | :param with_markdown:  write comparision result in a markdown table. | 
|  | (Default: False) | 
|  | :param show_all: show all objects in comparison result. (Default False) | 
|  | """ | 
|  | raise NotImplementedError | 
|  |  | 
|  |  | 
|  | class CodeSizeGeneratorWithSize(CodeSizeGenerator): | 
|  | """Code Size Base Class for size record saving and writing.""" | 
|  |  | 
|  | class SizeEntry: # pylint: disable=too-few-public-methods | 
|  | """Data Structure to only store information of code size.""" | 
|  | def __init__(self, text: int, data: int, bss: int, dec: int): | 
|  | self.text = text | 
|  | self.data = data | 
|  | self.bss = bss | 
|  | self.total = dec # total <=> dec | 
|  |  | 
|  | def __init__(self, logger: logging.Logger) -> None: | 
|  | """ Variable code_size is used to store size info for any Git revisions. | 
|  | :param code_size: | 
|  | Data Format as following: | 
|  | code_size = { | 
|  | git_rev: { | 
|  | module: { | 
|  | file_name: SizeEntry, | 
|  | ... | 
|  | }, | 
|  | ... | 
|  | }, | 
|  | ... | 
|  | } | 
|  | """ | 
|  | super().__init__(logger) | 
|  | self.code_size = {} #type: typing.Dict[str, typing.Dict] | 
|  | self.mod_total_suffix = '-' + 'TOTALS' | 
|  |  | 
|  | def _set_size_record(self, git_rev: str, mod: str, size_text: str) -> None: | 
|  | """Store size information for target Git revision and high-level module. | 
|  |  | 
|  | size_text Format: text data bss dec hex filename | 
|  | """ | 
|  | size_record = {} | 
|  | for line in size_text.splitlines()[1:]: | 
|  | data = line.split() | 
|  | if re.match(r'\s*\(TOTALS\)', data[5]): | 
|  | data[5] = mod + self.mod_total_suffix | 
|  | # file_name: SizeEntry(text, data, bss, dec) | 
|  | size_record[data[5]] = CodeSizeGeneratorWithSize.SizeEntry( | 
|  | int(data[0]), int(data[1]), int(data[2]), int(data[3])) | 
|  | self.code_size.setdefault(git_rev, {}).update({mod: size_record}) | 
|  |  | 
|  | def read_size_record(self, git_rev: str, fname: str) -> None: | 
|  | """Read size information from csv file and write it into code_size. | 
|  |  | 
|  | fname Format: filename text data bss dec | 
|  | """ | 
|  | mod = "" | 
|  | size_record = {} | 
|  | with open(fname, 'r') as csv_file: | 
|  | for line in csv_file: | 
|  | data = line.strip().split() | 
|  | # check if we find the beginning of a module | 
|  | if data and data[0] in MBEDTLS_STATIC_LIB: | 
|  | mod = data[0] | 
|  | continue | 
|  |  | 
|  | if mod: | 
|  | # file_name: SizeEntry(text, data, bss, dec) | 
|  | size_record[data[0]] = CodeSizeGeneratorWithSize.SizeEntry( | 
|  | int(data[1]), int(data[2]), int(data[3]), int(data[4])) | 
|  |  | 
|  | # check if we hit record for the end of a module | 
|  | m = re.match(r'\w+' + self.mod_total_suffix, line) | 
|  | if m: | 
|  | if git_rev in self.code_size: | 
|  | self.code_size[git_rev].update({mod: size_record}) | 
|  | else: | 
|  | self.code_size[git_rev] = {mod: size_record} | 
|  | mod = "" | 
|  | size_record = {} | 
|  |  | 
|  | def write_record( | 
|  | self, | 
|  | git_rev: str, | 
|  | code_size_text: typing.Dict[str, str], | 
|  | output: typing_util.Writable | 
|  | ) -> None: | 
|  | """Write size information to a file. | 
|  |  | 
|  | Writing Format: filename text data bss total(dec) | 
|  | """ | 
|  | for mod, size_text in code_size_text.items(): | 
|  | self._set_size_record(git_rev, mod, size_text) | 
|  |  | 
|  | format_string = "{:<30} {:>7} {:>7} {:>7} {:>7}\n" | 
|  | output.write(format_string.format("filename", | 
|  | "text", "data", "bss", "total")) | 
|  |  | 
|  | for mod, f_size in self.code_size[git_rev].items(): | 
|  | output.write("\n" + mod + "\n") | 
|  | for fname, size_entry in f_size.items(): | 
|  | output.write(format_string | 
|  | .format(fname, | 
|  | size_entry.text, size_entry.data, | 
|  | size_entry.bss, size_entry.total)) | 
|  |  | 
|  | def write_comparison( #pylint: disable=too-many-arguments | 
|  | self, | 
|  | old_rev: str, | 
|  | new_rev: str, | 
|  | output: typing_util.Writable, | 
|  | with_markdown=False, | 
|  | show_all=False | 
|  | ) -> None: | 
|  | # pylint: disable=too-many-locals | 
|  | """Write comparison result into a file. | 
|  |  | 
|  | Writing Format: | 
|  | Markdown Output: | 
|  | filename new(text) new(data) change(text) change(data) | 
|  | CSV Output: | 
|  | filename new(text) new(data) old(text) old(data) change(text) change(data) | 
|  | """ | 
|  | header_line = ["filename", "new(text)", "old(text)", "change(text)", | 
|  | "new(data)", "old(data)", "change(data)"] | 
|  | if with_markdown: | 
|  | dash_line = [":----", "----:", "----:", "----:", | 
|  | "----:", "----:", "----:"] | 
|  | # | filename | new(text) | new(data) | change(text) | change(data) | | 
|  | line_format = "| {0:<30} | {1:>9} | {4:>9} | {3:>12} | {6:>12} |\n" | 
|  | bold_text = lambda x: '**' + str(x) + '**' | 
|  | else: | 
|  | # filename new(text) new(data) old(text) old(data) change(text) change(data) | 
|  | line_format = "{0:<30} {1:>9} {4:>9} {2:>10} {5:>10} {3:>12} {6:>12}\n" | 
|  |  | 
|  | def cal_sect_change( | 
|  | old_size: typing.Optional[CodeSizeGeneratorWithSize.SizeEntry], | 
|  | new_size: typing.Optional[CodeSizeGeneratorWithSize.SizeEntry], | 
|  | sect: str | 
|  | ) -> typing.List: | 
|  | """Inner helper function to calculate size change for a section. | 
|  |  | 
|  | Convention for special cases: | 
|  | - If the object has been removed in new Git revision, | 
|  | the size is minus code size of old Git revision; | 
|  | the size change is marked as `Removed`, | 
|  | - If the object only exists in new Git revision, | 
|  | the size is code size of new Git revision; | 
|  | the size change is marked as `None`, | 
|  |  | 
|  | :param: old_size: code size for objects in old Git revision. | 
|  | :param: new_size: code size for objects in new Git revision. | 
|  | :param: sect: section to calculate from `size` tool. This could be | 
|  | any instance variable in SizeEntry. | 
|  | :return: List of [section size of objects for new Git revision, | 
|  | section size of objects for old Git revision, | 
|  | section size change of objects between two Git revisions] | 
|  | """ | 
|  | if old_size and new_size: | 
|  | new_attr = new_size.__dict__[sect] | 
|  | old_attr = old_size.__dict__[sect] | 
|  | delta = new_attr - old_attr | 
|  | change_attr = '{0:{1}}'.format(delta, '+' if delta else '') | 
|  | elif old_size: | 
|  | new_attr = 'Removed' | 
|  | old_attr = old_size.__dict__[sect] | 
|  | delta = - old_attr | 
|  | change_attr = '{0:{1}}'.format(delta, '+' if delta else '') | 
|  | elif new_size: | 
|  | new_attr = new_size.__dict__[sect] | 
|  | old_attr = 'NotCreated' | 
|  | delta = new_attr | 
|  | change_attr = '{0:{1}}'.format(delta, '+' if delta else '') | 
|  | else: | 
|  | # Should never happen | 
|  | new_attr = 'Error' | 
|  | old_attr = 'Error' | 
|  | change_attr = 'Error' | 
|  | return [new_attr, old_attr, change_attr] | 
|  |  | 
|  | # sort dictionary by key | 
|  | sort_by_k = lambda item: item[0].lower() | 
|  | def get_results( | 
|  | f_rev_size: | 
|  | typing.Dict[str, | 
|  | typing.Dict[str, | 
|  | CodeSizeGeneratorWithSize.SizeEntry]] | 
|  | ) -> typing.List: | 
|  | """Return List of results in the format of: | 
|  | [filename, new(text), old(text), change(text), | 
|  | new(data), old(data), change(data)] | 
|  | """ | 
|  | res = [] | 
|  | for fname, revs_size in sorted(f_rev_size.items(), key=sort_by_k): | 
|  | old_size = revs_size.get(old_rev) | 
|  | new_size = revs_size.get(new_rev) | 
|  |  | 
|  | text_sect = cal_sect_change(old_size, new_size, 'text') | 
|  | data_sect = cal_sect_change(old_size, new_size, 'data') | 
|  | # skip the files that haven't changed in code size | 
|  | if not show_all and text_sect[-1] == '0' and data_sect[-1] == '0': | 
|  | continue | 
|  |  | 
|  | res.append([fname, *text_sect, *data_sect]) | 
|  | return res | 
|  |  | 
|  | # write header | 
|  | output.write(line_format.format(*header_line)) | 
|  | if with_markdown: | 
|  | output.write(line_format.format(*dash_line)) | 
|  | for mod in MBEDTLS_STATIC_LIB: | 
|  | # convert self.code_size to: | 
|  | # { | 
|  | #   file_name: { | 
|  | #       old_rev: SizeEntry, | 
|  | #       new_rev: SizeEntry | 
|  | #   }, | 
|  | #   ... | 
|  | # } | 
|  | f_rev_size = {} #type: typing.Dict[str, typing.Dict] | 
|  | for fname, size_entry in self.code_size[old_rev][mod].items(): | 
|  | f_rev_size.setdefault(fname, {}).update({old_rev: size_entry}) | 
|  | for fname, size_entry in self.code_size[new_rev][mod].items(): | 
|  | f_rev_size.setdefault(fname, {}).update({new_rev: size_entry}) | 
|  |  | 
|  | mod_total_sz = f_rev_size.pop(mod + self.mod_total_suffix) | 
|  | res = get_results(f_rev_size) | 
|  | total_clm = get_results({mod + self.mod_total_suffix: mod_total_sz}) | 
|  | if with_markdown: | 
|  | # bold row of mod-TOTALS in markdown table | 
|  | total_clm = [[bold_text(j) for j in i] for i in total_clm] | 
|  | res += total_clm | 
|  |  | 
|  | # write comparison result | 
|  | for line in res: | 
|  | output.write(line_format.format(*line)) | 
|  |  | 
|  |  | 
|  | class CodeSizeComparison: | 
|  | """Compare code size between two Git revisions.""" | 
|  |  | 
|  | def __init__( #pylint: disable=too-many-arguments | 
|  | self, | 
|  | old_size_dist_info: CodeSizeDistinctInfo, | 
|  | new_size_dist_info: CodeSizeDistinctInfo, | 
|  | size_common_info: CodeSizeCommonInfo, | 
|  | result_options: CodeSizeResultInfo, | 
|  | logger: logging.Logger, | 
|  | ) -> None: | 
|  | """ | 
|  | :param old_size_dist_info: CodeSizeDistinctInfo containing old distinct | 
|  | info to compare code size with. | 
|  | :param new_size_dist_info: CodeSizeDistinctInfo containing new distinct | 
|  | info to take as comparision base. | 
|  | :param size_common_info: CodeSizeCommonInfo containing common info for | 
|  | both old and new size distinct info and | 
|  | measurement tool. | 
|  | :param result_options: CodeSizeResultInfo containing results options for | 
|  | code size record and comparision. | 
|  | :param logger: logging module | 
|  | """ | 
|  |  | 
|  | self.logger = logger | 
|  |  | 
|  | self.old_size_dist_info = old_size_dist_info | 
|  | self.new_size_dist_info = new_size_dist_info | 
|  | self.size_common_info = size_common_info | 
|  | # infer pre make command | 
|  | self.old_size_dist_info.pre_make_cmd = CodeSizeBuildInfo( | 
|  | self.old_size_dist_info, self.size_common_info.host_arch, | 
|  | self.logger).infer_pre_make_command() | 
|  | self.new_size_dist_info.pre_make_cmd = CodeSizeBuildInfo( | 
|  | self.new_size_dist_info, self.size_common_info.host_arch, | 
|  | self.logger).infer_pre_make_command() | 
|  | # infer make command | 
|  | self.old_size_dist_info.make_cmd = CodeSizeBuildInfo( | 
|  | self.old_size_dist_info, self.size_common_info.host_arch, | 
|  | self.logger).infer_make_command() | 
|  | self.new_size_dist_info.make_cmd = CodeSizeBuildInfo( | 
|  | self.new_size_dist_info, self.size_common_info.host_arch, | 
|  | self.logger).infer_make_command() | 
|  | # initialize size parser with corresponding measurement tool | 
|  | self.code_size_generator = self.__generate_size_parser() | 
|  |  | 
|  | self.result_options = result_options | 
|  | self.csv_dir = os.path.abspath(self.result_options.record_dir) | 
|  | os.makedirs(self.csv_dir, exist_ok=True) | 
|  | self.comp_dir = os.path.abspath(self.result_options.comp_dir) | 
|  | os.makedirs(self.comp_dir, exist_ok=True) | 
|  |  | 
|  | def __generate_size_parser(self): | 
|  | """Generate a parser for the corresponding measurement tool.""" | 
|  | if re.match(r'size', self.size_common_info.measure_cmd.strip()): | 
|  | return CodeSizeGeneratorWithSize(self.logger) | 
|  | else: | 
|  | self.logger.error("Unsupported measurement tool: `{}`." | 
|  | .format(self.size_common_info.measure_cmd | 
|  | .strip().split(' ')[0])) | 
|  | sys.exit(1) | 
|  |  | 
|  | def cal_code_size( | 
|  | self, | 
|  | size_dist_info: CodeSizeDistinctInfo | 
|  | ) -> typing.Dict[str, str]: | 
|  | """Calculate code size of library/*.o in a UTF-8 encoding""" | 
|  |  | 
|  | return CodeSizeCalculator(size_dist_info.git_rev, | 
|  | size_dist_info.pre_make_cmd, | 
|  | size_dist_info.make_cmd, | 
|  | self.size_common_info.measure_cmd, | 
|  | self.logger).cal_libraries_code_size() | 
|  |  | 
|  | def gen_code_size_report(self, size_dist_info: CodeSizeDistinctInfo) -> None: | 
|  | """Generate code size record and write it into a file.""" | 
|  |  | 
|  | self.logger.info("Start to generate code size record for {}." | 
|  | .format(size_dist_info.git_rev)) | 
|  | output_file = os.path.join( | 
|  | self.csv_dir, | 
|  | '{}-{}.csv' | 
|  | .format(size_dist_info.get_info_indication(), | 
|  | self.size_common_info.get_info_indication())) | 
|  | # Check if the corresponding record exists | 
|  | if size_dist_info.git_rev != "current" and \ | 
|  | os.path.exists(output_file): | 
|  | self.logger.debug("Code size csv file for {} already exists." | 
|  | .format(size_dist_info.git_rev)) | 
|  | self.code_size_generator.read_size_record( | 
|  | size_dist_info.git_rev, output_file) | 
|  | else: | 
|  | # measure code size | 
|  | code_size_text = self.cal_code_size(size_dist_info) | 
|  |  | 
|  | self.logger.debug("Generating code size csv for {}." | 
|  | .format(size_dist_info.git_rev)) | 
|  | output = open(output_file, "w") | 
|  | self.code_size_generator.write_record( | 
|  | size_dist_info.git_rev, code_size_text, output) | 
|  |  | 
|  | def gen_code_size_comparison(self) -> None: | 
|  | """Generate results of code size changes between two Git revisions, | 
|  | old and new. | 
|  |  | 
|  | - Measured code size result of these two Git revisions must be available. | 
|  | - The result is directed into either file / stdout depending on | 
|  | the option, size_common_info.result_options.stdout. (Default: file) | 
|  | """ | 
|  |  | 
|  | self.logger.info("Start to generate comparision result between "\ | 
|  | "{} and {}." | 
|  | .format(self.old_size_dist_info.git_rev, | 
|  | self.new_size_dist_info.git_rev)) | 
|  | if self.result_options.stdout: | 
|  | output = sys.stdout | 
|  | else: | 
|  | output_file = os.path.join( | 
|  | self.comp_dir, | 
|  | '{}-{}-{}.{}' | 
|  | .format(self.old_size_dist_info.get_info_indication(), | 
|  | self.new_size_dist_info.get_info_indication(), | 
|  | self.size_common_info.get_info_indication(), | 
|  | 'md' if self.result_options.with_markdown else 'csv')) | 
|  | output = open(output_file, "w") | 
|  |  | 
|  | self.logger.debug("Generating comparison results between {} and {}." | 
|  | .format(self.old_size_dist_info.git_rev, | 
|  | self.new_size_dist_info.git_rev)) | 
|  | if self.result_options.with_markdown or self.result_options.stdout: | 
|  | print("Measure code size between {} and {} by `{}`." | 
|  | .format(self.old_size_dist_info.get_info_indication(), | 
|  | self.new_size_dist_info.get_info_indication(), | 
|  | self.size_common_info.get_info_indication()), | 
|  | file=output) | 
|  | self.code_size_generator.write_comparison( | 
|  | self.old_size_dist_info.git_rev, | 
|  | self.new_size_dist_info.git_rev, | 
|  | output, self.result_options.with_markdown, | 
|  | self.result_options.show_all) | 
|  |  | 
|  | def get_comparision_results(self) -> None: | 
|  | """Compare size of library/*.o between self.old_size_dist_info and | 
|  | self.old_size_dist_info and generate the result file.""" | 
|  | build_tree.check_repo_path() | 
|  | self.gen_code_size_report(self.old_size_dist_info) | 
|  | self.gen_code_size_report(self.new_size_dist_info) | 
|  | self.gen_code_size_comparison() | 
|  |  | 
|  | def main(): | 
|  | parser = argparse.ArgumentParser(description=(__doc__)) | 
|  | group_required = parser.add_argument_group( | 
|  | 'required arguments', | 
|  | 'required arguments to parse for running ' + os.path.basename(__file__)) | 
|  | group_required.add_argument( | 
|  | '-o', '--old-rev', type=str, required=True, | 
|  | help='old Git revision for comparison.') | 
|  |  | 
|  | group_optional = parser.add_argument_group( | 
|  | 'optional arguments', | 
|  | 'optional arguments to parse for running ' + os.path.basename(__file__)) | 
|  | group_optional.add_argument( | 
|  | '--record-dir', type=str, default='code_size_records', | 
|  | help='directory where code size record is stored. ' | 
|  | '(Default: code_size_records)') | 
|  | group_optional.add_argument( | 
|  | '--comp-dir', type=str, default='comparison', | 
|  | help='directory where comparison result is stored. ' | 
|  | '(Default: comparison)') | 
|  | group_optional.add_argument( | 
|  | '-n', '--new-rev', type=str, default='current', | 
|  | help='new Git revision as comparison base. ' | 
|  | '(Default is the current work directory, including uncommitted ' | 
|  | 'changes.)') | 
|  | group_optional.add_argument( | 
|  | '-a', '--arch', type=str, default=detect_arch(), | 
|  | choices=list(map(lambda s: s.value, SupportedArch)), | 
|  | help='Specify architecture for code size comparison. ' | 
|  | '(Default is the host architecture.)') | 
|  | group_optional.add_argument( | 
|  | '-c', '--config', type=str, default=SupportedConfig.DEFAULT.value, | 
|  | choices=list(map(lambda s: s.value, SupportedConfig)), | 
|  | help='Specify configuration type for code size comparison. ' | 
|  | '(Default is the current Mbed TLS configuration.)') | 
|  | group_optional.add_argument( | 
|  | '--markdown', action='store_true', dest='markdown', | 
|  | help='Show comparision of code size in a markdown table. ' | 
|  | '(Only show the files that have changed).') | 
|  | group_optional.add_argument( | 
|  | '--stdout', action='store_true', dest='stdout', | 
|  | help='Set this option to direct comparison result into sys.stdout. ' | 
|  | '(Default: file)') | 
|  | group_optional.add_argument( | 
|  | '--show-all', action='store_true', dest='show_all', | 
|  | help='Show all the objects in comparison result, including the ones ' | 
|  | 'that haven\'t changed in code size. (Default: False)') | 
|  | group_optional.add_argument( | 
|  | '--verbose', action='store_true', dest='verbose', | 
|  | help='Show logs in detail for code size measurement. ' | 
|  | '(Default: False)') | 
|  | comp_args = parser.parse_args() | 
|  |  | 
|  | logger = logging.getLogger() | 
|  | logging_util.configure_logger(logger, split_level=logging.NOTSET) | 
|  | logger.setLevel(logging.DEBUG if comp_args.verbose else logging.INFO) | 
|  |  | 
|  | if os.path.isfile(comp_args.record_dir): | 
|  | logger.error("record directory: {} is not a directory" | 
|  | .format(comp_args.record_dir)) | 
|  | sys.exit(1) | 
|  | if os.path.isfile(comp_args.comp_dir): | 
|  | logger.error("comparison directory: {} is not a directory" | 
|  | .format(comp_args.comp_dir)) | 
|  | sys.exit(1) | 
|  |  | 
|  | comp_args.old_rev = CodeSizeCalculator.validate_git_revision( | 
|  | comp_args.old_rev) | 
|  | if comp_args.new_rev != 'current': | 
|  | comp_args.new_rev = CodeSizeCalculator.validate_git_revision( | 
|  | comp_args.new_rev) | 
|  |  | 
|  | # version, git_rev, arch, config, compiler, opt_level | 
|  | old_size_dist_info = CodeSizeDistinctInfo( | 
|  | 'old', comp_args.old_rev, comp_args.arch, comp_args.config, 'cc', '-Os') | 
|  | new_size_dist_info = CodeSizeDistinctInfo( | 
|  | 'new', comp_args.new_rev, comp_args.arch, comp_args.config, 'cc', '-Os') | 
|  | # host_arch, measure_cmd | 
|  | size_common_info = CodeSizeCommonInfo( | 
|  | detect_arch(), 'size -t') | 
|  | # record_dir, comp_dir, with_markdown, stdout, show_all | 
|  | result_options = CodeSizeResultInfo( | 
|  | comp_args.record_dir, comp_args.comp_dir, | 
|  | comp_args.markdown, comp_args.stdout, comp_args.show_all) | 
|  |  | 
|  | logger.info("Measure code size between {} and {} by `{}`." | 
|  | .format(old_size_dist_info.get_info_indication(), | 
|  | new_size_dist_info.get_info_indication(), | 
|  | size_common_info.get_info_indication())) | 
|  | CodeSizeComparison(old_size_dist_info, new_size_dist_info, | 
|  | size_common_info, result_options, | 
|  | logger).get_comparision_results() | 
|  |  | 
|  | if __name__ == "__main__": | 
|  | main() |