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