1#!/usr/bin/env python3 2 3""" 4This script is for comparing the size of the library files from two 5different Git revisions within an Mbed TLS repository. 6The results of the comparison is formatted as csv and stored at a 7configurable location. 8Note: 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 14import argparse 15import logging 16import os 17import re 18import shutil 19import subprocess 20import sys 21import typing 22from enum import Enum 23 24import framework_scripts_path # pylint: disable=unused-import 25from mbedtls_framework import build_tree 26from mbedtls_framework import logging_util 27from mbedtls_framework import typing_util 28 29class 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 38class SupportedConfig(Enum): 39 """Supported configuration for code size measurement.""" 40 DEFAULT = 'default' 41 TFM_MEDIUM = 'tfm-medium' 42 43 44# Static library 45MBEDTLS_STATIC_LIB = { 46 'CRYPTO': 'library/libmbedcrypto.a', 47 'X509': 'library/libmbedx509.a', 48 'TLS': 'library/libmbedtls.a', 49} 50 51class 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 87class 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 106class 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 132DETECT_ARCH_CMD = "cc -dM -E - < /dev/null" 133def 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 148TFM_MEDIUM_CONFIG_H = 'configs/ext/tfm_mbedcrypto_config_profile_medium.h' 149TFM_MEDIUM_CRYPTO_CONFIG_H = 'tf-psa-crypto/configs/ext/crypto_config_profile_medium.h' 150 151CONFIG_H = 'include/mbedtls/mbedtls_config.h' 152CRYPTO_CONFIG_H = 'tf-psa-crypto/include/psa/crypto_config.h' 153BACKUP_SUFFIX = '.code_size.bak' 154 155class 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 268class 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 427class 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 478class 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 710class 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 859def 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 952if __name__ == "__main__": 953 main() 954