code_size_compare: add CodeSizeCalculator to calculate code size

CodeSizeCalculator is aimed to calculate code size based on a Git
revision and code size measurement tool. The output of code size is
in utf-8 encoding.

Signed-off-by: Yanray Wang <yanray.wang@arm.com>
This commit is contained in:
Yanray Wang 2023-07-14 17:37:45 +08:00
parent 15c43f3407
commit e0e276046b

View file

@ -126,6 +126,123 @@ class CodeSizeInfo: # pylint: disable=too-few-public-methods
sys.exit(1) sys.exit(1)
class CodeSizeCalculator:
""" A calculator to calculate code size of library objects based on
Git revision and code size measurement tool.
"""
def __init__(
self,
revision: str,
make_cmd: str,
) -> None:
"""
revision: Git revision.(E.g: commit)
make_cmd: command to build library objects.
"""
self.repo_path = "."
self.git_command = "git"
self.make_clean = 'make clean'
self.revision = revision
self.make_cmd = make_cmd
@staticmethod
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: str) -> str:
"""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: str) -> None:
"""Build libraries in the specified worktree."""
my_environment = os.environ.copy()
try:
subprocess.check_output(
self.make_clean, env=my_environment, shell=True,
cwd=git_worktree_path, stderr=subprocess.STDOUT,
)
subprocess.check_output(
self.make_cmd, env=my_environment, shell=True,
cwd=git_worktree_path, stderr=subprocess.STDOUT,
)
except subprocess.CalledProcessError as e:
self._handle_called_process_error(e, git_worktree_path)
def _gen_raw_code_size(self, revision, git_worktree_path):
"""Calculate code size with measurement tool in UTF-8 encoding."""
if revision == "current":
print("Measuring code size in current work directory")
else:
print("Measuring code size for", revision)
res = {}
for mod, st_lib in MBEDTLS_STATIC_LIB.items():
try:
result = subprocess.check_output(
["size", st_lib, "-t"], cwd=git_worktree_path,
universal_newlines=True
)
res[mod] = result
except subprocess.CalledProcessError as e:
self._handle_called_process_error(e, git_worktree_path)
return res
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)
subprocess.check_output(
[self.git_command, "worktree", "remove", "--force",
git_worktree_path], cwd=self.repo_path,
stderr=subprocess.STDOUT
)
def _handle_called_process_error(self, e: subprocess.CalledProcessError,
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."""
# Tell the user what went wrong
print("The following command: {} failed and exited with code {}"
.format(e.cmd, e.returncode))
print("Process output:\n {}".format(str(e.output, "utf-8")))
# Quit gracefully by removing the existing worktree
self._remove_worktree(git_worktree_path)
sys.exit(-1)
def cal_libraries_code_size(self) -> typing.Dict:
"""Calculate code size of libraries by measurement tool."""
revision = self.revision
git_worktree_path = self._create_git_worktree(revision)
self._build_libraries(git_worktree_path)
res = self._gen_raw_code_size(revision, git_worktree_path)
self._remove_worktree(git_worktree_path)
return res
class CodeSizeGenerator: class CodeSizeGenerator:
""" A generator based on size measurement tool for library objects. """ A generator based on size measurement tool for library objects.
@ -328,7 +445,6 @@ class CodeSizeComparison:
result_dir: directory for comparison result. result_dir: directory for comparison result.
code_size_info: an object containing information to build library. code_size_info: an object containing information to build library.
""" """
super().__init__()
self.repo_path = "." self.repo_path = "."
self.result_dir = os.path.abspath(result_dir) self.result_dir = os.path.abspath(result_dir)
os.makedirs(self.result_dir, exist_ok=True) os.makedirs(self.result_dir, exist_ok=True)
@ -345,47 +461,7 @@ class CodeSizeComparison:
code_size_info.config code_size_info.config
self.code_size_generator = CodeSizeGeneratorWithSize() self.code_size_generator = CodeSizeGeneratorWithSize()
@staticmethod def _gen_code_size_csv(self, revision: str) -> None:
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: str) -> str:
"""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: str) -> None:
"""Build libraries in the specified worktree."""
my_environment = os.environ.copy()
try:
subprocess.check_output(
self.make_clean, env=my_environment, shell=True,
cwd=git_worktree_path, stderr=subprocess.STDOUT,
)
subprocess.check_output(
self.make_command, env=my_environment, shell=True,
cwd=git_worktree_path, stderr=subprocess.STDOUT,
)
except subprocess.CalledProcessError as e:
self._handle_called_process_error(e, git_worktree_path)
def _gen_code_size_csv(self, revision: str, git_worktree_path: str) -> None:
"""Generate code size csv file.""" """Generate code size csv file."""
if revision == "current": if revision == "current":
@ -393,31 +469,13 @@ class CodeSizeComparison:
else: else:
print("Measuring code size for", revision) print("Measuring code size for", revision)
for mod, st_lib in MBEDTLS_STATIC_LIB.items(): code_size_text = CodeSizeCalculator(revision, self.make_command).\
try: cal_libraries_code_size()
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.code_size_generator.set_size_record(revision, mod, size_text) csv_file = os.path.join(self.csv_dir, revision +
self.fname_suffix + ".csv")
print("Generating code size csv for", revision) self.code_size_generator.size_generator_write_record(revision,\
csv_file = open(os.path.join(self.csv_dir, revision + code_size_text, csv_file)
self.fname_suffix + ".csv"), "w")
self.code_size_generator.write_size_record(revision, csv_file)
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)
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: str) -> None: def _get_code_size_for_rev(self, revision: str) -> None:
"""Generate code size csv file for the specified git revision.""" """Generate code size csv file for the specified git revision."""
@ -430,24 +488,21 @@ class CodeSizeComparison:
self.code_size_generator.read_size_record(revision,\ self.code_size_generator.read_size_record(revision,\
os.path.join(self.csv_dir, csv_fname)) os.path.join(self.csv_dir, csv_fname))
else: else:
git_worktree_path = self._create_git_worktree(revision) self._gen_code_size_csv(revision)
self._build_libraries(git_worktree_path)
self._gen_code_size_csv(revision, git_worktree_path)
self._remove_worktree(git_worktree_path)
def _gen_code_size_comparison(self) -> int: def _gen_code_size_comparison(self) -> int:
"""Generate results of the size changes between two revisions, """Generate results of the size changes between two revisions,
old and new. Measured code size results of these two revisions old and new. Measured code size results of these two revisions
must be available.""" must be available."""
res_file = open(os.path.join(self.result_dir, "compare-" + res_file = os.path.join(self.result_dir, "compare-" +
self.old_rev + "-" + self.new_rev + self.old_rev + "-" + self.new_rev +
self.fname_suffix + self.fname_suffix + ".csv")
".csv"), "w")
print("\nGenerating comparison results between",\ print("\nGenerating comparison results between",\
self.old_rev, "and", self.new_rev) self.old_rev, "and", self.new_rev)
self.code_size_generator.write_comparison(self.old_rev, self.new_rev, res_file) self.code_size_generator.size_generator_write_comparison(\
self.old_rev, self.new_rev, res_file)
return 0 return 0
@ -459,20 +514,6 @@ class CodeSizeComparison:
self._get_code_size_for_rev(self.new_rev) self._get_code_size_for_rev(self.new_rev)
return self._gen_code_size_comparison() return self._gen_code_size_comparison()
def _handle_called_process_error(self, e: subprocess.CalledProcessError,
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."""
# Tell the user what went wrong
print("The following command: {} failed and exited with code {}"
.format(e.cmd, e.returncode))
print("Process output:\n {}".format(str(e.output, "utf-8")))
# Quit gracefully by removing the existing worktree
self._remove_worktree(git_worktree_path)
sys.exit(-1)
def main(): def main():
parser = argparse.ArgumentParser(description=(__doc__)) parser = argparse.ArgumentParser(description=(__doc__))
group_required = parser.add_argument_group( group_required = parser.add_argument_group(
@ -509,11 +550,11 @@ def main():
print("Error: {} is not a directory".format(comp_args.result_dir)) print("Error: {} is not a directory".format(comp_args.result_dir))
parser.exit() parser.exit()
validate_res = CodeSizeComparison.validate_revision(comp_args.old_rev) validate_res = CodeSizeCalculator.validate_revision(comp_args.old_rev)
old_revision = validate_res.decode().replace("\n", "") old_revision = validate_res.decode().replace("\n", "")
if comp_args.new_rev is not None: if comp_args.new_rev is not None:
validate_res = CodeSizeComparison.validate_revision(comp_args.new_rev) validate_res = CodeSizeCalculator.validate_revision(comp_args.new_rev)
new_revision = validate_res.decode().replace("\n", "") new_revision = validate_res.decode().replace("\n", "")
else: else:
new_revision = "current" new_revision = "current"