From 16ebc5725b803d814f8b2e16b76c3e9c146d677b Mon Sep 17 00:00:00 2001 From: Yanray Wang Date: Tue, 30 May 2023 18:10:20 +0800 Subject: [PATCH 1/4] code_size_compare.py: add a CodeSizeBase class CodeSizeBase class aims to store size information for a specific revision. It also has support to write csv report, comparison result in a more readable format. This commit lays out foundation to simplify code for CodeSizeComparison. Signed-off-by: Yanray Wang --- scripts/code_size_compare.py | 133 ++++++++++++++++++++++++++++++++++- 1 file changed, 132 insertions(+), 1 deletion(-) diff --git a/scripts/code_size_compare.py b/scripts/code_size_compare.py index de5249a5e7..a2b007c4aa 100755 --- a/scripts/code_size_compare.py +++ b/scripts/code_size_compare.py @@ -25,10 +25,13 @@ Note: must be run from Mbed TLS root. import argparse import os +import re import subprocess import sys +import typing from enum import Enum +from mbedtls_dev import typing_util from mbedtls_dev import build_tree class SupportedArch(Enum): @@ -46,6 +49,13 @@ class SupportedConfig(Enum): DEFAULT = 'default' TFM_MEDIUM = 'tfm-medium' +# Static library +MBEDTLS_STATIC_LIB = { + 'CRYPTO': 'library/libmbedcrypto.a', + 'X509': 'library/libmbedx509.a', + 'TLS': 'library/libmbedtls.a', +} + DETECT_ARCH_CMD = "cc -dM -E - < /dev/null" def detect_arch() -> str: """Auto-detect host architecture.""" @@ -114,8 +124,129 @@ class CodeSizeInfo: # pylint: disable=too-few-public-methods print(comb) sys.exit(1) +class SizeEntry: # pylint: disable=too-few-public-methods + """Data Structure to only store information of code size.""" + def __init__(self, text, data, bss, dec): + self.text = text + self.data = data + self.bss = bss + self.total = dec # total <=> dec -class CodeSizeComparison: +class CodeSizeBase: + """Code Size Base Class for size record saving and writing.""" + + def __init__(self) -> None: + """ Variable code_size is used to store size info for any revisions. + code_size: (data format) + {revision: {module: {file_name: SizeEntry, + etc ... + }, + etc ... + }, + etc ... + } + """ + self.code_size = {} #type: typing.Dict[str, typing.Dict] + + def set_size_record(self, revision: str, mod: str, size_text: str) -> None: + """Store size information for target revision and high-level module. + + size_text Format: text data bss dec hex filename + """ + size_record = {} + for line in size_text.splitlines()[1:]: + data = line.split() + size_record[data[5]] = SizeEntry(data[0], data[1], data[2], data[3]) + if revision in self.code_size: + self.code_size[revision].update({mod: size_record}) + else: + self.code_size[revision] = {mod: size_record} + + def read_size_record(self, revision: str, fname: str) -> None: + """Read size information from csv file and write it into code_size. + + fname Format: filename text data bss dec + """ + mod = "" + size_record = {} + with open(fname, 'r') as csv_file: + for line in csv_file: + data = line.strip().split() + # check if we find the beginning of a module + if data and data[0] in MBEDTLS_STATIC_LIB: + mod = data[0] + continue + + if mod: + size_record[data[0]] = \ + SizeEntry(data[1], data[2], data[3], data[4]) + + # check if we hit record for the end of a module + m = re.match(r'.?TOTALS', line) + if m: + if revision in self.code_size: + self.code_size[revision].update({mod: size_record}) + else: + self.code_size[revision] = {mod: size_record} + mod = "" + size_record = {} + + def _size_reader_helper( + self, + revision: str, + output: typing_util.Writable + ) -> typing.Iterator[tuple]: + """A helper function to peel code_size based on revision.""" + for mod, file_size in self.code_size[revision].items(): + output.write("\n" + mod + "\n") + for fname, size_entry in file_size.items(): + yield mod, fname, size_entry + + def write_size_record( + self, + revision: str, + output: typing_util.Writable + ) -> None: + """Write size information to a file. + + Writing Format: file_name text data bss total(dec) + """ + output.write("{:<30} {:>7} {:>7} {:>7} {:>7}\n" + .format("filename", "text", "data", "bss", "total")) + for _, fname, size_entry in self._size_reader_helper(revision, output): + output.write("{:<30} {:>7} {:>7} {:>7} {:>7}\n" + .format(fname, size_entry.text, size_entry.data,\ + size_entry.bss, size_entry.total)) + + def write_comparison( + self, + old_rev: str, + new_rev: str, + output: typing_util.Writable + ) -> None: + """Write comparison result into a file. + + Writing Format: file_name current(total) old(total) change(Byte) change_pct(%) + """ + output.write("{:<30} {:>7} {:>7} {:>7} {:>7}\n" + .format("filename", "current", "old", "change", "change%")) + for mod, fname, size_entry in self._size_reader_helper(new_rev, output): + new_size = int(size_entry.total) + # check if we have the file in old revision + if fname in self.code_size[old_rev][mod]: + old_size = int(self.code_size[old_rev][mod][fname].total) + change = new_size - old_size + if old_size != 0: + change_pct = change / old_size + else: + change_pct = 0 + output.write("{:<30} {:>7} {:>7} {:>7} {:>7.2%}\n" + .format(fname, new_size, old_size, change, change_pct)) + else: + output.write("{} {}\n".format(fname, new_size)) + + +class CodeSizeComparison(CodeSizeBase): """Compare code size between two Git revisions.""" def __init__(self, old_revision, new_revision, result_dir, code_size_info): From 8804db9d992d4302cd95923e414abba16ae87b0a Mon Sep 17 00:00:00 2001 From: Yanray Wang Date: Tue, 30 May 2023 18:18:18 +0800 Subject: [PATCH 2/4] code_size_compare.py: integrate code with CodeSizeBase The code size measurement script generates code size record / comparison csv file in a more readable format. The script won't generate new record file if there is an existing one. It reads the record and stores data into dictionary of code_size for comparison. Signed-off-by: Yanray Wang --- scripts/code_size_compare.py | 62 ++++++++++++------------------------ 1 file changed, 21 insertions(+), 41 deletions(-) diff --git a/scripts/code_size_compare.py b/scripts/code_size_compare.py index a2b007c4aa..0f65b15dbd 100755 --- a/scripts/code_size_compare.py +++ b/scripts/code_size_compare.py @@ -256,6 +256,7 @@ class CodeSizeComparison(CodeSizeBase): result_dir: directory for comparison result. code_size_info: an object containing information to build library. """ + super().__init__() self.repo_path = "." self.result_dir = os.path.abspath(result_dir) os.makedirs(self.result_dir, exist_ok=True) @@ -309,19 +310,26 @@ class CodeSizeComparison(CodeSizeBase): def _gen_code_size_csv(self, revision, git_worktree_path): """Generate code size csv file.""" - csv_fname = revision + self.fname_suffix + ".csv" if revision == "current": print("Measuring code size in current work directory.") else: print("Measuring code size for", revision) - result = subprocess.check_output( - ["size library/*.o"], cwd=git_worktree_path, shell=True - ) - size_text = result.decode() - csv_file = open(os.path.join(self.csv_dir, csv_fname), "w") - for line in size_text.splitlines()[1:]: - data = line.split() - csv_file.write("{}, {}\n".format(data[5], data[3])) + + for mod, st_lib in MBEDTLS_STATIC_LIB.items(): + try: + result = subprocess.check_output( + ["size", st_lib, "-t"], cwd=git_worktree_path + ) + except subprocess.CalledProcessError as e: + self._handle_called_process_error(e, git_worktree_path) + size_text = result.decode("utf-8") + + self.set_size_record(revision, mod, size_text) + + print("Generating code size csv for", revision) + csv_file = open(os.path.join(self.csv_dir, revision + + self.fname_suffix + ".csv"), "w") + self.write_size_record(revision, csv_file) def _remove_worktree(self, git_worktree_path): """Remove temporary worktree.""" @@ -341,54 +349,26 @@ class CodeSizeComparison(CodeSizeBase): if (revision != "current") and \ os.path.exists(os.path.join(self.csv_dir, csv_fname)): print("Code size csv file for", revision, "already exists.") + self.read_size_record(revision, os.path.join(self.csv_dir, csv_fname)) else: git_worktree_path = self._create_git_worktree(revision) self._build_libraries(git_worktree_path) self._gen_code_size_csv(revision, git_worktree_path) self._remove_worktree(git_worktree_path) - def compare_code_size(self): + def _gen_code_size_comparison(self): """Generate results of the size changes between two revisions, old and new. Measured code size results of these two revisions must be available.""" - old_file = open(os.path.join(self.csv_dir, self.old_rev + - self.fname_suffix + ".csv"), "r") - new_file = open(os.path.join(self.csv_dir, self.new_rev + - self.fname_suffix + ".csv"), "r") res_file = open(os.path.join(self.result_dir, "compare-" + self.old_rev + "-" + self.new_rev + self.fname_suffix + ".csv"), "w") - res_file.write("file_name, this_size, old_size, change, change %\n") print("Generating comparison results.") + self.write_comparison(self.old_rev, self.new_rev, res_file) - old_ds = {} - for line in old_file.readlines(): - cols = line.split(", ") - fname = cols[0] - size = int(cols[1]) - if size != 0: - old_ds[fname] = size - - new_ds = {} - for line in new_file.readlines(): - cols = line.split(", ") - fname = cols[0] - size = int(cols[1]) - new_ds[fname] = size - - for fname in new_ds: - this_size = new_ds[fname] - if fname in old_ds: - old_size = old_ds[fname] - change = this_size - old_size - change_pct = change / old_size - res_file.write("{}, {}, {}, {}, {:.2%}\n".format(fname, \ - this_size, old_size, change, float(change_pct))) - else: - res_file.write("{}, {}\n".format(fname, this_size)) return 0 def get_comparision_results(self): @@ -397,7 +377,7 @@ class CodeSizeComparison(CodeSizeBase): build_tree.check_repo_path() self._get_code_size_for_rev(self.old_rev) self._get_code_size_for_rev(self.new_rev) - return self.compare_code_size() + return self._gen_code_size_comparison() def _handle_called_process_error(self, e: subprocess.CalledProcessError, git_worktree_path): From 72b105f1ae6fe8683314ce4ca96018100fc1e8d6 Mon Sep 17 00:00:00 2001 From: Yanray Wang Date: Wed, 31 May 2023 15:20:39 +0800 Subject: [PATCH 3/4] code_size_compare: clarify input and return types Signed-off-by: Yanray Wang --- scripts/code_size_compare.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/scripts/code_size_compare.py b/scripts/code_size_compare.py index 0f65b15dbd..f5aae110fb 100755 --- a/scripts/code_size_compare.py +++ b/scripts/code_size_compare.py @@ -249,7 +249,13 @@ class CodeSizeBase: class CodeSizeComparison(CodeSizeBase): """Compare code size between two Git revisions.""" - def __init__(self, old_revision, new_revision, result_dir, code_size_info): + def __init__( + self, + old_revision: str, + new_revision: str, + result_dir: str, + code_size_info: CodeSizeInfo + ) -> None: """ old_revision: revision to compare against. new_revision: @@ -272,12 +278,12 @@ class CodeSizeComparison(CodeSizeBase): code_size_info.config @staticmethod - def validate_revision(revision): + def validate_revision(revision: str) -> bytes: result = subprocess.check_output(["git", "rev-parse", "--verify", revision + "^{commit}"], shell=False) return result - def _create_git_worktree(self, revision): + def _create_git_worktree(self, revision: str) -> str: """Make a separate worktree for revision. Do not modify the current worktree.""" @@ -295,7 +301,7 @@ class CodeSizeComparison(CodeSizeBase): return git_worktree_path - def _build_libraries(self, git_worktree_path): + def _build_libraries(self, git_worktree_path: str) -> None: """Build libraries in the specified worktree.""" my_environment = os.environ.copy() @@ -307,7 +313,7 @@ class CodeSizeComparison(CodeSizeBase): except subprocess.CalledProcessError as e: self._handle_called_process_error(e, git_worktree_path) - def _gen_code_size_csv(self, revision, git_worktree_path): + def _gen_code_size_csv(self, revision: str, git_worktree_path: str) -> None: """Generate code size csv file.""" if revision == "current": @@ -331,7 +337,7 @@ class CodeSizeComparison(CodeSizeBase): self.fname_suffix + ".csv"), "w") self.write_size_record(revision, csv_file) - def _remove_worktree(self, git_worktree_path): + def _remove_worktree(self, git_worktree_path: str) -> None: """Remove temporary worktree.""" if git_worktree_path != self.repo_path: print("Removing temporary worktree", git_worktree_path) @@ -341,7 +347,7 @@ class CodeSizeComparison(CodeSizeBase): stderr=subprocess.STDOUT ) - def _get_code_size_for_rev(self, revision): + def _get_code_size_for_rev(self, revision: str) -> None: """Generate code size csv file for the specified git revision.""" # Check if the corresponding record exists @@ -356,7 +362,7 @@ class CodeSizeComparison(CodeSizeBase): self._gen_code_size_csv(revision, git_worktree_path) self._remove_worktree(git_worktree_path) - def _gen_code_size_comparison(self): + def _gen_code_size_comparison(self) -> int: """Generate results of the size changes between two revisions, old and new. Measured code size results of these two revisions must be available.""" @@ -371,7 +377,7 @@ class CodeSizeComparison(CodeSizeBase): return 0 - def get_comparision_results(self): + def get_comparision_results(self) -> int: """Compare size of library/*.o between self.old_rev and self.new_rev, and generate the result file.""" build_tree.check_repo_path() @@ -380,7 +386,7 @@ class CodeSizeComparison(CodeSizeBase): return self._gen_code_size_comparison() def _handle_called_process_error(self, e: subprocess.CalledProcessError, - git_worktree_path): + git_worktree_path: str) -> None: """Handle a CalledProcessError and quit the program gracefully. Remove any extra worktrees so that the script may be called again.""" From c7a2a6d11dedf4d2fd881e676598e6a6cb19b25c Mon Sep 17 00:00:00 2001 From: Yanray Wang Date: Wed, 31 May 2023 15:47:25 +0800 Subject: [PATCH 4/4] code_size_compare.py: change prompt message in code size measurement Signed-off-by: Yanray Wang --- scripts/code_size_compare.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/code_size_compare.py b/scripts/code_size_compare.py index f5aae110fb..3bd3e4c327 100755 --- a/scripts/code_size_compare.py +++ b/scripts/code_size_compare.py @@ -288,7 +288,7 @@ class CodeSizeComparison(CodeSizeBase): Do not modify the current worktree.""" if revision == "current": - print("Using current work directory.") + print("Using current work directory") git_worktree_path = self.repo_path else: print("Creating git worktree for", revision) @@ -317,7 +317,7 @@ class CodeSizeComparison(CodeSizeBase): """Generate code size csv file.""" if revision == "current": - print("Measuring code size in current work directory.") + print("Measuring code size in current work directory") else: print("Measuring code size for", revision) @@ -372,7 +372,8 @@ class CodeSizeComparison(CodeSizeBase): self.fname_suffix + ".csv"), "w") - print("Generating comparison results.") + print("\nGenerating comparison results between",\ + self.old_rev, "and", self.new_rev) self.write_comparison(self.old_rev, self.new_rev, res_file) return 0 @@ -446,7 +447,7 @@ def main(): code_size_info = CodeSizeInfo(comp_args.arch, comp_args.config, detect_arch()) - print("Measure code size for architecture: {}, configuration: {}" + print("Measure code size for architecture: {}, configuration: {}\n" .format(code_size_info.arch, code_size_info.config)) result_dir = comp_args.result_dir size_compare = CodeSizeComparison(old_revision, new_revision, result_dir,