bef1acd7b8
Signed-off-by: Yanray Wang <yanray.wang@arm.com>
899 lines
36 KiB
Python
Executable file
899 lines
36 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
|
|
"""
|
|
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 logging
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import typing
|
|
from enum import Enum
|
|
|
|
from mbedtls_dev import build_tree
|
|
from mbedtls_dev import logging_util
|
|
from mbedtls_dev import typing_util
|
|
|
|
class SupportedArch(Enum):
|
|
"""Supported architecture for code size measurement."""
|
|
AARCH64 = 'aarch64'
|
|
AARCH32 = 'aarch32'
|
|
ARMV8_M = 'armv8-m'
|
|
X86_64 = 'x86_64'
|
|
X86 = 'x86'
|
|
|
|
|
|
class SupportedConfig(Enum):
|
|
"""Supported configuration for code size measurement."""
|
|
DEFAULT = 'default'
|
|
TFM_MEDIUM = 'tfm-medium'
|
|
|
|
|
|
# Static library
|
|
MBEDTLS_STATIC_LIB = {
|
|
'CRYPTO': 'library/libmbedcrypto.a',
|
|
'X509': 'library/libmbedx509.a',
|
|
'TLS': 'library/libmbedtls.a',
|
|
}
|
|
|
|
class CodeSizeDistinctInfo: # pylint: disable=too-few-public-methods
|
|
"""Data structure to store possibly distinct information for code size
|
|
comparison."""
|
|
def __init__( #pylint: disable=too-many-arguments
|
|
self,
|
|
version: str,
|
|
git_rev: str,
|
|
arch: str,
|
|
config: str,
|
|
compiler: str,
|
|
opt_level: str,
|
|
) -> None:
|
|
"""
|
|
:param: version: which version to compare with for code size.
|
|
:param: git_rev: Git revision to calculate code size.
|
|
:param: arch: architecture to measure code size on.
|
|
:param: config: Configuration type to calculate code size.
|
|
(See SupportedConfig)
|
|
:param: compiler: compiler used to build library/*.o.
|
|
:param: opt_level: Options that control optimization. (E.g. -Os)
|
|
"""
|
|
self.version = version
|
|
self.git_rev = git_rev
|
|
self.arch = arch
|
|
self.config = config
|
|
self.compiler = compiler
|
|
self.opt_level = opt_level
|
|
# Note: Variables below are not initialized by class instantiation.
|
|
self.pre_make_cmd = [] #type: typing.List[str]
|
|
self.make_cmd = ''
|
|
|
|
def get_info_indication(self):
|
|
"""Return a unique string to indicate Code Size Distinct Information."""
|
|
return '{rev}-{arch}-{config}-{cc}'\
|
|
.format(rev=self.git_rev, arch=self.arch, config=self.config,
|
|
cc=self.compiler)
|
|
|
|
|
|
class CodeSizeCommonInfo: # pylint: disable=too-few-public-methods
|
|
"""Data structure to store common information for code size comparison."""
|
|
def __init__(
|
|
self,
|
|
host_arch: str,
|
|
measure_cmd: str,
|
|
) -> None:
|
|
"""
|
|
:param host_arch: host architecture.
|
|
:param measure_cmd: command to measure code size for library/*.o.
|
|
"""
|
|
self.host_arch = host_arch
|
|
self.measure_cmd = measure_cmd
|
|
|
|
def get_info_indication(self):
|
|
"""Return a unique string to indicate Code Size Common Information."""
|
|
return '{measure_tool}'\
|
|
.format(measure_tool=self.measure_cmd.strip().split(' ')[0])
|
|
|
|
class CodeSizeResultInfo: # pylint: disable=too-few-public-methods
|
|
"""Data structure to store result options for code size comparison."""
|
|
def __init__(
|
|
self,
|
|
record_dir: str,
|
|
comp_dir: str,
|
|
with_markdown=False,
|
|
stdout=False,
|
|
) -> None:
|
|
"""
|
|
:param record_dir: directory to store code size record.
|
|
:param comp_dir: directory to store results of code size comparision.
|
|
:param with_markdown: write comparision result into a markdown table.
|
|
(Default: False)
|
|
:param stdout: direct comparison result into sys.stdout.
|
|
(Default False)
|
|
"""
|
|
self.record_dir = record_dir
|
|
self.comp_dir = comp_dir
|
|
self.with_markdown = with_markdown
|
|
self.stdout = stdout
|
|
|
|
|
|
DETECT_ARCH_CMD = "cc -dM -E - < /dev/null"
|
|
def detect_arch() -> str:
|
|
"""Auto-detect host architecture."""
|
|
cc_output = subprocess.check_output(DETECT_ARCH_CMD, shell=True).decode()
|
|
if '__aarch64__' in cc_output:
|
|
return SupportedArch.AARCH64.value
|
|
if '__arm__' in cc_output:
|
|
return SupportedArch.AARCH32.value
|
|
if '__x86_64__' in cc_output:
|
|
return SupportedArch.X86_64.value
|
|
if '__x86__' in cc_output:
|
|
return SupportedArch.X86.value
|
|
else:
|
|
print("Unknown host architecture, cannot auto-detect arch.")
|
|
sys.exit(1)
|
|
|
|
TFM_MEDIUM_CONFIG_H = 'configs/tfm_mbedcrypto_config_profile_medium.h'
|
|
TFM_MEDIUM_CRYPTO_CONFIG_H = 'configs/crypto_config_profile_medium.h'
|
|
|
|
CONFIG_H = 'include/mbedtls/mbedtls_config.h'
|
|
CRYPTO_CONFIG_H = 'include/psa/crypto_config.h'
|
|
BACKUP_SUFFIX = '.code_size.bak'
|
|
|
|
class CodeSizeBuildInfo: # pylint: disable=too-few-public-methods
|
|
"""Gather information used to measure code size.
|
|
|
|
It collects information about architecture, configuration in order to
|
|
infer build command for code size measurement.
|
|
"""
|
|
|
|
SupportedArchConfig = [
|
|
'-a ' + SupportedArch.AARCH64.value + ' -c ' + SupportedConfig.DEFAULT.value,
|
|
'-a ' + SupportedArch.AARCH32.value + ' -c ' + SupportedConfig.DEFAULT.value,
|
|
'-a ' + SupportedArch.X86_64.value + ' -c ' + SupportedConfig.DEFAULT.value,
|
|
'-a ' + SupportedArch.X86.value + ' -c ' + SupportedConfig.DEFAULT.value,
|
|
'-a ' + SupportedArch.ARMV8_M.value + ' -c ' + SupportedConfig.TFM_MEDIUM.value,
|
|
]
|
|
|
|
def __init__(
|
|
self,
|
|
size_dist_info: CodeSizeDistinctInfo,
|
|
host_arch: str,
|
|
logger: logging.Logger,
|
|
) -> None:
|
|
"""
|
|
:param size_dist_info:
|
|
CodeSizeDistinctInfo containing info for code size measurement.
|
|
- size_dist_info.arch: architecture to measure code size on.
|
|
- size_dist_info.config: configuration type to measure
|
|
code size with.
|
|
- size_dist_info.compiler: compiler used to build library/*.o.
|
|
- size_dist_info.opt_level: Options that control optimization.
|
|
(E.g. -Os)
|
|
:param host_arch: host architecture.
|
|
:param logger: logging module
|
|
"""
|
|
self.arch = size_dist_info.arch
|
|
self.config = size_dist_info.config
|
|
self.compiler = size_dist_info.compiler
|
|
self.opt_level = size_dist_info.opt_level
|
|
|
|
self.make_cmd = ['make', '-j', 'lib']
|
|
|
|
self.host_arch = host_arch
|
|
self.logger = logger
|
|
|
|
def check_correctness(self) -> bool:
|
|
"""Check whether we are using proper / supported combination
|
|
of information to build library/*.o."""
|
|
|
|
# default config
|
|
if self.config == SupportedConfig.DEFAULT.value and \
|
|
self.arch == self.host_arch:
|
|
return True
|
|
# TF-M
|
|
elif self.arch == SupportedArch.ARMV8_M.value and \
|
|
self.config == SupportedConfig.TFM_MEDIUM.value:
|
|
return True
|
|
|
|
return False
|
|
|
|
def infer_pre_make_command(self) -> typing.List[str]:
|
|
"""Infer command to set up proper configuration before running make."""
|
|
pre_make_cmd = [] #type: typing.List[str]
|
|
if self.config == SupportedConfig.TFM_MEDIUM.value:
|
|
pre_make_cmd.append('cp -r {src} {dest}'
|
|
.format(src=TFM_MEDIUM_CONFIG_H, dest=CONFIG_H))
|
|
pre_make_cmd.append('cp -r {src} {dest}'
|
|
.format(src=TFM_MEDIUM_CRYPTO_CONFIG_H,
|
|
dest=CRYPTO_CONFIG_H))
|
|
|
|
return pre_make_cmd
|
|
|
|
def infer_make_cflags(self) -> str:
|
|
"""Infer CFLAGS by instance attributes in CodeSizeDistinctInfo."""
|
|
cflags = [] #type: typing.List[str]
|
|
|
|
# set optimization level
|
|
cflags.append(self.opt_level)
|
|
# set compiler by config
|
|
if self.config == SupportedConfig.TFM_MEDIUM.value:
|
|
self.compiler = 'armclang'
|
|
cflags.append('-mcpu=cortex-m33')
|
|
# set target
|
|
if self.compiler == 'armclang':
|
|
cflags.append('--target=arm-arm-none-eabi')
|
|
|
|
return ' '.join(cflags)
|
|
|
|
def infer_make_command(self) -> str:
|
|
"""Infer make command by CFLAGS and CC."""
|
|
|
|
if self.check_correctness():
|
|
# set CFLAGS=
|
|
self.make_cmd.append('CFLAGS=\'{}\''.format(self.infer_make_cflags()))
|
|
# set CC=
|
|
self.make_cmd.append('CC={}'.format(self.compiler))
|
|
return ' '.join(self.make_cmd)
|
|
else:
|
|
self.logger.error("Unsupported combination of architecture: {} " \
|
|
"and configuration: {}.\n"
|
|
.format(self.arch,
|
|
self.config))
|
|
self.logger.error("Please use supported combination of " \
|
|
"architecture and configuration:")
|
|
for comb in CodeSizeBuildInfo.SupportedArchConfig:
|
|
self.logger.error(comb)
|
|
self.logger.error("")
|
|
self.logger.error("For your system, please use:")
|
|
for comb in CodeSizeBuildInfo.SupportedArchConfig:
|
|
if "default" in comb and self.host_arch not in comb:
|
|
continue
|
|
self.logger.error(comb)
|
|
sys.exit(1)
|
|
|
|
|
|
class CodeSizeCalculator:
|
|
""" A calculator to calculate code size of library/*.o based on
|
|
Git revision and code size measurement tool.
|
|
"""
|
|
|
|
def __init__( #pylint: disable=too-many-arguments
|
|
self,
|
|
git_rev: str,
|
|
pre_make_cmd: typing.List[str],
|
|
make_cmd: str,
|
|
measure_cmd: str,
|
|
logger: logging.Logger,
|
|
) -> None:
|
|
"""
|
|
:param git_rev: Git revision. (E.g: commit)
|
|
:param pre_make_cmd: command to set up proper config before running make.
|
|
:param make_cmd: command to build library/*.o.
|
|
:param measure_cmd: command to measure code size for library/*.o.
|
|
:param logger: logging module
|
|
"""
|
|
self.repo_path = "."
|
|
self.git_command = "git"
|
|
self.make_clean = 'make clean'
|
|
|
|
self.git_rev = git_rev
|
|
self.pre_make_cmd = pre_make_cmd
|
|
self.make_cmd = make_cmd
|
|
self.measure_cmd = measure_cmd
|
|
self.logger = logger
|
|
|
|
@staticmethod
|
|
def validate_git_revision(git_rev: str) -> str:
|
|
result = subprocess.check_output(["git", "rev-parse", "--verify",
|
|
git_rev + "^{commit}"],
|
|
shell=False, universal_newlines=True)
|
|
return result[:7]
|
|
|
|
def _create_git_worktree(self) -> str:
|
|
"""Create a separate worktree for Git revision.
|
|
If Git revision is current, use current worktree instead."""
|
|
|
|
if self.git_rev == 'current':
|
|
self.logger.debug("Using current work directory.")
|
|
git_worktree_path = self.repo_path
|
|
else:
|
|
self.logger.debug("Creating git worktree for {}."
|
|
.format(self.git_rev))
|
|
git_worktree_path = os.path.join(self.repo_path,
|
|
"temp-" + self.git_rev)
|
|
subprocess.check_output(
|
|
[self.git_command, "worktree", "add", "--detach",
|
|
git_worktree_path, self.git_rev], cwd=self.repo_path,
|
|
stderr=subprocess.STDOUT
|
|
)
|
|
|
|
return git_worktree_path
|
|
|
|
@staticmethod
|
|
def backup_config_files(restore: bool) -> None:
|
|
"""Backup / Restore config files."""
|
|
if restore:
|
|
shutil.move(CONFIG_H + BACKUP_SUFFIX, CONFIG_H)
|
|
shutil.move(CRYPTO_CONFIG_H + BACKUP_SUFFIX, CRYPTO_CONFIG_H)
|
|
else:
|
|
shutil.copy(CONFIG_H, CONFIG_H + BACKUP_SUFFIX)
|
|
shutil.copy(CRYPTO_CONFIG_H, CRYPTO_CONFIG_H + BACKUP_SUFFIX)
|
|
|
|
def _build_libraries(self, git_worktree_path: str) -> None:
|
|
"""Build library/*.o in the specified worktree."""
|
|
|
|
self.logger.debug("Building library/*.o for {}."
|
|
.format(self.git_rev))
|
|
my_environment = os.environ.copy()
|
|
try:
|
|
if self.git_rev == 'current':
|
|
self.backup_config_files(restore=False)
|
|
for pre_cmd in self.pre_make_cmd:
|
|
subprocess.check_output(
|
|
pre_cmd, env=my_environment, shell=True,
|
|
cwd=git_worktree_path, stderr=subprocess.STDOUT,
|
|
universal_newlines=True
|
|
)
|
|
subprocess.check_output(
|
|
self.make_clean, env=my_environment, shell=True,
|
|
cwd=git_worktree_path, stderr=subprocess.STDOUT,
|
|
universal_newlines=True
|
|
)
|
|
subprocess.check_output(
|
|
self.make_cmd, env=my_environment, shell=True,
|
|
cwd=git_worktree_path, stderr=subprocess.STDOUT,
|
|
universal_newlines=True
|
|
)
|
|
if self.git_rev == 'current':
|
|
self.backup_config_files(restore=True)
|
|
except subprocess.CalledProcessError as e:
|
|
self._handle_called_process_error(e, git_worktree_path)
|
|
|
|
def _gen_raw_code_size(self, git_worktree_path: str) -> typing.Dict[str, str]:
|
|
"""Measure code size by a tool and return in UTF-8 encoding."""
|
|
|
|
self.logger.debug("Measuring code size for {} by `{}`."
|
|
.format(self.git_rev,
|
|
self.measure_cmd.strip().split(' ')[0]))
|
|
|
|
res = {}
|
|
for mod, st_lib in MBEDTLS_STATIC_LIB.items():
|
|
try:
|
|
result = subprocess.check_output(
|
|
[self.measure_cmd + ' ' + st_lib], cwd=git_worktree_path,
|
|
shell=True, 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:
|
|
self.logger.debug("Removing temporary worktree {}."
|
|
.format(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
|
|
self.logger.error(e, exc_info=True)
|
|
self.logger.error("Process output:\n {}".format(e.output))
|
|
|
|
# Quit gracefully by removing the existing worktree
|
|
self._remove_worktree(git_worktree_path)
|
|
sys.exit(-1)
|
|
|
|
def cal_libraries_code_size(self) -> typing.Dict[str, str]:
|
|
"""Do a complete round to calculate code size of library/*.o
|
|
by measurement tool.
|
|
|
|
:return A dictionary of measured code size
|
|
- typing.Dict[mod: str]
|
|
"""
|
|
|
|
git_worktree_path = self._create_git_worktree()
|
|
self._build_libraries(git_worktree_path)
|
|
res = self._gen_raw_code_size(git_worktree_path)
|
|
self._remove_worktree(git_worktree_path)
|
|
|
|
return res
|
|
|
|
|
|
class CodeSizeGenerator:
|
|
""" A generator based on size measurement tool for library/*.o.
|
|
|
|
This is an abstract class. To use it, derive a class that implements
|
|
write_record and write_comparison methods, then call both of them with
|
|
proper arguments.
|
|
"""
|
|
def __init__(self, logger: logging.Logger) -> None:
|
|
"""
|
|
:param logger: logging module
|
|
"""
|
|
self.logger = logger
|
|
|
|
def write_record(
|
|
self,
|
|
git_rev: str,
|
|
code_size_text: typing.Dict[str, str],
|
|
output: typing_util.Writable
|
|
) -> None:
|
|
"""Write size record into a file.
|
|
|
|
:param git_rev: Git revision. (E.g: commit)
|
|
:param code_size_text:
|
|
string output (utf-8) from measurement tool of code size.
|
|
- typing.Dict[mod: str]
|
|
:param output: output stream which the code size record is written to.
|
|
(Note: Normally write code size record into File)
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
def write_comparison(
|
|
self,
|
|
old_rev: str,
|
|
new_rev: str,
|
|
output: typing_util.Writable,
|
|
with_markdown=False
|
|
) -> None:
|
|
"""Write a comparision result into a stream between two Git revisions.
|
|
|
|
:param old_rev: old Git revision to compared with.
|
|
:param new_rev: new Git revision to compared with.
|
|
:param output: output stream which the code size record is written to.
|
|
(File / sys.stdout)
|
|
:param with_markdown: write comparision result in a markdown table.
|
|
(Default: False)
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
|
|
class CodeSizeGeneratorWithSize(CodeSizeGenerator):
|
|
"""Code Size Base Class for size record saving and writing."""
|
|
|
|
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
|
|
|
|
def __init__(self, logger: logging.Logger) -> None:
|
|
""" Variable code_size is used to store size info for any Git revisions.
|
|
:param code_size:
|
|
Data Format as following:
|
|
{git_rev: {module: {file_name: [text, data, bss, dec],
|
|
etc ...
|
|
},
|
|
etc ...
|
|
},
|
|
etc ...
|
|
}
|
|
"""
|
|
super().__init__(logger)
|
|
self.code_size = {} #type: typing.Dict[str, typing.Dict]
|
|
|
|
def _set_size_record(self, git_rev: str, mod: str, size_text: str) -> None:
|
|
"""Store size information for target Git 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()
|
|
# file_name: SizeEntry(text, data, bss, dec)
|
|
size_record[data[5]] = CodeSizeGeneratorWithSize.SizeEntry(
|
|
data[0], data[1], data[2], data[3])
|
|
if git_rev in self.code_size:
|
|
self.code_size[git_rev].update({mod: size_record})
|
|
else:
|
|
self.code_size[git_rev] = {mod: size_record}
|
|
|
|
def read_size_record(self, git_rev: 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:
|
|
# file_name: SizeEntry(text, data, bss, dec)
|
|
size_record[data[0]] = CodeSizeGeneratorWithSize.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 git_rev in self.code_size:
|
|
self.code_size[git_rev].update({mod: size_record})
|
|
else:
|
|
self.code_size[git_rev] = {mod: size_record}
|
|
mod = ""
|
|
size_record = {}
|
|
|
|
def _size_reader_helper(
|
|
self,
|
|
git_rev: str,
|
|
output: typing_util.Writable,
|
|
with_markdown=False
|
|
) -> typing.Iterator[tuple]:
|
|
"""A helper function to peel code_size based on Git revision."""
|
|
for mod, file_size in self.code_size[git_rev].items():
|
|
if not with_markdown:
|
|
output.write("\n" + mod + "\n")
|
|
for fname, size_entry in file_size.items():
|
|
yield mod, fname, size_entry
|
|
|
|
def write_record(
|
|
self,
|
|
git_rev: str,
|
|
code_size_text: typing.Dict[str, str],
|
|
output: typing_util.Writable
|
|
) -> None:
|
|
"""Write size information to a file.
|
|
|
|
Writing Format: file_name text data bss total(dec)
|
|
"""
|
|
for mod, size_text in code_size_text.items():
|
|
self._set_size_record(git_rev, mod, size_text)
|
|
|
|
format_string = "{:<30} {:>7} {:>7} {:>7} {:>7}\n"
|
|
output.write(format_string.format("filename",
|
|
"text", "data", "bss", "total"))
|
|
for _, fname, size_entry in self._size_reader_helper(git_rev, output):
|
|
output.write(format_string.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,
|
|
with_markdown=False
|
|
) -> None:
|
|
"""Write comparison result into a file.
|
|
|
|
Writing Format: file_name current(text,data) old(text,data)\
|
|
change(text,data) change_pct%(text,data)
|
|
"""
|
|
|
|
def cal_size_section_variation(mod, fname, size_entry, attr):
|
|
new_size = int(size_entry.__dict__[attr])
|
|
# check if we have the file in old Git revision
|
|
if fname in self.code_size[old_rev][mod]:
|
|
old_size = int(self.code_size[old_rev][mod][fname].__dict__[attr])
|
|
change = new_size - old_size
|
|
if old_size != 0:
|
|
change_pct = change / old_size
|
|
else:
|
|
change_pct = 0
|
|
return [new_size, old_size, change, change_pct]
|
|
else:
|
|
return [new_size]
|
|
|
|
if with_markdown:
|
|
format_string = "| {:<30} | {:<18} | {:<14} | {:<17} | {:<18} |\n"
|
|
else:
|
|
format_string = "{:<30} {:<18} {:<14} {:<17} {:<18}\n"
|
|
|
|
output.write(format_string
|
|
.format("filename",
|
|
"current(text,data)", "old(text,data)",
|
|
"change(text,data)", "change%(text,data)"))
|
|
if with_markdown:
|
|
output.write(format_string
|
|
.format(":----", "----:", "----:", "----:", "----:"))
|
|
|
|
for mod, fname, size_entry in \
|
|
self._size_reader_helper(new_rev, output, with_markdown):
|
|
text_vari = cal_size_section_variation(mod, fname,
|
|
size_entry, 'text')
|
|
data_vari = cal_size_section_variation(mod, fname,
|
|
size_entry, 'data')
|
|
|
|
if len(text_vari) != 1:
|
|
# skip the files that haven't changed in code size if we write
|
|
# comparison result in a markdown table.
|
|
if with_markdown and text_vari[2] == 0 and data_vari[2] == 0:
|
|
continue
|
|
output.write(
|
|
format_string
|
|
.format(fname,
|
|
# current(text,data)
|
|
str(text_vari[0]) + "," + str(data_vari[0]),
|
|
# old(text,data)
|
|
str(text_vari[1]) + "," + str(data_vari[1]),
|
|
# change(text,data)
|
|
str(text_vari[2]) + "," + str(data_vari[2]),
|
|
# change%(text,data)
|
|
"{:.0%}".format(text_vari[3]) + ","
|
|
+ "{:.0%}".format(data_vari[3])))
|
|
else:
|
|
output.write(
|
|
format_string
|
|
.format(fname,
|
|
# current(text,data)
|
|
str(text_vari[0]) + "," + str(data_vari[0]),
|
|
'None', 'None', 'None'))
|
|
|
|
|
|
class CodeSizeComparison:
|
|
"""Compare code size between two Git revisions."""
|
|
|
|
def __init__( #pylint: disable=too-many-arguments
|
|
self,
|
|
old_size_dist_info: CodeSizeDistinctInfo,
|
|
new_size_dist_info: CodeSizeDistinctInfo,
|
|
size_common_info: CodeSizeCommonInfo,
|
|
result_options: CodeSizeResultInfo,
|
|
logger: logging.Logger,
|
|
) -> None:
|
|
"""
|
|
:param old_size_dist_info: CodeSizeDistinctInfo containing old distinct
|
|
info to compare code size with.
|
|
:param new_size_dist_info: CodeSizeDistinctInfo containing new distinct
|
|
info to take as comparision base.
|
|
:param size_common_info: CodeSizeCommonInfo containing common info for
|
|
both old and new size distinct info and
|
|
measurement tool.
|
|
:param result_options: CodeSizeResultInfo containing results options for
|
|
code size record and comparision.
|
|
:param logger: logging module
|
|
"""
|
|
|
|
self.logger = logger
|
|
|
|
self.old_size_dist_info = old_size_dist_info
|
|
self.new_size_dist_info = new_size_dist_info
|
|
self.size_common_info = size_common_info
|
|
# infer pre make command
|
|
self.old_size_dist_info.pre_make_cmd = CodeSizeBuildInfo(
|
|
self.old_size_dist_info, self.size_common_info.host_arch,
|
|
self.logger).infer_pre_make_command()
|
|
self.new_size_dist_info.pre_make_cmd = CodeSizeBuildInfo(
|
|
self.new_size_dist_info, self.size_common_info.host_arch,
|
|
self.logger).infer_pre_make_command()
|
|
# infer make command
|
|
self.old_size_dist_info.make_cmd = CodeSizeBuildInfo(
|
|
self.old_size_dist_info, self.size_common_info.host_arch,
|
|
self.logger).infer_make_command()
|
|
self.new_size_dist_info.make_cmd = CodeSizeBuildInfo(
|
|
self.new_size_dist_info, self.size_common_info.host_arch,
|
|
self.logger).infer_make_command()
|
|
# initialize size parser with corresponding measurement tool
|
|
self.code_size_generator = self.__generate_size_parser()
|
|
|
|
self.result_options = result_options
|
|
self.csv_dir = os.path.abspath(self.result_options.record_dir)
|
|
os.makedirs(self.csv_dir, exist_ok=True)
|
|
self.comp_dir = os.path.abspath(self.result_options.comp_dir)
|
|
os.makedirs(self.comp_dir, exist_ok=True)
|
|
|
|
def __generate_size_parser(self):
|
|
"""Generate a parser for the corresponding measurement tool."""
|
|
if re.match(r'size', self.size_common_info.measure_cmd.strip()):
|
|
return CodeSizeGeneratorWithSize(self.logger)
|
|
else:
|
|
self.logger.error("Unsupported measurement tool: `{}`."
|
|
.format(self.size_common_info.measure_cmd
|
|
.strip().split(' ')[0]))
|
|
sys.exit(1)
|
|
|
|
def cal_code_size(
|
|
self,
|
|
size_dist_info: CodeSizeDistinctInfo
|
|
) -> typing.Dict[str, str]:
|
|
"""Calculate code size of library/*.o in a UTF-8 encoding"""
|
|
|
|
return CodeSizeCalculator(size_dist_info.git_rev,
|
|
size_dist_info.pre_make_cmd,
|
|
size_dist_info.make_cmd,
|
|
self.size_common_info.measure_cmd,
|
|
self.logger).cal_libraries_code_size()
|
|
|
|
def gen_code_size_report(self, size_dist_info: CodeSizeDistinctInfo) -> None:
|
|
"""Generate code size record and write it into a file."""
|
|
|
|
self.logger.info("Start to generate code size record for {}."
|
|
.format(size_dist_info.git_rev))
|
|
output_file = os.path.join(
|
|
self.csv_dir,
|
|
'{}-{}.csv'
|
|
.format(size_dist_info.get_info_indication(),
|
|
self.size_common_info.get_info_indication()))
|
|
# Check if the corresponding record exists
|
|
if size_dist_info.git_rev != "current" and \
|
|
os.path.exists(output_file):
|
|
self.logger.debug("Code size csv file for {} already exists."
|
|
.format(size_dist_info.git_rev))
|
|
self.code_size_generator.read_size_record(
|
|
size_dist_info.git_rev, output_file)
|
|
else:
|
|
# measure code size
|
|
code_size_text = self.cal_code_size(size_dist_info)
|
|
|
|
self.logger.debug("Generating code size csv for {}."
|
|
.format(size_dist_info.git_rev))
|
|
output = open(output_file, "w")
|
|
self.code_size_generator.write_record(
|
|
size_dist_info.git_rev, code_size_text, output)
|
|
|
|
def gen_code_size_comparison(self) -> None:
|
|
"""Generate results of code size changes between two Git revisions,
|
|
old and new.
|
|
|
|
- Measured code size result of these two Git revisions must be available.
|
|
- The result is directed into either file / stdout depending on
|
|
the option, size_common_info.result_options.stdout. (Default: file)
|
|
"""
|
|
|
|
self.logger.info("Start to generate comparision result between "\
|
|
"{} and {}."
|
|
.format(self.old_size_dist_info.git_rev,
|
|
self.new_size_dist_info.git_rev))
|
|
if self.result_options.stdout:
|
|
output = sys.stdout
|
|
else:
|
|
output_file = os.path.join(
|
|
self.comp_dir,
|
|
'{}-{}-{}.csv'
|
|
.format(self.old_size_dist_info.get_info_indication(),
|
|
self.new_size_dist_info.get_info_indication(),
|
|
self.size_common_info.get_info_indication()))
|
|
output = open(output_file, "w")
|
|
|
|
self.logger.debug("Generating comparison results between {} and {}."
|
|
.format(self.old_size_dist_info.git_rev,
|
|
self.new_size_dist_info.git_rev))
|
|
if self.result_options.with_markdown or self.result_options.stdout:
|
|
print("Measure code size between {} and {} by `{}`."
|
|
.format(self.old_size_dist_info.get_info_indication(),
|
|
self.new_size_dist_info.get_info_indication(),
|
|
self.size_common_info.get_info_indication()),
|
|
file=output)
|
|
self.code_size_generator.write_comparison(
|
|
self.old_size_dist_info.git_rev,
|
|
self.new_size_dist_info.git_rev,
|
|
output, self.result_options.with_markdown)
|
|
|
|
def get_comparision_results(self) -> None:
|
|
"""Compare size of library/*.o between self.old_size_dist_info and
|
|
self.old_size_dist_info and generate the result file."""
|
|
build_tree.check_repo_path()
|
|
self.gen_code_size_report(self.old_size_dist_info)
|
|
self.gen_code_size_report(self.new_size_dist_info)
|
|
self.gen_code_size_comparison()
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=(__doc__))
|
|
group_required = parser.add_argument_group(
|
|
'required arguments',
|
|
'required arguments to parse for running ' + os.path.basename(__file__))
|
|
group_required.add_argument(
|
|
'-o', '--old-rev', type=str, required=True,
|
|
help='old Git revision for comparison.')
|
|
|
|
group_optional = parser.add_argument_group(
|
|
'optional arguments',
|
|
'optional arguments to parse for running ' + os.path.basename(__file__))
|
|
group_optional.add_argument(
|
|
'--record_dir', type=str, default='code_size_records',
|
|
help='directory where code size record is stored. '
|
|
'(Default: code_size_records)')
|
|
group_optional.add_argument(
|
|
'-r', '--comp-dir', type=str, default='comparison',
|
|
help='directory where comparison result is stored. '
|
|
'(Default: comparison)')
|
|
group_optional.add_argument(
|
|
'-n', '--new-rev', type=str, default=None,
|
|
help='new Git revision as comparison base. '
|
|
'(Default is the current work directory, including uncommitted '
|
|
'changes.)')
|
|
group_optional.add_argument(
|
|
'-a', '--arch', type=str, default=detect_arch(),
|
|
choices=list(map(lambda s: s.value, SupportedArch)),
|
|
help='Specify architecture for code size comparison. '
|
|
'(Default is the host architecture.)')
|
|
group_optional.add_argument(
|
|
'-c', '--config', type=str, default=SupportedConfig.DEFAULT.value,
|
|
choices=list(map(lambda s: s.value, SupportedConfig)),
|
|
help='Specify configuration type for code size comparison. '
|
|
'(Default is the current MbedTLS configuration.)')
|
|
group_optional.add_argument(
|
|
'--markdown', action='store_true', dest='markdown',
|
|
help='Show comparision of code size in a markdown table. '
|
|
'(Only show the files that have changed).')
|
|
group_optional.add_argument(
|
|
'--stdout', action='store_true', dest='stdout',
|
|
help='Set this option to direct comparison result into sys.stdout. '
|
|
'(Default: file)')
|
|
group_optional.add_argument(
|
|
'--verbose', action='store_true', dest='verbose',
|
|
help='Show logs in detail for code size measurement. '
|
|
'(Default: False)')
|
|
comp_args = parser.parse_args()
|
|
|
|
logger = logging.getLogger()
|
|
logging_util.configure_logger(logger)
|
|
if comp_args.stdout and not comp_args.verbose:
|
|
logger.setLevel(logging.ERROR)
|
|
else:
|
|
logger.setLevel(logging.DEBUG if comp_args.verbose else logging.INFO)
|
|
|
|
if os.path.isfile(comp_args.comp_dir):
|
|
logger.error("{} is not a directory".format(comp_args.comp_dir))
|
|
parser.exit()
|
|
|
|
old_revision = CodeSizeCalculator.validate_git_revision(comp_args.old_rev)
|
|
if comp_args.new_rev is not None:
|
|
new_revision = CodeSizeCalculator.validate_git_revision(
|
|
comp_args.new_rev)
|
|
else:
|
|
new_revision = 'current'
|
|
|
|
# version, git_rev, arch, config, compiler, opt_level
|
|
old_size_dist_info = CodeSizeDistinctInfo(
|
|
'old', old_revision, comp_args.arch, comp_args.config, 'cc', '-Os')
|
|
new_size_dist_info = CodeSizeDistinctInfo(
|
|
'new', new_revision, comp_args.arch, comp_args.config, 'cc', '-Os')
|
|
# host_arch, measure_cmd
|
|
size_common_info = CodeSizeCommonInfo(
|
|
detect_arch(), 'size -t')
|
|
# record_dir, comp_dir, with_markdown, stdout
|
|
result_options = CodeSizeResultInfo(
|
|
comp_args.record_dir, comp_args.comp_dir,
|
|
comp_args.markdown, comp_args.stdout)
|
|
|
|
logger.info("Measure code size between {} and {} by `{}`."
|
|
.format(old_size_dist_info.get_info_indication(),
|
|
new_size_dist_info.get_info_indication(),
|
|
size_common_info.get_info_indication()))
|
|
CodeSizeComparison(old_size_dist_info, new_size_dist_info,
|
|
size_common_info, result_options,
|
|
logger).get_comparision_results()
|
|
|
|
if __name__ == "__main__":
|
|
main()
|