code_size_compare: add logging module and tweak prompt message

Signed-off-by: Yanray Wang <yanray.wang@arm.com>
This commit is contained in:
Yanray Wang 2023-07-19 12:09:45 +08:00
parent 227576aaa4
commit 21127f7095
3 changed files with 163 additions and 92 deletions

View file

@ -24,6 +24,7 @@ Note: must be run from Mbed TLS root.
# limitations under the License. # limitations under the License.
import argparse import argparse
import logging
import os import os
import re import re
import subprocess import subprocess
@ -32,8 +33,9 @@ import typing
from enum import Enum from enum import Enum
from types import SimpleNamespace from types import SimpleNamespace
from mbedtls_dev import typing_util
from mbedtls_dev import build_tree from mbedtls_dev import build_tree
from mbedtls_dev import logging_util
from mbedtls_dev import typing_util
class SupportedArch(Enum): class SupportedArch(Enum):
"""Supported architecture for code size measurement.""" """Supported architecture for code size measurement."""
@ -91,7 +93,8 @@ class CodeSizeBuildInfo: # pylint: disable=too-few-public-methods
def __init__( def __init__(
self, self,
size_version: SimpleNamespace, size_version: SimpleNamespace,
host_arch: str host_arch: str,
logger: logging.Logger,
) -> None: ) -> None:
""" """
size_version: SimpleNamespace containing info for code size measurement. size_version: SimpleNamespace containing info for code size measurement.
@ -101,6 +104,7 @@ class CodeSizeBuildInfo: # pylint: disable=too-few-public-methods
""" """
self.size_version = size_version self.size_version = size_version
self.host_arch = host_arch self.host_arch = host_arch
self.logger = logger
def infer_make_command(self) -> str: def infer_make_command(self) -> str:
"""Infer build command based on architecture and configuration.""" """Infer build command based on architecture and configuration."""
@ -116,16 +120,20 @@ class CodeSizeBuildInfo: # pylint: disable=too-few-public-methods
-DMBEDTLS_CONFIG_FILE=\\\"' + CONFIG_TFM_MEDIUM_MBEDCRYPTO_H + '\\\" \ -DMBEDTLS_CONFIG_FILE=\\\"' + CONFIG_TFM_MEDIUM_MBEDCRYPTO_H + '\\\" \
-DMBEDTLS_PSA_CRYPTO_CONFIG_FILE=\\\"' + CONFIG_TFM_MEDIUM_PSA_CRYPTO_H + '\\\" \'' -DMBEDTLS_PSA_CRYPTO_CONFIG_FILE=\\\"' + CONFIG_TFM_MEDIUM_PSA_CRYPTO_H + '\\\" \''
else: else:
print("Unsupported combination of architecture: {} and configuration: {}" self.logger.error("Unsupported combination of architecture: {} " \
.format(self.size_version.arch, self.size_version.config)) "and configuration: {}.\n"
print("\nPlease use supported combination of architecture and configuration:") .format(self.size_version.arch,
self.size_version.config))
self.logger.info("Please use supported combination of " \
"architecture and configuration:")
for comb in CodeSizeBuildInfo.SupportedArchConfig: for comb in CodeSizeBuildInfo.SupportedArchConfig:
print(comb) self.logger.info(comb)
print("\nFor your system, please use:") self.logger.info("")
self.logger.info("For your system, please use:")
for comb in CodeSizeBuildInfo.SupportedArchConfig: for comb in CodeSizeBuildInfo.SupportedArchConfig:
if "default" in comb and self.host_arch not in comb: if "default" in comb and self.host_arch not in comb:
continue continue
print(comb) self.logger.info(comb)
sys.exit(1) sys.exit(1)
@ -138,7 +146,8 @@ class CodeSizeCalculator:
self, self,
revision: str, revision: str,
make_cmd: str, make_cmd: str,
measure_cmd: str measure_cmd: str,
logger: logging.Logger,
) -> None: ) -> None:
""" """
revision: Git revision.(E.g: commit) revision: Git revision.(E.g: commit)
@ -152,6 +161,7 @@ class CodeSizeCalculator:
self.revision = revision self.revision = revision
self.make_cmd = make_cmd self.make_cmd = make_cmd
self.measure_cmd = measure_cmd self.measure_cmd = measure_cmd
self.logger = logger
@staticmethod @staticmethod
def validate_revision(revision: str) -> bytes: def validate_revision(revision: str) -> bytes:
@ -159,19 +169,21 @@ class CodeSizeCalculator:
revision + "^{commit}"], shell=False) revision + "^{commit}"], shell=False)
return result return result
def _create_git_worktree(self, revision: str) -> str: def _create_git_worktree(self) -> str:
"""Make a separate worktree for revision. """Make a separate worktree for revision.
Do not modify the current worktree.""" Do not modify the current worktree."""
if revision == "current": if self.revision == "current":
print("Using current work directory") self.logger.debug("Using current work directory.")
git_worktree_path = self.repo_path git_worktree_path = self.repo_path
else: else:
print("Creating git worktree for", revision) self.logger.debug("Creating git worktree for {}."
git_worktree_path = os.path.join(self.repo_path, "temp-" + revision) .format(self.revision))
git_worktree_path = os.path.join(self.repo_path,
"temp-" + self.revision)
subprocess.check_output( subprocess.check_output(
[self.git_command, "worktree", "add", "--detach", [self.git_command, "worktree", "add", "--detach",
git_worktree_path, revision], cwd=self.repo_path, git_worktree_path, self.revision], cwd=self.repo_path,
stderr=subprocess.STDOUT stderr=subprocess.STDOUT
) )
@ -180,6 +192,8 @@ class CodeSizeCalculator:
def _build_libraries(self, git_worktree_path: str) -> None: def _build_libraries(self, git_worktree_path: str) -> None:
"""Build libraries in the specified worktree.""" """Build libraries in the specified worktree."""
self.logger.debug("Building objects of library for {}."
.format(self.revision))
my_environment = os.environ.copy() my_environment = os.environ.copy()
try: try:
subprocess.check_output( subprocess.check_output(
@ -193,12 +207,12 @@ class CodeSizeCalculator:
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
self._handle_called_process_error(e, git_worktree_path) self._handle_called_process_error(e, git_worktree_path)
def _gen_raw_code_size(self, revision, git_worktree_path): def _gen_raw_code_size(self, git_worktree_path: str) -> typing.Dict:
"""Calculate code size with measurement tool in UTF-8 encoding.""" """Calculate code size with measurement tool in UTF-8 encoding."""
if revision == "current":
print("Measuring code size in current work directory") self.logger.debug("Measuring code size for {} by `{}`."
else: .format(self.revision,
print("Measuring code size for", revision) self.measure_cmd.strip().split(' ')[0]))
res = {} res = {}
for mod, st_lib in MBEDTLS_STATIC_LIB.items(): for mod, st_lib in MBEDTLS_STATIC_LIB.items():
@ -216,7 +230,8 @@ class CodeSizeCalculator:
def _remove_worktree(self, git_worktree_path: str) -> None: def _remove_worktree(self, git_worktree_path: str) -> None:
"""Remove temporary worktree.""" """Remove temporary worktree."""
if git_worktree_path != self.repo_path: if git_worktree_path != self.repo_path:
print("Removing temporary worktree", git_worktree_path) self.logger.debug("Removing temporary worktree {}."
.format(git_worktree_path))
subprocess.check_output( subprocess.check_output(
[self.git_command, "worktree", "remove", "--force", [self.git_command, "worktree", "remove", "--force",
git_worktree_path], cwd=self.repo_path, git_worktree_path], cwd=self.repo_path,
@ -229,9 +244,8 @@ class CodeSizeCalculator:
Remove any extra worktrees so that the script may be called again.""" Remove any extra worktrees so that the script may be called again."""
# Tell the user what went wrong # Tell the user what went wrong
print("The following command: {} failed and exited with code {}" self.logger.error(e, exc_info=True)
.format(e.cmd, e.returncode)) self.logger.error("Process output:\n {}".format(str(e.output, "utf-8")))
print("Process output:\n {}".format(str(e.output, "utf-8")))
# Quit gracefully by removing the existing worktree # Quit gracefully by removing the existing worktree
self._remove_worktree(git_worktree_path) self._remove_worktree(git_worktree_path)
@ -240,10 +254,9 @@ class CodeSizeCalculator:
def cal_libraries_code_size(self) -> typing.Dict: def cal_libraries_code_size(self) -> typing.Dict:
"""Calculate code size of libraries by measurement tool.""" """Calculate code size of libraries by measurement tool."""
revision = self.revision git_worktree_path = self._create_git_worktree()
git_worktree_path = self._create_git_worktree(revision)
self._build_libraries(git_worktree_path) self._build_libraries(git_worktree_path)
res = self._gen_raw_code_size(revision, git_worktree_path) res = self._gen_raw_code_size(git_worktree_path)
self._remove_worktree(git_worktree_path) self._remove_worktree(git_worktree_path)
return res return res
@ -256,6 +269,9 @@ class CodeSizeGenerator:
size_generator_write_record and size_generator_write_comparison methods, size_generator_write_record and size_generator_write_comparison methods,
then call both of them with proper arguments. then call both of them with proper arguments.
""" """
def __init__(self, logger: logging.Logger) -> None:
self.logger = logger
def size_generator_write_record( def size_generator_write_record(
self, self,
revision: str, revision: str,
@ -301,7 +317,7 @@ class CodeSizeGeneratorWithSize(CodeSizeGenerator):
self.bss = bss self.bss = bss
self.total = dec # total <=> dec self.total = dec # total <=> dec
def __init__(self) -> None: def __init__(self, logger: logging.Logger) -> None:
""" Variable code_size is used to store size info for any revisions. """ Variable code_size is used to store size info for any revisions.
code_size: (data format) code_size: (data format)
{revision: {module: {file_name: [text, data, bss, dec], {revision: {module: {file_name: [text, data, bss, dec],
@ -312,6 +328,7 @@ class CodeSizeGeneratorWithSize(CodeSizeGenerator):
etc ... etc ...
} }
""" """
super().__init__(logger)
self.code_size = {} #type: typing.Dict[str, typing.Dict] self.code_size = {} #type: typing.Dict[str, typing.Dict]
def set_size_record(self, revision: str, mod: str, size_text: str) -> None: def set_size_record(self, revision: str, mod: str, size_text: str) -> None:
@ -458,10 +475,11 @@ class CodeSizeGeneratorWithSize(CodeSizeGenerator):
) -> None: ) -> None:
"""Write size record into a specified file based on Git revision and """Write size record into a specified file based on Git revision and
output from `size` tool.""" output from `size` tool."""
self.logger.debug("Generating code size csv for {}.".format(revision))
for mod, size_text in code_size_text.items(): for mod, size_text in code_size_text.items():
self.set_size_record(revision, mod, size_text) self.set_size_record(revision, mod, size_text)
print("Generating code size csv for", revision)
output = open(output_file, "w") output = open(output_file, "w")
self.write_size_record(revision, output) self.write_size_record(revision, output)
@ -473,6 +491,9 @@ class CodeSizeGeneratorWithSize(CodeSizeGenerator):
result_options: SimpleNamespace result_options: SimpleNamespace
) -> None: ) -> None:
"""Write a comparision result into a stream between two revisions.""" """Write a comparision result into a stream between two revisions."""
self.logger.debug("Generating comparison results between {} and {}."
.format(old_rev, new_rev))
if result_options.stdout: if result_options.stdout:
output = sys.stdout output = sys.stdout
else: else:
@ -488,6 +509,7 @@ class CodeSizeComparison:
old_size_version: SimpleNamespace, old_size_version: SimpleNamespace,
new_size_version: SimpleNamespace, new_size_version: SimpleNamespace,
code_size_common: SimpleNamespace, code_size_common: SimpleNamespace,
logger: logging.Logger,
) -> None: ) -> None:
""" """
old_revision: revision to compare against. old_revision: revision to compare against.
@ -501,36 +523,40 @@ class CodeSizeComparison:
self.csv_dir = os.path.abspath("code_size_records/") self.csv_dir = os.path.abspath("code_size_records/")
os.makedirs(self.csv_dir, exist_ok=True) os.makedirs(self.csv_dir, exist_ok=True)
self.logger = logger
self.old_size_version = old_size_version self.old_size_version = old_size_version
self.new_size_version = new_size_version self.new_size_version = new_size_version
self.code_size_common = code_size_common self.code_size_common = code_size_common
self.old_size_version.make_cmd = \ self.old_size_version.make_cmd = CodeSizeBuildInfo(
CodeSizeBuildInfo(self.old_size_version,\ self.old_size_version, self.code_size_common.host_arch,
self.code_size_common.host_arch).infer_make_command() self.logger).infer_make_command()
self.new_size_version.make_cmd = \ self.new_size_version.make_cmd = CodeSizeBuildInfo(
CodeSizeBuildInfo(self.new_size_version,\ self.new_size_version, self.code_size_common.host_arch,
self.code_size_common.host_arch).infer_make_command() self.logger).infer_make_command()
self.git_command = "git" self.git_command = "git"
self.make_clean = 'make clean' self.make_clean = 'make clean'
self.code_size_generator = self.__init_code_size_generator__(\ self.code_size_generator = self.__generate_size_parser()
self.code_size_common.measure_cmd)
@staticmethod def __generate_size_parser(self):
def __init_code_size_generator__(measure_cmd): if re.match(r'size', self.code_size_common.measure_cmd.strip()):
if re.match(r'size', measure_cmd.strip()): return CodeSizeGeneratorWithSize(self.logger)
return CodeSizeGeneratorWithSize()
else: else:
print("Error: unsupported tool:", measure_cmd.strip().split(' ')[0]) self.logger.error("Unsupported measurement tool: `{}`."
.format(self.code_size_common.measure_cmd
.strip().split(' ')[0]))
sys.exit(1) sys.exit(1)
def cal_code_size(self, size_version: SimpleNamespace): def cal_code_size(self, size_version: SimpleNamespace):
"""Calculate code size of library objects in a UTF-8 encoding""" """Calculate code size of library objects in a UTF-8 encoding"""
return CodeSizeCalculator(size_version.revision, size_version.make_cmd,\ return CodeSizeCalculator(size_version.revision, size_version.make_cmd,
self.code_size_common.measure_cmd).cal_libraries_code_size() self.code_size_common.measure_cmd,
self.logger).cal_libraries_code_size()
def gen_file_name(self, old_size_version, new_size_version=None): def gen_file_name(self, old_size_version, new_size_version=None):
"""Generate a literal string as csv file name."""
if new_size_version: if new_size_version:
return '{}-{}-{}-{}-{}-{}-{}.csv'\ return '{}-{}-{}-{}-{}-{}-{}.csv'\
.format(old_size_version.revision[:7], .format(old_size_version.revision[:7],
@ -547,11 +573,17 @@ class CodeSizeComparison:
def gen_code_size_report(self, size_version: SimpleNamespace): def gen_code_size_report(self, size_version: SimpleNamespace):
"""Generate code size record and write it into a file.""" """Generate code size record and write it into a file."""
output_file = os.path.join(self.csv_dir, self.gen_file_name(size_version)) self.logger.info("Start to generate code size record for {}."
.format(size_version.revision))
output_file = os.path.join(self.csv_dir,
self.gen_file_name(size_version))
# Check if the corresponding record exists # Check if the corresponding record exists
if (size_version.revision != "current") and os.path.exists(output_file): if size_version.revision != "current" and \
print("Code size csv file for", size_version.revision, "already exists.") os.path.exists(output_file):
self.code_size_generator.read_size_record(size_version.revision, output_file) self.logger.debug("Code size csv file for {} already exists."
.format(size_version.revision))
self.code_size_generator.read_size_record(
size_version.revision, output_file)
else: else:
self.code_size_generator.size_generator_write_record(\ self.code_size_generator.size_generator_write_record(\
size_version.revision, self.cal_code_size(size_version), size_version.revision, self.cal_code_size(size_version),
@ -562,14 +594,18 @@ class CodeSizeComparison:
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."""
output_file = os.path.join(self.result_dir,\ self.logger.info("Start to generate comparision result between "\
self.gen_file_name(self.old_size_version, self.new_size_version)) "{} and {}."
.format(self.old_size_version.revision,
self.new_size_version.revision))
output_file = os.path.join(
self.result_dir,
self.gen_file_name(self.old_size_version, self.new_size_version))
self.code_size_generator.size_generator_write_comparison(
self.old_size_version.revision, self.new_size_version.revision,
output_file, self.code_size_common.result_options)
print("\nGenerating comparison results between",\
self.old_size_version.revision, "and", self.new_size_version.revision)
self.code_size_generator.size_generator_write_comparison(\
self.old_size_version.revision, self.new_size_version.revision,\
output_file, self.code_size_common.result_options)
return 0 return 0
def get_comparision_results(self) -> int: def get_comparision_results(self) -> int:
@ -619,10 +655,17 @@ def main():
'--stdout', action='store_true', dest='stdout', '--stdout', action='store_true', dest='stdout',
help="Set this option to direct comparison result into sys.stdout.\ help="Set this option to direct comparison result into sys.stdout.\
(Default: file)") (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() comp_args = parser.parse_args()
logger = logging.getLogger()
logging_util.configure_logger(logger)
logger.setLevel(logging.DEBUG if comp_args.verbose else logging.INFO)
if os.path.isfile(comp_args.result_dir): if os.path.isfile(comp_args.result_dir):
print("Error: {} is not a directory".format(comp_args.result_dir)) logger.error("{} is not a directory".format(comp_args.result_dir))
parser.exit() parser.exit()
validate_res = CodeSizeCalculator.validate_revision(comp_args.old_rev) validate_res = CodeSizeCalculator.validate_revision(comp_args.old_rev)
@ -658,11 +701,16 @@ def main():
measure_cmd='size -t', measure_cmd='size -t',
) )
logger.info("Measure code size between {}:{}-{} and {}:{}-{} by `{}`."
.format(old_size_version.revision, old_size_version.config,
old_size_version.arch,
new_size_version.revision, old_size_version.config,
new_size_version.arch,
code_size_common.measure_cmd.strip().split(' ')[0]))
size_compare = CodeSizeComparison(old_size_version, new_size_version,\ size_compare = CodeSizeComparison(old_size_version, new_size_version,\
code_size_common) code_size_common, logger)
return_code = size_compare.get_comparision_results() return_code = size_compare.get_comparision_results()
sys.exit(return_code) sys.exit(return_code)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View file

@ -0,0 +1,55 @@
"""Auxiliary functions used for logging module.
"""
# 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 logging
import sys
def configure_logger(
logger: logging.Logger,
logger_format="[%(levelname)s]: %(message)s"
) -> None:
"""
Configure the logging.Logger instance so that:
- Format is set to any logger_format.
Default: "[%(levelname)s]: %(message)s"
- loglevel >= WARNING are printed to stderr.
- loglevel < WARNING are printed to stdout.
"""
class MaxLevelFilter(logging.Filter):
# pylint: disable=too-few-public-methods
def __init__(self, max_level, name=''):
super().__init__(name)
self.max_level = max_level
def filter(self, record: logging.LogRecord) -> bool:
return record.levelno <= self.max_level
log_formatter = logging.Formatter(logger_format)
# set loglevel >= WARNING to be printed to stderr
stderr_hdlr = logging.StreamHandler(sys.stderr)
stderr_hdlr.setLevel(logging.WARNING)
stderr_hdlr.setFormatter(log_formatter)
# set loglevel <= INFO to be printed to stdout
stdout_hdlr = logging.StreamHandler(sys.stdout)
stdout_hdlr.addFilter(MaxLevelFilter(logging.INFO))
stdout_hdlr.setFormatter(log_formatter)
logger.addHandler(stderr_hdlr)
logger.addHandler(stdout_hdlr)

View file

@ -24,7 +24,6 @@ from tests/data_files/ and tests/suites/*.data files by default.
""" """
import os import os
import sys
import re import re
import typing import typing
import argparse import argparse
@ -43,6 +42,7 @@ from generate_test_code import FileWrapper
import scripts_path # pylint: disable=unused-import import scripts_path # pylint: disable=unused-import
from mbedtls_dev import build_tree from mbedtls_dev import build_tree
from mbedtls_dev import logging_util
def check_cryptography_version(): def check_cryptography_version():
match = re.match(r'^[0-9]+', cryptography.__version__) match = re.match(r'^[0-9]+', cryptography.__version__)
@ -393,38 +393,6 @@ def list_all(audit_data: AuditData):
loc)) loc))
def configure_logger(logger: logging.Logger) -> None:
"""
Configure the logging.Logger instance so that:
- Format is set to "[%(levelname)s]: %(message)s".
- loglevel >= WARNING are printed to stderr.
- loglevel < WARNING are printed to stdout.
"""
class MaxLevelFilter(logging.Filter):
# pylint: disable=too-few-public-methods
def __init__(self, max_level, name=''):
super().__init__(name)
self.max_level = max_level
def filter(self, record: logging.LogRecord) -> bool:
return record.levelno <= self.max_level
log_formatter = logging.Formatter("[%(levelname)s]: %(message)s")
# set loglevel >= WARNING to be printed to stderr
stderr_hdlr = logging.StreamHandler(sys.stderr)
stderr_hdlr.setLevel(logging.WARNING)
stderr_hdlr.setFormatter(log_formatter)
# set loglevel <= INFO to be printed to stdout
stdout_hdlr = logging.StreamHandler(sys.stdout)
stdout_hdlr.addFilter(MaxLevelFilter(logging.INFO))
stdout_hdlr.setFormatter(log_formatter)
logger.addHandler(stderr_hdlr)
logger.addHandler(stdout_hdlr)
def main(): def main():
""" """
Perform argument parsing. Perform argument parsing.
@ -457,7 +425,7 @@ def main():
# start main routine # start main routine
# setup logger # setup logger
logger = logging.getLogger() logger = logging.getLogger()
configure_logger(logger) logging_util.configure_logger(logger)
logger.setLevel(logging.DEBUG if args.verbose else logging.ERROR) logger.setLevel(logging.DEBUG if args.verbose else logging.ERROR)
td_auditor = TestDataAuditor(logger) td_auditor = TestDataAuditor(logger)