quickjs-tart

quickjs-based runtime for wallet-core logic
Log | Files | Refs | README | LICENSE

code_size_compare.py (39546B)


      1 #!/usr/bin/env python3
      2 
      3 """
      4 This script is for comparing the size of the library files from two
      5 different Git revisions within an Mbed TLS repository.
      6 The results of the comparison is formatted as csv and stored at a
      7 configurable location.
      8 Note: must be run from Mbed TLS root.
      9 """
     10 
     11 # Copyright The Mbed TLS Contributors
     12 # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
     13 
     14 import argparse
     15 import logging
     16 import os
     17 import re
     18 import shutil
     19 import subprocess
     20 import sys
     21 import typing
     22 from enum import Enum
     23 
     24 import framework_scripts_path # pylint: disable=unused-import
     25 from mbedtls_framework import build_tree
     26 from mbedtls_framework import logging_util
     27 from mbedtls_framework import typing_util
     28 
     29 class SupportedArch(Enum):
     30     """Supported architecture for code size measurement."""
     31     AARCH64 = 'aarch64'
     32     AARCH32 = 'aarch32'
     33     ARMV8_M = 'armv8-m'
     34     X86_64 = 'x86_64'
     35     X86 = 'x86'
     36 
     37 
     38 class SupportedConfig(Enum):
     39     """Supported configuration for code size measurement."""
     40     DEFAULT = 'default'
     41     TFM_MEDIUM = 'tfm-medium'
     42 
     43 
     44 # Static library
     45 MBEDTLS_STATIC_LIB = {
     46     'CRYPTO': 'library/libmbedcrypto.a',
     47     'X509': 'library/libmbedx509.a',
     48     'TLS': 'library/libmbedtls.a',
     49 }
     50 
     51 class CodeSizeDistinctInfo: # pylint: disable=too-few-public-methods
     52     """Data structure to store possibly distinct information for code size
     53     comparison."""
     54     def __init__( #pylint: disable=too-many-arguments
     55             self,
     56             version: str,
     57             git_rev: str,
     58             arch: str,
     59             config: str,
     60             compiler: str,
     61             opt_level: str,
     62     ) -> None:
     63         """
     64         :param: version: which version to compare with for code size.
     65         :param: git_rev: Git revision to calculate code size.
     66         :param: arch: architecture to measure code size on.
     67         :param: config: Configuration type to calculate code size.
     68                         (See SupportedConfig)
     69         :param: compiler: compiler used to build library/*.o.
     70         :param: opt_level: Options that control optimization. (E.g. -Os)
     71         """
     72         self.version = version
     73         self.git_rev = git_rev
     74         self.arch = arch
     75         self.config = config
     76         self.compiler = compiler
     77         self.opt_level = opt_level
     78         # Note: Variables below are not initialized by class instantiation.
     79         self.pre_make_cmd = [] #type: typing.List[str]
     80         self.make_cmd = ''
     81 
     82     def get_info_indication(self):
     83         """Return a unique string to indicate Code Size Distinct Information."""
     84         return '{git_rev}-{arch}-{config}-{compiler}'.format(**self.__dict__)
     85 
     86 
     87 class CodeSizeCommonInfo: # pylint: disable=too-few-public-methods
     88     """Data structure to store common information for code size comparison."""
     89     def __init__(
     90             self,
     91             host_arch: str,
     92             measure_cmd: str,
     93     ) -> None:
     94         """
     95         :param host_arch: host architecture.
     96         :param measure_cmd: command to measure code size for library/*.o.
     97         """
     98         self.host_arch = host_arch
     99         self.measure_cmd = measure_cmd
    100 
    101     def get_info_indication(self):
    102         """Return a unique string to indicate Code Size Common Information."""
    103         return '{measure_tool}'\
    104                .format(measure_tool=self.measure_cmd.strip().split(' ')[0])
    105 
    106 class CodeSizeResultInfo: # pylint: disable=too-few-public-methods
    107     """Data structure to store result options for code size comparison."""
    108     def __init__( #pylint: disable=too-many-arguments
    109             self,
    110             record_dir: str,
    111             comp_dir: str,
    112             with_markdown=False,
    113             stdout=False,
    114             show_all=False,
    115     ) -> None:
    116         """
    117         :param record_dir: directory to store code size record.
    118         :param comp_dir: directory to store results of code size comparision.
    119         :param with_markdown: write comparision result into a markdown table.
    120                               (Default: False)
    121         :param stdout: direct comparison result into sys.stdout.
    122                        (Default False)
    123         :param show_all: show all objects in comparison result. (Default False)
    124         """
    125         self.record_dir = record_dir
    126         self.comp_dir = comp_dir
    127         self.with_markdown = with_markdown
    128         self.stdout = stdout
    129         self.show_all = show_all
    130 
    131 
    132 DETECT_ARCH_CMD = "cc -dM -E - < /dev/null"
    133 def detect_arch() -> str:
    134     """Auto-detect host architecture."""
    135     cc_output = subprocess.check_output(DETECT_ARCH_CMD, shell=True).decode()
    136     if '__aarch64__' in cc_output:
    137         return SupportedArch.AARCH64.value
    138     if '__arm__' in cc_output:
    139         return SupportedArch.AARCH32.value
    140     if '__x86_64__' in cc_output:
    141         return SupportedArch.X86_64.value
    142     if '__i386__' in cc_output:
    143         return SupportedArch.X86.value
    144     else:
    145         print("Unknown host architecture, cannot auto-detect arch.")
    146         sys.exit(1)
    147 
    148 TFM_MEDIUM_CONFIG_H = 'configs/ext/tfm_mbedcrypto_config_profile_medium.h'
    149 TFM_MEDIUM_CRYPTO_CONFIG_H = 'configs/ext/crypto_config_profile_medium.h'
    150 
    151 CONFIG_H = 'include/mbedtls/mbedtls_config.h'
    152 CRYPTO_CONFIG_H = 'include/psa/crypto_config.h'
    153 BACKUP_SUFFIX = '.code_size.bak'
    154 
    155 class CodeSizeBuildInfo: # pylint: disable=too-few-public-methods
    156     """Gather information used to measure code size.
    157 
    158     It collects information about architecture, configuration in order to
    159     infer build command for code size measurement.
    160     """
    161 
    162     SupportedArchConfig = [
    163         '-a ' + SupportedArch.AARCH64.value + ' -c ' + SupportedConfig.DEFAULT.value,
    164         '-a ' + SupportedArch.AARCH32.value + ' -c ' + SupportedConfig.DEFAULT.value,
    165         '-a ' + SupportedArch.X86_64.value  + ' -c ' + SupportedConfig.DEFAULT.value,
    166         '-a ' + SupportedArch.X86.value     + ' -c ' + SupportedConfig.DEFAULT.value,
    167         '-a ' + SupportedArch.ARMV8_M.value + ' -c ' + SupportedConfig.TFM_MEDIUM.value,
    168     ]
    169 
    170     def __init__(
    171             self,
    172             size_dist_info: CodeSizeDistinctInfo,
    173             host_arch: str,
    174             logger: logging.Logger,
    175     ) -> None:
    176         """
    177         :param size_dist_info:
    178             CodeSizeDistinctInfo containing info for code size measurement.
    179                 - size_dist_info.arch: architecture to measure code size on.
    180                 - size_dist_info.config: configuration type to measure
    181                                          code size with.
    182                 - size_dist_info.compiler: compiler used to build library/*.o.
    183                 - size_dist_info.opt_level: Options that control optimization.
    184                                             (E.g. -Os)
    185         :param host_arch: host architecture.
    186         :param logger: logging module
    187         """
    188         self.arch = size_dist_info.arch
    189         self.config = size_dist_info.config
    190         self.compiler = size_dist_info.compiler
    191         self.opt_level = size_dist_info.opt_level
    192 
    193         self.make_cmd = ['make', '-j', 'lib']
    194 
    195         self.host_arch = host_arch
    196         self.logger = logger
    197 
    198     def check_correctness(self) -> bool:
    199         """Check whether we are using proper / supported combination
    200         of information to build library/*.o."""
    201 
    202         # default config
    203         if self.config == SupportedConfig.DEFAULT.value and \
    204             self.arch == self.host_arch:
    205             return True
    206         # TF-M
    207         elif self.arch == SupportedArch.ARMV8_M.value and \
    208              self.config == SupportedConfig.TFM_MEDIUM.value:
    209             return True
    210 
    211         return False
    212 
    213     def infer_pre_make_command(self) -> typing.List[str]:
    214         """Infer command to set up proper configuration before running make."""
    215         pre_make_cmd = [] #type: typing.List[str]
    216         if self.config == SupportedConfig.TFM_MEDIUM.value:
    217             pre_make_cmd.append('cp {src} {dest}'
    218                                 .format(src=TFM_MEDIUM_CONFIG_H, dest=CONFIG_H))
    219             pre_make_cmd.append('cp {src} {dest}'
    220                                 .format(src=TFM_MEDIUM_CRYPTO_CONFIG_H,
    221                                         dest=CRYPTO_CONFIG_H))
    222 
    223         return pre_make_cmd
    224 
    225     def infer_make_cflags(self) -> str:
    226         """Infer CFLAGS by instance attributes in CodeSizeDistinctInfo."""
    227         cflags = [] #type: typing.List[str]
    228 
    229         # set optimization level
    230         cflags.append(self.opt_level)
    231         # set compiler by config
    232         if self.config == SupportedConfig.TFM_MEDIUM.value:
    233             self.compiler = 'armclang'
    234             cflags.append('-mcpu=cortex-m33')
    235         # set target
    236         if self.compiler == 'armclang':
    237             cflags.append('--target=arm-arm-none-eabi')
    238 
    239         return ' '.join(cflags)
    240 
    241     def infer_make_command(self) -> str:
    242         """Infer make command by CFLAGS and CC."""
    243 
    244         if self.check_correctness():
    245             # set CFLAGS=
    246             self.make_cmd.append('CFLAGS=\'{}\''.format(self.infer_make_cflags()))
    247             # set CC=
    248             self.make_cmd.append('CC={}'.format(self.compiler))
    249             return ' '.join(self.make_cmd)
    250         else:
    251             self.logger.error("Unsupported combination of architecture: {} " \
    252                               "and configuration: {}.\n"
    253                               .format(self.arch,
    254                                       self.config))
    255             self.logger.error("Please use supported combination of " \
    256                              "architecture and configuration:")
    257             for comb in CodeSizeBuildInfo.SupportedArchConfig:
    258                 self.logger.error(comb)
    259             self.logger.error("")
    260             self.logger.error("For your system, please use:")
    261             for comb in CodeSizeBuildInfo.SupportedArchConfig:
    262                 if "default" in comb and self.host_arch not in comb:
    263                     continue
    264                 self.logger.error(comb)
    265             sys.exit(1)
    266 
    267 
    268 class CodeSizeCalculator:
    269     """ A calculator to calculate code size of library/*.o based on
    270     Git revision and code size measurement tool.
    271     """
    272 
    273     def __init__( #pylint: disable=too-many-arguments
    274             self,
    275             git_rev: str,
    276             pre_make_cmd: typing.List[str],
    277             make_cmd: str,
    278             measure_cmd: str,
    279             logger: logging.Logger,
    280     ) -> None:
    281         """
    282         :param git_rev: Git revision. (E.g: commit)
    283         :param pre_make_cmd: command to set up proper config before running make.
    284         :param make_cmd: command to build library/*.o.
    285         :param measure_cmd: command to measure code size for library/*.o.
    286         :param logger: logging module
    287         """
    288         self.repo_path = "."
    289         self.git_command = "git"
    290         self.make_clean = 'make clean'
    291 
    292         self.git_rev = git_rev
    293         self.pre_make_cmd = pre_make_cmd
    294         self.make_cmd = make_cmd
    295         self.measure_cmd = measure_cmd
    296         self.logger = logger
    297 
    298     @staticmethod
    299     def validate_git_revision(git_rev: str) -> str:
    300         result = subprocess.check_output(["git", "rev-parse", "--verify",
    301                                           git_rev + "^{commit}"],
    302                                          shell=False, universal_newlines=True)
    303         return result[:7]
    304 
    305     def _create_git_worktree(self) -> str:
    306         """Create a separate worktree for Git revision.
    307         If Git revision is current, use current worktree instead."""
    308 
    309         if self.git_rev == 'current':
    310             self.logger.debug("Using current work directory.")
    311             git_worktree_path = self.repo_path
    312         else:
    313             self.logger.debug("Creating git worktree for {}."
    314                               .format(self.git_rev))
    315             git_worktree_path = os.path.join(self.repo_path,
    316                                              "temp-" + self.git_rev)
    317             subprocess.check_output(
    318                 [self.git_command, "worktree", "add", "--detach",
    319                  git_worktree_path, self.git_rev], cwd=self.repo_path,
    320                 stderr=subprocess.STDOUT
    321             )
    322 
    323         return git_worktree_path
    324 
    325     @staticmethod
    326     def backup_config_files(restore: bool) -> None:
    327         """Backup / Restore config files."""
    328         if restore:
    329             shutil.move(CONFIG_H + BACKUP_SUFFIX, CONFIG_H)
    330             shutil.move(CRYPTO_CONFIG_H + BACKUP_SUFFIX, CRYPTO_CONFIG_H)
    331         else:
    332             shutil.copy(CONFIG_H, CONFIG_H + BACKUP_SUFFIX)
    333             shutil.copy(CRYPTO_CONFIG_H, CRYPTO_CONFIG_H + BACKUP_SUFFIX)
    334 
    335     def _build_libraries(self, git_worktree_path: str) -> None:
    336         """Build library/*.o in the specified worktree."""
    337 
    338         self.logger.debug("Building library/*.o for {}."
    339                           .format(self.git_rev))
    340         my_environment = os.environ.copy()
    341         try:
    342             if self.git_rev == 'current':
    343                 self.backup_config_files(restore=False)
    344             for pre_cmd in self.pre_make_cmd:
    345                 subprocess.check_output(
    346                     pre_cmd, env=my_environment, shell=True,
    347                     cwd=git_worktree_path, stderr=subprocess.STDOUT,
    348                     universal_newlines=True
    349                 )
    350             subprocess.check_output(
    351                 self.make_clean, env=my_environment, shell=True,
    352                 cwd=git_worktree_path, stderr=subprocess.STDOUT,
    353                 universal_newlines=True
    354             )
    355             subprocess.check_output(
    356                 self.make_cmd, env=my_environment, shell=True,
    357                 cwd=git_worktree_path, stderr=subprocess.STDOUT,
    358                 universal_newlines=True
    359             )
    360             if self.git_rev == 'current':
    361                 self.backup_config_files(restore=True)
    362         except subprocess.CalledProcessError as e:
    363             self._handle_called_process_error(e, git_worktree_path)
    364 
    365     def _gen_raw_code_size(self, git_worktree_path: str) -> typing.Dict[str, str]:
    366         """Measure code size by a tool and return in UTF-8 encoding."""
    367 
    368         self.logger.debug("Measuring code size for {} by `{}`."
    369                           .format(self.git_rev,
    370                                   self.measure_cmd.strip().split(' ')[0]))
    371 
    372         res = {}
    373         for mod, st_lib in MBEDTLS_STATIC_LIB.items():
    374             try:
    375                 result = subprocess.check_output(
    376                     [self.measure_cmd + ' ' + st_lib], cwd=git_worktree_path,
    377                     shell=True, universal_newlines=True
    378                 )
    379                 res[mod] = result
    380             except subprocess.CalledProcessError as e:
    381                 self._handle_called_process_error(e, git_worktree_path)
    382 
    383         return res
    384 
    385     def _remove_worktree(self, git_worktree_path: str) -> None:
    386         """Remove temporary worktree."""
    387         if git_worktree_path != self.repo_path:
    388             self.logger.debug("Removing temporary worktree {}."
    389                               .format(git_worktree_path))
    390             subprocess.check_output(
    391                 [self.git_command, "worktree", "remove", "--force",
    392                  git_worktree_path], cwd=self.repo_path,
    393                 stderr=subprocess.STDOUT
    394             )
    395 
    396     def _handle_called_process_error(self, e: subprocess.CalledProcessError,
    397                                      git_worktree_path: str) -> None:
    398         """Handle a CalledProcessError and quit the program gracefully.
    399         Remove any extra worktrees so that the script may be called again."""
    400 
    401         # Tell the user what went wrong
    402         self.logger.error(e, exc_info=True)
    403         self.logger.error("Process output:\n {}".format(e.output))
    404 
    405         # Quit gracefully by removing the existing worktree
    406         self._remove_worktree(git_worktree_path)
    407         sys.exit(-1)
    408 
    409     def cal_libraries_code_size(self) -> typing.Dict[str, str]:
    410         """Do a complete round to calculate code size of library/*.o
    411         by measurement tool.
    412 
    413         :return A dictionary of measured code size
    414             - typing.Dict[mod: str]
    415         """
    416 
    417         git_worktree_path = self._create_git_worktree()
    418         try:
    419             self._build_libraries(git_worktree_path)
    420             res = self._gen_raw_code_size(git_worktree_path)
    421         finally:
    422             self._remove_worktree(git_worktree_path)
    423 
    424         return res
    425 
    426 
    427 class CodeSizeGenerator:
    428     """ A generator based on size measurement tool for library/*.o.
    429 
    430     This is an abstract class. To use it, derive a class that implements
    431     write_record and write_comparison methods, then call both of them with
    432     proper arguments.
    433     """
    434     def __init__(self, logger: logging.Logger) -> None:
    435         """
    436         :param logger: logging module
    437         """
    438         self.logger = logger
    439 
    440     def write_record(
    441             self,
    442             git_rev: str,
    443             code_size_text: typing.Dict[str, str],
    444             output: typing_util.Writable
    445     ) -> None:
    446         """Write size record into a file.
    447 
    448         :param git_rev: Git revision. (E.g: commit)
    449         :param code_size_text:
    450             string output (utf-8) from measurement tool of code size.
    451                 - typing.Dict[mod: str]
    452         :param output: output stream which the code size record is written to.
    453                        (Note: Normally write code size record into File)
    454         """
    455         raise NotImplementedError
    456 
    457     def write_comparison( #pylint: disable=too-many-arguments
    458             self,
    459             old_rev: str,
    460             new_rev: str,
    461             output: typing_util.Writable,
    462             with_markdown=False,
    463             show_all=False
    464     ) -> None:
    465         """Write a comparision result into a stream between two Git revisions.
    466 
    467         :param old_rev: old Git revision to compared with.
    468         :param new_rev: new Git revision to compared with.
    469         :param output: output stream which the code size record is written to.
    470                        (File / sys.stdout)
    471         :param with_markdown:  write comparision result in a markdown table.
    472                                (Default: False)
    473         :param show_all: show all objects in comparison result. (Default False)
    474         """
    475         raise NotImplementedError
    476 
    477 
    478 class CodeSizeGeneratorWithSize(CodeSizeGenerator):
    479     """Code Size Base Class for size record saving and writing."""
    480 
    481     class SizeEntry: # pylint: disable=too-few-public-methods
    482         """Data Structure to only store information of code size."""
    483         def __init__(self, text: int, data: int, bss: int, dec: int):
    484             self.text = text
    485             self.data = data
    486             self.bss = bss
    487             self.total = dec # total <=> dec
    488 
    489     def __init__(self, logger: logging.Logger) -> None:
    490         """ Variable code_size is used to store size info for any Git revisions.
    491         :param code_size:
    492             Data Format as following:
    493             code_size = {
    494                 git_rev: {
    495                     module: {
    496                         file_name: SizeEntry,
    497                         ...
    498                     },
    499                     ...
    500                 },
    501                 ...
    502             }
    503         """
    504         super().__init__(logger)
    505         self.code_size = {} #type: typing.Dict[str, typing.Dict]
    506         self.mod_total_suffix = '-' + 'TOTALS'
    507 
    508     def _set_size_record(self, git_rev: str, mod: str, size_text: str) -> None:
    509         """Store size information for target Git revision and high-level module.
    510 
    511         size_text Format: text data bss dec hex filename
    512         """
    513         size_record = {}
    514         for line in size_text.splitlines()[1:]:
    515             data = line.split()
    516             if re.match(r'\s*\(TOTALS\)', data[5]):
    517                 data[5] = mod + self.mod_total_suffix
    518             # file_name: SizeEntry(text, data, bss, dec)
    519             size_record[data[5]] = CodeSizeGeneratorWithSize.SizeEntry(
    520                 int(data[0]), int(data[1]), int(data[2]), int(data[3]))
    521         self.code_size.setdefault(git_rev, {}).update({mod: size_record})
    522 
    523     def read_size_record(self, git_rev: str, fname: str) -> None:
    524         """Read size information from csv file and write it into code_size.
    525 
    526         fname Format: filename text data bss dec
    527         """
    528         mod = ""
    529         size_record = {}
    530         with open(fname, 'r') as csv_file:
    531             for line in csv_file:
    532                 data = line.strip().split()
    533                 # check if we find the beginning of a module
    534                 if data and data[0] in MBEDTLS_STATIC_LIB:
    535                     mod = data[0]
    536                     continue
    537 
    538                 if mod:
    539                     # file_name: SizeEntry(text, data, bss, dec)
    540                     size_record[data[0]] = CodeSizeGeneratorWithSize.SizeEntry(
    541                         int(data[1]), int(data[2]), int(data[3]), int(data[4]))
    542 
    543                 # check if we hit record for the end of a module
    544                 m = re.match(r'\w+' + self.mod_total_suffix, line)
    545                 if m:
    546                     if git_rev in self.code_size:
    547                         self.code_size[git_rev].update({mod: size_record})
    548                     else:
    549                         self.code_size[git_rev] = {mod: size_record}
    550                     mod = ""
    551                     size_record = {}
    552 
    553     def write_record(
    554             self,
    555             git_rev: str,
    556             code_size_text: typing.Dict[str, str],
    557             output: typing_util.Writable
    558     ) -> None:
    559         """Write size information to a file.
    560 
    561         Writing Format: filename text data bss total(dec)
    562         """
    563         for mod, size_text in code_size_text.items():
    564             self._set_size_record(git_rev, mod, size_text)
    565 
    566         format_string = "{:<30} {:>7} {:>7} {:>7} {:>7}\n"
    567         output.write(format_string.format("filename",
    568                                           "text", "data", "bss", "total"))
    569 
    570         for mod, f_size in self.code_size[git_rev].items():
    571             output.write("\n" + mod + "\n")
    572             for fname, size_entry in f_size.items():
    573                 output.write(format_string
    574                              .format(fname,
    575                                      size_entry.text, size_entry.data,
    576                                      size_entry.bss, size_entry.total))
    577 
    578     def write_comparison( #pylint: disable=too-many-arguments
    579             self,
    580             old_rev: str,
    581             new_rev: str,
    582             output: typing_util.Writable,
    583             with_markdown=False,
    584             show_all=False
    585     ) -> None:
    586         # pylint: disable=too-many-locals
    587         """Write comparison result into a file.
    588 
    589         Writing Format:
    590             Markdown Output:
    591                 filename new(text) new(data) change(text) change(data)
    592             CSV Output:
    593                 filename new(text) new(data) old(text) old(data) change(text) change(data)
    594         """
    595         header_line = ["filename", "new(text)", "old(text)", "change(text)",
    596                        "new(data)", "old(data)", "change(data)"]
    597         if with_markdown:
    598             dash_line = [":----", "----:", "----:", "----:",
    599                          "----:", "----:", "----:"]
    600             # | filename | new(text) | new(data) | change(text) | change(data) |
    601             line_format = "| {0:<30} | {1:>9} | {4:>9} | {3:>12} | {6:>12} |\n"
    602             bold_text = lambda x: '**' + str(x) + '**'
    603         else:
    604             # filename new(text) new(data) old(text) old(data) change(text) change(data)
    605             line_format = "{0:<30} {1:>9} {4:>9} {2:>10} {5:>10} {3:>12} {6:>12}\n"
    606 
    607         def cal_sect_change(
    608                 old_size: typing.Optional[CodeSizeGeneratorWithSize.SizeEntry],
    609                 new_size: typing.Optional[CodeSizeGeneratorWithSize.SizeEntry],
    610                 sect: str
    611         ) -> typing.List:
    612             """Inner helper function to calculate size change for a section.
    613 
    614             Convention for special cases:
    615                 - If the object has been removed in new Git revision,
    616                   the size is minus code size of old Git revision;
    617                   the size change is marked as `Removed`,
    618                 - If the object only exists in new Git revision,
    619                   the size is code size of new Git revision;
    620                   the size change is marked as `None`,
    621 
    622             :param: old_size: code size for objects in old Git revision.
    623             :param: new_size: code size for objects in new Git revision.
    624             :param: sect: section to calculate from `size` tool. This could be
    625                           any instance variable in SizeEntry.
    626             :return: List of [section size of objects for new Git revision,
    627                      section size of objects for old Git revision,
    628                      section size change of objects between two Git revisions]
    629             """
    630             if old_size and new_size:
    631                 new_attr = new_size.__dict__[sect]
    632                 old_attr = old_size.__dict__[sect]
    633                 delta = new_attr - old_attr
    634                 change_attr = '{0:{1}}'.format(delta, '+' if delta else '')
    635             elif old_size:
    636                 new_attr = 'Removed'
    637                 old_attr = old_size.__dict__[sect]
    638                 delta = - old_attr
    639                 change_attr = '{0:{1}}'.format(delta, '+' if delta else '')
    640             elif new_size:
    641                 new_attr = new_size.__dict__[sect]
    642                 old_attr = 'NotCreated'
    643                 delta = new_attr
    644                 change_attr = '{0:{1}}'.format(delta, '+' if delta else '')
    645             else:
    646                 # Should never happen
    647                 new_attr = 'Error'
    648                 old_attr = 'Error'
    649                 change_attr = 'Error'
    650             return [new_attr, old_attr, change_attr]
    651 
    652         # sort dictionary by key
    653         sort_by_k = lambda item: item[0].lower()
    654         def get_results(
    655                 f_rev_size:
    656                 typing.Dict[str,
    657                             typing.Dict[str,
    658                                         CodeSizeGeneratorWithSize.SizeEntry]]
    659             ) -> typing.List:
    660             """Return List of results in the format of:
    661             [filename, new(text), old(text), change(text),
    662              new(data), old(data), change(data)]
    663             """
    664             res = []
    665             for fname, revs_size in sorted(f_rev_size.items(), key=sort_by_k):
    666                 old_size = revs_size.get(old_rev)
    667                 new_size = revs_size.get(new_rev)
    668 
    669                 text_sect = cal_sect_change(old_size, new_size, 'text')
    670                 data_sect = cal_sect_change(old_size, new_size, 'data')
    671                 # skip the files that haven't changed in code size
    672                 if not show_all and text_sect[-1] == '0' and data_sect[-1] == '0':
    673                     continue
    674 
    675                 res.append([fname, *text_sect, *data_sect])
    676             return res
    677 
    678         # write header
    679         output.write(line_format.format(*header_line))
    680         if with_markdown:
    681             output.write(line_format.format(*dash_line))
    682         for mod in MBEDTLS_STATIC_LIB:
    683         # convert self.code_size to:
    684         # {
    685         #   file_name: {
    686         #       old_rev: SizeEntry,
    687         #       new_rev: SizeEntry
    688         #   },
    689         #   ...
    690         # }
    691             f_rev_size = {} #type: typing.Dict[str, typing.Dict]
    692             for fname, size_entry in self.code_size[old_rev][mod].items():
    693                 f_rev_size.setdefault(fname, {}).update({old_rev: size_entry})
    694             for fname, size_entry in self.code_size[new_rev][mod].items():
    695                 f_rev_size.setdefault(fname, {}).update({new_rev: size_entry})
    696 
    697             mod_total_sz = f_rev_size.pop(mod + self.mod_total_suffix)
    698             res = get_results(f_rev_size)
    699             total_clm = get_results({mod + self.mod_total_suffix: mod_total_sz})
    700             if with_markdown:
    701                 # bold row of mod-TOTALS in markdown table
    702                 total_clm = [[bold_text(j) for j in i] for i in total_clm]
    703             res += total_clm
    704 
    705             # write comparison result
    706             for line in res:
    707                 output.write(line_format.format(*line))
    708 
    709 
    710 class CodeSizeComparison:
    711     """Compare code size between two Git revisions."""
    712 
    713     def __init__( #pylint: disable=too-many-arguments
    714             self,
    715             old_size_dist_info: CodeSizeDistinctInfo,
    716             new_size_dist_info: CodeSizeDistinctInfo,
    717             size_common_info: CodeSizeCommonInfo,
    718             result_options: CodeSizeResultInfo,
    719             logger: logging.Logger,
    720     ) -> None:
    721         """
    722         :param old_size_dist_info: CodeSizeDistinctInfo containing old distinct
    723                                    info to compare code size with.
    724         :param new_size_dist_info: CodeSizeDistinctInfo containing new distinct
    725                                    info to take as comparision base.
    726         :param size_common_info: CodeSizeCommonInfo containing common info for
    727                                  both old and new size distinct info and
    728                                  measurement tool.
    729         :param result_options: CodeSizeResultInfo containing results options for
    730                                code size record and comparision.
    731         :param logger: logging module
    732         """
    733 
    734         self.logger = logger
    735 
    736         self.old_size_dist_info = old_size_dist_info
    737         self.new_size_dist_info = new_size_dist_info
    738         self.size_common_info = size_common_info
    739         # infer pre make command
    740         self.old_size_dist_info.pre_make_cmd = CodeSizeBuildInfo(
    741             self.old_size_dist_info, self.size_common_info.host_arch,
    742             self.logger).infer_pre_make_command()
    743         self.new_size_dist_info.pre_make_cmd = CodeSizeBuildInfo(
    744             self.new_size_dist_info, self.size_common_info.host_arch,
    745             self.logger).infer_pre_make_command()
    746         # infer make command
    747         self.old_size_dist_info.make_cmd = CodeSizeBuildInfo(
    748             self.old_size_dist_info, self.size_common_info.host_arch,
    749             self.logger).infer_make_command()
    750         self.new_size_dist_info.make_cmd = CodeSizeBuildInfo(
    751             self.new_size_dist_info, self.size_common_info.host_arch,
    752             self.logger).infer_make_command()
    753         # initialize size parser with corresponding measurement tool
    754         self.code_size_generator = self.__generate_size_parser()
    755 
    756         self.result_options = result_options
    757         self.csv_dir = os.path.abspath(self.result_options.record_dir)
    758         os.makedirs(self.csv_dir, exist_ok=True)
    759         self.comp_dir = os.path.abspath(self.result_options.comp_dir)
    760         os.makedirs(self.comp_dir, exist_ok=True)
    761 
    762     def __generate_size_parser(self):
    763         """Generate a parser for the corresponding measurement tool."""
    764         if re.match(r'size', self.size_common_info.measure_cmd.strip()):
    765             return CodeSizeGeneratorWithSize(self.logger)
    766         else:
    767             self.logger.error("Unsupported measurement tool: `{}`."
    768                               .format(self.size_common_info.measure_cmd
    769                                       .strip().split(' ')[0]))
    770             sys.exit(1)
    771 
    772     def cal_code_size(
    773             self,
    774             size_dist_info: CodeSizeDistinctInfo
    775         ) -> typing.Dict[str, str]:
    776         """Calculate code size of library/*.o in a UTF-8 encoding"""
    777 
    778         return CodeSizeCalculator(size_dist_info.git_rev,
    779                                   size_dist_info.pre_make_cmd,
    780                                   size_dist_info.make_cmd,
    781                                   self.size_common_info.measure_cmd,
    782                                   self.logger).cal_libraries_code_size()
    783 
    784     def gen_code_size_report(self, size_dist_info: CodeSizeDistinctInfo) -> None:
    785         """Generate code size record and write it into a file."""
    786 
    787         self.logger.info("Start to generate code size record for {}."
    788                          .format(size_dist_info.git_rev))
    789         output_file = os.path.join(
    790             self.csv_dir,
    791             '{}-{}.csv'
    792             .format(size_dist_info.get_info_indication(),
    793                     self.size_common_info.get_info_indication()))
    794         # Check if the corresponding record exists
    795         if size_dist_info.git_rev != "current" and \
    796            os.path.exists(output_file):
    797             self.logger.debug("Code size csv file for {} already exists."
    798                               .format(size_dist_info.git_rev))
    799             self.code_size_generator.read_size_record(
    800                 size_dist_info.git_rev, output_file)
    801         else:
    802             # measure code size
    803             code_size_text = self.cal_code_size(size_dist_info)
    804 
    805             self.logger.debug("Generating code size csv for {}."
    806                               .format(size_dist_info.git_rev))
    807             output = open(output_file, "w")
    808             self.code_size_generator.write_record(
    809                 size_dist_info.git_rev, code_size_text, output)
    810 
    811     def gen_code_size_comparison(self) -> None:
    812         """Generate results of code size changes between two Git revisions,
    813         old and new.
    814 
    815         - Measured code size result of these two Git revisions must be available.
    816         - The result is directed into either file / stdout depending on
    817           the option, size_common_info.result_options.stdout. (Default: file)
    818         """
    819 
    820         self.logger.info("Start to generate comparision result between "\
    821                          "{} and {}."
    822                          .format(self.old_size_dist_info.git_rev,
    823                                  self.new_size_dist_info.git_rev))
    824         if self.result_options.stdout:
    825             output = sys.stdout
    826         else:
    827             output_file = os.path.join(
    828                 self.comp_dir,
    829                 '{}-{}-{}.{}'
    830                 .format(self.old_size_dist_info.get_info_indication(),
    831                         self.new_size_dist_info.get_info_indication(),
    832                         self.size_common_info.get_info_indication(),
    833                         'md' if self.result_options.with_markdown else 'csv'))
    834             output = open(output_file, "w")
    835 
    836         self.logger.debug("Generating comparison results between {} and {}."
    837                           .format(self.old_size_dist_info.git_rev,
    838                                   self.new_size_dist_info.git_rev))
    839         if self.result_options.with_markdown or self.result_options.stdout:
    840             print("Measure code size between {} and {} by `{}`."
    841                   .format(self.old_size_dist_info.get_info_indication(),
    842                           self.new_size_dist_info.get_info_indication(),
    843                           self.size_common_info.get_info_indication()),
    844                   file=output)
    845         self.code_size_generator.write_comparison(
    846             self.old_size_dist_info.git_rev,
    847             self.new_size_dist_info.git_rev,
    848             output, self.result_options.with_markdown,
    849             self.result_options.show_all)
    850 
    851     def get_comparision_results(self) -> None:
    852         """Compare size of library/*.o between self.old_size_dist_info and
    853         self.old_size_dist_info and generate the result file."""
    854         build_tree.check_repo_path()
    855         self.gen_code_size_report(self.old_size_dist_info)
    856         self.gen_code_size_report(self.new_size_dist_info)
    857         self.gen_code_size_comparison()
    858 
    859 def main():
    860     parser = argparse.ArgumentParser(description=(__doc__))
    861     group_required = parser.add_argument_group(
    862         'required arguments',
    863         'required arguments to parse for running ' + os.path.basename(__file__))
    864     group_required.add_argument(
    865         '-o', '--old-rev', type=str, required=True,
    866         help='old Git revision for comparison.')
    867 
    868     group_optional = parser.add_argument_group(
    869         'optional arguments',
    870         'optional arguments to parse for running ' + os.path.basename(__file__))
    871     group_optional.add_argument(
    872         '--record-dir', type=str, default='code_size_records',
    873         help='directory where code size record is stored. '
    874              '(Default: code_size_records)')
    875     group_optional.add_argument(
    876         '--comp-dir', type=str, default='comparison',
    877         help='directory where comparison result is stored. '
    878              '(Default: comparison)')
    879     group_optional.add_argument(
    880         '-n', '--new-rev', type=str, default='current',
    881         help='new Git revision as comparison base. '
    882              '(Default is the current work directory, including uncommitted '
    883              'changes.)')
    884     group_optional.add_argument(
    885         '-a', '--arch', type=str, default=detect_arch(),
    886         choices=list(map(lambda s: s.value, SupportedArch)),
    887         help='Specify architecture for code size comparison. '
    888              '(Default is the host architecture.)')
    889     group_optional.add_argument(
    890         '-c', '--config', type=str, default=SupportedConfig.DEFAULT.value,
    891         choices=list(map(lambda s: s.value, SupportedConfig)),
    892         help='Specify configuration type for code size comparison. '
    893              '(Default is the current Mbed TLS configuration.)')
    894     group_optional.add_argument(
    895         '--markdown', action='store_true', dest='markdown',
    896         help='Show comparision of code size in a markdown table. '
    897              '(Only show the files that have changed).')
    898     group_optional.add_argument(
    899         '--stdout', action='store_true', dest='stdout',
    900         help='Set this option to direct comparison result into sys.stdout. '
    901              '(Default: file)')
    902     group_optional.add_argument(
    903         '--show-all', action='store_true', dest='show_all',
    904         help='Show all the objects in comparison result, including the ones '
    905              'that haven\'t changed in code size. (Default: False)')
    906     group_optional.add_argument(
    907         '--verbose', action='store_true', dest='verbose',
    908         help='Show logs in detail for code size measurement. '
    909              '(Default: False)')
    910     comp_args = parser.parse_args()
    911 
    912     logger = logging.getLogger()
    913     logging_util.configure_logger(logger, split_level=logging.NOTSET)
    914     logger.setLevel(logging.DEBUG if comp_args.verbose else logging.INFO)
    915 
    916     if os.path.isfile(comp_args.record_dir):
    917         logger.error("record directory: {} is not a directory"
    918                      .format(comp_args.record_dir))
    919         sys.exit(1)
    920     if os.path.isfile(comp_args.comp_dir):
    921         logger.error("comparison directory: {} is not a directory"
    922                      .format(comp_args.comp_dir))
    923         sys.exit(1)
    924 
    925     comp_args.old_rev = CodeSizeCalculator.validate_git_revision(
    926         comp_args.old_rev)
    927     if comp_args.new_rev != 'current':
    928         comp_args.new_rev = CodeSizeCalculator.validate_git_revision(
    929             comp_args.new_rev)
    930 
    931     # version, git_rev, arch, config, compiler, opt_level
    932     old_size_dist_info = CodeSizeDistinctInfo(
    933         'old', comp_args.old_rev, comp_args.arch, comp_args.config, 'cc', '-Os')
    934     new_size_dist_info = CodeSizeDistinctInfo(
    935         'new', comp_args.new_rev, comp_args.arch, comp_args.config, 'cc', '-Os')
    936     # host_arch, measure_cmd
    937     size_common_info = CodeSizeCommonInfo(
    938         detect_arch(), 'size -t')
    939     # record_dir, comp_dir, with_markdown, stdout, show_all
    940     result_options = CodeSizeResultInfo(
    941         comp_args.record_dir, comp_args.comp_dir,
    942         comp_args.markdown, comp_args.stdout, comp_args.show_all)
    943 
    944     logger.info("Measure code size between {} and {} by `{}`."
    945                 .format(old_size_dist_info.get_info_indication(),
    946                         new_size_dist_info.get_info_indication(),
    947                         size_common_info.get_info_indication()))
    948     CodeSizeComparison(old_size_dist_info, new_size_dist_info,
    949                        size_common_info, result_options,
    950                        logger).get_comparision_results()
    951 
    952 if __name__ == "__main__":
    953     main()