diff --git a/scripts/code_size_compare.py b/scripts/code_size_compare.py new file mode 100755 index 000000000..85393d031 --- /dev/null +++ b/scripts/code_size_compare.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 + +""" +Purpose + +This script is for comparing the size of the library files from two +different Git revisions within an Mbed TLS repository. +The results of the comparison is formatted as csv and stored at a +configurable location. +Note: must be run from Mbed TLS root. +""" + +# Copyright The Mbed TLS Contributors +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import os +import subprocess +import sys + +class CodeSizeComparison: + """Compare code size between two Git revisions.""" + + def __init__(self, old_revision, new_revision, result_dir): + """ + old_revision: revision to compare against + new_revision: + result_dir: directory for comparision result + """ + self.repo_path = "." + self.result_dir = os.path.abspath(result_dir) + os.makedirs(self.result_dir, exist_ok=True) + + self.csv_dir = os.path.abspath("code_size_records/") + os.makedirs(self.csv_dir, exist_ok=True) + + self.old_rev = old_revision + self.new_rev = new_revision + self.git_command = "git" + self.make_command = "make" + + @staticmethod + def check_repo_path(): + if not all(os.path.isdir(d) for d in ["include", "library", "tests"]): + raise Exception("Must be run from Mbed TLS root") + + @staticmethod + def validate_revision(revision): + result = subprocess.check_output(["git", "rev-parse", "--verify", + revision + "^{commit}"], shell=False) + return result + + def _create_git_worktree(self, revision): + """Make a separate worktree for revision. + Do not modify the current worktree.""" + + if revision == "current": + print("Using current work directory.") + git_worktree_path = self.repo_path + else: + print("Creating git worktree for", revision) + git_worktree_path = os.path.join(self.repo_path, "temp-" + revision) + subprocess.check_output( + [self.git_command, "worktree", "add", "--detach", + git_worktree_path, revision], cwd=self.repo_path, + stderr=subprocess.STDOUT + ) + return git_worktree_path + + def _build_libraries(self, git_worktree_path): + """Build libraries in the specified worktree.""" + + my_environment = os.environ.copy() + subprocess.check_output( + [self.make_command, "-j", "lib"], env=my_environment, + cwd=git_worktree_path, stderr=subprocess.STDOUT, + ) + + def _gen_code_size_csv(self, revision, git_worktree_path): + """Generate code size csv file.""" + + csv_fname = revision + ".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])) + + def _remove_worktree(self, git_worktree_path): + """Remove temporary worktree.""" + if git_worktree_path != self.repo_path: + print("Removing temporary worktree", git_worktree_path) + subprocess.check_output( + [self.git_command, "worktree", "remove", "--force", + git_worktree_path], cwd=self.repo_path, + stderr=subprocess.STDOUT + ) + + def _get_code_size_for_rev(self, revision): + """Generate code size csv file for the specified git revision.""" + + # Check if the corresponding record exists + csv_fname = revision + ".csv" + if (revision != "current") and \ + os.path.exists(os.path.join(self.csv_dir, csv_fname)): + print("Code size csv file for", revision, "already exists.") + 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): + """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 + ".csv"), "r") + new_file = open(os.path.join(self.csv_dir, self.new_rev + ".csv"), "r") + res_file = open(os.path.join(self.result_dir, "compare-" + self.old_rev + + "-" + self.new_rev + ".csv"), "w") + + res_file.write("file_name, this_size, old_size, change, change %\n") + print("Generating comparision results.") + + old_ds = {} + for line in old_file.readlines()[1:]: + 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()[1:]: + 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): + """Compare size of library/*.o between self.old_rev and self.new_rev, + and generate the result file.""" + self.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() + +def main(): + parser = argparse.ArgumentParser( + description=( + """This script is for comparing the size of the library files + from two different Git revisions within an Mbed TLS repository. + The results of the comparison is formatted as csv, and stored at + a configurable location. + Note: must be run from Mbed TLS root.""" + ) + ) + parser.add_argument( + "-r", "--result-dir", type=str, default="comparison", + help="directory where comparison result is stored, \ + default is comparison", + ) + parser.add_argument( + "-o", "--old-rev", type=str, help="old revision for comparison.", + required=True, + ) + parser.add_argument( + "-n", "--new-rev", type=str, default=None, + help="new revision for comparison, default is the current work \ + directory, including uncommited changes." + ) + comp_args = parser.parse_args() + + if os.path.isfile(comp_args.result_dir): + print("Error: {} is not a directory".format(comp_args.result_dir)) + parser.exit() + + validate_res = CodeSizeComparison.validate_revision(comp_args.old_rev) + old_revision = validate_res.decode().replace("\n", "") + + if comp_args.new_rev is not None: + validate_res = CodeSizeComparison.validate_revision(comp_args.new_rev) + new_revision = validate_res.decode().replace("\n", "") + else: + new_revision = "current" + + result_dir = comp_args.result_dir + size_compare = CodeSizeComparison(old_revision, new_revision, result_dir) + return_code = size_compare.get_comparision_results() + sys.exit(return_code) + + +if __name__ == "__main__": + main()